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}`);
|
||||
}
|
||||
|
||||
submitCorrection(body: {
|
||||
handle: string;
|
||||
category: string;
|
||||
summary?: string;
|
||||
originalBody?: string;
|
||||
correctedBody?: string;
|
||||
}): Promise<unknown> {
|
||||
return this.post('/prospector/corrections', body);
|
||||
}
|
||||
|
||||
getSettings(): Promise<unknown> {
|
||||
return this.get('/prospector/settings');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,24 @@ server.registerTool(
|
|||
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(
|
||||
'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 { AuditModule } from './audit/audit.module.js';
|
||||
import { ClientsModule } from './clients/clients.module.js';
|
||||
import { CorrectionsModule } from './corrections/corrections.module.js';
|
||||
import { databaseConfig } from './config/database.config.js';
|
||||
import { HealthModule } from './health/health.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
|
||||
RunnerModule, // the AFK decision pipeline (state -> Gate-2 -> mode)
|
||||
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
|
||||
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 { 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 { 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,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"incremental": true,
|
||||
"incremental": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue