kbot cpp structured
This commit is contained in:
parent
a462282339
commit
c90063d8a1
@ -23,8 +23,9 @@
|
||||
* Stress (batch repeats, one worker): KBOT_CLASSIFIER_STRESS_RUNS=N (default 1)
|
||||
* npm run test:ipc:classifier:openrouter:stress → OpenRouter + 5 runs (override N via env)
|
||||
*
|
||||
* Reports (reports.js): cwd/tests/test-ipc-classifier__HH-mm.{json,md}; array-only distances in
|
||||
* Reports (reports.js): cwd/tests/test-ipc-classifier__HH-mm.{json,md}; distances in
|
||||
* test-ipc-classifier-distances__HH-mm.json (same timestamp as the main JSON).
|
||||
* With -F structured, the prompt asks for {"items":[...]} to match json_object APIs.
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
@ -214,12 +215,15 @@ export const JOB_VIEWER_MACHINE_LABELS = [
|
||||
|
||||
const ANCHOR = 'machine workshop';
|
||||
|
||||
/** Build one prompt: model returns a JSON array only. */
|
||||
/** Keys we accept for the batch array when API forces a JSON object (e.g. response_format json_object). */
|
||||
const BATCH_ARRAY_OBJECT_KEYS = ['items', 'distances', 'results', 'data', 'labels', 'rows'];
|
||||
|
||||
/** Build one prompt: plain mode = JSON array root; structured (-F structured) = JSON object with "items" (json_object API). */
|
||||
function classifierBatchPrompt(labels) {
|
||||
const numbered = labels.map((l, i) => `${i + 1}. ${JSON.stringify(l)}`).join('\n');
|
||||
return `You classify business types against one anchor. Output ONLY a JSON array, no markdown fences, no commentary.
|
||||
const structured = classifierFeatures.has('structured');
|
||||
|
||||
Rules for each element:
|
||||
const rules = `Rules for each element:
|
||||
- Use shape: {"label": <exact string from the list below>, "distance": <number>}
|
||||
- "distance" is semantic distance from 0 (same as anchor or direct synonym) to 10 (unrelated). One decimal allowed.
|
||||
- Include EXACTLY one object per line item below, in the SAME ORDER, with "label" copied character-for-character from the list.
|
||||
@ -227,29 +231,86 @@ Rules for each element:
|
||||
Anchor business type: ${ANCHOR}
|
||||
|
||||
Candidate labels (in order):
|
||||
${numbered}
|
||||
${numbered}`;
|
||||
|
||||
if (structured) {
|
||||
return `You classify business types against one anchor. Output ONLY valid JSON: one object, no markdown fences, no commentary.
|
||||
The API requires a JSON object (not a top-level array). Use exactly one top-level key "items" whose value is the array.
|
||||
|
||||
${rules}
|
||||
|
||||
Example: {"items":[{"label":"Example","distance":2.5},...]}`;
|
||||
}
|
||||
|
||||
return `You classify business types against one anchor. Output ONLY a JSON array, no markdown fences, no commentary.
|
||||
|
||||
${rules}
|
||||
|
||||
Output: one JSON array, e.g. [{"label":"...","distance":2.5},...]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse model text into the batch array: root [...] or {"items":[...]} (json_object).
|
||||
* @returns {unknown[] | null}
|
||||
*/
|
||||
function extractJsonArray(text) {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
let s = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/u, '').trim();
|
||||
|
||||
/** @param {unknown} v */
|
||||
const arrayFromParsed = (v) => {
|
||||
if (Array.isArray(v)) return v;
|
||||
if (!v || typeof v !== 'object') return null;
|
||||
const o = /** @type {Record<string, unknown>} */ (v);
|
||||
for (const key of BATCH_ARRAY_OBJECT_KEYS) {
|
||||
const a = o[key];
|
||||
if (Array.isArray(a)) return a;
|
||||
}
|
||||
for (const val of Object.values(o)) {
|
||||
if (
|
||||
Array.isArray(val) &&
|
||||
val.length > 0 &&
|
||||
val[0] &&
|
||||
typeof val[0] === 'object' &&
|
||||
val[0] !== null &&
|
||||
'label' in val[0]
|
||||
) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
try {
|
||||
const v = JSON.parse(s);
|
||||
return Array.isArray(v) ? v : null;
|
||||
return arrayFromParsed(v);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
|
||||
const i = s.indexOf('[');
|
||||
const j = s.lastIndexOf(']');
|
||||
if (i < 0 || j <= i) return null;
|
||||
try {
|
||||
const v = JSON.parse(s.slice(i, j + 1));
|
||||
return Array.isArray(v) ? v : null;
|
||||
} catch {
|
||||
return null;
|
||||
if (i >= 0 && j > i) {
|
||||
try {
|
||||
const v = JSON.parse(s.slice(i, j + 1));
|
||||
if (Array.isArray(v)) return v;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
const oi = s.indexOf('{');
|
||||
const oj = s.lastIndexOf('}');
|
||||
if (oi >= 0 && oj > oi) {
|
||||
try {
|
||||
const v = JSON.parse(s.slice(oi, oj + 1));
|
||||
return arrayFromParsed(v);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,9 +342,9 @@ function normalizeBatchArray(arr, expectedLabels) {
|
||||
|
||||
function batchTimeoutMs() {
|
||||
const raw = process.env.KBOT_CLASSIFIER_TIMEOUT_MS;
|
||||
if (raw === undefined || raw === '') return 300_000;
|
||||
if (raw === undefined || raw === '') return 30_000;
|
||||
const n = Number.parseInt(raw, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 300_000;
|
||||
return Number.isFinite(n) && n > 0 ? n : 30_000;
|
||||
}
|
||||
|
||||
/** Sequential batch iterations on one worker (stress). Default 1 = single run. */
|
||||
@ -326,7 +387,7 @@ function summarizeMs(values) {
|
||||
|
||||
/** Log progress while awaiting a long LLM call (no silent hang). */
|
||||
function withHeartbeat(promise, ipcTimeoutMs, backendLabel) {
|
||||
const every = 15_000;
|
||||
const every = 10_000;
|
||||
let n = 0;
|
||||
const id = setInterval(() => {
|
||||
n += 1;
|
||||
@ -385,8 +446,8 @@ function processBatchResponse(p, labels) {
|
||||
parseError = `missing: ${missing.join('; ')}`;
|
||||
}
|
||||
} else {
|
||||
assert(false, 'batch response parses as JSON array');
|
||||
parseError = 'could not parse JSON array from model text';
|
||||
assert(false, 'batch response parses as JSON array or {"items":[...]}');
|
||||
parseError = 'could not parse batch array from model text';
|
||||
}
|
||||
} else {
|
||||
assert(false, 'kbot-ai success');
|
||||
@ -424,7 +485,9 @@ async function run() {
|
||||
` ⚠️ -F structured: ignored for local llama (use --backend remote for response_format json_object)\n`
|
||||
);
|
||||
} else {
|
||||
console.log(` Structured: kbot-ai sends response_format {"type":"json_object"}\n`);
|
||||
console.log(
|
||||
` Structured: response_format json_object + prompt asks for {"items":[...]} (not a top-level array)\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +107,7 @@ LLMResponse LLMClient::execute_chat(const std::string& prompt) {
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
response_format);
|
||||
logger::info("LLMClient: ChatCompletion returned (HTTP " + std::to_string(response.status_code) + ")");
|
||||
logger::debug("LLMClient::execute_chat: Got response with status: " + std::to_string(response.status_code));
|
||||
|
||||
// liboai may not populate raw_json for custom base URLs — parse content directly.
|
||||
|
||||
@ -1325,16 +1325,35 @@ void liboai::netimpl::Session::SetOption(const components::Timeout& timeout) {
|
||||
}
|
||||
|
||||
void liboai::netimpl::Session::SetTimeout(const components::Timeout& timeout) {
|
||||
CURLcode e = curl_easy_setopt(this->curl_, CURLOPT_TIMEOUT_MS, timeout.Milliseconds());
|
||||
const long ms = timeout.Milliseconds();
|
||||
CURLcode e = curl_easy_setopt(this->curl_, CURLOPT_TIMEOUT_MS, ms);
|
||||
ErrorCheck(e, "liboai::netimpl::Session::SetTimeout()");
|
||||
|
||||
/* Connection phase (DNS/TCP/TLS) is governed separately; without this, some stacks
|
||||
can sit in connect/handshake longer than expected while the transfer timer is idle. */
|
||||
if (ms > 0) {
|
||||
/* Avoid std::min — Windows headers may define min() as a macro. */
|
||||
const long connect_ms = (ms < 120000L) ? ms : 120000L;
|
||||
e = curl_easy_setopt(this->curl_, CURLOPT_CONNECTTIMEOUT_MS, connect_ms);
|
||||
ErrorCheck(e, "liboai::netimpl::Session::SetTimeout() (connect)");
|
||||
|
||||
#if defined(LIBOAI_DEBUG)
|
||||
_liboai_dbg(
|
||||
"[dbg] [@%s] Set connect timeout to %ld milliseconds\n",
|
||||
__func__, connect_ms
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
e = curl_easy_setopt(this->curl_, CURLOPT_NOSIGNAL, 1L);
|
||||
ErrorCheck(e, "liboai::netimpl::Session::SetTimeout() (nosignal)");
|
||||
|
||||
#if defined(LIBOAI_DEBUG)
|
||||
_liboai_dbg(
|
||||
"[dbg] [@%s] Set timeout to %ld milliseconds\n",
|
||||
__func__, timeout.Milliseconds()
|
||||
__func__, ms
|
||||
);
|
||||
#endif
|
||||
|
||||
ErrorCheck(e, "liboai::netimpl::Session::SetTimeout()");
|
||||
}
|
||||
|
||||
void liboai::netimpl::Session::SetOption(const components::Proxies& proxies) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user