// @ts-nocheck import { ollamaUrl, loadOllamaConfig, loadThinkingEnabled } from '../utils/config.js'; import { reportTokens, reportStreamDelta, reportStreamEnd } from '../utils/metrics-client.js'; import { AGENT_IDENTITY_CORE } from '../core/agent-identity-core.js'; import { DOMAIN_RULES } from '../core/domain-rules.js'; import { scanToolCallBuffer, finalizeToolCallBuffer, cleanToolCallArtifacts } from '../utils/tool-call-parse.js'; import { createThinkSplitter, stripThinkTags } from '../core/think-parse.js'; import { friendlyLocalFetchError } from '../utils/model-manager.js'; let _warmedUp = false; export async function warmup(model) { if (_warmedUp) return true; try { const r = await fetch(ollamaUrl(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model || loadOllamaConfig().model, messages: [ { role: 'system', content: IDENTITY }, { role: 'user', content: 'ping' }, ], stream: false, options: { num_predict: 1, temperature: 0 }, keep_alive: '30m', }), }); if (r.ok) { _warmedUp = true; return true; } return false; } catch (e) { return false; } } const IDENTITY = ` ${AGENT_IDENTITY_CORE} 你是"ccSparkle Agent",地铁乘务专业的智能助手,基于本地部署的 Qwen2.5 模型,通过 MCP 协议操作本地地铁运营图 Electron 应用。 【回答风格】 - **只用简体中文**回复用户,禁止整句英文(见上方【语言】规则)。 - 用自然口语化的中文,有温度,不要机械复述身份描述。 - 不要说自己是其他厂商的模型(如 OpenAI / ChatGPT 等)。 【工具调用规则】 - **仅当本轮提供了 tools、且用户有明确操作指令时**才可调用工具。 - 自我介绍、向评委讲解、寒暄:**禁止**输出 \`\`,直接写中文正文。 - 有工具可调时格式: \`{"name":"metro_xxx","arguments":{...}}\` (不要用 ICF 等其他格式) - metro_load_timetable_full 只传 query(如"工作日"),不要臆造 dayType 等字段 - 工具失败时如实告知错误。 - 多候选时列给用户选,不自己代决定。 ${DOMAIN_RULES} `; export async function chat({ messages, tools, model, signal, onTextDelta, onThinkingDelta, maxTokens: maxTokensOpt, temperature }) { const cfg = loadOllamaConfig(); const useModel = model || cfg.model; const thinkOn = loadThinkingEnabled(); const leadIdx = messages.findIndex(m => m.role !== 'system'); const leadEnd = leadIdx === -1 ? messages.length : leadIdx; const systemExtra = messages.slice(0, leadEnd) .map(m => m.content).filter(Boolean).join('\n\n'); const ollamaMessages = [{ role: 'system', content: systemExtra ? `${IDENTITY}\n\n${systemExtra}` : IDENTITY, }]; for (const m of messages.slice(leadEnd)) { if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) { ollamaMessages.push({ role: 'assistant', content: m.content || '', tool_calls: m.toolCalls.map(tc => ({ function: { name: tc.name, arguments: tc.arguments || {} }, })), }); } else if (m.role === 'tool') { ollamaMessages.push({ role: 'tool', content: m.content }); } else { ollamaMessages.push({ role: m.role, content: m.content || '' }); } } const payload = { model: useModel, messages: ollamaMessages, stream: true, think: thinkOn, }; if (maxTokensOpt != null) { payload.options = { ...(payload.options || {}), num_predict: maxTokensOpt }; } if (temperature != null) { payload.options = { ...(payload.options || {}), temperature }; } if (tools && tools.length > 0) { payload.tools = tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description || '', parameters: t.inputSchema || { type: 'object', properties: {} }, }, })); } let r; try { r = await fetch(ollamaUrl(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal, }); } catch (e) { throw friendlyLocalFetchError('ollama', e); } if (!r.ok) { const text = await r.text(); throw new Error(`Ollama HTTP ${r.status}: ${text.slice(0, 200)}`); } return consumeStream(r.body, { onTextDelta, onThinkingDelta, thinkOn }); } async function consumeStream(body, { onTextDelta, onThinkingDelta, thinkOn }) { const reader = body.getReader(); const decoder = new TextDecoder(); let lineBuf = ''; let textBuf = ''; let fullText = ''; let fullThinking = ''; let toolCalls = []; let evalCount = 0; const t0 = performance.now(); const thinkSplit = createThinkSplitter(thinkOn); const emitContent = (raw) => { if (!raw) return; const split = thinkSplit.push(raw); if (split.thinking) { fullThinking += split.thinking; if (thinkOn) onThinkingDelta?.(split.thinking); } if (split.content) { textBuf += split.content; fullText += split.content; const result = scanToolCallBuffer(textBuf); if (result.safeText && onTextDelta) onTextDelta(result.safeText); textBuf = result.remainder; if (result.toolCalls.length > 0) toolCalls.push(...result.toolCalls); reportStreamDelta(split.content.length); } }; while (true) { const { done, value } = await reader.read(); if (done) break; lineBuf += decoder.decode(value, { stream: true }); const lines = lineBuf.split('\n'); lineBuf = lines.pop(); for (const line of lines) { if (!line.trim()) continue; let chunk; try { chunk = JSON.parse(line); } catch (e) { continue; } const thinkingPiece = chunk.message?.thinking || ''; if (thinkingPiece) { fullThinking += thinkingPiece; if (thinkOn) onThinkingDelta?.(thinkingPiece); reportStreamDelta(thinkingPiece.length); } const content = chunk.message?.content || ''; if (content) emitContent(content); if (chunk.message?.tool_calls) { for (const tc of chunk.message.tool_calls) { toolCalls.push({ name: tc.function?.name || tc.name, arguments: typeof tc.function?.arguments === 'string' ? safeParse(tc.function.arguments) : (tc.function?.arguments || {}), }); } } if (chunk.done) { evalCount = chunk.eval_count || evalCount; } } } if (lineBuf.trim()) { try { const chunk = JSON.parse(lineBuf); if (chunk.message?.thinking) { fullThinking += chunk.message.thinking; if (thinkOn) onThinkingDelta?.(chunk.message.thinking); } if (chunk.message?.content) emitContent(chunk.message.content); if (chunk.done) evalCount = chunk.eval_count || evalCount; } catch (e) {} } const tail = thinkSplit.flush(); if (tail.thinking) { fullThinking += tail.thinking; if (thinkOn) onThinkingDelta?.(tail.thinking); } if (tail.content) { textBuf += tail.content; fullText += tail.content; } const final = finalizeToolCallBuffer(textBuf); if (final.safeText && onTextDelta) onTextDelta(final.safeText); if (final.toolCalls.length > 0) toolCalls.push(...final.toolCalls); const cleanedText = stripThinkTags(cleanToolCallArtifacts(fullText)); if (evalCount > 0) { const durationMs = performance.now() - t0; reportTokens(evalCount, durationMs); reportStreamEnd(evalCount); } return { text: cleanedText, thinking: fullThinking, toolCalls, evalCount }; } function safeParse(s) { try { return JSON.parse(s); } catch (e) { return { _raw: s }; } }