| 10 |
10 |
import Anthropic from "@anthropic-ai/sdk"; |
| 11 |
11 |
import fs from "fs"; |
| 12 |
12 |
import path from "path"; |
| 13 |
|
−import { buildModelSchema, PROMPT_VERSION, schemaForApi, validateAndRepair, type ModelOutput, type ValidationIssue } from "./schema"; |
|
13 |
+import { buildModelSchema, MAX_MISSING_SECTIONS, missingTopLevelSections, PROMPT_VERSION, schemaForApi, validateAndRepair, type ModelOutput, type ValidationIssue } from "./schema"; |
| 14 |
14 |
import type { ImageMediaType } from "./images"; |
| 15 |
15 |
|
| 16 |
16 |
export const DEFAULT_MODEL = "claude-sonnet-5"; |
| 78 |
78 |
let attempts = 0; |
| 79 |
79 |
let lastIssues: ValidationIssue[] = []; |
| 80 |
80 |
let output: ModelOutput | null = null; |
| 81 |
|
− while (attempts < 2 && !output) { |
|
81 |
+ // tool_choice : `auto` au 1er essai — un outil FORCÉ (`type:"tool"`) sans réflexion fait parfois |
|
82 |
+ // répondre Sonnet 5 par un placeholder ({"property_analysis":{"placeholder":true}}, 44 jetons) sur ce |
|
83 |
+ // schéma de ~150 champs (constaté 2026-09-08 sur une annonce Facebook Marketplace ; la même requête en |
|
84 |
+ // `auto` produit 21 Ko d'analyse). `any` en relance : le modèle doit appeler un outil, avec le retour d'erreur. |
|
85 |
+ const MAX_ATTEMPTS = 3; |
|
86 |
+ while (attempts < MAX_ATTEMPTS && !output) { |
| 82 |
87 |
attempts++; |
| 83 |
88 |
const stream = this.client.messages.stream({ |
| 84 |
|
− model: this.model, max_tokens: 24000, system, messages, tools, tool_choice: { type: "tool", name: TOOL_NAME }, thinking: { type: "disabled" }, |
|
89 |
+ model: this.model, max_tokens: 24000, system, messages, tools, tool_choice: attempts === 1 ? { type: "auto" } : { type: "any" }, thinking: { type: "disabled" }, |
| 85 |
90 |
}); |
| 86 |
91 |
const msg = await stream.finalMessage(); |
| 87 |
92 |
inTok += msg.usage.input_tokens; outTok += msg.usage.output_tokens; |
| 93 |
98 |
} else { |
| 94 |
99 |
const { value, issues } = validateAndRepair(tu.input, schema); |
| 95 |
100 |
const hard = issues.filter((i) => !i.repaired); |
| 96 |
|
− lastIssues = issues; |
| 97 |
|
− if (!hard.length) { output = value as ModelOutput; break; } |
|
101 |
+ const missing = missingTopLevelSections(issues); |
|
102 |
+ lastIssues = missing.length > MAX_MISSING_SECTIONS |
|
103 |
+ ? [{ path: "$", message: `sortie vide ou placeholder — ${missing.length} sections absentes (${missing.slice(0, 6).join(", ")}…)`, repaired: false }, ...issues] |
|
104 |
+ : issues; |
|
105 |
+ if (!hard.length && missing.length <= MAX_MISSING_SECTIONS) { output = value as ModelOutput; break; } |
| 98 |
106 |
} |
| 99 |
|
− // relance unique avec les erreurs de validation |
|
107 |
+ if (attempts >= MAX_ATTEMPTS) break; |
|
108 |
+ // relance avec les erreurs de validation (la sortie fautive reste dans l'historique du tour) |
| 100 |
109 |
messages.push({ role: "assistant", content: msg.content.map((b) => (b.type === "tool_use" ? { type: "tool_use" as const, id: b.id, name: b.name, input: b.input } : b.type === "text" ? { type: "text" as const, text: b.text } : { type: "text" as const, text: "" })).filter((b) => b.type !== "text" || b.text) }); |
| 101 |
110 |
const toolId = msg.content.find((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")?.id; |
| 102 |
|
− messages.push({ role: "user", content: toolId ? [{ type: "tool_result", tool_use_id: toolId, is_error: true, content: `Schema validation failed:\n${lastIssues.slice(0, 40).map((i) => `${i.path}: ${i.message}`).join("\n")}\nCall ${TOOL_NAME} again with a fully valid input.` }] : `Schema validation failed. Call ${TOOL_NAME} again with a fully valid input.` }); |
|
111 |
+ const feedback = `Schema validation failed:\n${lastIssues.slice(0, 40).map((i) => `${i.path}: ${i.message}`).join("\n")}\nThis was not an analysis. Look at every photo and the listing context, then call ${TOOL_NAME} again with the COMPLETE input: all top-level sections (property, geometry, construction, exterior, interior, kitchens, bathrooms, mechanical, electrical, plumbing, basement, garage, exterior_improvements, quality, condition, estimated_effective_age, renovations, estimated_quantities, assemblies, uncertainties, privacy) filled, null only where evidence is missing. Never return a placeholder.`; |
|
112 |
+ messages.push({ role: "user", content: toolId ? [{ type: "tool_result", tool_use_id: toolId, is_error: true, content: feedback }] : feedback }); |
| 103 |
113 |
} |
| 104 |
114 |
if (!output) throw new Error(`Sortie du modèle invalide après ${attempts} tentative(s) : ${lastIssues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join(" ; ")}`); |
| 105 |
115 |
return { |