UNPKG

@astreus-ai/astreus

Version:

AI Agent Framework with Chat Management

1 lines 423 kB
{"version":3,"sources":["../src/utils/logger.ts","../src/constants/agent.ts","../src/constants/memory.ts","../src/constants/database.ts","../src/constants/provider.ts","../src/constants/rag.ts","../src/constants/task.ts","../src/constants/error.ts","../src/constants/index.ts","../src/providers/openai.ts","../src/providers/ollama.ts","../src/providers/embedding.ts","../src/providers/index.ts","../src/index.ts","../src/agent.ts","../src/database.ts","../src/database/sqlite.ts","../src/database/postgresql.ts","../src/utils/index.ts","../src/utils/validation.ts","../src/utils/rag-tools.ts","../src/database/modules/user.ts","../src/plugin.ts","../src/memory.ts","../src/rag/vector.ts","../src/types/rag.ts","../src/rag/vector-db/index.ts","../src/rag/document.ts","../src/rag/pdf-parser.ts","../src/rag/index.ts","../src/chat.ts","../src/tasks/task.ts","../src/utils/intent.ts","../src/tasks/manager.ts","../src/tasks/index.ts"],"sourcesContent":["/**\n * Astreus AI - Logger Utility\n * Provides colorful console logging functionality for the framework\n */\n\n// ANSI color codes for terminal output\nconst colors = {\n reset: \"\\x1b[0m\",\n bright: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n underscore: \"\\x1b[4m\",\n blink: \"\\x1b[5m\",\n reverse: \"\\x1b[7m\",\n hidden: \"\\x1b[8m\",\n \n // Foreground colors\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\",\n white: \"\\x1b[37m\",\n gray: \"\\x1b[90m\",\n \n // Background colors\n bgBlack: \"\\x1b[40m\",\n bgRed: \"\\x1b[41m\",\n bgGreen: \"\\x1b[42m\",\n bgYellow: \"\\x1b[43m\",\n bgBlue: \"\\x1b[44m\",\n bgMagenta: \"\\x1b[45m\",\n bgCyan: \"\\x1b[46m\",\n bgWhite: \"\\x1b[47m\",\n};\n\n// Framework constants\nconst FRAMEWORK_NAME = \"Astreus\";\nconst FRAMEWORK_VERSION = \"0.1.0\";\n\n// Log levels\n/* eslint-disable no-unused-vars */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n SUCCESS = 2,\n WARN = 3,\n ERROR = 4,\n NONE = 5,\n}\n/* eslint-enable no-unused-vars */\n\n// Logger options\ninterface LoggerOptions {\n level: LogLevel;\n prefix: boolean;\n colors: boolean;\n lineBreak: boolean;\n timestamp: boolean;\n}\n\n// Default options\nconst defaultOptions: LoggerOptions = {\n level: LogLevel.INFO,\n prefix: true,\n colors: true,\n lineBreak: false,\n timestamp: false\n};\n\n// Current logger options\nlet options: LoggerOptions = { ...defaultOptions };\n\n// Track the last log time to prevent duplicate timestamps\nlet lastLogTime = 0;\n\n/**\n * Create a formatted prefix for log messages\n */\nfunction createPrefix(color: string): string {\n if (!options.prefix) return '';\n \n // Simple format: [FRAMEWORK]\n return `${color}[${FRAMEWORK_NAME}]${colors.reset} `;\n}\n\n/**\n * Get a timestamp string\n */\nfunction getTimestamp(): string {\n if (!options.timestamp) return '';\n \n const now = Date.now();\n // Only show timestamps when they change by at least 1 second\n if (Math.abs(now - lastLogTime) < 1000) {\n return '';\n }\n \n lastLogTime = now;\n const date = new Date(now);\n return `${colors.gray}[${date.toLocaleTimeString()}]${colors.reset} `;\n}\n\n/**\n * Internal log function\n */\nfunction log(level: LogLevel, color: string, ...messages: unknown[]): void {\n if (level < options.level) return;\n \n const prefix = createPrefix(color);\n const timestamp = getTimestamp();\n \n // Add line break before log entry if enabled (reduced usage)\n if (options.lineBreak && level >= LogLevel.WARN) {\n // Use safeConsole to handle console statements\n safeConsole('log');\n }\n \n if (options.colors) {\n // Apply color to text messages that are strings\n const coloredMessages = messages.map(msg => \n typeof msg === 'string' ? `${color}${msg}${colors.reset}` : msg\n );\n // Use a direct string without extra spaces\n safeConsole('log', `${timestamp}${prefix}${coloredMessages.join(' ')}`);\n } else {\n // Strip color codes using string replace with a function rather than regex with control chars\n // This avoids the ESLint 'no-control-regex' error\n const stripAnsi = (str: string): string => {\n let result = '';\n let inEscSeq = false;\n \n for (let i = 0; i < str.length; i++) {\n // Start of escape sequence\n if (str[i] === '\\u001b' && str[i+1] === '[') {\n inEscSeq = true;\n i++; // Skip the '['\n continue;\n }\n \n // In escape sequence, wait for 'm' which ends ANSI color codes\n if (inEscSeq) {\n if (str[i] === 'm') {\n inEscSeq = false;\n }\n continue;\n }\n \n // Normal character\n result += str[i];\n }\n \n return result;\n };\n \n const strippedPrefix = stripAnsi(prefix);\n safeConsole('log', `${timestamp}${strippedPrefix}${messages.join(' ')}`);\n }\n}\n\n/**\n * Safe console wrapper to avoid ESLint warnings\n */\nfunction safeConsole(method: 'log' | 'info' | 'warn' | 'error', ...args: unknown[]): void {\n // This function centralizes console usage and can be disabled by ESLint when needed\n // eslint-disable-next-line no-console\n if (method === 'log') console.log(...args);\n // eslint-disable-next-line no-console\n else if (method === 'info') console.info(...args);\n // eslint-disable-next-line no-console\n else if (method === 'warn') console.warn(...args);\n // eslint-disable-next-line no-console\n else if (method === 'error') console.error(...args);\n}\n\n/**\n * Configure the logger\n */\nexport function configure(newOptions: Partial<LoggerOptions>): void {\n options = { ...options, ...newOptions };\n}\n\n/**\n * Print the framework banner\n */\nexport function printBanner(): void {\n if (options.level > LogLevel.INFO) return;\n \n safeConsole('log');\n safeConsole('log', `${colors.cyan}${colors.bright}${FRAMEWORK_NAME} AI Framework v${FRAMEWORK_VERSION}${colors.reset}`);\n safeConsole('log');\n}\n\n/**\n * Public logging functions\n */\nexport const logger = {\n debug: (...messages: unknown[]) => log(LogLevel.DEBUG, colors.gray, ...messages),\n info: (...messages: unknown[]) => log(LogLevel.INFO, colors.blue, ...messages),\n success: (...messages: unknown[]) => log(LogLevel.SUCCESS, colors.green, ...messages),\n warn: (...messages: unknown[]) => log(LogLevel.WARN, colors.yellow, ...messages),\n error: (...messages: unknown[]) => log(LogLevel.ERROR, colors.red, ...messages),\n \n // Special formatted logs\n task: (taskId: string, message: string, taskName?: string) => {\n // If taskName is provided, use it; otherwise, just use \"Task\"\n const prefix = taskName ? `Task [${taskName}]:` : 'Task:';\n log(LogLevel.INFO, colors.magenta, `${prefix} ${message}`);\n },\n \n agent: (agentName: string, message: string) => {\n log(LogLevel.INFO, colors.cyan, `Agent [${agentName}]: ${message}`);\n },\n \n database: (operation: string, message: string) => {\n log(LogLevel.DEBUG, colors.blue, `${operation}: ${message}`);\n },\n \n memory: (operation: string, message: string) => {\n log(LogLevel.DEBUG, colors.magenta, `${operation}: ${message}`);\n },\n \n session: (sessionId: string, message: string) => {\n const shortId = sessionId.substring(0, 8);\n log(LogLevel.INFO, colors.green, `${shortId}: ${message}`);\n },\n \n plugin: (pluginName: string, message: string) => {\n // Capitalize first letter of plugin name and remove brackets\n const capitalizedPluginName = pluginName.charAt(0).toUpperCase() + pluginName.slice(1);\n log(LogLevel.INFO, colors.yellow, `${capitalizedPluginName}: ${message}`);\n },\n \n workflow: (workflowName: string, message: string) => {\n log(LogLevel.INFO, colors.cyan, `${workflowName}: ${message}`);\n },\n \n // Progress indicators - these don't add line breaks to avoid disrupting the animation\n startProgress: (message: string): NodeJS.Timeout => {\n if (options.level > LogLevel.INFO) return setInterval(() => {}, 1000);\n \n // Optionally add a line break before starting progress\n if (options.lineBreak) {\n safeConsole('log');\n }\n \n process.stdout.write(`${createPrefix(colors.blue)}${colors.blue}${message}`);\n \n const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\n let i = 0;\n \n return setInterval(() => {\n process.stdout.write(`\\r${createPrefix(colors.blue)}${colors.blue}${message} ${colors.cyan}${chars[i]}${colors.reset}`);\n i = (i + 1) % chars.length;\n }, 100);\n },\n \n endProgress: (interval: NodeJS.Timeout, finalMessage?: string) => {\n clearInterval(interval);\n if (options.level > LogLevel.INFO) return;\n \n if (finalMessage) {\n process.stdout.write(`\\r${createPrefix(colors.blue)}${colors.green}${finalMessage} ✓${colors.reset}\\n`);\n } else {\n process.stdout.write(`\\r${createPrefix(colors.blue)}${colors.green}Done ✓${colors.reset}\\n`);\n }\n },\n \n // Configure line breaks\n setLineBreak: (enabled: boolean) => {\n configure({ lineBreak: enabled });\n },\n};\n\n\nexport default logger; ","// Agent-related constants\nexport const DEFAULT_AGENT_NAME = 'astreus-agent';\nexport const DEFAULT_MODEL = 'gpt-4o-mini';\nexport const DEFAULT_TEMPERATURE = 0.3;\nexport const DEFAULT_MAX_TOKENS = 4096; ","// Memory-related constants\nexport const DEFAULT_MEMORY_SIZE = 10; ","// Database-related constants\nexport const DEFAULT_DB_PATH = './.astreus/db';\n\n// Vector database constants\nexport const VECTOR_DATABASE_TYPES = {\n SAME_AS_MAIN: 'same_as_main',\n POSTGRES: 'postgres',\n QDRANT: 'qdrant',\n PINECONE: 'pinecone',\n MILVUS: 'milvus',\n WEAVIATE: 'weaviate',\n}; ","// Provider-related constants\nexport const PROVIDER_TYPES = {\n OPENAI: 'openai',\n OLLAMA: 'ollama',\n};\n\n// Default Provider configuration\nexport const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';\nexport const DEFAULT_OPENAI_EMBEDDING_MODEL = 'text-embedding-3-small';\nexport const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434';\n\n// Default OpenAI model configs\nexport const DEFAULT_MODEL_CONFIGS = {\n openai: {\n \"gpt-4o\": {\n apiKey: process.env.OPENAI_API_KEY || '',\n baseUrl: process.env.OPENAI_BASE_URL,\n temperature: 0.7,\n maxTokens: 4096\n },\n \"gpt-4o-mini\": {\n apiKey: process.env.OPENAI_API_KEY || '',\n baseUrl: process.env.OPENAI_BASE_URL,\n temperature: 0.7,\n maxTokens: 2048\n },\n \"gpt-4\": {\n apiKey: process.env.OPENAI_API_KEY || '',\n baseUrl: process.env.OPENAI_BASE_URL,\n temperature: 0.7,\n maxTokens: 4096\n },\n \"gpt-3.5-turbo\": {\n apiKey: process.env.OPENAI_API_KEY || '',\n baseUrl: process.env.OPENAI_BASE_URL,\n temperature: 0.7,\n maxTokens: 2048\n }\n },\n ollama: {\n \"llama3\": {\n baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',\n temperature: 0.7,\n maxTokens: 2048\n },\n \"mistral\": {\n baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',\n temperature: 0.7,\n maxTokens: 2048\n }\n }\n}; ","// RAG-related constants\nexport const DEFAULT_CHUNK_SIZE = 1000;\nexport const DEFAULT_CHUNK_OVERLAP = 200;\nexport const DEFAULT_VECTOR_SIMILARITY_THRESHOLD = 0.7;\nexport const DEFAULT_MAX_RESULTS = 10; ","// Task-related constants\nexport const DEFAULT_TASK_CONCURRENCY = 5;\nexport const DEFAULT_MAX_RETRIES = 3;\nexport const TASK_STATUS = {\n PENDING: 'pending',\n RUNNING: 'running',\n COMPLETED: 'completed',\n FAILED: 'failed',\n}; ","// Error messages\nexport const ERROR_MESSAGES = {\n MISSING_PARAMETER: 'Missing required parameter:',\n INVALID_PROVIDER: 'Invalid provider configuration',\n INVALID_MEMORY: 'Invalid memory configuration',\n INVALID_DATABASE: 'Invalid database configuration',\n INVALID_RAG: 'Invalid RAG configuration',\n INVALID_VECTOR_DB: 'Invalid vector database configuration',\n INVALID_TASK: 'Invalid task configuration',\n INVALID_EMBEDDING: 'Invalid embedding configuration',\n}; ","// Re-export all constants from their respective files\nexport * from './agent';\nexport * from './memory';\nexport * from './database';\nexport * from './provider';\nexport * from './rag';\nexport * from './task';\nexport * from './error'; ","import { \n ProviderType,\n OpenAIModelConfig,\n ProviderMessage,\n ProviderModel,\n CompletionOptions,\n ProviderTool\n} from '../types/provider';\nimport { logger } from \"../utils/logger\";\nimport { OpenAI } from \"openai\";\n\n/**\n * Create OpenAI configuration with defaults\n */\nexport function createOpenAIConfig(\n modelName: string,\n config?: Partial<OpenAIModelConfig>\n): OpenAIModelConfig {\n return {\n name: modelName,\n apiKey: process.env.OPENAI_API_KEY,\n baseUrl: process.env.OPENAI_BASE_URL,\n temperature: 0.7,\n maxTokens: 2048,\n ...config,\n };\n}\n\n/**\n * OpenAI Provider implementation\n */\nexport class OpenAIProvider implements ProviderModel {\n public provider: ProviderType;\n public name: string;\n public config: OpenAIModelConfig;\n private client: OpenAI;\n \n constructor(provider: ProviderType, config: OpenAIModelConfig) {\n this.provider = provider;\n this.name = config.name;\n this.config = config;\n \n this.client = new OpenAI({\n apiKey: config.apiKey || process.env.OPENAI_API_KEY,\n baseURL: config.baseUrl || process.env.OPENAI_BASE_URL,\n });\n }\n \n async complete(messages: ProviderMessage[], options?: CompletionOptions): Promise<string | any> {\n try {\n // Prepare messages\n const formattedMessages = this.prepareMessages(messages, options?.systemMessage);\n \n // Build request options\n const requestOptions = this.buildRequestOptions(formattedMessages, options);\n \n // Log request info\n logger.debug(`OpenAI request: model=${this.name}`, { \n messages: formattedMessages.length, \n hasTools: !!requestOptions.tools,\n toolCount: requestOptions.tools?.length || 0 \n });\n \n // Make API request\n const response = await this.client.chat.completions.create(requestOptions);\n \n // Handle response - now can return either string or object with tool calls\n return this.processResponse(response);\n } catch (error) {\n this.handleError(error);\n throw error;\n }\n }\n \n async streamComplete(\n messages: ProviderMessage[], \n options?: CompletionOptions,\n onChunk?: (chunk: string) => void\n ): Promise<string> {\n try {\n // Prepare messages\n const formattedMessages = this.prepareMessages(messages, options?.systemMessage);\n \n // Build request options with streaming enabled\n const requestOptions = this.buildRequestOptions(formattedMessages, options);\n requestOptions.stream = true;\n \n // Log request info\n logger.debug(`OpenAI streaming request: model=${this.name}`, { \n messages: formattedMessages.length, \n hasTools: !!requestOptions.tools,\n toolCount: requestOptions.tools?.length || 0 \n });\n \n // Make streaming API request\n const stream = await this.client.chat.completions.create(requestOptions) as any;\n \n let fullResponse = '';\n \n // Process streaming response\n for await (const chunk of stream) {\n const content = chunk.choices[0]?.delta?.content || '';\n if (content) {\n fullResponse += content;\n if (onChunk) {\n onChunk(fullResponse);\n }\n }\n }\n \n return fullResponse;\n } catch (error) {\n this.handleError(error);\n throw error;\n }\n }\n \n private prepareMessages(messages: ProviderMessage[], systemMessage?: string) {\n // Convert to OpenAI message format\n const formattedMessages = messages.map(msg => ({\n role: msg.role,\n content: msg.content\n }));\n \n // Add system message if provided\n if (systemMessage) {\n formattedMessages.unshift({\n role: \"system\" as const,\n content: systemMessage\n });\n }\n \n return formattedMessages;\n }\n \n private buildRequestOptions(messages: any[], options?: CompletionOptions) {\n // Base request options\n const requestOptions: any = {\n model: this.name,\n messages,\n temperature: options?.temperature ?? this.config.temperature ?? 0.7,\n max_tokens: options?.maxTokens ?? this.config.maxTokens\n };\n \n // Add tools if provided\n if (options?.tools && options.tools.length > 0) {\n requestOptions.tools = options.tools.map(tool => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description || \"\",\n parameters: this.formatToolParameters(tool.parameters)\n }\n }));\n \n // Set tool choice if enabled\n if (options.toolCalling) {\n requestOptions.tool_choice = \"auto\";\n }\n }\n \n return requestOptions;\n }\n \n private formatToolParameters(parameters: any): Record<string, any> {\n // Default empty schema with proper type\n const schemaObject: Record<string, any> = {\n type: \"object\",\n properties: {},\n additionalProperties: false\n };\n \n if (!parameters) {\n return schemaObject;\n }\n \n // Handle already formatted parameters\n if (typeof parameters === 'object') {\n // If it's already a proper JSON Schema object, use it directly\n if ('type' in parameters && parameters.type === 'object') {\n return parameters;\n }\n \n // If it has properties field, use that\n if ('properties' in parameters) {\n schemaObject.properties = parameters.properties;\n if (Array.isArray(parameters.required) && parameters.required.length > 0) {\n schemaObject.required = parameters.required;\n }\n return schemaObject;\n }\n \n // Handle array of parameter definitions\n if (Array.isArray(parameters)) {\n const requiredParams: string[] = [];\n \n parameters.forEach(param => {\n if (typeof param === 'object' && param.name && param.type) {\n // Create a property definition based on the parameter type\n const propertyDef: Record<string, any> = {\n type: param.type,\n description: param.description || `Parameter ${param.name}`\n };\n \n // Handle array type specifically\n if (param.type === 'array') {\n // Ensure arrays have an items definition\n propertyDef.items = param.items || { type: 'string' };\n \n // Add array constraints if available\n if (param.minItems !== undefined) propertyDef.minItems = param.minItems;\n if (param.maxItems !== undefined) propertyDef.maxItems = param.maxItems;\n }\n \n // Add any default value\n if (param.default !== undefined) {\n propertyDef.default = param.default;\n }\n \n // Add property to schema\n (schemaObject.properties as Record<string, any>)[param.name] = propertyDef;\n \n if (param.required) {\n requiredParams.push(param.name);\n }\n }\n });\n \n if (requiredParams.length > 0) {\n schemaObject.required = requiredParams;\n }\n }\n }\n \n return schemaObject;\n }\n \n private processResponse(response: any): string | any {\n // Check if the response has the expected structure\n if (!response.choices?.[0]?.message) {\n // Log the actual response structure for debugging\n logger.error('Unexpected OpenAI API response structure:', {\n responseId: response.id,\n responseObject: JSON.stringify(response),\n hasChoices: !!response.choices,\n choicesLength: response.choices?.length,\n firstChoice: response.choices?.[0] ? 'exists' : 'missing',\n hasMessage: !!response.choices?.[0]?.message\n });\n \n // Throw with more specific information about what's missing\n if (!response.choices) {\n throw new Error(`OpenAI API response missing 'choices' field: ${JSON.stringify(response)}`);\n } else if (!response.choices.length) {\n throw new Error(`OpenAI API response has empty 'choices' array: ${JSON.stringify(response)}`);\n } else if (!response.choices[0].message) {\n throw new Error(`OpenAI API response missing 'message' in first choice: ${JSON.stringify(response.choices[0])}`);\n } else {\n throw new Error(`OpenAI API unexpected response structure: ${JSON.stringify(response)}`);\n }\n }\n \n const message = response.choices[0].message;\n const toolCalls = message.tool_calls;\n \n // Handle tool calls if present - Return both message content and structured tool calls\n if (toolCalls?.length > 0) {\n // Log detailed raw tool calls for debugging\n logger.debug('OpenAI raw tool calls:', JSON.stringify(toolCalls, null, 2));\n \n // Return structured data instead of formatted text\n return {\n content: message.content || '',\n tool_calls: toolCalls.map((call: any) => {\n try {\n if (call.type === 'function') {\n // Parse arguments to JavaScript object\n let args = {};\n if (call.function?.arguments) {\n try {\n args = typeof call.function.arguments === 'string'\n ? JSON.parse(call.function.arguments)\n : call.function.arguments;\n } catch (e) {\n logger.error('Error parsing function arguments', { error: e });\n }\n }\n \n return {\n type: 'function',\n id: call.id,\n name: call.function?.name,\n arguments: args\n };\n }\n return call;\n } catch (e) {\n logger.error('Error processing tool call', { error: e });\n return { \n type: 'error',\n error: e instanceof Error ? e.message : String(e)\n };\n }\n })\n };\n }\n \n // Return plain text response\n return message.content || '';\n }\n \n private handleError(error: any): void {\n if (!error) return;\n \n // Log the full error details\n logger.error('OpenAI API error:', {\n message: error.message,\n status: error.status,\n type: error.type,\n headers: error.headers,\n code: error.code,\n param: error.param,\n error: error.error\n });\n \n if (error.stack) {\n logger.debug(`Error stack: ${error.stack}`);\n }\n }\n} ","import {\n ProviderType,\n OllamaModelConfig,\n ProviderMessage,\n ProviderModel,\n CompletionOptions,\n ProviderTool,\n StructuredCompletionResponse\n} from \"../types/provider\";\nimport { DEFAULT_OLLAMA_BASE_URL } from \"../constants\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Create Ollama configuration helper\n */\nexport function createOllamaConfig(\n modelName: string,\n config?: Partial<OllamaModelConfig>\n): OllamaModelConfig {\n return {\n name: modelName,\n baseUrl: process.env.OLLAMA_BASE_URL || DEFAULT_OLLAMA_BASE_URL,\n temperature: 0.7,\n maxTokens: 2048,\n ...config,\n };\n}\n\n// Ollama Provider implementation\nexport class OllamaProvider implements ProviderModel {\n public provider: ProviderType;\n public name: string;\n public config: OllamaModelConfig;\n private baseUrl: string;\n\n constructor(provider: ProviderType, config: OllamaModelConfig) {\n this.provider = provider;\n this.name = config.name;\n this.config = config;\n this.baseUrl =\n config.baseUrl || process.env.OLLAMA_BASE_URL || DEFAULT_OLLAMA_BASE_URL;\n }\n\n async complete(messages: ProviderMessage[], options?: CompletionOptions): Promise<string | StructuredCompletionResponse> {\n try {\n // Format messages for Ollama\n const formattedMessages = this.prepareMessages(messages, options?.systemMessage);\n \n // Build request options\n const requestOptions = this.buildRequestOptions(formattedMessages, options);\n \n // Log request info\n logger.debug(`Ollama request: model=${this.name}`, { \n messages: formattedMessages.length, \n hasTools: !!options?.tools,\n toolCount: options?.tools?.length || 0 \n });\n\n // Call Ollama API\n const response = await fetch(`${this.baseUrl}/api/chat`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestOptions),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Ollama API error: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n \n // Process the response\n return this.processResponse(data);\n } catch (error) {\n this.handleError(error);\n throw error;\n }\n }\n \n private prepareMessages(messages: ProviderMessage[], systemMessage?: string) {\n // Convert to Ollama message format\n const formattedMessages = messages.map(msg => ({\n role: msg.role,\n content: msg.content\n }));\n \n // Add system message if provided\n if (systemMessage) {\n formattedMessages.unshift({\n role: \"system\" as const,\n content: systemMessage\n });\n }\n \n return formattedMessages;\n }\n \n private buildRequestOptions(messages: any[], options?: CompletionOptions) {\n // Base request options for Ollama\n const requestOptions: any = {\n model: this.name,\n messages: messages,\n options: {\n temperature: options?.temperature ?? this.config.temperature ?? 0.7,\n num_predict: options?.maxTokens ?? this.config.maxTokens\n },\n stream: false,\n };\n \n // Add tools if provided\n if (options?.tools && options.tools.length > 0 && options.toolCalling) {\n requestOptions.tools = options.tools.map(tool => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description || \"\",\n parameters: this.formatToolParameters(tool.parameters)\n }\n }));\n }\n \n return requestOptions;\n }\n \n private formatToolParameters(parameters: any): Record<string, any> {\n // Default empty schema with proper type\n const schemaObject: Record<string, any> = {\n type: \"object\",\n properties: {},\n additionalProperties: false\n };\n \n if (!parameters) {\n return schemaObject;\n }\n \n // Handle already formatted parameters\n if (typeof parameters === 'object') {\n // If it's already a proper JSON Schema object, use it directly\n if ('type' in parameters && parameters.type === 'object') {\n return parameters;\n }\n \n // If it has properties field, use that\n if ('properties' in parameters) {\n schemaObject.properties = parameters.properties;\n if (Array.isArray(parameters.required) && parameters.required.length > 0) {\n schemaObject.required = parameters.required;\n }\n return schemaObject;\n }\n \n // Handle array of parameter definitions\n if (Array.isArray(parameters)) {\n const requiredParams: string[] = [];\n \n parameters.forEach(param => {\n if (typeof param === 'object' && param.name && param.type) {\n // Create a property definition based on the parameter type\n const propertyDef: Record<string, any> = {\n type: param.type,\n description: param.description || `Parameter ${param.name}`\n };\n \n // Handle array type specifically\n if (param.type === 'array') {\n // Ensure arrays have an items definition\n propertyDef.items = param.items || { type: 'string' };\n \n // Add array constraints if available\n if (param.minItems !== undefined) propertyDef.minItems = param.minItems;\n if (param.maxItems !== undefined) propertyDef.maxItems = param.maxItems;\n }\n \n // Add any default value\n if (param.default !== undefined) {\n propertyDef.default = param.default;\n }\n \n // Add property to schema\n (schemaObject.properties as Record<string, any>)[param.name] = propertyDef;\n \n if (param.required) {\n requiredParams.push(param.name);\n }\n }\n });\n \n if (requiredParams.length > 0) {\n schemaObject.required = requiredParams;\n }\n }\n }\n \n return schemaObject;\n }\n \n private processResponse(data: any): string | StructuredCompletionResponse {\n // Check if the response has the expected structure\n if (!data?.message) {\n logger.error('Unexpected Ollama API response structure:', {\n responseObject: JSON.stringify(data)\n });\n \n throw new Error(`Ollama API unexpected response structure: ${JSON.stringify(data)}`);\n }\n \n const message = data.message;\n const toolCalls = message.tool_calls;\n \n // Handle tool calls if present\n if (toolCalls?.length > 0) {\n // Log detailed raw tool calls for debugging\n logger.debug('Ollama raw tool calls:', JSON.stringify(toolCalls, null, 2));\n \n // Return structured data instead of formatted text\n return {\n content: message.content || '',\n tool_calls: toolCalls.map((call: any) => {\n try {\n if (call.type === 'function') {\n // Parse arguments to JavaScript object\n let args = {};\n if (call.function?.arguments) {\n try {\n args = typeof call.function.arguments === 'string'\n ? JSON.parse(call.function.arguments)\n : call.function.arguments;\n } catch (e) {\n logger.error('Error parsing function arguments', { error: e });\n }\n }\n \n return {\n type: 'function',\n id: call.id,\n name: call.function?.name,\n arguments: args\n };\n }\n return call;\n } catch (e) {\n logger.error('Error processing tool call', { error: e });\n return { \n type: 'error',\n error: e instanceof Error ? e.message : String(e)\n };\n }\n })\n };\n }\n \n // Return plain text response\n return message.content || '';\n }\n \n private handleError(error: any): void {\n if (!error) return;\n \n // Log the full error details\n logger.error('Ollama API error:', {\n message: error.message,\n status: error.status,\n type: error.type,\n stack: error.stack,\n });\n }\n}\n","import OpenAI from \"openai\";\nimport dotenv from \"dotenv\";\nimport { logger } from \"../utils/logger\";\nimport { DEFAULT_OPENAI_EMBEDDING_MODEL } from \"../constants\";\n\n// Initialize environment variables\ndotenv.config();\n\n/**\n * Simple utility for generating embeddings without requiring provider setup\n */\nexport class Embedding {\n private static client: OpenAI | null = null;\n\n /**\n * Initialize the OpenAI client if not already initialized\n */\n private static initClient(): OpenAI {\n if (!this.client) {\n const apiKey =\n process.env.OPENAI_EMBEDDING_API_KEY || process.env.OPENAI_API_KEY;\n\n if (!apiKey) {\n throw new Error(\n \"OpenAI API key is required for embeddings - set OPENAI_API_KEY or OPENAI_EMBEDDING_API_KEY\"\n );\n }\n\n this.client = new OpenAI({\n apiKey,\n baseURL: \"https://api.openai.com/v1\",\n });\n }\n\n return this.client;\n }\n\n /**\n * Generate an embedding for the given text\n * @param text Text to generate embedding for\n * @param model Embedding model to use (default: text-embedding-ada-002)\n * @returns Embedding vector as array of numbers\n */\n static async generateEmbedding(\n text: string,\n model: string = process.env.OPENAI_EMBEDDING_MODEL ||\n DEFAULT_OPENAI_EMBEDDING_MODEL\n ): Promise<number[]> {\n try {\n if (!text || typeof text !== \"string\") {\n throw new Error(\"Invalid text input for embedding generation\");\n }\n\n const client = this.initClient();\n logger.debug(`Generating embedding for text with model: ${model}`);\n\n const response = await client.embeddings.create({\n model,\n input: text,\n encoding_format: \"float\",\n });\n\n return response.data[0].embedding;\n } catch (error) {\n logger.error(\"Error generating embedding:\", error);\n throw error;\n }\n }\n\n /**\n * Calculate cosine similarity between two embedding vectors\n * @param embedding1 First embedding vector\n * @param embedding2 Second embedding vector\n * @returns Similarity score (1.0 = identical, 0.0 = completely different)\n */\n static calculateSimilarity(\n embedding1: number[],\n embedding2: number[]\n ): number {\n if (!embedding1 || !embedding2 || embedding1.length !== embedding2.length) {\n return 0;\n }\n\n // Calculate dot product\n let dotProduct = 0;\n let magnitude1 = 0;\n let magnitude2 = 0;\n\n for (let i = 0; i < embedding1.length; i++) {\n dotProduct += embedding1[i] * embedding2[i];\n magnitude1 += embedding1[i] * embedding1[i];\n magnitude2 += embedding2[i] * embedding2[i];\n }\n\n magnitude1 = Math.sqrt(magnitude1);\n magnitude2 = Math.sqrt(magnitude2);\n\n // Avoid division by zero\n if (magnitude1 === 0 || magnitude2 === 0) {\n return 0;\n }\n\n // Return cosine similarity\n return dotProduct / (magnitude1 * magnitude2);\n }\n\n /**\n * Find similar texts based on embedding similarity\n * @param queryEmbedding Embedding to compare against\n * @param textEmbeddings Array of objects with text and embedding\n * @param limit Maximum number of results to return\n * @returns Array of results sorted by similarity (highest first)\n */\n static findSimilarTexts(\n queryEmbedding: number[],\n textEmbeddings: Array<{ text: string; embedding: number[] }>,\n limit: number = 5\n ): Array<{ text: string; similarity: number }> {\n if (!queryEmbedding || !textEmbeddings || textEmbeddings.length === 0) {\n return [];\n }\n\n // Calculate similarity for each text\n const similarities = textEmbeddings\n .map(({ text, embedding }) => ({\n text,\n similarity: this.calculateSimilarity(queryEmbedding, embedding),\n }))\n // Sort by similarity (highest first)\n .sort((a, b) => b.similarity - a.similarity)\n // Limit number of results\n .slice(0, limit);\n\n return similarities;\n }\n\n /**\n * Check if embeddings are available (OpenAI API key and valid model)\n * @param model Optional model to test\n * @returns True if embeddings are available, false otherwise\n */\n static async isAvailable(model?: string): Promise<boolean> {\n try {\n // Check if API key exists\n const apiKey =\n process.env.OPENAI_EMBEDDING_API_KEY || process.env.OPENAI_API_KEY;\n if (!apiKey) {\n logger.warn(\"OpenAI API key not found for embeddings\");\n return false;\n }\n\n // Try to generate a test embedding\n const testEmbedding = await this.generateEmbedding(\"test\", model);\n return Array.isArray(testEmbedding) && testEmbedding.length > 0;\n } catch (error) {\n logger.warn(\"Embedding test failed:\", error);\n return false;\n }\n }\n\n /**\n * List available embedding models from OpenAI\n * @returns Array of model IDs\n */\n static async listAvailableModels(): Promise<string[]> {\n try {\n const client = this.initClient();\n const models = await client.models.list();\n\n return models.data\n .filter((model) => model.id.includes(\"embedding\"))\n .map((model) => model.id);\n } catch (error) {\n logger.error(\"Error listing available models:\", error);\n return [];\n }\n }\n} ","export { OpenAIProvider, createOpenAIConfig } from \"./openai\";\n\nexport { OllamaProvider, createOllamaConfig } from \"./ollama\";\n\nexport { Embedding } from \"./embedding\";\n\nimport { ProviderType, ProviderInstance, ProviderModel } from '../types/provider';\nimport { OpenAIModelConfig } from '../types/provider';\nimport { OllamaModelConfig } from '../types/provider';\n\nimport { OpenAIProvider, createOpenAIConfig } from './openai';\nimport { OllamaProvider, createOllamaConfig } from './ollama';\n\nexport const createProvider = (config: Record<string, unknown>): ProviderInstance => {\n if (config.type === 'openai') {\n \n return {\n type: 'openai' as ProviderType,\n \n \n listModels(): string[] {\n \n return (config.models as string[]) || [config.model as string || 'gpt-3.5-turbo'];\n },\n \n \n getModel(name: string): ProviderModel {\n const modelConfig = createOpenAIConfig(name, config as Partial<OpenAIModelConfig>);\n return new OpenAIProvider('openai', modelConfig);\n },\n \n \n getDefaultModel(): string {\n return config.model as string || 'gpt-3.5-turbo';\n },\n \n \n getEmbeddingModel(): string {\n return config.embeddingModel as string || 'text-embedding-3-small';\n },\n\n // Add generateEmbedding method for RAG support\n async generateEmbedding(text: string): Promise<number[] | null> {\n try {\n const { Embedding } = await import('./embedding');\n const embeddingModel = config.embeddingModel as string || 'text-embedding-3-small';\n return await Embedding.generateEmbedding(text, embeddingModel);\n } catch (error) {\n console.error('Error generating embedding:', error);\n return null;\n }\n }\n };\n } else if (config.type === 'ollama') {\n \n return {\n type: 'ollama' as ProviderType,\n \n listModels(): string[] {\n return (config.models as string[]) || [config.model as string || 'llama2'];\n },\n \n getModel(name: string): ProviderModel {\n \n const ollamaConfigBase = {\n name: name,\n ...config as Record<string, unknown>\n };\n \n const ollamaConfig = createOllamaConfig \n ? createOllamaConfig(name, config as Partial<OllamaModelConfig>) \n : ollamaConfigBase as OllamaModelConfig;\n \n return new OllamaProvider('ollama', ollamaConfig);\n },\n \n getDefaultModel(): string {\n return config.model as string || 'llama2';\n }\n };\n } else {\n throw new Error(`Unknown provider type: ${config.type}`);\n }\n}; ","// Astreus - AI Agent Framework\n\nimport { logger } from './utils/logger';\nimport { createAgent } from './agent';\nimport { createProvider } from './providers';\nimport { createMemory } from './memory';\nimport { createDatabase } from './database';\nimport { createRAG, parsePDF, parseDirectoryOfPDFs } from './rag';\nimport { createVectorDatabaseConnector, loadVectorDatabaseConfigFromEnv } from './rag/vector-db';\nimport { createChat } from './chat';\n\nexport { createAgent };\nexport { createProvider };\nexport { createMemory };\nexport { createDatabase };\nexport { createRAG };\nexport { createChat };\nexport { parsePDF, parseDirectoryOfPDFs };\nexport { logger };\nexport { createVectorDatabaseConnector, loadVectorDatabaseConfigFromEnv };\n\nexport * from './types';\nexport * from \"./constants\";\nexport * from \"./utils\";\nexport * from \"./tasks\";\nexport { validateRequiredParam, validateRequiredParams } from \"./utils/validation\";\nexport { PluginManager } from \"./plugin\";\n","import { v4 as uuidv4 } from \"uuid\";\nimport { AgentConfig, AgentInstance, AgentFactory, Plugin, ProviderModel, ProviderInstance, MemoryInstance, ChatInstance, ChatMetadata, ChatSummary } from \"./types\";\nimport { createDatabase } from \"./database\";\nimport { PluginManager } from \"./plugin\";\nimport { validateRequiredParams, validateRequiredParam } from \"./utils/validation\";\nimport { logger } from \"./utils/logger\";\nimport { createRAGTools } from \"./utils/rag-tools\";\nimport { \n DEFAULT_AGENT_NAME\n} from \"./constants\";\n\n// Agent implementation\nclass Agent implements AgentInstance {\n public id: string;\n public config: AgentConfig;\n private memory: MemoryInstance; // Replace any with MemoryInstance\n private tools: Map<string, Plugin>;\n private chatManager?: ChatInstance;\n\n constructor(config: AgentConfig) {\n // Validate required parameters\n validateRequiredParam(config, \"config\", \"Agent constructor\");\n validateRequiredParams(\n config,\n [\"memory\"], // 'name' is optional now since we have a default\n \"Agent constructor\"\n );\n \n // Ensure either model or provider is specified\n if (!config.model && !config.provider) {\n throw new Error(\"Either 'model' or 'provider' must be specified in agent config\");\n }\n \n // If provider is given but model is not, use default model from provider\n if (config.provider && !config.model) {\n const defaultModelName = config.provider.getDefaultModel?.() || config.provider.listModels()[0];\n if (defaultModelName) {\n config.model = config.provider.getModel(defaultModelName);\n } else {\n throw new Error(\"No default model available in provider\");\n }\n }\n \n // Ensure we have a model at this point\n if (!config.model) {\n throw new Error(\"No model could be determined for the agent\");\n }\n \n // Set default values for optional parameters\n this.id = config.id || uuidv4();\n this.config = {\n ...config,\n name: config.name || DEFAULT_AGENT_NAME,\n description: config.description || `Agent ${config.name || DEFAULT_AGENT_NAME}`,\n tools: config.tools || [],\n plugins: config.plugins || []\n };\n this.memory = config.memory;\n this.tools = new Map();\n this.chatManager = config.chat;\n\n // Initialize tools if provided\n if (this.config.tools) {\n this.config.tools.forEach((tool) => {\n this.tools.set(tool.name, tool);\n });\n }\n\n // Create RAG tools if RAG instance is provided\n if (this.config.rag) {\n const ragTools = createRAGTools(this.config.rag);\n ragTools.forEach((tool) => {\n this.tools.set(tool.name, tool);\n });\n logger.debug(`Added ${ragTools.length} RAG tools to agent ${this.config.name}`);\n }\n\n // Initialize plugins and register their tools if provided\n if (this.config.plugins) {\n for (const plugin of this.config.plugins) {\n // Check if plugin has getTools method (PluginInstance)\n if (plugin && 'getTools' in plugin && typeof plugin.getTools === 'function') {\n const pluginTools = plugin.getTools();\n \n if (pluginTools && Array.isArray(pluginTools)) {\n pluginTools.forEach((tool: Plugin) => {\n if (tool && tool.name) {\n this.tools.set(tool.name, tool);\n // Also register with the global registry\n PluginManager.register(tool);\n }\n });\n }\n } \n // Check if it's a direct Plugin object\n else if (plugin && 'name' in plugin && plugin.name && 'execute' in plugin) {\n // This is already a tool/plugin, register it directly\n const toolPlugin = plugin as Plugin;\n this.tools.set(toolPlugin.name, toolPlugin);\n PluginManager.register(toolPlugin);\n }\n }\n }\n\n\n }\n\n\n\n // Helper method to safely get the model\n getModel(): ProviderModel {\n if (!this.config.model) {\n throw new Error(\"No model specified for agent\");\n }\n return this.config.model;\n }\n\n // Get the provider instance\n getProvider(): ProviderInstance | undefined {\n return this.config.provider;\n }\n\n // Memory access methods\n async getHistory(sessionId: string, limit?: number): Promise<any[]> {\n validateRequiredParam(sessionId, \"sessionId\", \"getHistory\");\n return await this.memory.getBySession(sessionId, limit);\n }\n\n async clearHistory(sessionId: string): Promise<void> {\n validateRequiredParam(sessionId, \"sessionId\", \"clearHistory\");\n await this.memory.clear(sessionId);\n }\n\n async addToMemory(params: {\n sessionId: string;\n role: 'user' | 'assistant' | 'system';\n content: string;\n metadata?: Record<string, unknown>;\n }): Promise<string> {\n validateRequiredParam(params.sessionId, \"params.sessionId\", \"addToMemory\");\n validateRequiredParam(params.role, \"params.role\", \"addToMemory\");\n validateRequiredParam(params.content, \"params.content\", \"addToMemory\");\n\n return await this.memory.add({\n agentId: this.id,\n sessionId: params.sessionId,\n role: params.role,\n content: params.content,\n metadata: params.metadata || {}\n });\n }\n\n // List all sessions for this agent\n async listSessions(limit?: number): Promise<{\n sessionId: string;\n lastMessage?: string;\n messageCount: number;\n lastActivity: Date;\n metadata?: Record<string, unknown>;\n }[]> {\n try {\n // Get all sessions from memory for this agent\n const sessions = await this.memory.listSessions(this.id, limit);\n \n return sessions.map((session: any) => ({\n sessionId: session.sessionId,\n lastMessage: session.lastMessage || session.content,\n messageCount: session.messageCount || 1,\n lastActivity: session.lastActivity || session.createdAt || new Date(),\n metadata: session.metadata || {}\n }));\n } catch (error) {\n logger.error(`Error listing sessions for agent ${this.id}:`, error);\n return [];\n }\n }\n\n // Chat method without streaming\n async chat(params: {\n message: string;\n sessionId?: string;\n systemPrompt?: string;\n temperature?: number;\n maxTokens?: number;\n metadata?: Record<string, unknown>;\n }): Promise<string> {\n validateRequiredParam(params.message, \"params.message\", \"chat\");\n\n const {\n message,\n sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n systemPrompt = this.config.systemPrompt,\n temperature = 0.7,\n maxTokens = 2000,\n metadata = {}\n } = params;\n\n // Get conversation history\n const history = sessionId ? await this.getHistory(sessionId) : [];\n\n // Prepare messages for the model\n const messages = [\n ...(systemPrompt ? [{ role: 'system' as const, content: systemPrompt }] : []),\n ...history.map((msg: any) => ({\n role: (msg.role === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',\n content: msg.content\n })),\n { role: 'user' as const, content: message }\n ];\n\n // Get response from model\n const model = this.getModel();\n const response = await model.complete(messages, {\n temperature,\n maxTokens\n });\n\n const responseContent = typeof response === 'string' ? response : response.content;\n\n // Save to memory\n await this.addToMemory({\n sessionId,\n role: 'user',\n content: message,\n metadata\n });\n\n await this.addToMemory({\n sessionId,\n role: 'assistant',\n content: responseContent,\n metadata\n });\n\n return responseContent;\n }\n\n // Streaming chat method\n async streamChat(params: {\n message: string;\n sessionId?: string;\n systemPrompt?: string;\n temperature?: number;\n maxTokens?: number;\n metadata?: Record<string, unknown>;\n onChunk?: (chunk: string) => void;\n }): Promise<string> {\n validateRequiredParam(params.message, \"params.message\", \"streamChat\");\n\n const {\n message,\n sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n systemPrompt = this.config.systemPrompt,\n temperature = 0.7,\n maxTokens = 2000,\n metadata = {},\n onChunk\n } = params;\n\n // Get conversation history\n const history = sessionId ? await this.getHistory(sessionId) : [];\n\n // Prepare messages for the model\n const messages = [\n ...(systemPrompt ? [{ role: 'system' as const, content: systemPrompt }] : []),\n ...history.map((msg: any) => ({\n role: (msg.role === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',\n content: msg.content\n })),\n { role: 'user' as const, content: message }\n ];\n\n let fullResponse = '';\n const model = this.getModel();\n