UNPKG

@smythos/sdk

Version:
272 lines (242 loc) 10.4 kB
import { TLLMModel, Conversation, TLLMEvent, ILLMContextStore, AccessCandidate } from '@smythos/sre'; import { EventEmitter } from 'events'; import { uid } from '../utils/general.utils'; import { AgentData, ChatOptions } from '../types/SDKTypes'; import { SDKObject } from '../Core/SDKObject.class'; import { StorageInstance } from '../Storage/StorageInstance.class'; import { SDKLog } from '../utils/console.utils'; const console = SDKLog; class LocalChatStore extends SDKObject implements ILLMContextStore { private _storage: StorageInstance; constructor(private _conversationId: string, candidate: AccessCandidate) { super(); this._storage = new StorageInstance(null, null, candidate); } async save(messages: any[]): Promise<void> { try { await this._storage.write(`${this._conversationId}`, JSON.stringify(messages)); } catch (error) { console.error('Error saving chat messages: ', error); throw error; } } async load(count?: number): Promise<any[]> { try { const buffer: Buffer = await this._storage.read(`${this._conversationId}`); if (!buffer) return []; const messages = JSON.parse(buffer.toString()); return messages; } catch (error) { console.error('Error loading chat messages: ', error); throw error; } } async getMessage(message_id: string): Promise<any[]> { const messages = await this.load(); const message = messages.find((m) => m.__smyth_data__?.message_id === message_id); return message; } } class ChatCommand { private _conversation: Conversation; constructor(private prompt: string, private chat: Chat) { this._conversation = chat._conversation; } then(resolve: (value: string) => void, reject?: (reason: any) => void) { return this.run().then(resolve, reject); } private async run(): Promise<string> { await this.chat.ready; const result = await this._conversation.streamPrompt(this.prompt); return result; } /** * Execute the chat command as a streaming response. * * **Available Events:** * - `'data'` - Text chunk received from the agent * - `'end'` - The agent has finished responding * - `'error'` - The agent encountered an error * * @returns Promise that resolves to an EventEmitter for streaming updates * * @example * ```typescript * const chat = agent.chat('my_chat_id'); * * const stream = await chat.prompt("Tell me a long story").stream(); * stream.on('data', (chunk) => process.stdout.write(chunk)); * stream.on('end', () => console.log('\nStory completed!')); * stream.on('error', (err) => console.error('Error:', err)); * ``` */ async stream(): Promise<EventEmitter> { await this.chat.ready; const eventEmitter = new EventEmitter(); const toolInfoHandler = (toolInfo: any) => { eventEmitter.emit(TLLMEvent.ToolInfo, toolInfo); this.chat.emit(TLLMEvent.ToolInfo, toolInfo); }; const interruptedHandler = (interrupted: any) => { eventEmitter.emit(TLLMEvent.Interrupted, interrupted); this.chat.emit(TLLMEvent.Interrupted, interrupted); }; const contentHandler = (content: string) => { eventEmitter.emit(TLLMEvent.Content, content); this.chat.emit(TLLMEvent.Content, content); }; const toolCallHandler = (toolCall: any) => { eventEmitter.emit(TLLMEvent.ToolCall, toolCall); this.chat.emit(TLLMEvent.ToolCall, toolCall); }; const toolResultHandler = (toolResult: any) => { eventEmitter.emit(TLLMEvent.ToolResult, toolResult); this.chat.emit(TLLMEvent.ToolResult, toolResult); }; const endHandler = () => { eventEmitter.emit(TLLMEvent.End); this.chat.emit(TLLMEvent.End); removeHandlers(); }; const errorHandler = (error: any) => { eventEmitter.emit(TLLMEvent.Error, error); this.chat.emit(TLLMEvent.Error, error); removeHandlers(); }; const usageHandler = (usage: any) => { eventEmitter.emit(TLLMEvent.Usage, usage); this.chat.emit(TLLMEvent.Usage, usage); }; const removeHandlers = () => { this._conversation.off(TLLMEvent.ToolCall, toolCallHandler); this._conversation.off(TLLMEvent.ToolResult, toolResultHandler); this._conversation.off(TLLMEvent.Usage, usageHandler); this._conversation.off(TLLMEvent.End, endHandler); this._conversation.off(TLLMEvent.Error, errorHandler); this._conversation.off(TLLMEvent.Content, contentHandler); this._conversation.off(TLLMEvent.ToolInfo, toolInfoHandler); this._conversation.off(TLLMEvent.Interrupted, interruptedHandler); }; this._conversation.on(TLLMEvent.ToolCall, toolCallHandler); this._conversation.on(TLLMEvent.ToolResult, toolResultHandler); this._conversation.on(TLLMEvent.End, endHandler); this._conversation.on(TLLMEvent.Error, errorHandler); this._conversation.on(TLLMEvent.Content, contentHandler); this._conversation.on(TLLMEvent.ToolInfo, toolInfoHandler); this._conversation.on(TLLMEvent.Interrupted, interruptedHandler); this._conversation.streamPrompt(this.prompt); return eventEmitter; } } export class Chat extends SDKObject { private _id: string; public _conversation: Conversation; public get id() { return this._id; } private _data: any = { version: '1.0.0', name: 'Agent', behavior: '', components: [], connections: [], defaultModel: '', id: uid(), }; public get agentData() { return this._data; } constructor(options: ChatOptions & { candidate: AccessCandidate }, _model: string | TLLMModel, _data?: any, private _convOptions: any = {}) { super(); this._data = { ...this._data, ..._data, defaultModel: _model }; this._id = options.id || uid(); if (options.persist) { if (!options.candidate) { //no explicit id provided, generaed Agent IDs are not eligible for persistance console.warn('Agent ID or Team ID are required to use chat persistance.'); console.warn('Chat persistance disabled!'); } else { if (!this._convOptions?.store && typeof options.persist === 'boolean') { this._convOptions.store = new LocalChatStore(this._id, options.candidate); } if (!this._convOptions?.store && this.isValidPersistanceObject(options.persist)) { this._convOptions.store = options.persist; } } } if (options.maxContextSize) { this._convOptions.maxContextSize = options.maxContextSize; } if (options.maxOutputTokens) { this._convOptions.maxOutputTokens = options.maxOutputTokens; } this._conversation = createConversation(this._data, this._convOptions); } private isValidPersistanceObject(persistance: any) { return typeof persistance === 'object' && 'save' in persistance && 'load' in persistance && 'getMessage' in persistance; } protected async init() { await super.init(); await registerProcessSkills(this._conversation, this._data); } /** * Send a prompt to the chat and get a response. * * The returned command can be executed in multiple ways: * - **Promise mode**: `await chat.prompt("question")` - returns final result * - **Explicit execution**: `await chat.prompt("question").run()` - same as above * - **Streaming mode**: `await chat.prompt("question").stream()` - returns event emitter * * @example * ```typescript * const chat = agent.chat('my_chat_id'); * * // Simple prompt (promise mode) * const answer = await chat.prompt("What is the capital of France?"); * * * // Streaming for long responses * const stream = await chat.prompt("Write a detailed report").stream(); * stream.on('data', chunk => console.log(chunk)); * stream.on('end', () => console.log('Complete!')); * ``` * * @param prompt - The message or question to send to the chat * @returns ChatCommand that can be executed or streamed */ prompt(prompt: string) { return new ChatCommand(prompt, this); } } function createConversation(agentData: AgentData, options?: any) { const filteredAgentData = { ...agentData, components: agentData.components.filter((c) => !c.process), }; const conversation = new Conversation(agentData.defaultModel, filteredAgentData, { agentId: agentData.id, ...options, }); conversation.on(TLLMEvent.Error, (error) => { console.error('An error occurred while running the agent: ', error.message); }); return conversation; } async function registerProcessSkills(conversation: Conversation, agentData: AgentData) { const processSkills: any[] = agentData.components.filter((c) => c.process); for (const skill of processSkills) { await conversation.addTool({ name: skill.data.endpoint, description: skill.data.description, //arguments: _arguments, handler: skill.process, inputs: skill.inputs, }); } } export async function prepareConversation(agentData: AgentData, options?: any) { const conversation = createConversation(agentData, options); // Register process skills as custom tools await registerProcessSkills(conversation, agentData); return conversation; }