feat(broker): add conversation memory to telegram AI (10-turn window)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,33 +178,99 @@ function getClient(): Anthropic {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Conversation history (per chat, rolling window)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const MAX_HISTORY = 10;
|
||||||
|
const HISTORY_TTL_MS = 30 * 60 * 1000; // 30 min
|
||||||
|
|
||||||
|
interface HistoryEntry {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatHistory = new Map<number, HistoryEntry[]>();
|
||||||
|
|
||||||
|
// Clean stale histories every 10 min
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [chatId, entries] of chatHistory) {
|
||||||
|
const fresh = entries.filter(e => now - e.ts < HISTORY_TTL_MS);
|
||||||
|
if (fresh.length === 0) chatHistory.delete(chatId);
|
||||||
|
else chatHistory.set(chatId, fresh);
|
||||||
|
}
|
||||||
|
}, 10 * 60 * 1000);
|
||||||
|
|
||||||
|
function getHistory(chatId: number): HistoryEntry[] {
|
||||||
|
return chatHistory.get(chatId) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushHistory(chatId: number, role: "user" | "assistant", content: string): void {
|
||||||
|
const entries = chatHistory.get(chatId) ?? [];
|
||||||
|
entries.push({ role, content, ts: Date.now() });
|
||||||
|
if (entries.length > MAX_HISTORY * 2) entries.splice(0, entries.length - MAX_HISTORY * 2);
|
||||||
|
chatHistory.set(chatId, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a tool result in conversation history so the AI knows what happened.
|
||||||
|
*/
|
||||||
|
export function recordToolResult(chatId: number, toolName: string, resultSummary: string): void {
|
||||||
|
pushHistory(chatId, "assistant", `[Tool ${toolName} result]: ${resultSummary}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a natural language message through Claude and return the intent.
|
* Process a natural language message through Claude and return the intent.
|
||||||
*/
|
*/
|
||||||
export async function processMessage(
|
export async function processMessage(
|
||||||
|
chatId: number,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
context: { meshSlug?: string; meshSlugs?: string[]; userName?: string; recentPeers?: string[] },
|
context: { meshSlug?: string; meshSlugs?: string[]; userName?: string; recentPeers?: string[] },
|
||||||
): Promise<AiResult> {
|
): Promise<AiResult> {
|
||||||
try {
|
try {
|
||||||
const anthropic = getClient();
|
const anthropic = getClient();
|
||||||
|
|
||||||
|
// Record user message in history
|
||||||
|
pushHistory(chatId, "user", userMessage);
|
||||||
|
|
||||||
const contextInfo = [
|
const contextInfo = [
|
||||||
context.meshSlugs?.length ? `Connected meshes: ${context.meshSlugs.join(", ")}` : context.meshSlug ? `Current mesh: ${context.meshSlug}` : null,
|
context.meshSlugs?.length ? `Connected meshes: ${context.meshSlugs.join(", ")}` : context.meshSlug ? `Current mesh: ${context.meshSlug}` : null,
|
||||||
context.userName ? `User's name: ${context.userName}` : null,
|
context.userName ? `User's name: ${context.userName}` : null,
|
||||||
context.recentPeers?.length ? `Known peers: ${context.recentPeers.join(", ")}` : null,
|
context.recentPeers?.length ? `Known peers: ${context.recentPeers.join(", ")}` : null,
|
||||||
].filter(Boolean).join(". ");
|
].filter(Boolean).join(". ");
|
||||||
|
|
||||||
|
// Build message history for multi-turn context
|
||||||
|
const history = getHistory(chatId);
|
||||||
|
const messages: Array<{ role: "user" | "assistant"; content: string }> = [];
|
||||||
|
for (const entry of history) {
|
||||||
|
// Alternate roles — Claude API requires user/assistant alternation
|
||||||
|
if (messages.length === 0 || messages[messages.length - 1]!.role !== entry.role) {
|
||||||
|
messages.push({ role: entry.role, content: entry.content });
|
||||||
|
} else {
|
||||||
|
// Same role consecutive — merge into the last message
|
||||||
|
messages[messages.length - 1]!.content += "\n" + entry.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure messages start with user and alternate
|
||||||
|
if (messages.length > 0 && messages[0]!.role !== "user") {
|
||||||
|
messages.shift();
|
||||||
|
}
|
||||||
|
|
||||||
const response = await anthropic.messages.create({
|
const response = await anthropic.messages.create({
|
||||||
model: "claude-haiku-4-5-20251001",
|
model: "claude-haiku-4-5-20251001",
|
||||||
max_tokens: 500,
|
max_tokens: 500,
|
||||||
system: SYSTEM_PROMPT + (contextInfo ? `\n\nContext: ${contextInfo}` : ""),
|
system: SYSTEM_PROMPT + (contextInfo ? `\n\nContext: ${contextInfo}` : ""),
|
||||||
tools: TOOLS as Anthropic.Messages.Tool[],
|
tools: TOOLS as Anthropic.Messages.Tool[],
|
||||||
messages: [{ role: "user", content: userMessage }],
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for tool use
|
// Check for tool use
|
||||||
for (const block of response.content) {
|
for (const block of response.content) {
|
||||||
if (block.type === "tool_use") {
|
if (block.type === "tool_use") {
|
||||||
|
pushHistory(chatId, "assistant", `[Using tool: ${block.name}(${JSON.stringify(block.input).slice(0, 100)})]`);
|
||||||
return {
|
return {
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
toolCall: { name: block.name, input: block.input as Record<string, unknown> },
|
toolCall: { name: block.name, input: block.input as Record<string, unknown> },
|
||||||
@@ -212,6 +278,7 @@ export async function processMessage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
|
pushHistory(chatId, "assistant", block.text);
|
||||||
return { type: "text", text: block.text };
|
return { type: "text", text: block.text };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1204,12 +1204,11 @@ function setupBotCommands(
|
|||||||
await ctx.answerCallbackQuery({ text: "Executing..." });
|
await ctx.answerCallbackQuery({ text: "Executing..." });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { formatResult } = await import("./telegram-ai");
|
const { formatResult, recordToolResult } = await import("./telegram-ai");
|
||||||
const result = await executeAiToolCall(pending.toolCall, pending.meshIds);
|
const result = await executeAiToolCall(pending.toolCall, pending.meshIds);
|
||||||
await ctx.editMessageText(
|
const resultText = formatResult(pending.toolCall.name, result);
|
||||||
formatResult(pending.toolCall.name, result),
|
recordToolResult(chatId, pending.toolCall.name, resultText.replace(/<[^>]+>/g, "").slice(0, 200));
|
||||||
{ parse_mode: "HTML" },
|
await ctx.editMessageText(resultText, { parse_mode: "HTML" });
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await ctx.editMessageText(`❌ Failed: ${err instanceof Error ? err.message : String(err)}`);
|
await ctx.editMessageText(`❌ Failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -1612,7 +1611,7 @@ function setupBotCommands(
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await processMessage(text, {
|
const result = await processMessage(chatId, text, {
|
||||||
meshSlug: allMeshSlugs[0],
|
meshSlug: allMeshSlugs[0],
|
||||||
meshSlugs: allMeshSlugs,
|
meshSlugs: allMeshSlugs,
|
||||||
userName: ctx.from?.first_name,
|
userName: ctx.from?.first_name,
|
||||||
@@ -1656,7 +1655,11 @@ function setupBotCommands(
|
|||||||
} else {
|
} else {
|
||||||
// Read-only action — execute immediately
|
// Read-only action — execute immediately
|
||||||
const execResult = await executeAiToolCall(result.toolCall, meshIds);
|
const execResult = await executeAiToolCall(result.toolCall, meshIds);
|
||||||
await ctx.reply(formatResult(result.toolCall.name, execResult), {
|
const resultText = formatResult(result.toolCall.name, execResult);
|
||||||
|
// Record in conversation history
|
||||||
|
const { recordToolResult } = await import("./telegram-ai");
|
||||||
|
recordToolResult(chatId, result.toolCall.name, resultText.replace(/<[^>]+>/g, "").slice(0, 200));
|
||||||
|
await ctx.reply(resultText, {
|
||||||
parse_mode: "HTML",
|
parse_mode: "HTML",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user