diff --git a/server/ftchat/services/agent-engine.js b/server/ftchat/services/agent-engine.js new file mode 100644 index 0000000..2f47515 --- /dev/null +++ b/server/ftchat/services/agent-engine.js @@ -0,0 +1,140 @@ +/** + * Agent engine: context builder + toolchain orchestrator + * + * Builds the message context for vLLM inference: + * - No system prompt (model trained on real conversations) + * - Previous conversation memory (mother tongue imprint) + * - Tool results (Notion, repo) injected as user/assistant messages + * - Auto conversation rotation at threshold + */ +'use strict'; + +const vllm = require('./vllm-proxy'); +const sessionStore = require('./session-store'); +const memoryAgent = require('./memory-agent'); +const toolRegistry = require('./tool-registry'); + +const MAX_MESSAGES_BEFORE_ROTATE = 50; +const MAX_CONTEXT_TOKENS = 3072; + +/** + * Build the full message context for vLLM + * + * Context structure: + * [ + * {role:'user', content:'[Mother Tongue Imprint from last session]'}, + * {role:'assistant', content:'[Understood. Continuing from where we left off.]'}, + * ...previous messages, + * {role:'user', content:'current message'} + * ] + * + * NO system prompt - the model was trained on real conversations without system prompts. + */ +function buildContext(userHash, slotIndex, newMessage, conversationHistory, options = {}) { + const messages = []; + + // 1. Cross-session memory imprint (if exists) + const imprint = sessionStore.getLatestImprint(userHash); + if (imprint) { + messages.push({ + role: 'user', + content: '[Previous conversation memory]\n' + imprint + }); + messages.push({ + role: 'assistant', + content: '[I understand. I will continue based on our previous conversation.]' + }); + } + + // 2. Current conversation history + const history = conversationHistory || []; + for (const msg of history) { + if (msg.role === 'system') continue; // Never inject system messages + messages.push({ + role: msg.role, + content: msg.content || '' + }); + } + + // 3. Current user message + messages.push({ + role: 'user', + content: newMessage || '' + }); + + return messages; +} + +/** + * Process a chat message through the agent pipeline: + * 1. Build context (memory + history) + * 2. Send to vLLM + * 3. Check if model requested a tool call + * 4. If tool call: execute tool, inject result, re-request + * 5. Return final response + */ +async function processMessage(userHash, slotIndex, message, conversationHistory = []) { + const context = buildContext(userHash, slotIndex, message, conversationHistory); + + // Send to vLLM + let response = await vllm.chatOnce(context, { + maxTokens: 1024, + temperature: 0.7 + }); + + // Check for tool calls + const toolCall = toolRegistry.parseToolCall(response); + if (toolCall) { + context.push({ role: 'assistant', content: response }); + + try { + const toolResult = await toolRegistry.executeToolCall(toolCall); + context.push({ role: 'user', content: '[Tool Result]\n' + JSON.stringify(toolResult, null, 2) }); + + // Re-request with tool result + response = await vllm.chatOnce(context, { + maxTokens: 1024, + temperature: 0.7 + }); + } catch (err) { + context.push({ role: 'user', content: '[Tool Error]\n' + err.message }); + response = await vllm.chatOnce(context, { + maxTokens: 512, + temperature: 0.7 + }); + } + } + + return { + response, + shouldRotate: conversationHistory.length >= MAX_MESSAGES_BEFORE_ROTATE + }; +} + +/** + * Stream a chat message through the pipeline, writing SSE to res + */ +async function streamProcessMessage(userHash, slotIndex, message, conversationHistory, res) { + const context = buildContext(userHash, slotIndex, message, conversationHistory); + + // Stream from vLLM + vllm.pipeChat(context, res, { + maxTokens: 1024, + temperature: 0.7 + }); + + return { + shouldRotate: conversationHistory.length >= MAX_MESSAGES_BEFORE_ROTATE + }; +} + +function getMaxMessages() { + return MAX_MESSAGES_BEFORE_ROTATE; +} + +module.exports = { + buildContext, + processMessage, + streamProcessMessage, + getMaxMessages +};