| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- 'use strict';
- /**
- * 智能问数智能体 · 对话模型通道
- * ----------------------------------------------------------------------------
- * OpenAI 兼容 /v1/chat/completions 手写 HTTP(零依赖纪律,与 nl-query 同款)。
- * chatCompletion / extractJson 自 nl-query.js 抽出共用(nl-query 改为 require 本模块)。
- *
- * parseAgentAction 是 harness 的输入闸门:模型每回合只允许输出一个 JSON 对象,
- * 二选一 —— 调工具 {"thought","action":{tool,args}} 或收敛 {"thought","final":{answer,…}}。
- * 解析失败抛带可读 reason 的错误,供主循环回注模型自修复。
- */
- const https = require('https');
- const http = require('http');
- function chatCompletion({ apiBase, apiKey, model, messages, timeoutMs = 45000, temperature = 0 }) {
- return chatCompletionDetailed({ apiBase, apiKey, model, messages, timeoutMs, temperature }).then((out) => out.content);
- }
- /**
- * 同 chatCompletion,但返回 { content, usage }(usage 来自 OpenAI 兼容响应的
- * usage 字段:prompt_tokens / completion_tokens;网关不给时为 null)。
- * 智能体主循环用它记录每回合的 token 用量(编排时间线展示)。
- */
- function chatCompletionDetailed({ apiBase, apiKey, model, messages, timeoutMs = 45000, temperature = 0, jsonMode = false }) {
- return new Promise((resolve, reject) => {
- let base;
- try { base = new URL(apiBase); } catch (e) { return reject(new Error('对话模型地址不合法: ' + apiBase)); }
- const body = { model, messages, temperature, stream: false };
- // 结构化解码:网关支持 JSON mode 时动作输出更稳(配置开关 chatJsonMode,默认关)
- if (jsonMode) body.response_format = { type: 'json_object' };
- const payload = JSON.stringify(body);
- const options = {
- hostname: base.hostname,
- port: base.port || 443,
- path: (base.pathname.replace(/\/+$/, '') || '') + '/v1/chat/completions',
- method: 'POST',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- 'Content-Type': 'application/json; charset=utf-8',
- 'Content-Length': Buffer.byteLength(payload),
- },
- timeout: timeoutMs,
- };
- const req = (base.protocol === 'http:' ? http : https).request(options, (res) => {
- const chunks = [];
- res.on('data', (c) => chunks.push(c));
- res.on('end', () => {
- const raw = Buffer.concat(chunks).toString('utf8');
- let json = null;
- try { json = JSON.parse(raw); } catch { /* 保留 raw */ }
- if (res.statusCode !== 200) {
- const msg = (json && json.error && json.error.message) || raw.slice(0, 200) || `HTTP ${res.statusCode}`;
- return reject(new Error(`对话模型请求失败: ${msg}`));
- }
- const content = json && json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content;
- if (!content) return reject(new Error('对话模型返回为空'));
- const u = (json && json.usage) || null;
- resolve({
- content,
- usage: u ? {
- promptTokens: Number(u.prompt_tokens) || null,
- completionTokens: Number(u.completion_tokens) || null,
- } : null,
- });
- });
- });
- req.on('error', reject);
- req.on('timeout', () => req.destroy(new Error('对话模型请求超时')));
- req.write(payload);
- req.end();
- });
- }
- /** 从模型输出中稳健地提取第一个 JSON 对象(支持 ```json 围栏与前后杂讯) */
- function extractJson(text) {
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
- const candidate = fenced ? fenced[1] : text;
- const start = candidate.indexOf('{');
- const end = candidate.lastIndexOf('}');
- if (start === -1 || end <= start) throw new Error('模型未返回 JSON');
- return JSON.parse(candidate.slice(start, end + 1));
- }
- /**
- * 解析一回合模型输出为结构化动作。
- * 返回 { type:'tool', thought, tool, args } 或 { type:'final', thought, final }。
- * chart / applied 只收编形状,指标白名单等业务钳制在 guard / 工具层做。
- */
- function parseAgentAction(text) {
- let obj;
- try {
- obj = extractJson(text);
- } catch (e) {
- throw new Error('输出不是可解析的 JSON 对象:' + (e.message || e));
- }
- if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw new Error('输出不是 JSON 对象');
- const thought = String(obj.thought || '').slice(0, 500);
- if (obj.final && typeof obj.final === 'object' && !Array.isArray(obj.final)) {
- const answer = String(obj.final.answer || '').trim();
- if (!answer) throw new Error('final.answer 不能为空');
- const final = { answer: answer.slice(0, 4000) };
- if (obj.final.chart && typeof obj.final.chart === 'object') {
- final.chart = {
- type: obj.final.chart.type,
- x: obj.final.chart.x,
- y: Array.isArray(obj.final.chart.y) ? obj.final.chart.y : [],
- title: obj.final.chart.title,
- };
- }
- return { type: 'final', thought, final };
- }
- if (obj.action && typeof obj.action === 'object' && !Array.isArray(obj.action)) {
- const tool = String(obj.action.tool || '').trim();
- if (!tool) throw new Error('action.tool 不能为空');
- const rawArgs = obj.action.args;
- const args = (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) ? rawArgs : {};
- return { type: 'tool', thought, tool: tool.slice(0, 64), args };
- }
- throw new Error('输出缺少 action 或 final:每回合必须二选一');
- }
- module.exports = { chatCompletion, chatCompletionDetailed, extractJson, parseAgentAction };
|