feat(confidence-engine): constrain reconstruction chat output
This commit is contained in:
@@ -82,7 +82,9 @@
|
|||||||
|
|
||||||
- Zod upgraded from 3.25.76 to 4.5.4. Reconstruction and direct Zod-owner suites pass, and native `z.toJSONSchema()` converts the production reconstruction schema with required closed enum constraints.
|
- Zod upgraded from 3.25.76 to 4.5.4. Reconstruction and direct Zod-owner suites pass, and native `z.toJSONSchema()` converts the production reconstruction schema with required closed enum constraints.
|
||||||
- Zod-3-specific error-message assertions are version-independent while retaining semantic validation evidence. Production schema semantics are unchanged and no third-party JSON-schema conversion dependency was added.
|
- Zod-3-specific error-message assertions are version-independent while retaining semantic validation evidence. Production schema semantics are unchanged and no third-party JSON-schema conversion dependency was added.
|
||||||
- Next unknown: whether Ollama/Qwen accepts and obeys the generated schema through `/api/chat` format.
|
- Reconstruction `/api/chat` now sends `z.toJSONSchema(reconstructionV2Schema)` rather than generic `format: "json"`; the same canonical Zod contract constrains generation and remains the post-hoc validator.
|
||||||
|
- The deterministic provider-boundary test passes 2/2 under Node. A direct configured Ollama/Qwen `/api/chat` call accepted and obeyed the full schema in one call; `/api/generate`, chat detection, fallback, prompt, temperature, retries, and schema semantics are unchanged.
|
||||||
|
- Production repeatability remains untested after this change. Next restart point: a small repeated `/api/cases/start` stability observation using the fixed manufacturing scenario.
|
||||||
|
|
||||||
## Current product architecture
|
## Current product architecture
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -1,3 +1,6 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { reconstructionV2Schema } from "../reconstruction/schema.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider abstraction — the app calls getProvider() which returns an object
|
* Provider abstraction — the app calls getProvider() which returns an object
|
||||||
* with a generateReconstruction(scenario, modelName) method.
|
* with a generateReconstruction(scenario, modelName) method.
|
||||||
@@ -59,6 +62,7 @@ function recoverJson(raw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _chatSupported = null;
|
let _chatSupported = null;
|
||||||
|
const reconstructionJsonSchema = z.toJSONSchema(reconstructionV2Schema);
|
||||||
|
|
||||||
async function detectChatSupport(baseUrl, modelName) {
|
async function detectChatSupport(baseUrl, modelName) {
|
||||||
if (_chatSupported !== null) return _chatSupported;
|
if (_chatSupported !== null) return _chatSupported;
|
||||||
@@ -113,7 +117,7 @@ class OllamaLlmProvider {
|
|||||||
} catch { /* failed silently — defaults to false */ }
|
} catch { /* failed silently — defaults to false */ }
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Step 2: Try /api/chat if supported and format:json works
|
// Step 2: Try /api/chat if supported with the reconstruction schema
|
||||||
// ================================================================
|
// ================================================================
|
||||||
if (chatSupported) {
|
if (chatSupported) {
|
||||||
try {
|
try {
|
||||||
@@ -128,7 +132,7 @@ class OllamaLlmProvider {
|
|||||||
model: modelName,
|
model: modelName,
|
||||||
messages: [{ role: "user", content: prompt }],
|
messages: [{ role: "user", content: prompt }],
|
||||||
stream: false,
|
stream: false,
|
||||||
format: "json",
|
format: reconstructionJsonSchema,
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ describe("OllamaLlmProvider chat capability detection", () => {
|
|||||||
expect(fetchSpy.mock.calls[0][0]).toBe("http://ollama.test/api/chat");
|
expect(fetchSpy.mock.calls[0][0]).toBe("http://ollama.test/api/chat");
|
||||||
expect(fetchSpy.mock.calls[1][0]).toBe("http://ollama.test/api/chat");
|
expect(fetchSpy.mock.calls[1][0]).toBe("http://ollama.test/api/chat");
|
||||||
expect(fetchSpy.mock.calls[1][0]).not.toContain("/api/generate");
|
expect(fetchSpy.mock.calls[1][0]).not.toContain("/api/generate");
|
||||||
|
const chatRequest = JSON.parse(fetchSpy.mock.calls[1][1].body);
|
||||||
|
expect(chatRequest).toMatchObject({
|
||||||
|
model: "configured-model",
|
||||||
|
messages: [{ role: "user", content: "prompt" }],
|
||||||
|
stream: false,
|
||||||
|
});
|
||||||
|
expect(chatRequest.format).toBeTypeOf("object");
|
||||||
|
expect(chatRequest.format).not.toBe("json");
|
||||||
|
const formatText = JSON.stringify(chatRequest.format);
|
||||||
|
expect(formatText).toContain("relationship");
|
||||||
|
expect(formatText).toContain("evidenceType");
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
response: {},
|
response: {},
|
||||||
providerApiPath: "/api/chat",
|
providerApiPath: "/api/chat",
|
||||||
|
|||||||
Reference in New Issue
Block a user