Une seule feature livree en une seule PR, en 5 phases : - Phase 1 : enrichit le corpus NLP (tech-step-training-data.ts) et ajoute un harness d'evaluation (precision/rappel/F1) avec un jeu de test etiquete - la premiere metrique objective de qualite pour ce classifieur. - Phase 2 : schema Prisma (StepTechStepCorrection, TechStepTrainingSuggestion) + endpoints utilisateur (POST/GET corrections, ouverts a tout viewer, pas seulement l'auteur) + endpoints internes /internal/tech-steps/* proteges par secret partage (requireInternalWorker). - Phase 3 : UI de highlight/correction cote web (selection de texte -> association a une technique, ou clic sur un highlight existant pour le corriger/supprimer) - verifiee via Cypress (component + e2e, en Chrome reel). - Phase 4 : worker LLM autonome (services/tech-step-llm-worker, hors du monorepo pnpm comme experiments/llm-tech-step-poc) qui audite les clauses a faible confiance et transforme les corrections utilisateur en suggestions d'entrainement, sans jamais toucher le chemin interactif. - Phase 5 : script retrain-tech-steps.ts (gate de regression F1 + backfill) et list-pending-training-suggestions.ts pour la revue humaine avant application au corpus. Verification effectuee cette session : tsc/biome sur l'ensemble du repo, build complet (pnpm build), suite Cypress complete (component 39/39, e2e 75/76 - le seul echec est preexistant et sans rapport, cote recipe-form.feature/ingredient-picker), tests unitaires du worker (6/6) et son install/typecheck reels contre node-llama-cpp. Les tests Mocha d'apps/api (Phases 1 et 2) n'ont pas pu etre executes dans cette session (pas de Postgres local disponible) - a lancer avant merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
4.4 KiB
TypeScript
106 lines
4.4 KiB
TypeScript
import { env } from "./config.js";
|
|
|
|
/**
|
|
* Thin fetch wrapper around `apps/api`'s `/internal/tech-steps/*` and
|
|
* `/reference/tech-steps` — the only two surfaces this worker ever talks
|
|
* to (see `tech-step-worker.service.ts`/`tech-step-worker.routes.ts` on
|
|
* that side). No Prisma client, no direct database access at all: every
|
|
* read/write goes through here, over HTTP, authenticated with
|
|
* `INTERNAL_WORKER_SECRET` — see `requireInternalWorker`
|
|
* (`apps/api/src/middlewares/require-internal-worker.ts`) for why that's a
|
|
* separate mechanism from a user session.
|
|
*/
|
|
|
|
/** One reference technique, as returned by the public `GET /reference/tech-steps` — this worker's only source of the taxonomy it audits/labels against, never a hardcoded copy (see `tech-step-taxonomy.ts`). */
|
|
export interface TechStepReference {
|
|
id: number;
|
|
key: string;
|
|
}
|
|
|
|
/** Mirrors `TechStepAuditClauseView` (`packages/shared`) — duplicated here rather than importing from `@batch-cooking/shared`, since this worker deliberately lives outside the pnpm workspace (see `package.json`'s own doc comment) and so can't depend on a workspace package. */
|
|
export interface AuditClause {
|
|
stepId: number;
|
|
recipeId: number;
|
|
clauseText: string;
|
|
anchorKey: string | null;
|
|
intentKey: string | null;
|
|
score: number;
|
|
locale: string;
|
|
}
|
|
|
|
/** Mirrors `PendingTechStepCorrectionView` (`packages/shared`) — same "duplicated, not imported" reasoning as {@link AuditClause}. */
|
|
export interface PendingCorrection {
|
|
id: number;
|
|
stepId: number;
|
|
recipeId: number;
|
|
clauseText: string;
|
|
start: number;
|
|
end: number;
|
|
previousTechStepKey: string | null;
|
|
correctedTechStepKey: string | null;
|
|
}
|
|
|
|
/** One suggestion `postTrainingSuggestions` submits — mirrors one entry of `SubmitTrainingSuggestionsInput["suggestions"]` (`packages/shared`). */
|
|
export interface TrainingSuggestionInput {
|
|
techStepKey: string;
|
|
locale: string;
|
|
suggestedSynonyms: string[];
|
|
suggestedUtterances: string[];
|
|
sourceType: "correction" | "llm_audit";
|
|
sourceCorrectionId?: number | null;
|
|
}
|
|
|
|
async function request<TResponseBody>(
|
|
path: string,
|
|
init: RequestInit = {},
|
|
): Promise<TResponseBody> {
|
|
try {
|
|
const response = await fetch(`${env.API_BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Internal-Worker-Secret": env.INTERNAL_WORKER_SECRET,
|
|
...init.headers,
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.text().catch(() => "");
|
|
throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status} ${body}`);
|
|
}
|
|
return (await response.json()) as TResponseBody;
|
|
} catch (err) {
|
|
// Rethrown as-is — every caller (the scheduler's per-job try/catch,
|
|
// see `scheduler.ts`) already decides what to do with a failed run;
|
|
// this is just the one place the `await` itself has to sit inside a
|
|
// try/catch, same convention `apps/api` follows for the same reason.
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/** The full reference technique catalog — `apps/api`'s `TechStep` table, read fresh (never cached beyond one process's lifetime) so a catalog change is picked up on the worker's next restart without a code change here. */
|
|
export function getTechStepReference(): Promise<TechStepReference[]> {
|
|
return request("/reference/tech-steps");
|
|
}
|
|
|
|
/** Low-confidence clauses sampled from existing recipes — `audit-low-confidence`'s raw material. */
|
|
export function getAuditBatch(locale: string, limit: number): Promise<AuditClause[]> {
|
|
const params = new URLSearchParams({ locale, limit: String(limit) });
|
|
return request(`/internal/tech-steps/audit-batch?${params}`);
|
|
}
|
|
|
|
/** Corrections not yet turned into a suggestion — `transform-corrections`'s raw material. */
|
|
export function getPendingCorrections(limit: number): Promise<PendingCorrection[]> {
|
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
return request(`/internal/tech-steps/pending-corrections?${params}`);
|
|
}
|
|
|
|
/** Submits a batch of suggestions — a no-op (resolves immediately) if `suggestions` is empty, so a job with nothing to report doesn't need its own guard at every call site. */
|
|
export function postTrainingSuggestions(
|
|
suggestions: TrainingSuggestionInput[],
|
|
): Promise<{ created: number }> {
|
|
if (suggestions.length === 0) return Promise.resolve({ created: 0 });
|
|
return request("/internal/tech-steps/training-suggestions", {
|
|
method: "POST",
|
|
body: JSON.stringify({ suggestions }),
|
|
});
|
|
}
|