Files
accounted/extensions/general/ai-chat/chatbot/agent.ts
T

134 lines
3.8 KiB
TypeScript

import { ChatAnthropic } from '@langchain/anthropic'
import { createReactAgent } from '@langchain/langgraph/prebuilt'
import { HumanMessage, AIMessage } from '@langchain/core/messages'
import type { StructuredToolInterface } from '@langchain/core/tools'
import { CHATBOT_CONFIG } from './config'
import {
SYSTEM_PROMPT_DATA,
SYSTEM_PROMPT_HYBRID,
formatConversationHistory,
} from './prompts'
import type { ChatMessage } from '@/types/chat'
import type { RouteType } from './router'
export interface AgentStreamEvent {
type: 'tool_start' | 'content' | 'done'
toolName?: string
content?: string
toolResults?: ToolResultEntry[]
}
export interface ToolResultEntry {
toolName: string
result: string
}
/**
* Run the LangGraph agent with tool calling and stream events.
*/
export async function* streamAgentResponse(options: {
query: string
route: RouteType
tools: StructuredToolInterface[]
conversationHistory: ChatMessage[]
ragContext?: string
}): AsyncGenerator<AgentStreamEvent> {
const { query, route, tools, conversationHistory, ragContext } = options
// Build system prompt based on route
const historyText = formatConversationHistory(
conversationHistory.slice(-CHATBOT_CONFIG.maxHistoryMessages).map((m) => ({
role: m.role,
content: m.content,
}))
)
let systemPrompt: string
if (route === 'data') {
systemPrompt = SYSTEM_PROMPT_DATA.replace('{history}', historyText)
} else {
const context = ragContext || 'Ingen specifik kontext hittades i kunskapsbasen.'
systemPrompt = SYSTEM_PROMPT_HYBRID
.replace('{context}', context)
.replace('{history}', historyText)
}
// Create the model
const model = new ChatAnthropic({
modelName: CHATBOT_CONFIG.agentModel,
maxTokens: CHATBOT_CONFIG.agentMaxTokens,
temperature: CHATBOT_CONFIG.temperature,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
})
// Create the agent
const agent = createReactAgent({
llm: model,
tools,
prompt: systemPrompt,
})
// Build input messages
const messages: (HumanMessage | AIMessage)[] = []
// Add recent history as messages for the agent
const recent = conversationHistory.slice(-CHATBOT_CONFIG.maxHistoryMessages)
for (const msg of recent) {
if (msg.role === 'user') {
messages.push(new HumanMessage(msg.content))
} else {
messages.push(new AIMessage(msg.content))
}
}
messages.push(new HumanMessage(query))
// Track tool results for artifact generation
const toolResults: ToolResultEntry[] = []
// Stream the agent execution using streamEvents for fine-grained control
const eventStream = agent.streamEvents(
{ messages },
{
version: 'v2',
recursionLimit: CHATBOT_CONFIG.maxAgentIterations * 2 + 1,
}
)
for await (const event of eventStream) {
// Tool start events
if (event.event === 'on_tool_start') {
yield { type: 'tool_start', toolName: event.name }
}
// Tool end events — capture results
if (event.event === 'on_tool_end') {
const output = event.data?.output
const result = typeof output === 'string' ? output : JSON.stringify(output ?? '')
toolResults.push({
toolName: event.name,
result,
})
}
// LLM streaming tokens (final response text)
if (event.event === 'on_chat_model_stream') {
const chunk = event.data?.chunk
if (chunk) {
const content = typeof chunk.content === 'string'
? chunk.content
: Array.isArray(chunk.content)
? chunk.content
.filter((c: { type: string }) => c.type === 'text')
.map((c: { text: string }) => c.text)
.join('')
: ''
if (content) {
yield { type: 'content', content }
}
}
}
}
yield { type: 'done', toolResults }
}