feat(service): teach-loop corrections + MCP tool (vetting + OSS tuning data)
POST/GET prospector/corrections (own-DB prospect_corrections): operator/coworker records where the backend's decision or draft was wrong — the trial vetting signal and the substrate for tuning the OSS models. Adds prospector_correction MCP tool. Disable tsc incremental (nest deleteOutDir + stale tsbuildinfo dropped emitted files). tsc clean; corrections POST/GET verified live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
92c98b9ade
commit
56e93b8cfb
11 changed files with 208 additions and 3 deletions
|
|
@ -33,6 +33,16 @@ export class ProspectorClient {
|
||||||
return this.get(`/prospector/digest?sinceHours=${sinceHours}`);
|
return this.get(`/prospector/digest?sinceHours=${sinceHours}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submitCorrection(body: {
|
||||||
|
handle: string;
|
||||||
|
category: string;
|
||||||
|
summary?: string;
|
||||||
|
originalBody?: string;
|
||||||
|
correctedBody?: string;
|
||||||
|
}): Promise<unknown> {
|
||||||
|
return this.post('/prospector/corrections', body);
|
||||||
|
}
|
||||||
|
|
||||||
getSettings(): Promise<unknown> {
|
getSettings(): Promise<unknown> {
|
||||||
return this.get('/prospector/settings');
|
return this.get('/prospector/settings');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,24 @@ server.registerTool(
|
||||||
async ({ sinceHours }) => json(await client.getDigest(sinceHours ?? 12)),
|
async ({ sinceHours }) => json(await client.getDigest(sinceHours ?? 12)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'prospector_correction',
|
||||||
|
{
|
||||||
|
title: 'Record a teach-loop correction',
|
||||||
|
description:
|
||||||
|
"Record a correction when the backend's decision/draft was wrong — the vetting signal during the trial and the tuning data for the OSS models.",
|
||||||
|
inputSchema: {
|
||||||
|
handle: z.string(),
|
||||||
|
category: z.enum(['voice', 'tone', 'safety', 'logistics', 'classification', 'other']),
|
||||||
|
summary: z.string().optional(),
|
||||||
|
originalBody: z.string().optional().describe('What the backend produced'),
|
||||||
|
correctedBody: z.string().optional().describe('What it should have been'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ handle, category, summary, originalBody, correctedBody }) =>
|
||||||
|
json(await client.submitCorrection({ handle, category, summary, originalBody, correctedBody })),
|
||||||
|
);
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'prospector_get_mode',
|
'prospector_get_mode',
|
||||||
{
|
{
|
||||||
|
|
|
||||||
14
migrations/0003_corrections.sql
Normal file
14
migrations/0003_corrections.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- prospector service — teach-loop corrections (own DB, black:25462).
|
||||||
|
-- Vetting signal during the trial + training substrate for OSS model tuning.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS prospect_corrections (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
handle TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL, -- voice | tone | safety | logistics | classification | other
|
||||||
|
summary TEXT,
|
||||||
|
original_body TEXT, -- what the backend produced
|
||||||
|
corrected_body TEXT, -- what it should have been
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_prospect_corrections_created ON prospect_corrections(created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_prospect_corrections_category ON prospect_corrections(category, created_at DESC);
|
||||||
|
|
@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ServiceTokenGuard } from './auth/service-token.guard.js';
|
import { ServiceTokenGuard } from './auth/service-token.guard.js';
|
||||||
import { AuditModule } from './audit/audit.module.js';
|
import { AuditModule } from './audit/audit.module.js';
|
||||||
import { ClientsModule } from './clients/clients.module.js';
|
import { ClientsModule } from './clients/clients.module.js';
|
||||||
|
import { CorrectionsModule } from './corrections/corrections.module.js';
|
||||||
import { databaseConfig } from './config/database.config.js';
|
import { databaseConfig } from './config/database.config.js';
|
||||||
import { HealthModule } from './health/health.module.js';
|
import { HealthModule } from './health/health.module.js';
|
||||||
import { InboundModule } from './inbound/inbound.module.js';
|
import { InboundModule } from './inbound/inbound.module.js';
|
||||||
|
|
@ -24,6 +25,7 @@ import { SettingsModule } from './settings/settings.module.js';
|
||||||
SettingsModule, // GO/PAUSE/AWAY kill-switch + settings
|
SettingsModule, // GO/PAUSE/AWAY kill-switch + settings
|
||||||
RunnerModule, // the AFK decision pipeline (state -> Gate-2 -> mode)
|
RunnerModule, // the AFK decision pipeline (state -> Gate-2 -> mode)
|
||||||
AuditModule, // decision audit trail + activity/held/digest endpoints
|
AuditModule, // decision audit trail + activity/held/digest endpoints
|
||||||
|
CorrectionsModule, // teach-loop corrections (vetting signal + OSS tuning data)
|
||||||
InboundModule, // event-driven webhook: macsync inbound -> classify -> decide -> dispatch
|
InboundModule, // event-driven webhook: macsync inbound -> classify -> decide -> dispatch
|
||||||
SchedulerModule, // in-service AFK heartbeat / follow-up tick
|
SchedulerModule, // in-service AFK heartbeat / follow-up tick
|
||||||
],
|
],
|
||||||
|
|
|
||||||
27
src/corrections/corrections.controller.ts
Normal file
27
src/corrections/corrections.controller.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Body, Controller, DefaultValuePipe, Get, HttpCode, ParseIntPipe, Post, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { CreateCorrectionDto } from './dto/create-correction.dto.js';
|
||||||
|
import { CorrectionsService } from './corrections.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teach-loop corrections. The trial coworker (and operator) record disagreements
|
||||||
|
* with the backend's decisions/drafts here — the vetting signal and the OSS
|
||||||
|
* model-tuning substrate. (Inbound auth via the global guard.)
|
||||||
|
*/
|
||||||
|
@ApiTags('corrections')
|
||||||
|
@Controller('prospector/corrections')
|
||||||
|
export class CorrectionsController {
|
||||||
|
constructor(private readonly corrections: CorrectionsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(201)
|
||||||
|
async create(@Body() dto: CreateCorrectionDto) {
|
||||||
|
return this.corrections.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list(@Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit: number) {
|
||||||
|
return { items: await this.corrections.list(limit) };
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/corrections/corrections.module.ts
Normal file
14
src/corrections/corrections.module.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { ProspectCorrectionEntity } from '../entities/index.js';
|
||||||
|
import { CorrectionsController } from './corrections.controller.js';
|
||||||
|
import { CorrectionsService } from './corrections.service.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([ProspectCorrectionEntity])],
|
||||||
|
controllers: [CorrectionsController],
|
||||||
|
providers: [CorrectionsService],
|
||||||
|
exports: [CorrectionsService],
|
||||||
|
})
|
||||||
|
export class CorrectionsModule {}
|
||||||
54
src/corrections/corrections.service.ts
Normal file
54
src/corrections/corrections.service.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ProspectCorrectionEntity } from '../entities/index.js';
|
||||||
|
import type { CreateCorrectionDto } from './dto/create-correction.dto.js';
|
||||||
|
|
||||||
|
export interface CorrectionItem {
|
||||||
|
readonly id: string;
|
||||||
|
readonly handle: string;
|
||||||
|
readonly category: string;
|
||||||
|
readonly summary: string | null;
|
||||||
|
readonly originalBody: string | null;
|
||||||
|
readonly correctedBody: string | null;
|
||||||
|
readonly createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toItem = (r: ProspectCorrectionEntity): CorrectionItem => ({
|
||||||
|
id: r.id,
|
||||||
|
handle: r.handle,
|
||||||
|
category: r.category,
|
||||||
|
summary: r.summary,
|
||||||
|
originalBody: r.original_body,
|
||||||
|
correctedBody: r.corrected_body,
|
||||||
|
createdAt: r.created_at.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CorrectionsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ProspectCorrectionEntity) private readonly repo: Repository<ProspectCorrectionEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(dto: CreateCorrectionDto): Promise<CorrectionItem> {
|
||||||
|
const saved = await this.repo.save(
|
||||||
|
this.repo.create({
|
||||||
|
handle: dto.handle,
|
||||||
|
category: dto.category,
|
||||||
|
summary: dto.summary ?? null,
|
||||||
|
original_body: dto.originalBody ?? null,
|
||||||
|
corrected_body: dto.correctedBody ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return toItem(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(limit: number): Promise<readonly CorrectionItem[]> {
|
||||||
|
const rows = await this.repo.find({
|
||||||
|
order: { created_at: 'DESC' },
|
||||||
|
take: Math.min(Math.max(limit, 1), 500),
|
||||||
|
});
|
||||||
|
return rows.map(toItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/corrections/dto/create-correction.dto.ts
Normal file
30
src/corrections/dto/create-correction.dto.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export const CORRECTION_CATEGORIES = ['voice', 'tone', 'safety', 'logistics', 'classification', 'other'] as const;
|
||||||
|
export type CorrectionCategory = (typeof CORRECTION_CATEGORIES)[number];
|
||||||
|
|
||||||
|
/** Body for POST /prospector/corrections — a teach-loop correction of a decision/draft. */
|
||||||
|
export class CreateCorrectionDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(256)
|
||||||
|
handle!: string;
|
||||||
|
|
||||||
|
@IsIn(CORRECTION_CATEGORIES as readonly string[])
|
||||||
|
category!: CorrectionCategory;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000)
|
||||||
|
summary?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(8000)
|
||||||
|
originalBody?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(8000)
|
||||||
|
correctedBody?: string;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { ProspectCorrectionEntity } from './prospect-correction.entity.js';
|
||||||
import { ProspectDraftEntity } from './prospect-draft.entity.js';
|
import { ProspectDraftEntity } from './prospect-draft.entity.js';
|
||||||
import { ProspectorSettingsEntity } from './prospector-settings.entity.js';
|
import { ProspectorSettingsEntity } from './prospector-settings.entity.js';
|
||||||
|
|
||||||
export const entities = [ProspectorSettingsEntity, ProspectDraftEntity] as const;
|
export const entities = [ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity] as const;
|
||||||
|
|
||||||
export { ProspectorSettingsEntity, ProspectDraftEntity };
|
export { ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity };
|
||||||
export type { ProspectorMode } from './prospector-settings.entity.js';
|
export type { ProspectorMode } from './prospector-settings.entity.js';
|
||||||
export { PROSPECTOR_MODES } from './prospector-settings.entity.js';
|
export { PROSPECTOR_MODES } from './prospector-settings.entity.js';
|
||||||
|
|
|
||||||
35
src/entities/prospect-correction.entity.ts
Normal file
35
src/entities/prospect-correction.entity.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teach-loop record: an operator/coworker correction of a backend decision or
|
||||||
|
* draft. This is the vetting signal during the trial AND the training substrate
|
||||||
|
* for tuning the OSS models (classification / draft). One row per correction.
|
||||||
|
*/
|
||||||
|
@Entity({ name: 'prospect_corrections' })
|
||||||
|
@Index('idx_prospect_corrections_created', ['created_at'])
|
||||||
|
@Index('idx_prospect_corrections_category', ['category', 'created_at'])
|
||||||
|
export class ProspectCorrectionEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
handle!: string;
|
||||||
|
|
||||||
|
/** What kind of correction: voice | tone | safety | logistics | classification | other. */
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
category!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
summary!: string | null;
|
||||||
|
|
||||||
|
/** What the backend produced (the draft/decision being corrected). */
|
||||||
|
@Column({ name: 'original_body', type: 'text', nullable: true })
|
||||||
|
original_body!: string | null;
|
||||||
|
|
||||||
|
/** What it should have been. */
|
||||||
|
@Column({ name: 'corrected_body', type: 'text', nullable: true })
|
||||||
|
corrected_body!: string | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
created_at!: Date;
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"incremental": true,
|
"incremental": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue