Files
Memoh/agent/src/agent.ts
T
2026-02-08 01:02:04 +08:00

336 lines
9.2 KiB
TypeScript

import { generateText, ImagePart, LanguageModelUsage, ModelMessage, stepCountIs, streamText, UserModelMessage } from 'ai'
import { AgentInput, AgentParams, allActions, HTTPMCPConnection, MCPConnection, Schedule } from './types'
import { system, schedule, user, subagentSystem } from './prompts'
import { AuthFetcher } from './index'
import { createModel } from './model'
import { AgentAction } from './types/action'
import { getTools } from './tools'
import {
extractAttachmentsFromText,
stripAttachmentsFromMessages,
dedupeAttachments,
AttachmentsStreamExtractor,
} from './utils/attachments'
import type { ContainerFileAttachment } from './types/attachment'
import { getMCPTools } from './tools/mcp'
export const createAgent = ({
model: modelConfig,
activeContextTime = 24 * 60,
brave,
language = 'Same as the user input',
allowedActions = allActions,
channels = [],
mcpConnections = [],
currentChannel = 'Unknown Channel',
identity = {
botId: '',
sessionId: '',
containerId: '',
contactId: '',
contactName: '',
},
auth,
}: AgentParams, fetch: AuthFetcher) => {
const model = createModel(modelConfig)
const getDefaultMCPConnections = (): MCPConnection[] => {
const fs: HTTPMCPConnection = {
type: 'http',
name: 'fs',
url: `${auth.baseUrl}/bots/${identity.botId}/container/fs`,
headers: {
'Authorization': `Bearer ${auth.bearer}`,
},
}
return [fs]
}
const generateSystemPrompt = () => {
return system({
date: new Date(),
language,
maxContextLoadTime: activeContextTime,
channels,
skills: [],
enabledSkills: [],
})
}
const getAgentTools = async () => {
const tools = getTools(allowedActions, {
fetch,
model: modelConfig,
brave,
identity,
})
const defaultMCPConnections = getDefaultMCPConnections()
const { tools: mcpTools, close: closeMCP } = await getMCPTools([
...defaultMCPConnections,
...mcpConnections,
])
Object.assign(tools, mcpTools)
return {
tools,
close: closeMCP,
}
}
const generateUserPrompt = (input: AgentInput) => {
const images = input.attachments.filter(attachment => attachment.type === 'image')
const files = input.attachments.filter((a): a is ContainerFileAttachment => a.type === 'file')
const text = user(input.query, {
contactId: identity.contactId,
contactName: identity.contactName,
channel: currentChannel,
date: new Date(),
attachments: files,
})
const userMessage: UserModelMessage = {
role: 'user',
content: [
{ type: 'text', text },
...images.map(image => ({ type: 'image', image: image.base64 }) as ImagePart),
]
}
return userMessage
}
const ask = async (input: AgentInput) => {
const userPrompt = generateUserPrompt(input)
const messages = [...input.messages, userPrompt]
const systemPrompt = generateSystemPrompt()
const { tools, close } = await getAgentTools()
const { response, reasoning, text, usage } = await generateText({
model,
messages,
system: systemPrompt,
stopWhen: stepCountIs(Infinity),
prepareStep: () => {
return {
system: systemPrompt,
}
},
onFinish: async () => {
await close()
},
tools,
})
const { cleanedText, attachments: textAttachments } = extractAttachmentsFromText(text)
const { messages: strippedMessages, attachments: messageAttachments } = stripAttachmentsFromMessages(response.messages)
const allAttachments = dedupeAttachments([...textAttachments, ...messageAttachments])
return {
messages: [userPrompt, ...strippedMessages],
reasoning: reasoning.map(part => part.text),
usage,
text: cleanedText,
attachments: allAttachments,
}
}
const askAsSubagent = async (params: {
input: string
name: string
description: string
messages: ModelMessage[]
}) => {
const userPrompt: UserModelMessage = {
role: 'user',
content: [
{ type: 'text', text: params.input },
]
}
const generateSubagentSystemPrompt = () => {
return subagentSystem({
date: new Date(),
name: params.name,
description: params.description,
})
}
const messages = [...params.messages, userPrompt]
const { tools, close } = await getAgentTools()
const { response, reasoning, text, usage } = await generateText({
model,
messages,
system: generateSubagentSystemPrompt(),
stopWhen: stepCountIs(Infinity),
prepareStep: () => {
return {
system: generateSubagentSystemPrompt(),
}
},
onFinish: async () => {
await close()
},
tools,
})
return {
messages: [userPrompt, ...response.messages],
reasoning: reasoning.map(part => part.text),
usage,
text,
}
}
const triggerSchedule = async (params: {
schedule: Schedule
messages: ModelMessage[]
}) => {
const scheduleMessage: UserModelMessage = {
role: 'user',
content: [
{ type: 'text', text: schedule({ schedule: params.schedule, date: new Date() }) },
]
}
const messages = [...params.messages, scheduleMessage]
const { tools, close } = await getAgentTools()
const { response, reasoning, text, usage } = await generateText({
model,
messages,
system: generateSystemPrompt(),
stopWhen: stepCountIs(Infinity),
onFinish: async () => {
await close()
},
tools,
})
return {
messages: [scheduleMessage, ...response.messages],
reasoning: reasoning.map(part => part.text),
usage,
text,
}
}
async function* stream(input: AgentInput): AsyncGenerator<AgentAction> {
const userPrompt = generateUserPrompt(input)
const messages = [...input.messages, userPrompt]
const systemPrompt = generateSystemPrompt()
const attachmentsExtractor = new AttachmentsStreamExtractor()
const result: {
messages: ModelMessage[]
reasoning: string[]
usage: LanguageModelUsage | null
} = {
messages: [],
reasoning: [],
usage: null
}
const { tools, close } = await getAgentTools()
const { fullStream } = streamText({
model,
messages,
system: systemPrompt,
stopWhen: stepCountIs(Infinity),
prepareStep: () => {
return {
system: systemPrompt,
}
},
tools,
onFinish: async ({ usage, reasoning, response }) => {
await close()
result.usage = usage as never
result.reasoning = reasoning.map(part => part.text)
result.messages = response.messages
}
})
yield {
type: 'agent_start',
input,
}
for await (const chunk of fullStream) {
switch (chunk.type) {
case 'reasoning-start': yield {
type: 'reasoning_start',
metadata: chunk
}; break
case 'reasoning-delta': yield {
type: 'reasoning_delta',
delta: chunk.text
}; break
case 'reasoning-end': yield {
type: 'reasoning_end',
metadata: chunk
}; break
case 'text-start': yield {
type: 'text_start',
}; break
case 'text-delta': {
const { visibleText, attachments } = attachmentsExtractor.push(chunk.text)
if (visibleText) {
yield {
type: 'text_delta',
delta: visibleText,
}
}
if (attachments.length) {
yield {
type: 'attachment_delta',
attachments,
}
}
break
}
case 'text-end': {
// Flush any remaining buffered content before ending the text stream.
const remainder = attachmentsExtractor.flushRemainder()
if (remainder.visibleText) {
yield {
type: 'text_delta',
delta: remainder.visibleText,
}
}
if (remainder.attachments.length) {
yield {
type: 'attachment_delta',
attachments: remainder.attachments,
}
}
yield {
type: 'text_end',
metadata: chunk,
}
break
}
case 'tool-call': yield {
type: 'tool_call_start',
toolName: chunk.toolName,
toolCallId: chunk.toolCallId,
input: chunk.input,
metadata: chunk
}; break
case 'tool-result': yield {
type: 'tool_call_end',
toolName: chunk.toolName,
toolCallId: chunk.toolCallId,
input: chunk.input,
result: chunk.output,
metadata: chunk
}; break
case 'file': yield {
type: 'image_delta',
image: chunk.file.base64,
metadata: chunk
}
}
}
const { messages: strippedMessages } = stripAttachmentsFromMessages(result.messages)
yield {
type: 'agent_end',
messages: [userPrompt, ...strippedMessages],
skills: [],
reasoning: result.reasoning,
usage: result.usage!,
}
}
return {
stream,
ask,
askAsSubagent,
triggerSchedule,
}
}