export const onInbound = action({ args: { instanceId: v.id("instances"), chatJid: v.string(), maxHistory: v.optional(v.number()), }, handler: async (ctx, { instanceId, chatJid, maxHistory = 20 }) => { console.log("got to inbound"); // Load instance/org const instance = (await ctx.runQuery(api.db.instances.get, { id: instanceId, })) as Doc<"instances">; if (!instance) return { error: "Instance not found" }; const organizationId = instance.organizationId; // Policy gate const allowed = await ctx.runQuery( api.db.autoResponderRules.shouldRespond, { instanceId, chatJid } ); console.log("allowed", allowed); if (!allowed) return { ok: true, skipped: "policy blocked" }; const alreadyEscalated = await ctx.runQuery( api.db.escalations.hasOpenForChat, { instanceId, chatJid, } ); if (alreadyEscalated) { return { ok: true, skipped: "escalated" }; } // Load short chat history (oldest..newest) const page: PaginationResult> = await ctx.runQuery( api.db.messages.getMessagesForChat, { instanceId, remoteJid: chatJid, paginationOpts: { cursor: null, numItems: maxHistory }, } ); const history = (page?.page ?? []).slice().reverse(); // Identify latest inbound user message const latestInbound = history.filter((m) => !(m.fromMe && instanceId === m.instanceId)).at(-1); console.log("got here before latest inbound message", latestInbound) if (!latestInbound) return { ok: true, skipped: "no inbound content" }; // Prepare tools + messages to send to the model (history-aware decision inside) const { tools, messages, decision } = await prepareAgentSession(ctx, { organizationId, instanceId, chatJid, history, // reuse loaded history to avoid re-fetch }); console.log("message", JSON.stringify(messages)); const system = `You are a helpful, concise WhatsApp assistant. - Always use tools like sendText/sendMedia to reply, e.g when you want something to be clarified or answering a user question, always use on of the whatsapp tool ultimately, - This is because we are using whatsapp as our primary user interface, so you must always respond there using one of the whatsapp tools provided. - Only include optional parameters when truly required. - You are intelligent not menu based, so just interact with user dont directly give them options to select if not asked, interact with them like a human would. - If you have access to the generate image tool, please send media message after generating the image, not just a text. if you dont have that tool, just respond with you cannot make images. - Be warm and kind. - You have access to search context tool to get more information from the knowledge base. - The current chat JID is ${chatJid}. Use it when a tool requests the chat identifier. - If the user asks for a human, an agent, or escalation, call the escalate_to_human tool (no WhatsApp message afterward) and end the turn. - Once you escalate, do not send or schedule any WhatsApp messages until a human resolves it. - Please dont talk about anything else outside of the knowledge base and system instructions, politely decline, only talk about the provided information, and the business, nothing else After using the sendText/sendMedia tools to reply, just respond with ok as the final output text, dont respond ok using tools, only send relevant information through the sendText/sendMedia e.t.c ${instance.instructions ?? ""}`; const debugTools = withDebugTools(tools, 8); console.log("[llm] providerModel:", chatModelIdDefault); console.log("[llm] tools:", Object.keys(debugTools)); console.log("[llm] decision:", decision); console.log("[llm] messages.count:", messages.length); try { const res = await generateText({ model: chatModel(chatModelIdDefault), system, tools: debugTools, stopWhen: stepCountIs(5), messages, }); const responseTimestamp = res.response?.timestamp instanceof Date ? res.response.timestamp.getTime() : Date.now(); const responseMessages = res.response?.messages ?? []; const assistantToolCallMessages = responseMessages.filter( (m): m is AssistantModelMessage => m.role === "assistant" && Array.isArray(m.content) && m.content.some((part) => part.type === "tool-call") ); for (const assistantMessage of assistantToolCallMessages) { // Add type guard to ensure content is an array if (typeof assistantMessage.content === "string") continue; const maybeMessageId = typeof (assistantMessage as any).id === "string" ? ((assistantMessage as any).id as string) : undefined; const toolCallParts = assistantMessage.content.filter( (part): part is ToolCallPart => part.type === "tool-call" ); if (toolCallParts.length === 0) continue; const textPart = assistantMessage.content.find( (part) => part.type === "text" ) as { type: "text"; text: string } | undefined; const assistantTimestamp = responseTimestamp > 0 ? responseTimestamp - 1 : responseTimestamp; try { await ctx.runMutation(internal.db.messages.logAssistantToolCall, { instanceId, organizationId, chatJid, calls: toolCallParts.map((part) => ({ toolCallId: part.toolCallId, toolName: part.toolName, args: part.input, })), text: textPart?.text, messageId: maybeMessageId, timestamp: assistantTimestamp, }); } catch (error) { console.error( "[assist.onInbound] Failed to log assistant tool call", { toolCallIds: toolCallParts.map((part) => part.toolCallId), error, } ); } } const toolMessages = responseMessages.filter( (m): m is ToolModelMessage => m.role === "tool" ); for (const toolMessage of toolMessages) { const maybeMessageId = typeof (toolMessage as any).id === "string" ? ((toolMessage as any).id as string) : undefined; for (const part of toolMessage.content) { try { await ctx.runMutation(internal.db.messages.logToolResult, { instanceId, organizationId, chatJid, toolResult: { type: "tool-result", toolCallId: part.toolCallId, toolName: part.toolName, output: part.output, providerOptions: part.providerOptions, }, messageId: maybeMessageId ? `${maybeMessageId}:${part.toolCallId}` : undefined, timestamp: responseTimestamp, }); } catch (error) { console.error("[assist.onInbound] Failed to log tool result", { toolCallId: part.toolCallId, error, }); } } } console.log("[llm] finishReason:", res.finishReason); if (res.warnings?.length) console.warn("[llm] warnings:", res.warnings); // Usage is provider-dependent; OpenAI-style providers often return this. if (res.usage) { console.log("[llm] usage:", res.usage); // { promptTokens, completionTokens, totalTokens } } console.log("[assistant] final text:", res.text); return { ok: true, continued: !decision.newThread, usedHistoryCount: messages.length, usage: res.usage ?? null, finishReason: res.finishReason ?? null, }; } catch (e) { console.error("[assist.onInbound] generateText failed", e); return { ok: false, error: "agent_error" }; } }, });