mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 11:41:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0f0f653fe | ||
|
|
98a4ff8450 | ||
|
|
d6333aaf33 | ||
|
|
8005094267 |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.5.5" \
|
||||
"praisonai>=2.5.7" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.5.5" \
|
||||
"praisonai>=2.5.7" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.5.5" \
|
||||
"praisonai>=2.5.7" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Basic Agent Example
|
||||
* Demonstrates simple agent usage with OpenAI
|
||||
*/
|
||||
|
||||
import { Agent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const agent = new Agent({
|
||||
name: 'Assistant',
|
||||
instructions: 'You are a helpful assistant. Be concise and informative.',
|
||||
llm: 'openai/gpt-4o-mini'
|
||||
});
|
||||
|
||||
const response = await agent.chat('What is TypeScript?');
|
||||
console.log('Response:', response.text);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Context Agent Example
|
||||
* Demonstrates RAG-enabled agent with knowledge base
|
||||
*/
|
||||
|
||||
import { ContextAgent, createContextAgent, KnowledgeBase } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create knowledge base
|
||||
const kb = new KnowledgeBase();
|
||||
|
||||
// Add documents
|
||||
await kb.add({
|
||||
id: 'doc1',
|
||||
content: 'TypeScript is a strongly typed programming language that builds on JavaScript.',
|
||||
metadata: { topic: 'typescript' }
|
||||
});
|
||||
|
||||
await kb.add({
|
||||
id: 'doc2',
|
||||
content: 'TypeScript adds optional static typing and class-based object-oriented programming.',
|
||||
metadata: { topic: 'typescript' }
|
||||
});
|
||||
|
||||
await kb.add({
|
||||
id: 'doc3',
|
||||
content: 'PraisonAI is a framework for building AI agents in TypeScript and Python.',
|
||||
metadata: { topic: 'praisonai' }
|
||||
});
|
||||
|
||||
// Create context agent with knowledge base
|
||||
const agent = createContextAgent({
|
||||
name: 'KnowledgeAgent',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
knowledgeBase: kb,
|
||||
maxContextDocs: 3
|
||||
});
|
||||
|
||||
// Query with context
|
||||
const response = await agent.chat('What is TypeScript?');
|
||||
console.log('Response:', response.text);
|
||||
|
||||
// Check conversation history
|
||||
console.log('\nConversation history:', agent.getHistory().length, 'messages');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Image Agent Example
|
||||
* Demonstrates image analysis capabilities
|
||||
*/
|
||||
|
||||
import { ImageAgent, createImageAgent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const agent = createImageAgent({
|
||||
name: 'VisionAgent',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
verbose: true
|
||||
});
|
||||
|
||||
console.log('Image Agent created:', agent.name);
|
||||
|
||||
// Note: For actual image analysis, provide a real image URL
|
||||
// const analysis = await agent.analyze({
|
||||
// imageUrl: 'https://example.com/image.jpg',
|
||||
// prompt: 'Describe this image in detail'
|
||||
// });
|
||||
// console.log('Analysis:', analysis);
|
||||
|
||||
// Chat without image
|
||||
const response = await agent.chat('What types of images can you analyze?');
|
||||
console.log('Response:', response);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Multi-Agent Example
|
||||
* Demonstrates multiple agents working together
|
||||
*/
|
||||
|
||||
import { Agent, PraisonAIAgents, Task } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create specialized agents
|
||||
const researcher = new Agent({
|
||||
name: 'Researcher',
|
||||
role: 'Research Specialist',
|
||||
goal: 'Find accurate information on topics',
|
||||
instructions: 'You research topics thoroughly and provide factual information.'
|
||||
});
|
||||
|
||||
const writer = new Agent({
|
||||
name: 'Writer',
|
||||
role: 'Content Writer',
|
||||
goal: 'Create engaging content from research',
|
||||
instructions: 'You write clear, engaging content based on provided research.'
|
||||
});
|
||||
|
||||
// Create tasks
|
||||
const researchTask = new Task({
|
||||
description: 'Research the benefits of TypeScript over JavaScript',
|
||||
expectedOutput: 'A list of key benefits with explanations',
|
||||
agent: researcher
|
||||
});
|
||||
|
||||
const writeTask = new Task({
|
||||
description: 'Write a short article about TypeScript benefits',
|
||||
expectedOutput: 'A 200-word article',
|
||||
agent: writer
|
||||
});
|
||||
|
||||
// Run agents
|
||||
const agents = new PraisonAIAgents({
|
||||
agents: [researcher, writer],
|
||||
tasks: [researchTask, writeTask],
|
||||
process: 'sequential'
|
||||
});
|
||||
|
||||
const result = await agents.start();
|
||||
console.log('Result:', result);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Deep Research Agent Example
|
||||
* Demonstrates comprehensive research with citations
|
||||
*/
|
||||
|
||||
import { DeepResearchAgent, createDeepResearchAgent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const agent = createDeepResearchAgent({
|
||||
name: 'Scholar',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
maxIterations: 5,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
console.log('Research Agent:', agent.name);
|
||||
|
||||
// Conduct research
|
||||
const result = await agent.research('What are the key benefits of TypeScript?');
|
||||
|
||||
console.log('\n=== Research Results ===');
|
||||
console.log('Answer:', result.answer.substring(0, 500) + '...');
|
||||
console.log('\nConfidence:', result.confidence);
|
||||
console.log('Reasoning steps:', result.reasoning.length);
|
||||
|
||||
// Show reasoning
|
||||
console.log('\n=== Reasoning Process ===');
|
||||
result.reasoning.forEach(step => {
|
||||
console.log(`Step ${step.step}: ${step.thought}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Router Agent Example
|
||||
* Demonstrates routing requests to specialized agents
|
||||
*/
|
||||
|
||||
import { RouterAgent, createRouter, routeConditions, Agent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create specialized agents
|
||||
const mathAgent = {
|
||||
name: 'MathAgent',
|
||||
chat: async (input: string) => ({ text: `Math answer for: ${input}` })
|
||||
} as any;
|
||||
|
||||
const codeAgent = {
|
||||
name: 'CodeAgent',
|
||||
chat: async (input: string) => ({ text: `Code solution for: ${input}` })
|
||||
} as any;
|
||||
|
||||
const generalAgent = {
|
||||
name: 'GeneralAgent',
|
||||
chat: async (input: string) => ({ text: `General response: ${input}` })
|
||||
} as any;
|
||||
|
||||
// Create router with conditions
|
||||
const router = createRouter({
|
||||
name: 'SmartRouter',
|
||||
routes: [
|
||||
{
|
||||
agent: mathAgent,
|
||||
condition: routeConditions.keywords(['math', 'calculate', 'number', 'equation']),
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
agent: codeAgent,
|
||||
condition: routeConditions.keywords(['code', 'program', 'function', 'javascript']),
|
||||
priority: 10
|
||||
}
|
||||
],
|
||||
defaultAgent: generalAgent
|
||||
});
|
||||
|
||||
// Test routing
|
||||
const queries = [
|
||||
'Calculate 2 + 2',
|
||||
'Write a function to sort an array',
|
||||
'What is the weather today?'
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
const result = await router.route(query);
|
||||
console.log(`Query: "${query}"`);
|
||||
console.log(`Routed to: ${result?.agent.name || 'default'}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* AutoAgents Example
|
||||
* Demonstrates automatic agent generation from task descriptions
|
||||
*/
|
||||
|
||||
import { AutoAgents, createAutoAgents } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const auto = createAutoAgents({
|
||||
pattern: 'sequential',
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Analyze task complexity
|
||||
console.log('=== Complexity Analysis ===');
|
||||
const tasks = [
|
||||
'Write a hello world function',
|
||||
'Build a web scraper that extracts product data',
|
||||
'Create a multi-step data pipeline with validation, transformation, and multiple output formats'
|
||||
];
|
||||
|
||||
tasks.forEach(task => {
|
||||
const complexity = auto.analyzeComplexity(task);
|
||||
console.log(`"${task.substring(0, 40)}..." -> ${complexity}`);
|
||||
});
|
||||
|
||||
// Recommend patterns
|
||||
console.log('\n=== Pattern Recommendations ===');
|
||||
const patternTasks = [
|
||||
'Process items in parallel',
|
||||
'Route requests to different handlers',
|
||||
'Orchestrate multiple workers',
|
||||
'Evaluate and optimize results'
|
||||
];
|
||||
|
||||
patternTasks.forEach(task => {
|
||||
const pattern = auto.recommendPattern(task);
|
||||
console.log(`"${task}" -> ${pattern}`);
|
||||
});
|
||||
|
||||
// Generate agent configuration
|
||||
console.log('\n=== Generate Agent Config ===');
|
||||
console.log('Generating agents for: "Build a web scraper"');
|
||||
|
||||
const team = await auto.generate('Build a web scraper that extracts product prices from e-commerce sites');
|
||||
|
||||
console.log('\nGenerated Team:');
|
||||
console.log('Pattern:', team.pattern);
|
||||
console.log('Agents:', team.agents.length);
|
||||
team.agents.forEach(agent => {
|
||||
console.log(` - ${agent.name}: ${agent.role}`);
|
||||
});
|
||||
console.log('Tasks:', team.tasks.length);
|
||||
team.tasks.forEach(task => {
|
||||
console.log(` - ${task.description.substring(0, 50)}...`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Redis Database Example
|
||||
*/
|
||||
|
||||
import { createMemoryRedis } from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
// Use in-memory Redis for this example
|
||||
const redis = createMemoryRedis();
|
||||
await redis.connect();
|
||||
|
||||
console.log('Connected to Redis');
|
||||
|
||||
// Basic get/set
|
||||
await redis.set('user:123', { name: 'John', role: 'admin' });
|
||||
const user = await redis.get('user:123');
|
||||
console.log('User:', user);
|
||||
|
||||
// Set with TTL
|
||||
await redis.set('session:abc', { token: 'xyz' }, 60);
|
||||
const ttl = await redis.ttl('session:abc');
|
||||
console.log('Session TTL:', ttl, 'seconds');
|
||||
|
||||
// Hash operations
|
||||
await redis.hset('profile:123', 'email', '[email protected]');
|
||||
await redis.hset('profile:123', 'phone', '555-1234');
|
||||
const profile = await redis.hgetall('profile:123');
|
||||
console.log('Profile:', profile);
|
||||
|
||||
// List operations
|
||||
await redis.rpush('queue:tasks', { id: 1, type: 'process' });
|
||||
await redis.rpush('queue:tasks', { id: 2, type: 'notify' });
|
||||
const tasks = await redis.lrange('queue:tasks', 0, -1);
|
||||
console.log('Tasks:', tasks);
|
||||
|
||||
// Pub/Sub (memory only)
|
||||
await redis.subscribe('events', (message) => {
|
||||
console.log('Received event:', message);
|
||||
});
|
||||
await redis.publish('events', JSON.stringify({ type: 'user_joined' }));
|
||||
|
||||
// Cleanup
|
||||
await redis.disconnect();
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Evaluation Framework Example
|
||||
* Demonstrates accuracy, performance, and reliability evaluation
|
||||
*/
|
||||
|
||||
import { accuracyEval, performanceEval, reliabilityEval, EvalSuite } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Accuracy Evaluation ===');
|
||||
const accuracyResult = await accuracyEval({
|
||||
input: 'What is 2 + 2?',
|
||||
expectedOutput: '4',
|
||||
actualOutput: 'The answer is 4'
|
||||
});
|
||||
console.log('Passed:', accuracyResult.passed);
|
||||
console.log('Score:', accuracyResult.score);
|
||||
console.log('Duration:', accuracyResult.duration, 'ms');
|
||||
|
||||
console.log('\n=== Performance Evaluation ===');
|
||||
const perfResult = await performanceEval({
|
||||
func: async () => {
|
||||
// Simulate some work
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
return 'done';
|
||||
},
|
||||
iterations: 5,
|
||||
warmupRuns: 1
|
||||
});
|
||||
console.log('Iterations:', perfResult.times.length);
|
||||
console.log('Avg time:', perfResult.avgTime.toFixed(2), 'ms');
|
||||
console.log('Min time:', perfResult.minTime.toFixed(2), 'ms');
|
||||
console.log('Max time:', perfResult.maxTime.toFixed(2), 'ms');
|
||||
|
||||
console.log('\n=== Reliability Evaluation ===');
|
||||
const reliabilityResult = await reliabilityEval({
|
||||
expectedToolCalls: ['search', 'calculate'],
|
||||
actualToolCalls: ['search', 'calculate']
|
||||
});
|
||||
console.log('Passed:', reliabilityResult.passed);
|
||||
console.log('Score:', reliabilityResult.score);
|
||||
|
||||
console.log('\n=== Eval Suite ===');
|
||||
const suite = new EvalSuite();
|
||||
|
||||
await suite.runAccuracy('test1', {
|
||||
input: 'Hello',
|
||||
expectedOutput: 'Hi',
|
||||
actualOutput: 'Hi there!'
|
||||
});
|
||||
|
||||
await suite.runAccuracy('test2', {
|
||||
input: 'Goodbye',
|
||||
expectedOutput: 'Bye',
|
||||
actualOutput: 'Bye!'
|
||||
});
|
||||
|
||||
const summary = suite.getSummary();
|
||||
console.log('Total tests:', summary.total);
|
||||
console.log('Passed:', summary.passed);
|
||||
console.log('Failed:', summary.failed);
|
||||
console.log('Pass rate:', (summary.passRate * 100).toFixed(1) + '%');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Content Guardrails Example
|
||||
* Demonstrates input/output validation
|
||||
*/
|
||||
|
||||
import { guardrail, GuardrailManager, builtinGuardrails, LLMGuardrail } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create custom guardrail
|
||||
const noSpamGuardrail = guardrail({
|
||||
name: 'no_spam',
|
||||
check: (content) => {
|
||||
const spamWords = ['buy now', 'free money', 'click here'];
|
||||
const hasSpam = spamWords.some(word =>
|
||||
content.toLowerCase().includes(word)
|
||||
);
|
||||
return {
|
||||
status: hasSpam ? 'failed' : 'passed',
|
||||
message: hasSpam ? 'Content contains spam' : undefined
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Use built-in guardrails
|
||||
const maxLength = builtinGuardrails.maxLength(500);
|
||||
const minLength = builtinGuardrails.minLength(10);
|
||||
const blockedWords = builtinGuardrails.blockedWords(['badword1', 'badword2']);
|
||||
|
||||
// Create manager
|
||||
const manager = new GuardrailManager();
|
||||
manager.add(noSpamGuardrail);
|
||||
manager.add(maxLength);
|
||||
manager.add(minLength);
|
||||
manager.add(blockedWords);
|
||||
|
||||
// Test content
|
||||
const testCases = [
|
||||
'This is a normal, helpful response about programming.',
|
||||
'BUY NOW! Free money if you click here!',
|
||||
'Short',
|
||||
'This is a valid response that provides useful information about TypeScript and its benefits for developers.'
|
||||
];
|
||||
|
||||
console.log('=== Guardrail Tests ===\n');
|
||||
|
||||
for (const content of testCases) {
|
||||
console.log(`Content: "${content.substring(0, 40)}..."`);
|
||||
const result = await manager.runAll(content);
|
||||
console.log(` Passed: ${result.passed}`);
|
||||
if (!result.passed) {
|
||||
result.results
|
||||
.filter(r => r.status === 'failed')
|
||||
.forEach(r => console.log(` Failed: ${r.message}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// LLM Guardrail example
|
||||
console.log('=== LLM Guardrail ===');
|
||||
const llmGuard = new LLMGuardrail({
|
||||
name: 'quality_check',
|
||||
criteria: 'Content must be helpful and professional'
|
||||
});
|
||||
console.log('LLM Guardrail created:', llmGuard.name);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Knowledge Base (RAG) Example
|
||||
* Demonstrates retrieval-augmented generation
|
||||
*/
|
||||
|
||||
import { KnowledgeBase, createKnowledgeBase, Chunking } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create knowledge base
|
||||
const kb = createKnowledgeBase();
|
||||
|
||||
// Add documents
|
||||
const documents = [
|
||||
{
|
||||
id: 'ts-intro',
|
||||
content: 'TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.',
|
||||
metadata: { category: 'introduction', language: 'typescript' }
|
||||
},
|
||||
{
|
||||
id: 'ts-types',
|
||||
content: 'TypeScript adds optional static typing and class-based object-oriented programming to the language.',
|
||||
metadata: { category: 'features', language: 'typescript' }
|
||||
},
|
||||
{
|
||||
id: 'ts-benefits',
|
||||
content: 'Benefits of TypeScript include catching errors early, better IDE support, and improved code maintainability.',
|
||||
metadata: { category: 'benefits', language: 'typescript' }
|
||||
},
|
||||
{
|
||||
id: 'js-intro',
|
||||
content: 'JavaScript is a lightweight, interpreted programming language with first-class functions.',
|
||||
metadata: { category: 'introduction', language: 'javascript' }
|
||||
}
|
||||
];
|
||||
|
||||
for (const doc of documents) {
|
||||
await kb.add(doc);
|
||||
}
|
||||
|
||||
console.log('Documents added:', kb.size);
|
||||
|
||||
// Search
|
||||
console.log('\n=== Search: "TypeScript benefits" ===');
|
||||
const results = await kb.search('TypeScript benefits', 3);
|
||||
results.forEach(r => {
|
||||
console.log(`Score: ${r.score.toFixed(2)} - ${r.document.content.substring(0, 50)}...`);
|
||||
});
|
||||
|
||||
// Build context
|
||||
console.log('\n=== Context for LLM ===');
|
||||
const context = kb.buildContext(results);
|
||||
console.log(context);
|
||||
|
||||
// Chunking example
|
||||
console.log('\n=== Chunking Example ===');
|
||||
const chunker = new Chunking({ chunkSize: 100, overlap: 20 });
|
||||
const longText = 'This is a long document. '.repeat(20);
|
||||
const chunks = chunker.chunk(longText);
|
||||
console.log(`Chunked into ${chunks.length} pieces`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Conversation Memory Example
|
||||
* Demonstrates memory management for conversations
|
||||
*/
|
||||
|
||||
import { Memory, createMemory } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const memory = createMemory({
|
||||
maxEntries: 100,
|
||||
maxTokens: 10000
|
||||
});
|
||||
|
||||
// Simulate a conversation
|
||||
await memory.add('What is TypeScript?', 'user');
|
||||
await memory.add('TypeScript is a strongly typed programming language that builds on JavaScript.', 'assistant');
|
||||
await memory.add('What are its main benefits?', 'user');
|
||||
await memory.add('Key benefits include type safety, better IDE support, and easier refactoring.', 'assistant');
|
||||
await memory.add('Can you give an example?', 'user');
|
||||
|
||||
console.log('Memory size:', memory.size, 'entries\n');
|
||||
|
||||
// Get recent messages
|
||||
console.log('=== Recent Messages ===');
|
||||
const recent = memory.getRecent(3);
|
||||
recent.forEach(entry => {
|
||||
console.log(`[${entry.role}]: ${entry.content.substring(0, 50)}...`);
|
||||
});
|
||||
|
||||
// Search memory
|
||||
console.log('\n=== Search Results for "TypeScript" ===');
|
||||
const results = await memory.search('TypeScript');
|
||||
results.forEach(r => {
|
||||
console.log(`Score: ${r.score.toFixed(2)} - ${r.entry.content.substring(0, 40)}...`);
|
||||
});
|
||||
|
||||
// Build context
|
||||
console.log('\n=== Full Context ===');
|
||||
console.log(memory.buildContext());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Observability Example
|
||||
* Demonstrates tracing and monitoring
|
||||
*/
|
||||
|
||||
import { MemoryObservabilityAdapter, ConsoleObservabilityAdapter } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Memory adapter - stores traces in memory
|
||||
const memoryAdapter = new MemoryObservabilityAdapter();
|
||||
|
||||
// Start a trace
|
||||
const trace = memoryAdapter.startTrace('agent-execution');
|
||||
console.log('Trace started:', trace.traceId);
|
||||
|
||||
// Create spans for different operations
|
||||
const llmSpan = trace.startSpan('llm-call', 'llm');
|
||||
llmSpan.setAttribute('model', 'gpt-4o-mini');
|
||||
llmSpan.setAttribute('tokens', 150);
|
||||
await new Promise(r => setTimeout(r, 100)); // Simulate work
|
||||
llmSpan.end();
|
||||
|
||||
const toolSpan = trace.startSpan('tool-execution', 'tool');
|
||||
toolSpan.setAttribute('tool', 'calculator');
|
||||
toolSpan.addEvent('tool-started', { input: '2+2' });
|
||||
await new Promise(r => setTimeout(r, 50)); // Simulate work
|
||||
toolSpan.addEvent('tool-completed', { output: '4' });
|
||||
toolSpan.end();
|
||||
|
||||
// End trace
|
||||
trace.end();
|
||||
|
||||
// Retrieve trace data
|
||||
const traceData = memoryAdapter.getTrace(trace.traceId);
|
||||
console.log('\n=== Trace Data ===');
|
||||
console.log('Trace ID:', traceData?.traceId);
|
||||
console.log('Spans:', traceData?.spans.length);
|
||||
console.log('Duration:', traceData?.endTime! - traceData?.startTime!, 'ms');
|
||||
|
||||
// Show spans
|
||||
console.log('\n=== Spans ===');
|
||||
traceData?.spans.forEach(span => {
|
||||
console.log(`- ${span.name} (${span.kind}): ${span.endTime! - span.startTime}ms`);
|
||||
if (span.attributes) {
|
||||
Object.entries(span.attributes).forEach(([k, v]) => {
|
||||
console.log(` ${k}: ${v}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Console adapter example
|
||||
console.log('\n=== Console Adapter ===');
|
||||
const consoleAdapter = new ConsoleObservabilityAdapter();
|
||||
const trace2 = consoleAdapter.startTrace('demo-trace');
|
||||
const span = trace2.startSpan('demo-span', 'custom');
|
||||
span.end();
|
||||
trace2.end();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Planning System Example
|
||||
* Demonstrates plans, steps, and todo lists
|
||||
*/
|
||||
|
||||
import { Plan, PlanStep, TodoList, TodoItem, PlanStorage, createPlan, createTodoList } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create a project plan
|
||||
console.log('=== Project Plan ===');
|
||||
const plan = createPlan({
|
||||
name: 'Build AI Agent',
|
||||
description: 'Create a fully functional AI agent'
|
||||
});
|
||||
|
||||
plan.addStep(new PlanStep({ description: 'Design agent architecture' }));
|
||||
plan.addStep(new PlanStep({ description: 'Implement core functionality' }));
|
||||
plan.addStep(new PlanStep({ description: 'Add tool support' }));
|
||||
plan.addStep(new PlanStep({ description: 'Write tests' }));
|
||||
plan.addStep(new PlanStep({ description: 'Deploy to production' }));
|
||||
|
||||
console.log('Plan:', plan.name);
|
||||
console.log('Steps:', plan.steps.length);
|
||||
|
||||
// Execute steps
|
||||
plan.start();
|
||||
plan.steps[0].start();
|
||||
plan.steps[0].complete();
|
||||
plan.steps[1].start();
|
||||
plan.steps[1].complete();
|
||||
|
||||
const progress = plan.getProgress();
|
||||
console.log(`Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)`);
|
||||
|
||||
// Create todo list
|
||||
console.log('\n=== Todo List ===');
|
||||
const todos = createTodoList('Sprint Tasks');
|
||||
|
||||
todos.add(new TodoItem({ content: 'Review PR #123', priority: 'high' }));
|
||||
todos.add(new TodoItem({ content: 'Fix bug in router', priority: 'high' }));
|
||||
todos.add(new TodoItem({ content: 'Update documentation', priority: 'medium' }));
|
||||
todos.add(new TodoItem({ content: 'Refactor tests', priority: 'low' }));
|
||||
|
||||
console.log('Total items:', todos.items.length);
|
||||
console.log('High priority:', todos.getByPriority('high').length);
|
||||
|
||||
// Complete some items
|
||||
todos.items[0].complete();
|
||||
todos.items[1].start();
|
||||
|
||||
console.log('Pending:', todos.getPending().length);
|
||||
console.log('Completed:', todos.getCompleted().length);
|
||||
|
||||
const todoProgress = todos.getProgress();
|
||||
console.log(`Progress: ${todoProgress.completed}/${todoProgress.total} (${todoProgress.percentage}%)`);
|
||||
|
||||
// Use storage
|
||||
console.log('\n=== Plan Storage ===');
|
||||
const storage = new PlanStorage();
|
||||
storage.savePlan(plan);
|
||||
storage.saveTodoList(todos);
|
||||
|
||||
console.log('Saved plans:', storage.listPlans().length);
|
||||
console.log('Saved todo lists:', storage.listTodoLists().length);
|
||||
|
||||
// Retrieve
|
||||
const retrieved = storage.getPlan(plan.id);
|
||||
console.log('Retrieved plan:', retrieved?.name);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Skills System Example
|
||||
* Demonstrates skill management and parsing
|
||||
*/
|
||||
|
||||
import { SkillManager, parseSkillFile, createSkillManager } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Parse a skill file
|
||||
const skillContent = `---
|
||||
name: code-review
|
||||
description: Reviews code for best practices and potential issues
|
||||
license: MIT
|
||||
metadata:
|
||||
author: praisonai
|
||||
version: "1.0"
|
||||
allowed-tools: Read Grep
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
When reviewing code:
|
||||
1. Check for syntax errors
|
||||
2. Look for security vulnerabilities
|
||||
3. Suggest performance improvements
|
||||
4. Ensure proper error handling
|
||||
`;
|
||||
|
||||
console.log('=== Parse Skill File ===');
|
||||
const skill = parseSkillFile(skillContent);
|
||||
console.log('Name:', skill.metadata.name);
|
||||
console.log('Description:', skill.metadata.description);
|
||||
console.log('License:', skill.metadata.license);
|
||||
console.log('Instructions preview:', skill.instructions.substring(0, 100) + '...');
|
||||
|
||||
// Create skill manager
|
||||
console.log('\n=== Skill Manager ===');
|
||||
const manager = createSkillManager({ paths: [] });
|
||||
|
||||
// Register skills manually
|
||||
manager.register({
|
||||
metadata: {
|
||||
name: 'typescript-expert',
|
||||
description: 'Expert knowledge in TypeScript development'
|
||||
},
|
||||
instructions: 'You are a TypeScript expert. Help with type definitions, generics, and best practices.'
|
||||
});
|
||||
|
||||
manager.register({
|
||||
metadata: {
|
||||
name: 'api-designer',
|
||||
description: 'Designs RESTful APIs following best practices'
|
||||
},
|
||||
instructions: 'You design clean, RESTful APIs with proper versioning and documentation.'
|
||||
});
|
||||
|
||||
// List skills
|
||||
console.log('Registered skills:');
|
||||
manager.list().forEach(s => {
|
||||
console.log(`- ${s.metadata.name}: ${s.metadata.description}`);
|
||||
});
|
||||
|
||||
// Get specific skill
|
||||
const tsSkill = manager.get('typescript-expert');
|
||||
console.log('\n=== TypeScript Expert Skill ===');
|
||||
console.log('Instructions:', tsSkill?.instructions);
|
||||
|
||||
// Generate prompt
|
||||
console.log('\n=== Generated Prompt ===');
|
||||
const prompt = manager.generatePrompt(['typescript-expert']);
|
||||
console.log(prompt.substring(0, 200) + '...');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Basic Tools Example
|
||||
* Demonstrates tool creation and usage
|
||||
*/
|
||||
|
||||
import { tool, ToolRegistry, getRegistry } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create tools
|
||||
const calculator = tool({
|
||||
name: 'calculator',
|
||||
description: 'Perform basic math calculations',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expression: { type: 'string', description: 'Math expression to evaluate' }
|
||||
},
|
||||
required: ['expression']
|
||||
},
|
||||
execute: async ({ expression }) => {
|
||||
try {
|
||||
const result = eval(expression);
|
||||
return `Result: ${result}`;
|
||||
} catch (e) {
|
||||
return 'Error: Invalid expression';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const greeter = tool({
|
||||
name: 'greeter',
|
||||
description: 'Generate a greeting',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Name to greet' }
|
||||
},
|
||||
required: ['name']
|
||||
},
|
||||
execute: async ({ name }) => `Hello, ${name}! Welcome to PraisonAI.`
|
||||
});
|
||||
|
||||
// Create registry and register tools
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(calculator);
|
||||
registry.register(greeter);
|
||||
|
||||
console.log('Registered tools:', registry.list().map(t => t.name));
|
||||
|
||||
// Execute tools
|
||||
const calcResult = await calculator.execute({ expression: '10 * 5 + 2' });
|
||||
console.log('\nCalculator:', calcResult);
|
||||
|
||||
const greetResult = await greeter.execute({ name: 'Developer' });
|
||||
console.log('Greeter:', greetResult);
|
||||
|
||||
// Get OpenAI format
|
||||
console.log('\nOpenAI tool format:', JSON.stringify(calculator.toOpenAITool(), null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Custom Tool Example
|
||||
* Demonstrates creating custom tools using BaseTool class (plugin pattern)
|
||||
*/
|
||||
|
||||
import { BaseTool, createTool, tool, ToolRegistry } from 'praisonai';
|
||||
|
||||
// Method 1: Extend BaseTool class (recommended for complex tools)
|
||||
class WeatherTool extends BaseTool<{ location: string; units?: string }, { temp: number; condition: string }> {
|
||||
name = 'get_weather';
|
||||
description = 'Get current weather for a location';
|
||||
parameters = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
location: { type: 'string', description: 'City name' },
|
||||
units: { type: 'string', description: 'celsius or fahrenheit', default: 'celsius' }
|
||||
},
|
||||
required: ['location']
|
||||
};
|
||||
|
||||
async run(params: { location: string; units?: string }): Promise<{ temp: number; condition: string }> {
|
||||
// Simulate API call
|
||||
console.log(`Getting weather for ${params.location} in ${params.units || 'celsius'}`);
|
||||
return { temp: 22, condition: 'sunny' };
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Use createTool function (quick inline tools)
|
||||
const calculatorTool = createTool({
|
||||
name: 'calculator',
|
||||
description: 'Evaluate a math expression',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expression: { type: 'string', description: 'Math expression to evaluate' }
|
||||
},
|
||||
required: ['expression']
|
||||
},
|
||||
run: (params: { expression: string }) => {
|
||||
try {
|
||||
return eval(params.expression);
|
||||
} catch {
|
||||
return 'Error: Invalid expression';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Method 3: Use tool() function with config object
|
||||
const greeterTool = tool({
|
||||
name: 'greeter',
|
||||
description: 'Generate a personalized greeting',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Name to greet' },
|
||||
style: { type: 'string', description: 'Greeting style: formal or casual' }
|
||||
},
|
||||
required: ['name']
|
||||
},
|
||||
execute: async (params: { name: string; style?: string }) => {
|
||||
if (params.style === 'formal') {
|
||||
return `Good day, ${params.name}. How may I assist you?`;
|
||||
}
|
||||
return `Hey ${params.name}! What's up?`;
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
console.log('=== Custom Tool Examples ===\n');
|
||||
|
||||
// Test WeatherTool (class-based)
|
||||
console.log('--- WeatherTool (class-based) ---');
|
||||
const weather = new WeatherTool();
|
||||
console.log('Name:', weather.name);
|
||||
console.log('Description:', weather.description);
|
||||
|
||||
const weatherResult = await weather.run({ location: 'New York' });
|
||||
console.log('Result:', weatherResult);
|
||||
|
||||
// Safe execution with error handling
|
||||
const safeResult = await weather.safeRun({ location: 'London' });
|
||||
console.log('Safe result:', safeResult);
|
||||
|
||||
// Get OpenAI schema
|
||||
console.log('OpenAI Schema:', JSON.stringify(weather.getSchema(), null, 2));
|
||||
|
||||
// Test calculatorTool (createTool)
|
||||
console.log('\n--- Calculator Tool (createTool) ---');
|
||||
const calcResult = await calculatorTool.run({ expression: '10 * 5 + 2' });
|
||||
console.log('10 * 5 + 2 =', calcResult);
|
||||
|
||||
// Test greeterTool (tool function)
|
||||
console.log('\n--- Greeter Tool (tool function) ---');
|
||||
const greeting1 = await greeterTool.execute({ name: 'Alice' });
|
||||
console.log('Casual:', greeting1);
|
||||
|
||||
const greeting2 = await greeterTool.execute({ name: 'Mr. Smith', style: 'formal' });
|
||||
console.log('Formal:', greeting2);
|
||||
|
||||
// Register tools in registry
|
||||
console.log('\n--- Tool Registry ---');
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(greeterTool);
|
||||
|
||||
console.log('Registered tools:', registry.list().map(t => t.name));
|
||||
console.log('OpenAI tools format:', registry.toOpenAITools().length, 'tools');
|
||||
|
||||
// Validate tool
|
||||
console.log('\n--- Validation ---');
|
||||
console.log('WeatherTool valid:', weather.validate());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Pinecone Vector Store Example
|
||||
*/
|
||||
|
||||
import { createPineconeStore } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const pinecone = createPineconeStore({
|
||||
apiKey: process.env.PINECONE_API_KEY!
|
||||
});
|
||||
|
||||
const indexName = 'test-index';
|
||||
|
||||
// Create index
|
||||
console.log('Creating index...');
|
||||
await pinecone.createIndex({
|
||||
indexName,
|
||||
dimension: 3,
|
||||
metric: 'cosine'
|
||||
});
|
||||
|
||||
// List indexes
|
||||
const indexes = await pinecone.listIndexes();
|
||||
console.log('Indexes:', indexes);
|
||||
|
||||
// Upsert vectors
|
||||
console.log('Upserting vectors...');
|
||||
await pinecone.upsert({
|
||||
indexName,
|
||||
vectors: [
|
||||
{ id: 'vec-1', vector: [0.1, 0.2, 0.3], metadata: { category: 'A' } },
|
||||
{ id: 'vec-2', vector: [0.4, 0.5, 0.6], metadata: { category: 'B' } },
|
||||
{ id: 'vec-3', vector: [0.7, 0.8, 0.9], metadata: { category: 'A' } }
|
||||
]
|
||||
});
|
||||
|
||||
// Query
|
||||
console.log('Querying...');
|
||||
const results = await pinecone.query({
|
||||
indexName,
|
||||
vector: [0.1, 0.2, 0.3],
|
||||
topK: 2,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
console.log('Results:');
|
||||
for (const r of results) {
|
||||
console.log(` ${r.id}: score=${r.score.toFixed(4)}, metadata=${JSON.stringify(r.metadata)}`);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
console.log('Deleting index...');
|
||||
await pinecone.deleteIndex(indexName);
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Voice/TTS Example
|
||||
*/
|
||||
|
||||
import { createOpenAIVoice } from '../../../src/praisonai-ts/src';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
async function main() {
|
||||
const voice = createOpenAIVoice({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
});
|
||||
|
||||
// Check available voices
|
||||
const speakers = await voice.getSpeakers();
|
||||
console.log('Available voices:', speakers.map(s => s.name).join(', '));
|
||||
|
||||
// Generate speech
|
||||
console.log('Generating speech...');
|
||||
const audio = await voice.speak('Hello! Welcome to PraisonAI TypeScript SDK.', {
|
||||
voice: 'nova',
|
||||
speed: 1.0,
|
||||
format: 'mp3'
|
||||
});
|
||||
|
||||
// Save to file
|
||||
writeFileSync('output.mp3', audio);
|
||||
console.log('Audio saved to output.mp3');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Basic Workflow Example
|
||||
* Demonstrates sequential workflow execution
|
||||
*/
|
||||
|
||||
import { Workflow, parallel, route, loop } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create a simple workflow
|
||||
const workflow = new Workflow<string, string>('DataProcessor')
|
||||
.step('validate', async (input) => {
|
||||
console.log('Step 1: Validating input...');
|
||||
return input.trim();
|
||||
})
|
||||
.step('transform', async (input) => {
|
||||
console.log('Step 2: Transforming...');
|
||||
return input.toUpperCase();
|
||||
})
|
||||
.step('format', async (input) => {
|
||||
console.log('Step 3: Formatting...');
|
||||
return `[PROCESSED] ${input}`;
|
||||
});
|
||||
|
||||
const result = await workflow.run(' hello world ');
|
||||
console.log('\nFinal output:', result.output);
|
||||
console.log('Steps executed:', result.steps.length);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Conditional Workflow Example
|
||||
* Demonstrates routing based on conditions
|
||||
*/
|
||||
|
||||
import { route } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const processRequest = async (type: string) => {
|
||||
console.log(`Processing request type: ${type}`);
|
||||
|
||||
const result = await route([
|
||||
{
|
||||
condition: () => type === 'urgent',
|
||||
execute: async () => {
|
||||
console.log(' -> Urgent handler activated');
|
||||
return 'Processed urgently';
|
||||
}
|
||||
},
|
||||
{
|
||||
condition: () => type === 'normal',
|
||||
execute: async () => {
|
||||
console.log(' -> Normal handler activated');
|
||||
return 'Processed normally';
|
||||
}
|
||||
},
|
||||
{
|
||||
condition: () => true, // Default
|
||||
execute: async () => {
|
||||
console.log(' -> Default handler activated');
|
||||
return 'Processed with default handler';
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Test different request types
|
||||
const types = ['urgent', 'normal', 'unknown'];
|
||||
|
||||
for (const type of types) {
|
||||
const result = await processRequest(type);
|
||||
console.log(`Result: ${result}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Parallel Workflow Example
|
||||
* Demonstrates parallel execution of tasks
|
||||
*/
|
||||
|
||||
import { parallel } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
console.log('Starting parallel tasks...\n');
|
||||
|
||||
const results = await parallel([
|
||||
async () => {
|
||||
console.log('Task 1: Starting...');
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
console.log('Task 1: Complete');
|
||||
return 'Result 1';
|
||||
},
|
||||
async () => {
|
||||
console.log('Task 2: Starting...');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
console.log('Task 2: Complete');
|
||||
return 'Result 2';
|
||||
},
|
||||
async () => {
|
||||
console.log('Task 3: Starting...');
|
||||
await new Promise(r => setTimeout(r, 750));
|
||||
console.log('Task 3: Complete');
|
||||
return 'Result 3';
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('\nAll tasks complete!');
|
||||
console.log('Results:', results);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"lastUpdated": "2024-12-26",
|
||||
"implementationStatus": "Phase 3 Complete - Full Documentation",
|
||||
"implementationStatus": "Phase 4 Complete - Full Feature Parity",
|
||||
"summary": {
|
||||
"pythonCoreFeatures": 85,
|
||||
"pythonWrapperFeatures": 45,
|
||||
"typescriptFeatures": 48,
|
||||
"gapCount": 25,
|
||||
"priorityP0": 12,
|
||||
"priorityP1": 15,
|
||||
"priorityP2": 20,
|
||||
"priorityP3": 20
|
||||
"typescriptFeatures": 75,
|
||||
"gapCount": 10,
|
||||
"priorityP0": 0,
|
||||
"priorityP1": 3,
|
||||
"priorityP2": 5,
|
||||
"priorityP3": 2
|
||||
},
|
||||
"pythonCoreSDK": {
|
||||
"path": "/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents",
|
||||
@@ -58,92 +58,115 @@
|
||||
},
|
||||
"typescriptSDK": {
|
||||
"path": "/Users/praison/praisonai-package/src/praisonai-ts/src",
|
||||
"exports": ["Agent", "PraisonAIAgents", "Task", "Tool", "BaseTool", "Memory", "BaseMemoryStore", "Process", "BaseProcess", "OpenAIService", "Logger"],
|
||||
"exports": [
|
||||
"Agent", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent",
|
||||
"RouterAgent", "Handoff", "AutoAgents", "Task", "BaseTool", "ToolResult", "tool", "FunctionTool",
|
||||
"ToolRegistry", "createTool", "validateTool", "Session", "SessionManager", "Memory", "KnowledgeBase",
|
||||
"Chunking", "MemoryAdapter", "createDbFactory", "Workflow", "parallel", "route", "Guardrail",
|
||||
"GuardrailManager", "LLMGuardrail", "accuracyEval", "performanceEval", "reliabilityEval", "EvalSuite",
|
||||
"MemoryObservabilityAdapter", "ConsoleObservabilityAdapter", "SkillManager", "TelemetryCollector",
|
||||
"Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage",
|
||||
"OpenAIProvider", "AnthropicProvider", "GoogleProvider", "createProvider"
|
||||
],
|
||||
"modules": {
|
||||
"agent": ["Agent (simple)", "Agent (types/proxy)", "PraisonAIAgents"],
|
||||
"agent": ["Agent", "ImageAgent", "ContextAgent", "DeepResearchAgent", "QueryRewriterAgent", "PromptExpanderAgent", "RouterAgent", "Handoff"],
|
||||
"auto": ["AutoAgents", "createAutoAgents"],
|
||||
"task": ["Task"],
|
||||
"tools": ["Tool interface", "BaseTool", "arxivTools", "mcpSse"],
|
||||
"llm": ["OpenAIService (OpenAI only)"],
|
||||
"memory": ["Memory interface", "BaseMemoryStore (in-memory only)"],
|
||||
"process": ["Process interface", "BaseProcess"],
|
||||
"utils": ["Logger"]
|
||||
"tools": ["BaseTool", "ToolResult", "tool", "FunctionTool", "ToolRegistry", "createTool", "validateTool", "arxivTools", "mcpSse"],
|
||||
"llm": ["OpenAIProvider", "AnthropicProvider", "GoogleProvider", "createProvider", "BaseProvider"],
|
||||
"memory": ["Memory", "createMemory", "MemoryEntry"],
|
||||
"knowledge": ["KnowledgeBase", "Chunking", "GraphRAG", "GraphStore", "BaseReranker", "CohereReranker", "CrossEncoderReranker", "LLMReranker"],
|
||||
"db": ["MemoryAdapter", "createDbFactory", "SQLiteAdapter", "UpstashRedisAdapter", "MemoryRedisAdapter", "NeonPostgresAdapter", "MemoryPostgresAdapter", "PostgresSessionStorage"],
|
||||
"session": ["Session", "SessionManager"],
|
||||
"workflows": ["Workflow", "parallel", "route", "loop", "repeat", "WorkflowStep", "WorkflowContext", "parseYAMLWorkflow", "createWorkflowFromYAML"],
|
||||
"guardrails": ["Guardrail", "GuardrailManager", "LLMGuardrail", "builtInGuardrails"],
|
||||
"eval": ["accuracyEval", "performanceEval", "reliabilityEval", "EvalSuite"],
|
||||
"observability": ["MemoryObservabilityAdapter", "ConsoleObservabilityProvider", "LangfuseObservabilityProvider", "BaseObservabilityProvider"],
|
||||
"skills": ["SkillManager", "parseSkillFile", "createSkillManager"],
|
||||
"telemetry": ["TelemetryCollector", "getTelemetry", "enableTelemetry", "disableTelemetry"],
|
||||
"planning": ["Plan", "PlanStep", "TodoList", "TodoItem", "PlanStorage"],
|
||||
"cache": ["MemoryCache", "FileCache", "BaseCache", "createMemoryCache", "createFileCache"],
|
||||
"events": ["EventEmitterPubSub", "AgentEventBus", "PubSub", "createEventBus", "createPubSub", "AgentEvents"],
|
||||
"vector": ["BaseVectorStore", "MemoryVectorStore", "PineconeVectorStore", "WeaviateVectorStore", "QdrantVectorStore", "ChromaVectorStore"],
|
||||
"voice": ["BaseVoiceProvider", "OpenAIVoiceProvider", "ElevenLabsVoiceProvider"],
|
||||
"cli": ["chat", "listProviders", "version", "help"]
|
||||
}
|
||||
},
|
||||
"gapMatrix": {
|
||||
"P0_CoreParity": [
|
||||
{"feature": "Multi-provider LLM support (LiteLLM equivalent)", "python": true, "typescript": false, "priority": "P0", "effort": "high"},
|
||||
{"feature": "Streaming responses", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Structured outputs (JSON schema)", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Tool/function calling with validation", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Session management (session_id, run_id)", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Multi-agent orchestration patterns", "python": true, "typescript": "partial", "priority": "P0", "effort": "high"},
|
||||
{"feature": "Agent handoff", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Tool decorator (@tool)", "python": true, "typescript": false, "priority": "P0", "effort": "low"},
|
||||
{"feature": "Tool registry", "python": true, "typescript": false, "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Async agent execution", "python": true, "typescript": "partial", "priority": "P0", "effort": "low"},
|
||||
{"feature": "Error handling and retries", "python": true, "typescript": "partial", "priority": "P0", "effort": "medium"},
|
||||
{"feature": "Verbose/debug logging", "python": true, "typescript": "partial", "priority": "P0", "effort": "low"}
|
||||
{"feature": "Multi-provider LLM support (OpenAI, Anthropic, Google)", "python": true, "typescript": true, "priority": "P0", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Streaming responses", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Structured outputs (JSON schema)", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Tool/function calling with validation", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Session management (session_id, run_id)", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Multi-agent orchestration patterns", "python": true, "typescript": true, "priority": "P0", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Agent handoff", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Tool decorator (@tool)", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"},
|
||||
{"feature": "Tool registry", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Async agent execution", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"},
|
||||
{"feature": "Error handling and retries", "python": true, "typescript": true, "priority": "P0", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Verbose/debug logging", "python": true, "typescript": true, "priority": "P0", "effort": "low", "status": "DONE"}
|
||||
],
|
||||
"P1_Persistence": [
|
||||
{"feature": "Database adapter protocol", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
|
||||
{"feature": "SQLite adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "PostgreSQL adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Redis adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Message persistence", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Run/trace persistence", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Memory persistence (vector)", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
|
||||
{"feature": "Knowledge base (RAG)", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
|
||||
{"feature": "Chunking strategies", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Vector store integration", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
|
||||
{"feature": "Semantic search/recall", "python": true, "typescript": false, "priority": "P1", "effort": "high"},
|
||||
{"feature": "Working memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "File-based memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Auto-memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium"},
|
||||
{"feature": "Session export/import", "python": true, "typescript": false, "priority": "P1", "effort": "medium"}
|
||||
{"feature": "Database adapter protocol", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
|
||||
{"feature": "SQLite adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "PostgreSQL adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Redis adapter", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Message persistence", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Run/trace persistence", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Memory persistence (vector)", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Knowledge base (RAG)", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Chunking strategies", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Vector store integration", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Semantic search/recall", "python": true, "typescript": true, "priority": "P1", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Working memory", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "File-based memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Auto-memory", "python": true, "typescript": false, "priority": "P1", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Session export/import", "python": true, "typescript": true, "priority": "P1", "effort": "medium", "status": "DONE"}
|
||||
],
|
||||
"P2_CLI": [
|
||||
{"feature": "CLI entrypoint", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Slash commands", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Interactive TUI", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
|
||||
{"feature": "Cost tracking", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Repo map", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
|
||||
{"feature": "Git integration", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Sandbox executor", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Autonomy modes", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Agent scheduler", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
|
||||
{"feature": "Background jobs", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Checkpoints", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Flow display", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Profiling", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Metrics", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "External agent integrations", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
|
||||
{"feature": "N8N integration", "python": true, "typescript": false, "priority": "P2", "effort": "high"},
|
||||
{"feature": "MCP CLI support", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Skills CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Eval CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"},
|
||||
{"feature": "Workflow CLI", "python": true, "typescript": false, "priority": "P2", "effort": "medium"}
|
||||
{"feature": "CLI entrypoint", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Slash commands", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Interactive TUI", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
|
||||
{"feature": "Cost tracking", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Repo map", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
|
||||
{"feature": "Git integration", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Sandbox executor", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Autonomy modes", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Agent scheduler", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
|
||||
{"feature": "Background jobs", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Checkpoints", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Flow display", "python": true, "typescript": false, "priority": "P2", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Profiling", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Metrics", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "External agent integrations", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
|
||||
{"feature": "N8N integration", "python": true, "typescript": false, "priority": "P2", "effort": "high", "status": "TODO"},
|
||||
{"feature": "MCP CLI support", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Skills CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Eval CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Workflow CLI", "python": true, "typescript": true, "priority": "P2", "effort": "medium", "status": "DONE"}
|
||||
],
|
||||
"P3_Advanced": [
|
||||
{"feature": "Workflows/Pipelines", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
|
||||
{"feature": "Route pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Parallel pattern", "python": true, "typescript": "partial", "priority": "P3", "effort": "low"},
|
||||
{"feature": "Loop pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Repeat pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "YAML workflow parser", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Guardrails", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "LLM Guardrail", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Evaluation framework", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
|
||||
{"feature": "Accuracy eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Performance eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Reliability eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Criteria eval", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Observability protocol", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Telemetry", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Performance monitoring", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Token tracking", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Skills system", "python": true, "typescript": false, "priority": "P3", "effort": "high"},
|
||||
{"feature": "Fast context", "python": true, "typescript": false, "priority": "P3", "effort": "medium"},
|
||||
{"feature": "Image agent", "python": true, "typescript": false, "priority": "P3", "effort": "medium"}
|
||||
{"feature": "Workflows/Pipelines", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Route pattern", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Parallel pattern", "python": true, "typescript": true, "priority": "P3", "effort": "low", "status": "DONE"},
|
||||
{"feature": "Loop pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Repeat pattern", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "YAML workflow parser", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Guardrails", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "LLM Guardrail", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Evaluation framework", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Accuracy eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Performance eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Reliability eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Criteria eval", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Observability protocol", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Telemetry", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Performance monitoring", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Token tracking", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"},
|
||||
{"feature": "Skills system", "python": true, "typescript": true, "priority": "P3", "effort": "high", "status": "DONE"},
|
||||
{"feature": "Fast context", "python": true, "typescript": false, "priority": "P3", "effort": "medium", "status": "TODO"},
|
||||
{"feature": "Image agent", "python": true, "typescript": true, "priority": "P3", "effort": "medium", "status": "DONE"}
|
||||
]
|
||||
},
|
||||
"mastraCapabilities": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "praisonai",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# PraisonAI CLI Specification v1.0.0
|
||||
# This is the canonical contract for CLI behavior across all runtimes (Python, TypeScript, etc.)
|
||||
# Any CLI implementation MUST conform to this spec for parity.
|
||||
|
||||
version: "1.0.0"
|
||||
spec_name: "praisonai-cli"
|
||||
|
||||
# Binary names per ecosystem (NO COLLISION)
|
||||
binaries:
|
||||
python: praisonai
|
||||
typescript: praisonai-ts
|
||||
|
||||
# Commands (all implementations MUST support)
|
||||
commands:
|
||||
chat:
|
||||
description: "Chat with an AI agent"
|
||||
args:
|
||||
- name: prompt
|
||||
type: string
|
||||
required: true
|
||||
position: 0
|
||||
flags:
|
||||
- name: model
|
||||
short: m
|
||||
type: string
|
||||
description: "Model to use (e.g., openai/gpt-4o-mini)"
|
||||
- name: stream
|
||||
short: s
|
||||
type: boolean
|
||||
default: false
|
||||
description: "Enable streaming output"
|
||||
- name: session
|
||||
type: string
|
||||
description: "Session ID for conversation continuity"
|
||||
|
||||
run:
|
||||
description: "Run an agent with a task"
|
||||
args:
|
||||
- name: task
|
||||
type: string
|
||||
required: true
|
||||
position: 0
|
||||
flags:
|
||||
- name: agent
|
||||
short: a
|
||||
type: string
|
||||
description: "Agent configuration file or name"
|
||||
- name: tools
|
||||
short: t
|
||||
type: string
|
||||
description: "Comma-separated list of tools"
|
||||
|
||||
workflow:
|
||||
description: "Execute a multi-agent workflow"
|
||||
args:
|
||||
- name: file
|
||||
type: string
|
||||
required: true
|
||||
position: 0
|
||||
description: "Workflow YAML file path"
|
||||
flags:
|
||||
- name: parallel
|
||||
type: boolean
|
||||
default: false
|
||||
description: "Run workflow steps in parallel where possible"
|
||||
|
||||
eval:
|
||||
description: "Evaluate agent performance"
|
||||
subcommands:
|
||||
accuracy:
|
||||
description: "Run accuracy evaluation"
|
||||
flags:
|
||||
- name: input
|
||||
type: string
|
||||
required: true
|
||||
- name: expected
|
||||
type: string
|
||||
required: true
|
||||
- name: iterations
|
||||
type: integer
|
||||
default: 1
|
||||
performance:
|
||||
description: "Run performance benchmark"
|
||||
flags:
|
||||
- name: iterations
|
||||
type: integer
|
||||
default: 10
|
||||
- name: warmup
|
||||
type: integer
|
||||
default: 2
|
||||
reliability:
|
||||
description: "Run reliability check"
|
||||
flags:
|
||||
- name: expected-tools
|
||||
type: string
|
||||
description: "Comma-separated expected tool calls"
|
||||
|
||||
providers:
|
||||
description: "List available LLM providers"
|
||||
flags: []
|
||||
|
||||
tools:
|
||||
description: "List or manage tools"
|
||||
subcommands:
|
||||
list:
|
||||
description: "List available tools"
|
||||
info:
|
||||
description: "Show tool information"
|
||||
args:
|
||||
- name: name
|
||||
type: string
|
||||
required: true
|
||||
position: 0
|
||||
|
||||
version:
|
||||
description: "Show CLI version"
|
||||
flags: []
|
||||
|
||||
help:
|
||||
description: "Show help information"
|
||||
args:
|
||||
- name: command
|
||||
type: string
|
||||
required: false
|
||||
position: 0
|
||||
|
||||
# Global flags (apply to all commands)
|
||||
global_flags:
|
||||
- name: verbose
|
||||
short: v
|
||||
type: boolean
|
||||
default: false
|
||||
description: "Enable verbose output"
|
||||
- name: config
|
||||
short: c
|
||||
type: string
|
||||
description: "Path to config file"
|
||||
- name: profile
|
||||
short: p
|
||||
type: string
|
||||
description: "Profile name to use"
|
||||
- name: output
|
||||
short: o
|
||||
type: string
|
||||
enum: [json, text, pretty]
|
||||
default: pretty
|
||||
description: "Output format"
|
||||
- name: json
|
||||
type: boolean
|
||||
default: false
|
||||
description: "Shorthand for --output json"
|
||||
|
||||
# Exit codes (MUST be identical across implementations)
|
||||
exit_codes:
|
||||
success: 0
|
||||
runtime_error: 1
|
||||
invalid_arguments: 2
|
||||
config_error: 3
|
||||
network_error: 4
|
||||
auth_error: 5
|
||||
|
||||
# Output schema for JSON mode (MUST be byte-compatible)
|
||||
output_schema:
|
||||
success:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
value: true
|
||||
data:
|
||||
type: object
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
duration_ms:
|
||||
type: number
|
||||
model:
|
||||
type: string
|
||||
tokens:
|
||||
type: object
|
||||
properties:
|
||||
input:
|
||||
type: integer
|
||||
output:
|
||||
type: integer
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
value: false
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
|
||||
# Environment variables (MUST be respected)
|
||||
environment:
|
||||
PRAISONAI_MODEL:
|
||||
description: "Default model to use"
|
||||
example: "openai/gpt-4o-mini"
|
||||
PRAISONAI_PROFILE:
|
||||
description: "Default profile name"
|
||||
PRAISONAI_VERBOSE:
|
||||
description: "Enable verbose mode (true/false)"
|
||||
PRAISONAI_CONFIG:
|
||||
description: "Path to config file"
|
||||
OPENAI_API_KEY:
|
||||
description: "OpenAI API key"
|
||||
ANTHROPIC_API_KEY:
|
||||
description: "Anthropic API key"
|
||||
GOOGLE_API_KEY:
|
||||
description: "Google API key"
|
||||
|
||||
# Config file schema reference
|
||||
config:
|
||||
files:
|
||||
- .praisonai.yaml
|
||||
- .praisonai.json
|
||||
resolution_order:
|
||||
- cli_flags
|
||||
- environment_variables
|
||||
- project_config
|
||||
- user_config
|
||||
- defaults
|
||||
schema:
|
||||
model:
|
||||
type: string
|
||||
default: "openai/gpt-4o-mini"
|
||||
verbose:
|
||||
type: boolean
|
||||
default: false
|
||||
stream:
|
||||
type: boolean
|
||||
default: false
|
||||
profiles:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
verbose:
|
||||
type: boolean
|
||||
stream:
|
||||
type: boolean
|
||||
|
||||
# Tool interface contract
|
||||
tool_schema:
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
description:
|
||||
type: string
|
||||
required: true
|
||||
inputs:
|
||||
type: object
|
||||
description: "JSON Schema for tool inputs"
|
||||
outputs:
|
||||
type: object
|
||||
description: "JSON Schema for tool outputs"
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* ImageAgent - Agent for image generation and analysis
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
|
||||
export interface ImageGenerationConfig {
|
||||
prompt: string;
|
||||
size?: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792';
|
||||
quality?: 'standard' | 'hd';
|
||||
style?: 'vivid' | 'natural';
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export interface ImageAnalysisConfig {
|
||||
imageUrl: string;
|
||||
prompt?: string;
|
||||
detail?: 'low' | 'high' | 'auto';
|
||||
}
|
||||
|
||||
export interface ImageAgentConfig {
|
||||
name?: string;
|
||||
llm?: string;
|
||||
imageModel?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageAgent - Agent for image generation and analysis
|
||||
*/
|
||||
export class ImageAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private imageModel: string;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: ImageAgentConfig = {}) {
|
||||
this.name = config.name || `ImageAgent_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.imageModel = config.imageModel || 'dall-e-3';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze an image
|
||||
*/
|
||||
async analyze(config: ImageAnalysisConfig): Promise<string> {
|
||||
const prompt = config.prompt || 'Describe this image in detail.';
|
||||
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: config.imageUrl,
|
||||
detail: config.detail || 'auto'
|
||||
}
|
||||
}
|
||||
] as any
|
||||
}
|
||||
];
|
||||
|
||||
const result = await this.provider.generateText({ messages });
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[ImageAgent] Analysis: ${result.text.substring(0, 100)}...`);
|
||||
}
|
||||
|
||||
return result.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image (requires OpenAI DALL-E)
|
||||
*/
|
||||
async generate(config: ImageGenerationConfig): Promise<string[]> {
|
||||
// This would require direct OpenAI API call for image generation
|
||||
// For now, return a placeholder
|
||||
if (this.verbose) {
|
||||
console.log(`[ImageAgent] Generating image: ${config.prompt}`);
|
||||
}
|
||||
|
||||
throw new Error('Image generation requires direct OpenAI API integration. Use the OpenAI SDK directly for DALL-E.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat with image context
|
||||
*/
|
||||
async chat(prompt: string, imageUrl?: string): Promise<string> {
|
||||
if (imageUrl) {
|
||||
return this.analyze({ imageUrl, prompt });
|
||||
}
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
return result.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two images
|
||||
*/
|
||||
async compare(imageUrl1: string, imageUrl2: string, prompt?: string): Promise<string> {
|
||||
const comparePrompt = prompt || 'Compare these two images and describe the differences and similarities.';
|
||||
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: comparePrompt },
|
||||
{ type: 'image_url', image_url: { url: imageUrl1 } },
|
||||
{ type: 'image_url', image_url: { url: imageUrl2 } }
|
||||
] as any
|
||||
}
|
||||
];
|
||||
|
||||
const result = await this.provider.generateText({ messages });
|
||||
return result.text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ImageAgent
|
||||
*/
|
||||
export function createImageAgent(config?: ImageAgentConfig): ImageAgent {
|
||||
return new ImageAgent(config);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* PromptExpanderAgent - Expand and enhance prompts
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
|
||||
export type ExpandStrategy = 'detail' | 'context' | 'examples' | 'constraints' | 'auto';
|
||||
|
||||
export interface ExpandResult {
|
||||
original: string;
|
||||
expanded: string;
|
||||
strategy: ExpandStrategy;
|
||||
additions: string[];
|
||||
}
|
||||
|
||||
export interface PromptExpanderConfig {
|
||||
name?: string;
|
||||
llm?: string;
|
||||
defaultStrategy?: ExpandStrategy;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* PromptExpanderAgent - Expand prompts with more detail and context
|
||||
*/
|
||||
export class PromptExpanderAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private defaultStrategy: ExpandStrategy;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: PromptExpanderConfig = {}) {
|
||||
this.name = config.name || `PromptExpander_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.defaultStrategy = config.defaultStrategy || 'auto';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a prompt
|
||||
*/
|
||||
async expand(prompt: string, strategy?: ExpandStrategy): Promise<ExpandResult> {
|
||||
const useStrategy = strategy || this.defaultStrategy;
|
||||
const actualStrategy = useStrategy === 'auto' ? this.detectStrategy(prompt) : useStrategy;
|
||||
|
||||
const systemPrompt = this.getSystemPrompt(actualStrategy);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Expand this prompt: ${prompt}` }
|
||||
]
|
||||
});
|
||||
|
||||
const expanded = result.text;
|
||||
const additions = this.extractAdditions(prompt, expanded);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[PromptExpander] Strategy: ${actualStrategy}, Added ${additions.length} elements`);
|
||||
}
|
||||
|
||||
return {
|
||||
original: prompt,
|
||||
expanded,
|
||||
strategy: actualStrategy,
|
||||
additions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the best expansion strategy
|
||||
*/
|
||||
detectStrategy(prompt: string): ExpandStrategy {
|
||||
const words = prompt.split(/\s+/).length;
|
||||
const lower = prompt.toLowerCase();
|
||||
|
||||
if (words < 5) return 'detail';
|
||||
if (lower.includes('why') || lower.includes('explain') || lower.includes('context')) return 'context';
|
||||
if (lower.includes('example') || lower.includes('show') || lower.includes('demonstrate')) return 'examples';
|
||||
if (lower.includes('must') || lower.includes('should') || lower.includes('require')) return 'constraints';
|
||||
|
||||
return 'detail';
|
||||
}
|
||||
|
||||
private getSystemPrompt(strategy: ExpandStrategy): string {
|
||||
const prompts: Record<ExpandStrategy, string> = {
|
||||
detail: 'Expand this prompt by adding specific details, requirements, and clarifications. Make it more precise and actionable.',
|
||||
context: 'Expand this prompt by adding relevant background context, explaining the purpose, and providing necessary information.',
|
||||
examples: 'Expand this prompt by adding concrete examples, sample inputs/outputs, and demonstrations of expected behavior.',
|
||||
constraints: 'Expand this prompt by adding explicit constraints, requirements, limitations, and success criteria.',
|
||||
auto: 'Expand this prompt to make it clearer, more specific, and more actionable. Add relevant details and context.'
|
||||
};
|
||||
return prompts[strategy];
|
||||
}
|
||||
|
||||
private extractAdditions(original: string, expanded: string): string[] {
|
||||
const originalWords = new Set(original.toLowerCase().split(/\s+/));
|
||||
const expandedWords = expanded.toLowerCase().split(/\s+/);
|
||||
|
||||
const newWords = expandedWords.filter(w => !originalWords.has(w) && w.length > 3);
|
||||
const uniqueNew = [...new Set(newWords)];
|
||||
|
||||
return uniqueNew.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PromptExpanderAgent
|
||||
*/
|
||||
export function createPromptExpanderAgent(config?: PromptExpanderConfig): PromptExpanderAgent {
|
||||
return new PromptExpanderAgent(config);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* QueryRewriterAgent - Rewrite and optimize queries
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
|
||||
export type RewriteStrategy = 'expand' | 'simplify' | 'decompose' | 'rephrase' | 'auto';
|
||||
|
||||
export interface RewriteResult {
|
||||
original: string;
|
||||
rewritten: string[];
|
||||
strategy: RewriteStrategy;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface QueryRewriterConfig {
|
||||
name?: string;
|
||||
llm?: string;
|
||||
defaultStrategy?: RewriteStrategy;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* QueryRewriterAgent - Optimize and rewrite queries
|
||||
*/
|
||||
export class QueryRewriterAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private defaultStrategy: RewriteStrategy;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: QueryRewriterConfig = {}) {
|
||||
this.name = config.name || `QueryRewriter_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.defaultStrategy = config.defaultStrategy || 'auto';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a query
|
||||
*/
|
||||
async rewrite(query: string, strategy?: RewriteStrategy): Promise<RewriteResult> {
|
||||
const useStrategy = strategy || this.defaultStrategy;
|
||||
const actualStrategy = useStrategy === 'auto' ? this.detectStrategy(query) : useStrategy;
|
||||
|
||||
const prompt = this.buildPrompt(query, actualStrategy);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: this.getSystemPrompt(actualStrategy) },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
|
||||
const rewritten = this.parseRewrittenQueries(result.text);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[QueryRewriter] Strategy: ${actualStrategy}, Rewrites: ${rewritten.length}`);
|
||||
}
|
||||
|
||||
return {
|
||||
original: query,
|
||||
rewritten,
|
||||
strategy: actualStrategy,
|
||||
confidence: rewritten.length > 0 ? 0.85 : 0.5
|
||||
};
|
||||
}
|
||||
|
||||
private detectStrategy(query: string): RewriteStrategy {
|
||||
const words = query.split(/\s+/).length;
|
||||
|
||||
if (words < 3) return 'expand';
|
||||
if (words > 20) return 'simplify';
|
||||
if (query.includes(' and ') || query.includes(' or ')) return 'decompose';
|
||||
return 'rephrase';
|
||||
}
|
||||
|
||||
private getSystemPrompt(strategy: RewriteStrategy): string {
|
||||
const prompts: Record<RewriteStrategy, string> = {
|
||||
expand: 'Expand this query with more context and detail. Generate 3 expanded versions.',
|
||||
simplify: 'Simplify this query to its core intent. Generate 3 simpler versions.',
|
||||
decompose: 'Break this query into smaller, focused sub-queries. Generate 3-5 sub-queries.',
|
||||
rephrase: 'Rephrase this query in different ways while keeping the same meaning. Generate 3 versions.',
|
||||
auto: 'Optimize this query for better search results. Generate 3 improved versions.'
|
||||
};
|
||||
return prompts[strategy] + ' Output one query per line.';
|
||||
}
|
||||
|
||||
private buildPrompt(query: string, strategy: RewriteStrategy): string {
|
||||
return `Query: ${query}\n\nRewrite using ${strategy} strategy:`;
|
||||
}
|
||||
|
||||
private parseRewrittenQueries(response: string): string[] {
|
||||
return response
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\d+[\.\)]\s*/, '').trim())
|
||||
.filter(line => line.length > 0 && line.length < 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a QueryRewriterAgent
|
||||
*/
|
||||
export function createQueryRewriterAgent(config?: QueryRewriterConfig): QueryRewriterAgent {
|
||||
return new QueryRewriterAgent(config);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* DeepResearchAgent - Agent for comprehensive research tasks
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
|
||||
export interface Citation {
|
||||
title: string;
|
||||
url: string;
|
||||
snippet?: string;
|
||||
}
|
||||
|
||||
export interface ReasoningStep {
|
||||
step: number;
|
||||
thought: string;
|
||||
action?: string;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface ResearchResponse {
|
||||
answer: string;
|
||||
citations: Citation[];
|
||||
reasoning: ReasoningStep[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface DeepResearchConfig {
|
||||
name?: string;
|
||||
llm?: string;
|
||||
maxIterations?: number;
|
||||
searchTool?: (query: string) => Promise<Citation[]>;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeepResearchAgent - Comprehensive research with citations
|
||||
*/
|
||||
export class DeepResearchAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private maxIterations: number;
|
||||
private searchTool?: (query: string) => Promise<Citation[]>;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: DeepResearchConfig = {}) {
|
||||
this.name = config.name || `ResearchAgent_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.maxIterations = config.maxIterations ?? 5;
|
||||
this.searchTool = config.searchTool;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conduct deep research on a topic
|
||||
*/
|
||||
async research(query: string): Promise<ResearchResponse> {
|
||||
const reasoning: ReasoningStep[] = [];
|
||||
const citations: Citation[] = [];
|
||||
let iteration = 0;
|
||||
|
||||
// Step 1: Analyze the query
|
||||
reasoning.push({
|
||||
step: ++iteration,
|
||||
thought: `Analyzing research query: "${query}"`,
|
||||
action: 'query_analysis'
|
||||
});
|
||||
|
||||
// Step 2: Generate search queries
|
||||
const searchQueries = await this.generateSearchQueries(query);
|
||||
reasoning.push({
|
||||
step: ++iteration,
|
||||
thought: `Generated ${searchQueries.length} search queries`,
|
||||
action: 'generate_queries',
|
||||
result: searchQueries.join(', ')
|
||||
});
|
||||
|
||||
// Step 3: Execute searches if tool available
|
||||
if (this.searchTool) {
|
||||
for (const sq of searchQueries.slice(0, 3)) {
|
||||
try {
|
||||
const results = await this.searchTool(sq);
|
||||
citations.push(...results);
|
||||
reasoning.push({
|
||||
step: ++iteration,
|
||||
thought: `Searched for: "${sq}"`,
|
||||
action: 'web_search',
|
||||
result: `Found ${results.length} results`
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.verbose) {
|
||||
console.log(`[Research] Search failed for: ${sq}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Synthesize answer
|
||||
const answer = await this.synthesizeAnswer(query, citations);
|
||||
reasoning.push({
|
||||
step: ++iteration,
|
||||
thought: 'Synthesizing final answer from gathered information',
|
||||
action: 'synthesize',
|
||||
result: 'Answer generated'
|
||||
});
|
||||
|
||||
// Calculate confidence based on citations
|
||||
const confidence = Math.min(0.5 + (citations.length * 0.1), 0.95);
|
||||
|
||||
return {
|
||||
answer,
|
||||
citations,
|
||||
reasoning,
|
||||
confidence
|
||||
};
|
||||
}
|
||||
|
||||
private async generateSearchQueries(query: string): Promise<string[]> {
|
||||
const result = await this.provider.generateText({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Generate 3 search queries to research this topic. Output only the queries, one per line.'
|
||||
},
|
||||
{ role: 'user', content: query }
|
||||
]
|
||||
});
|
||||
|
||||
return result.text.split('\n').filter(q => q.trim().length > 0).slice(0, 3);
|
||||
}
|
||||
|
||||
private async synthesizeAnswer(query: string, citations: Citation[]): Promise<string> {
|
||||
const context = citations.length > 0
|
||||
? `\n\nRelevant sources:\n${citations.map(c => `- ${c.title}: ${c.snippet || ''}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a research assistant. Provide a comprehensive, well-structured answer based on the available information. Cite sources when available.'
|
||||
},
|
||||
{ role: 'user', content: `${query}${context}` }
|
||||
]
|
||||
});
|
||||
|
||||
return result.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set search tool
|
||||
*/
|
||||
setSearchTool(tool: (query: string) => Promise<Citation[]>): void {
|
||||
this.searchTool = tool;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DeepResearchAgent
|
||||
*/
|
||||
export function createDeepResearchAgent(config?: DeepResearchConfig): DeepResearchAgent {
|
||||
return new DeepResearchAgent(config);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* AutoAgents - Automatic agent generation from task descriptions
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
|
||||
export interface AgentConfig {
|
||||
name: string;
|
||||
role: string;
|
||||
goal: string;
|
||||
backstory?: string;
|
||||
instructions?: string;
|
||||
tools?: string[];
|
||||
}
|
||||
|
||||
export interface TaskConfig {
|
||||
description: string;
|
||||
expectedOutput?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export interface TeamStructure {
|
||||
agents: AgentConfig[];
|
||||
tasks: TaskConfig[];
|
||||
pattern: 'sequential' | 'parallel' | 'hierarchical';
|
||||
}
|
||||
|
||||
export interface AutoAgentsConfig {
|
||||
llm?: string;
|
||||
pattern?: 'sequential' | 'parallel' | 'routing' | 'orchestrator-workers' | 'evaluator-optimizer';
|
||||
singleAgent?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AutoAgents - Generate agent configurations from task descriptions
|
||||
*/
|
||||
export class AutoAgents {
|
||||
private provider: LLMProvider;
|
||||
private pattern: string;
|
||||
private singleAgent: boolean;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: AutoAgentsConfig = {}) {
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.pattern = config.pattern || 'sequential';
|
||||
this.singleAgent = config.singleAgent ?? false;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agent configuration from task description
|
||||
*/
|
||||
async generate(taskDescription: string): Promise<TeamStructure> {
|
||||
const prompt = this.buildPrompt(taskDescription);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: this.getSystemPrompt() },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
|
||||
return this.parseResponse(result.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommend a pattern for the task
|
||||
*/
|
||||
recommendPattern(taskDescription: string): string {
|
||||
const lower = taskDescription.toLowerCase();
|
||||
|
||||
if (lower.includes('parallel') || lower.includes('concurrent') || lower.includes('simultaneously')) {
|
||||
return 'parallel';
|
||||
}
|
||||
if (lower.includes('route') || lower.includes('classify') || lower.includes('categorize')) {
|
||||
return 'routing';
|
||||
}
|
||||
if (lower.includes('orchestrat') || lower.includes('coordinat') || lower.includes('manage')) {
|
||||
return 'orchestrator-workers';
|
||||
}
|
||||
if (lower.includes('evaluat') || lower.includes('optimi') || lower.includes('improv')) {
|
||||
return 'evaluator-optimizer';
|
||||
}
|
||||
|
||||
return 'sequential';
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze task complexity
|
||||
*/
|
||||
analyzeComplexity(taskDescription: string): 'simple' | 'moderate' | 'complex' {
|
||||
const words = taskDescription.split(/\s+/).length;
|
||||
const hasMultipleSteps = /step|then|after|before|first|second|third|finally/i.test(taskDescription);
|
||||
const hasMultipleAgents = /team|multiple|several|different|various/i.test(taskDescription);
|
||||
|
||||
if (words < 20 && !hasMultipleSteps && !hasMultipleAgents) {
|
||||
return 'simple';
|
||||
}
|
||||
if (words > 100 || (hasMultipleSteps && hasMultipleAgents)) {
|
||||
return 'complex';
|
||||
}
|
||||
return 'moderate';
|
||||
}
|
||||
|
||||
private getSystemPrompt(): string {
|
||||
return `You are an AI agent architect. Your job is to analyze task descriptions and generate optimal agent configurations.
|
||||
|
||||
Output a JSON object with the following structure:
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"name": "AgentName",
|
||||
"role": "Role description",
|
||||
"goal": "Agent's goal",
|
||||
"backstory": "Optional backstory",
|
||||
"instructions": "Specific instructions",
|
||||
"tools": ["tool1", "tool2"]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"description": "Task description",
|
||||
"expectedOutput": "What the task should produce",
|
||||
"agent": "AgentName"
|
||||
}
|
||||
],
|
||||
"pattern": "sequential|parallel|hierarchical"
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- For simple tasks, use 1 agent
|
||||
- For moderate tasks, use 2-3 agents
|
||||
- For complex tasks, use 3-5 agents
|
||||
- Match the pattern to the task requirements
|
||||
- Be specific about roles and goals`;
|
||||
}
|
||||
|
||||
private buildPrompt(taskDescription: string): string {
|
||||
const complexity = this.analyzeComplexity(taskDescription);
|
||||
const recommendedPattern = this.recommendPattern(taskDescription);
|
||||
|
||||
return `Task Description: ${taskDescription}
|
||||
|
||||
Complexity: ${complexity}
|
||||
Recommended Pattern: ${this.pattern || recommendedPattern}
|
||||
Single Agent Mode: ${this.singleAgent}
|
||||
|
||||
Generate an optimal agent configuration for this task.`;
|
||||
}
|
||||
|
||||
private parseResponse(response: string): TeamStructure {
|
||||
try {
|
||||
// Extract JSON from response
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback to default structure
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
agents: [{
|
||||
name: 'GeneralAgent',
|
||||
role: 'General purpose agent',
|
||||
goal: 'Complete the assigned task',
|
||||
instructions: 'Follow the task description carefully'
|
||||
}],
|
||||
tasks: [{
|
||||
description: 'Complete the task',
|
||||
agent: 'GeneralAgent'
|
||||
}],
|
||||
pattern: 'sequential'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AutoAgents instance
|
||||
*/
|
||||
export function createAutoAgents(config?: AutoAgentsConfig): AutoAgents {
|
||||
return new AutoAgents(config);
|
||||
}
|
||||
Vendored
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Cache System - In-memory and persistent caching for agents
|
||||
* Inspired by mastra's cache module
|
||||
*/
|
||||
|
||||
export interface CacheConfig {
|
||||
name?: string;
|
||||
ttl?: number; // Time to live in milliseconds
|
||||
maxSize?: number; // Maximum number of entries
|
||||
}
|
||||
|
||||
export interface CacheEntry<T = any> {
|
||||
value: T;
|
||||
createdAt: number;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for cache implementations
|
||||
*/
|
||||
export abstract class BaseCache {
|
||||
readonly name: string;
|
||||
|
||||
constructor(config: CacheConfig = {}) {
|
||||
this.name = config.name || 'cache';
|
||||
}
|
||||
|
||||
abstract get<T = any>(key: string): Promise<T | undefined>;
|
||||
abstract set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
|
||||
abstract delete(key: string): Promise<boolean>;
|
||||
abstract clear(): Promise<void>;
|
||||
abstract has(key: string): Promise<boolean>;
|
||||
abstract keys(): Promise<string[]>;
|
||||
abstract size(): Promise<number>;
|
||||
|
||||
// List operations
|
||||
abstract listPush<T = any>(key: string, value: T): Promise<void>;
|
||||
abstract listLength(key: string): Promise<number>;
|
||||
abstract listRange<T = any>(key: string, start: number, end?: number): Promise<T[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory cache implementation
|
||||
*/
|
||||
export class MemoryCache extends BaseCache {
|
||||
private cache: Map<string, CacheEntry> = new Map();
|
||||
private lists: Map<string, any[]> = new Map();
|
||||
private ttl?: number;
|
||||
private maxSize?: number;
|
||||
|
||||
constructor(config: CacheConfig = {}) {
|
||||
super(config);
|
||||
this.ttl = config.ttl;
|
||||
this.maxSize = config.maxSize;
|
||||
}
|
||||
|
||||
async get<T = any>(key: string): Promise<T | undefined> {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
// Check expiration
|
||||
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.value as T;
|
||||
}
|
||||
|
||||
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
// Enforce max size
|
||||
if (this.maxSize && this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
||||
// Remove oldest entry
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey) this.cache.delete(firstKey);
|
||||
}
|
||||
|
||||
const effectiveTtl = ttl ?? this.ttl;
|
||||
const entry: CacheEntry<T> = {
|
||||
value,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: effectiveTtl ? Date.now() + effectiveTtl : undefined
|
||||
};
|
||||
|
||||
this.cache.set(key, entry);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
const existed = this.cache.has(key);
|
||||
this.cache.delete(key);
|
||||
this.lists.delete(key);
|
||||
return existed;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.cache.clear();
|
||||
this.lists.clear();
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return false;
|
||||
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
return Array.from(this.cache.keys());
|
||||
}
|
||||
|
||||
async size(): Promise<number> {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
async listPush<T = any>(key: string, value: T): Promise<void> {
|
||||
if (!this.lists.has(key)) {
|
||||
this.lists.set(key, []);
|
||||
}
|
||||
this.lists.get(key)!.push(value);
|
||||
}
|
||||
|
||||
async listLength(key: string): Promise<number> {
|
||||
return this.lists.get(key)?.length ?? 0;
|
||||
}
|
||||
|
||||
async listRange<T = any>(key: string, start: number, end?: number): Promise<T[]> {
|
||||
const list = this.lists.get(key) ?? [];
|
||||
return list.slice(start, end) as T[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File-based cache implementation
|
||||
*/
|
||||
export class FileCache extends BaseCache {
|
||||
private cacheDir: string;
|
||||
private fs: any;
|
||||
private path: any;
|
||||
|
||||
constructor(config: CacheConfig & { cacheDir?: string } = {}) {
|
||||
super(config);
|
||||
this.cacheDir = config.cacheDir || '.cache';
|
||||
}
|
||||
|
||||
private async ensureDir(): Promise<void> {
|
||||
if (!this.fs) {
|
||||
this.fs = await import('fs/promises');
|
||||
this.path = await import('path');
|
||||
}
|
||||
try {
|
||||
await this.fs.mkdir(this.cacheDir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private getFilePath(key: string): string {
|
||||
const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');
|
||||
return this.path.join(this.cacheDir, `${safeKey}.json`);
|
||||
}
|
||||
|
||||
async get<T = any>(key: string): Promise<T | undefined> {
|
||||
await this.ensureDir();
|
||||
try {
|
||||
const data = await this.fs.readFile(this.getFilePath(key), 'utf-8');
|
||||
const entry: CacheEntry<T> = JSON.parse(data);
|
||||
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||||
await this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return entry.value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
await this.ensureDir();
|
||||
const entry: CacheEntry<T> = {
|
||||
value,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: ttl ? Date.now() + ttl : undefined
|
||||
};
|
||||
await this.fs.writeFile(this.getFilePath(key), JSON.stringify(entry));
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
await this.ensureDir();
|
||||
try {
|
||||
await this.fs.unlink(this.getFilePath(key));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.ensureDir();
|
||||
try {
|
||||
const files = await this.fs.readdir(this.cacheDir);
|
||||
await Promise.all(
|
||||
files.filter((f: string) => f.endsWith('.json'))
|
||||
.map((f: string) => this.fs.unlink(this.path.join(this.cacheDir, f)))
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return (await this.get(key)) !== undefined;
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
await this.ensureDir();
|
||||
try {
|
||||
const files = await this.fs.readdir(this.cacheDir);
|
||||
return files
|
||||
.filter((f: string) => f.endsWith('.json'))
|
||||
.map((f: string) => f.replace('.json', ''));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async size(): Promise<number> {
|
||||
return (await this.keys()).length;
|
||||
}
|
||||
|
||||
async listPush<T = any>(key: string, value: T): Promise<void> {
|
||||
const list = (await this.get<T[]>(`list_${key}`)) || [];
|
||||
list.push(value);
|
||||
await this.set(`list_${key}`, list);
|
||||
}
|
||||
|
||||
async listLength(key: string): Promise<number> {
|
||||
const list = (await this.get<any[]>(`list_${key}`)) || [];
|
||||
return list.length;
|
||||
}
|
||||
|
||||
async listRange<T = any>(key: string, start: number, end?: number): Promise<T[]> {
|
||||
const list = (await this.get<T[]>(`list_${key}`)) || [];
|
||||
return list.slice(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createMemoryCache(config?: CacheConfig): MemoryCache {
|
||||
return new MemoryCache(config);
|
||||
}
|
||||
|
||||
export function createFileCache(config?: CacheConfig & { cacheDir?: string }): FileCache {
|
||||
return new FileCache(config);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Chat command - Chat with an AI agent
|
||||
*/
|
||||
|
||||
import { createProvider } from '../../llm/providers';
|
||||
import { Session } from '../../session';
|
||||
import { resolveConfig } from '../config/resolve';
|
||||
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
import { ERROR_CODES, normalizeError } from '../output/errors';
|
||||
|
||||
export interface ChatOptions {
|
||||
model?: string;
|
||||
stream?: boolean;
|
||||
session?: string;
|
||||
verbose?: boolean;
|
||||
profile?: string;
|
||||
config?: string;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export async function execute(args: string[], options: ChatOptions): Promise<void> {
|
||||
const prompt = args[0];
|
||||
|
||||
if (!prompt) {
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt');
|
||||
} else {
|
||||
await pretty.error('Please provide a prompt');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
// Resolve config with precedence
|
||||
const config = resolveConfig({
|
||||
configPath: options.config,
|
||||
profile: options.profile,
|
||||
model: options.model,
|
||||
verbose: options.verbose,
|
||||
stream: options.stream
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
try {
|
||||
const provider = createProvider(config.model);
|
||||
const session = new Session({ id: options.session });
|
||||
session.addMessage({ role: 'user', content: prompt });
|
||||
|
||||
let responseText = '';
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
|
||||
if (config.stream && outputFormat !== 'json') {
|
||||
// Streaming mode (only for non-JSON output)
|
||||
const stream = await provider.streamText({
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
onToken: (token: string) => {
|
||||
process.stdout.write(token);
|
||||
responseText += token;
|
||||
}
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// Stream is consumed by onToken
|
||||
}
|
||||
console.log(); // New line after streaming
|
||||
} else {
|
||||
// Non-streaming mode
|
||||
const result = await provider.generateText({
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
responseText = result.text;
|
||||
inputTokens = result.usage?.promptTokens || 0;
|
||||
outputTokens = result.usage?.completionTokens || 0;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
const duration = Date.now() - startTime;
|
||||
outputJson(formatSuccess(
|
||||
{ response: responseText },
|
||||
{
|
||||
duration_ms: duration,
|
||||
model: config.model,
|
||||
tokens: {
|
||||
input: inputTokens,
|
||||
output: outputTokens
|
||||
}
|
||||
}
|
||||
));
|
||||
} else {
|
||||
console.log(responseText);
|
||||
}
|
||||
}
|
||||
|
||||
session.addMessage({ role: 'assistant', content: responseText });
|
||||
|
||||
} catch (error) {
|
||||
const cliError = normalizeError(error);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(cliError.code, cliError.message, cliError.details));
|
||||
} else {
|
||||
await pretty.error(cliError.message);
|
||||
if (config.verbose && error instanceof Error && error.stack) {
|
||||
await pretty.dim(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(cliError.exitCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Config utilities index
|
||||
*/
|
||||
|
||||
export * from './schema';
|
||||
export * from './load';
|
||||
export * from './resolve';
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Config file loading
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CONFIG_FILES } from '../spec/cli-spec';
|
||||
import { FullConfig, validateConfig, DEFAULT_CONFIG } from './schema';
|
||||
|
||||
export interface LoadConfigOptions {
|
||||
configPath?: string;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find config file in directory hierarchy
|
||||
*/
|
||||
function findConfigFile(startDir: string): string | null {
|
||||
let currentDir = startDir;
|
||||
|
||||
while (true) {
|
||||
for (const filename of CONFIG_FILES) {
|
||||
const filePath = path.join(currentDir, filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
break; // Reached root
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user config file
|
||||
*/
|
||||
function findUserConfigFile(): string | null {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
||||
if (!homeDir) return null;
|
||||
|
||||
for (const filename of CONFIG_FILES) {
|
||||
const filePath = path.join(homeDir, filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse config file content
|
||||
*/
|
||||
function parseConfigFile(filePath: string): unknown {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (ext === '.json') {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
// Simple YAML parsing for basic config
|
||||
// For complex YAML, we'd need a proper parser
|
||||
return parseSimpleYaml(content);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported config file format: ${ext}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple YAML parser for basic key-value configs
|
||||
* Handles: strings, booleans, numbers, simple objects
|
||||
*/
|
||||
function parseSimpleYaml(content: string): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
const lines = content.split('\n');
|
||||
let currentObject: Record<string, unknown> | null = null;
|
||||
let currentKey: string | null = null;
|
||||
let baseIndent = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip empty lines and comments
|
||||
if (!line.trim() || line.trim().startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indent = line.search(/\S/);
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Check for key: value pattern
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
if (indent === 0) {
|
||||
// Top-level key
|
||||
if (value === '' || value === '{}') {
|
||||
// Start of nested object
|
||||
currentObject = {};
|
||||
currentKey = key;
|
||||
baseIndent = indent;
|
||||
result[key] = currentObject;
|
||||
} else {
|
||||
result[key] = parseYamlValue(value);
|
||||
currentObject = null;
|
||||
currentKey = null;
|
||||
}
|
||||
} else if (currentObject !== null && indent > baseIndent) {
|
||||
// Nested key
|
||||
if (value === '' || value === '{}') {
|
||||
// Nested object within nested object
|
||||
const nestedObj: Record<string, unknown> = {};
|
||||
currentObject[key] = nestedObj;
|
||||
// For simplicity, we only support one level of nesting in profiles
|
||||
} else {
|
||||
currentObject[key] = parseYamlValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseYamlValue(value: string): string | number | boolean {
|
||||
// Remove quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
|
||||
// Boolean
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
|
||||
// Number
|
||||
const num = Number(value);
|
||||
if (!isNaN(num) && value !== '') return num;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config from file
|
||||
*/
|
||||
export function loadConfigFile(filePath: string): FullConfig {
|
||||
try {
|
||||
const parsed = parseConfigFile(filePath);
|
||||
if (!validateConfig(parsed)) {
|
||||
throw new Error('Invalid config format');
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config with resolution order:
|
||||
* 1. Explicit config path
|
||||
* 2. Project config (current dir and parents)
|
||||
* 3. User config (~/.praisonai.yaml)
|
||||
* 4. Defaults
|
||||
*/
|
||||
export function loadConfig(options: LoadConfigOptions = {}): FullConfig {
|
||||
const configs: Partial<FullConfig>[] = [DEFAULT_CONFIG];
|
||||
|
||||
// User config
|
||||
const userConfigPath = findUserConfigFile();
|
||||
if (userConfigPath) {
|
||||
try {
|
||||
configs.push(loadConfigFile(userConfigPath));
|
||||
} catch {
|
||||
// Ignore user config errors
|
||||
}
|
||||
}
|
||||
|
||||
// Project config
|
||||
const cwd = options.cwd || process.cwd();
|
||||
const projectConfigPath = findConfigFile(cwd);
|
||||
if (projectConfigPath) {
|
||||
try {
|
||||
configs.push(loadConfigFile(projectConfigPath));
|
||||
} catch {
|
||||
// Ignore project config errors
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit config path
|
||||
if (options.configPath) {
|
||||
configs.push(loadConfigFile(options.configPath));
|
||||
}
|
||||
|
||||
// Merge all configs
|
||||
const result: FullConfig = { ...DEFAULT_CONFIG };
|
||||
for (const config of configs) {
|
||||
if (config.model !== undefined) result.model = config.model;
|
||||
if (config.verbose !== undefined) result.verbose = config.verbose;
|
||||
if (config.stream !== undefined) result.stream = config.stream;
|
||||
if (config.profiles !== undefined) {
|
||||
result.profiles = { ...result.profiles, ...config.profiles };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Config resolution with profiles and CLI overrides
|
||||
*/
|
||||
|
||||
import { loadConfig } from './load';
|
||||
import { loadEnvConfig } from '../runtime/env';
|
||||
import { FullConfig, ProfileConfig, DEFAULT_CONFIG } from './schema';
|
||||
|
||||
export interface ResolvedConfig {
|
||||
model: string;
|
||||
verbose: boolean;
|
||||
stream: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveOptions {
|
||||
configPath?: string;
|
||||
profile?: string;
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve final config with precedence:
|
||||
* 1. CLI flags (highest)
|
||||
* 2. Environment variables
|
||||
* 3. Profile settings
|
||||
* 4. Config file
|
||||
* 5. Defaults (lowest)
|
||||
*/
|
||||
export function resolveConfig(options: ResolveOptions = {}): ResolvedConfig {
|
||||
// Load base config from files
|
||||
const fileConfig = loadConfig({ configPath: options.configPath });
|
||||
|
||||
// Load env config
|
||||
const envConfig = loadEnvConfig();
|
||||
|
||||
// Get profile config if specified
|
||||
let profileConfig: ProfileConfig = {};
|
||||
const profileName = options.profile || envConfig.profile;
|
||||
if (profileName && fileConfig.profiles?.[profileName]) {
|
||||
profileConfig = fileConfig.profiles[profileName];
|
||||
}
|
||||
|
||||
// Resolve with precedence
|
||||
const resolved: ResolvedConfig = {
|
||||
model: DEFAULT_CONFIG.model!,
|
||||
verbose: DEFAULT_CONFIG.verbose!,
|
||||
stream: DEFAULT_CONFIG.stream!
|
||||
};
|
||||
|
||||
// Apply file config
|
||||
if (fileConfig.model !== undefined) resolved.model = fileConfig.model;
|
||||
if (fileConfig.verbose !== undefined) resolved.verbose = fileConfig.verbose;
|
||||
if (fileConfig.stream !== undefined) resolved.stream = fileConfig.stream;
|
||||
|
||||
// Apply profile config
|
||||
if (profileConfig.model !== undefined) resolved.model = profileConfig.model;
|
||||
if (profileConfig.verbose !== undefined) resolved.verbose = profileConfig.verbose;
|
||||
if (profileConfig.stream !== undefined) resolved.stream = profileConfig.stream;
|
||||
|
||||
// Apply env config
|
||||
if (envConfig.model !== undefined) resolved.model = envConfig.model;
|
||||
if (envConfig.verbose !== undefined) resolved.verbose = envConfig.verbose;
|
||||
|
||||
// Apply CLI flags (highest precedence)
|
||||
if (options.model !== undefined) resolved.model = options.model;
|
||||
if (options.verbose !== undefined) resolved.verbose = options.verbose;
|
||||
if (options.stream !== undefined) resolved.stream = options.stream;
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available profile names
|
||||
*/
|
||||
export function getAvailableProfiles(configPath?: string): string[] {
|
||||
const config = loadConfig({ configPath });
|
||||
return Object.keys(config.profiles || {});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Config schema definitions
|
||||
*/
|
||||
|
||||
import { ConfigSchema } from '../spec/cli-spec';
|
||||
|
||||
export interface ProfileConfig {
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
export interface FullConfig extends ConfigSchema {
|
||||
profiles?: Record<string, ProfileConfig>;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: FullConfig = {
|
||||
model: 'openai/gpt-4o-mini',
|
||||
verbose: false,
|
||||
stream: false,
|
||||
profiles: {}
|
||||
};
|
||||
|
||||
export function validateConfig(config: unknown): config is FullConfig {
|
||||
if (typeof config !== 'object' || config === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const c = config as Record<string, unknown>;
|
||||
|
||||
if (c.model !== undefined && typeof c.model !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.verbose !== undefined && typeof c.verbose !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.stream !== undefined && typeof c.stream !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.profiles !== undefined) {
|
||||
if (typeof c.profiles !== 'object' || c.profiles === null) {
|
||||
return false;
|
||||
}
|
||||
for (const profile of Object.values(c.profiles as Record<string, unknown>)) {
|
||||
if (typeof profile !== 'object' || profile === null) {
|
||||
return false;
|
||||
}
|
||||
const p = profile as Record<string, unknown>;
|
||||
if (p.model !== undefined && typeof p.model !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (p.verbose !== undefined && typeof p.verbose !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
if (p.stream !== undefined && typeof p.stream !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function mergeConfigs(...configs: Partial<FullConfig>[]): FullConfig {
|
||||
const result: FullConfig = { ...DEFAULT_CONFIG };
|
||||
|
||||
for (const config of configs) {
|
||||
if (config.model !== undefined) result.model = config.model;
|
||||
if (config.verbose !== undefined) result.verbose = config.verbose;
|
||||
if (config.stream !== undefined) result.stream = config.stream;
|
||||
if (config.profiles !== undefined) {
|
||||
result.profiles = { ...result.profiles, ...config.profiles };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Environment variable handling for CLI
|
||||
*/
|
||||
|
||||
import { ENV_VARS } from '../spec/cli-spec';
|
||||
|
||||
export interface EnvConfig {
|
||||
model?: string;
|
||||
profile?: string;
|
||||
verbose?: boolean;
|
||||
configPath?: string;
|
||||
}
|
||||
|
||||
export function loadEnvConfig(): EnvConfig {
|
||||
const config: EnvConfig = {};
|
||||
|
||||
const model = process.env[ENV_VARS.PRAISONAI_MODEL];
|
||||
if (model) config.model = model;
|
||||
|
||||
const profile = process.env[ENV_VARS.PRAISONAI_PROFILE];
|
||||
if (profile) config.profile = profile;
|
||||
|
||||
const verbose = process.env[ENV_VARS.PRAISONAI_VERBOSE];
|
||||
if (verbose) config.verbose = verbose.toLowerCase() === 'true';
|
||||
|
||||
const configPath = process.env[ENV_VARS.PRAISONAI_CONFIG];
|
||||
if (configPath) config.configPath = configPath;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function hasApiKey(provider: 'openai' | 'anthropic' | 'google'): boolean {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return !!process.env[ENV_VARS.OPENAI_API_KEY];
|
||||
case 'anthropic':
|
||||
return !!process.env[ENV_VARS.ANTHROPIC_API_KEY];
|
||||
case 'google':
|
||||
return !!process.env[ENV_VARS.GOOGLE_API_KEY];
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiKey(provider: 'openai' | 'anthropic' | 'google'): string | undefined {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return process.env[ENV_VARS.OPENAI_API_KEY];
|
||||
case 'anthropic':
|
||||
return process.env[ENV_VARS.ANTHROPIC_API_KEY];
|
||||
case 'google':
|
||||
return process.env[ENV_VARS.GOOGLE_API_KEY];
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Exit code handling for CLI
|
||||
*/
|
||||
|
||||
import { EXIT_CODES, ExitCode } from '../spec/cli-spec';
|
||||
|
||||
export function exit(code: ExitCode): never {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
export function exitSuccess(): never {
|
||||
exit(EXIT_CODES.SUCCESS);
|
||||
}
|
||||
|
||||
export function exitError(message?: string): never {
|
||||
if (message) {
|
||||
console.error(message);
|
||||
}
|
||||
exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
|
||||
export function exitInvalidArgs(message?: string): never {
|
||||
if (message) {
|
||||
console.error(`Error: ${message}`);
|
||||
}
|
||||
exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
export function exitConfigError(message?: string): never {
|
||||
if (message) {
|
||||
console.error(`Config Error: ${message}`);
|
||||
}
|
||||
exit(EXIT_CODES.CONFIG_ERROR);
|
||||
}
|
||||
|
||||
export function exitNetworkError(message?: string): never {
|
||||
if (message) {
|
||||
console.error(`Network Error: ${message}`);
|
||||
}
|
||||
exit(EXIT_CODES.NETWORK_ERROR);
|
||||
}
|
||||
|
||||
export function exitAuthError(message?: string): never {
|
||||
if (message) {
|
||||
console.error(`Auth Error: ${message}`);
|
||||
}
|
||||
exit(EXIT_CODES.AUTH_ERROR);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Runtime utilities index
|
||||
*/
|
||||
|
||||
export * from './env';
|
||||
export * from './exit';
|
||||
export * from './lazy';
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Lazy loading utilities for CLI commands
|
||||
* Ensures startup time stays <100ms by only loading command code when invoked
|
||||
*/
|
||||
|
||||
import { validateCommand } from '../spec/cli-spec';
|
||||
|
||||
export type CommandModule = {
|
||||
execute: (args: string[], options: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
const commandCache = new Map<string, CommandModule>();
|
||||
|
||||
export async function loadCommand(command: string): Promise<CommandModule> {
|
||||
if (!validateCommand(command)) {
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (commandCache.has(command)) {
|
||||
return commandCache.get(command)!;
|
||||
}
|
||||
|
||||
// Lazy import the command module
|
||||
const module = await import(`../commands/${command}`);
|
||||
commandCache.set(command, module);
|
||||
return module;
|
||||
}
|
||||
|
||||
export function clearCommandCache(): void {
|
||||
commandCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load optional dependencies
|
||||
* Returns undefined if not available
|
||||
*/
|
||||
export async function tryLoadOptional<T>(moduleName: string): Promise<T | undefined> {
|
||||
try {
|
||||
return await import(moduleName);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an optional dependency is available
|
||||
*/
|
||||
export async function hasOptionalDep(moduleName: string): Promise<boolean> {
|
||||
try {
|
||||
await import(moduleName);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* PraisonAI CLI Specification - TypeScript Runtime
|
||||
* Parsed and validated spec from cli-spec.v1.yaml
|
||||
*/
|
||||
|
||||
export const CLI_SPEC_VERSION = '1.0.0';
|
||||
|
||||
export interface CommandArg {
|
||||
name: string;
|
||||
type: 'string' | 'integer' | 'boolean';
|
||||
required: boolean;
|
||||
position?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CommandFlag {
|
||||
name: string;
|
||||
short?: string;
|
||||
type: 'string' | 'integer' | 'boolean';
|
||||
default?: string | number | boolean;
|
||||
enum?: string[];
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface Subcommand {
|
||||
description: string;
|
||||
args?: CommandArg[];
|
||||
flags?: CommandFlag[];
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
description: string;
|
||||
args?: CommandArg[];
|
||||
flags?: CommandFlag[];
|
||||
subcommands?: Record<string, Subcommand>;
|
||||
}
|
||||
|
||||
export const COMMANDS: Record<string, Command> = {
|
||||
chat: {
|
||||
description: 'Chat with an AI agent',
|
||||
args: [
|
||||
{ name: 'prompt', type: 'string', required: true, position: 0 }
|
||||
],
|
||||
flags: [
|
||||
{ name: 'model', short: 'm', type: 'string', description: 'Model to use (e.g., openai/gpt-4o-mini)' },
|
||||
{ name: 'stream', short: 's', type: 'boolean', default: false, description: 'Enable streaming output' },
|
||||
{ name: 'session', type: 'string', description: 'Session ID for conversation continuity' }
|
||||
]
|
||||
},
|
||||
run: {
|
||||
description: 'Run an agent with a task',
|
||||
args: [
|
||||
{ name: 'task', type: 'string', required: true, position: 0 }
|
||||
],
|
||||
flags: [
|
||||
{ name: 'agent', short: 'a', type: 'string', description: 'Agent configuration file or name' },
|
||||
{ name: 'tools', short: 't', type: 'string', description: 'Comma-separated list of tools' }
|
||||
]
|
||||
},
|
||||
workflow: {
|
||||
description: 'Execute a multi-agent workflow',
|
||||
args: [
|
||||
{ name: 'file', type: 'string', required: true, position: 0, description: 'Workflow YAML file path' }
|
||||
],
|
||||
flags: [
|
||||
{ name: 'parallel', type: 'boolean', default: false, description: 'Run workflow steps in parallel where possible' }
|
||||
]
|
||||
},
|
||||
eval: {
|
||||
description: 'Evaluate agent performance',
|
||||
subcommands: {
|
||||
accuracy: {
|
||||
description: 'Run accuracy evaluation',
|
||||
flags: [
|
||||
{ name: 'input', type: 'string', required: true },
|
||||
{ name: 'expected', type: 'string', required: true },
|
||||
{ name: 'iterations', type: 'integer', default: 1 }
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
description: 'Run performance benchmark',
|
||||
flags: [
|
||||
{ name: 'iterations', type: 'integer', default: 10 },
|
||||
{ name: 'warmup', type: 'integer', default: 2 }
|
||||
]
|
||||
},
|
||||
reliability: {
|
||||
description: 'Run reliability check',
|
||||
flags: [
|
||||
{ name: 'expected-tools', type: 'string', description: 'Comma-separated expected tool calls' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
providers: {
|
||||
description: 'List available LLM providers',
|
||||
flags: []
|
||||
},
|
||||
tools: {
|
||||
description: 'List or manage tools',
|
||||
subcommands: {
|
||||
list: {
|
||||
description: 'List available tools'
|
||||
},
|
||||
info: {
|
||||
description: 'Show tool information',
|
||||
args: [
|
||||
{ name: 'name', type: 'string', required: true, position: 0 }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
version: {
|
||||
description: 'Show CLI version',
|
||||
flags: []
|
||||
},
|
||||
help: {
|
||||
description: 'Show help information',
|
||||
args: [
|
||||
{ name: 'command', type: 'string', required: false, position: 0 }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export const GLOBAL_FLAGS: CommandFlag[] = [
|
||||
{ name: 'verbose', short: 'v', type: 'boolean', default: false, description: 'Enable verbose output' },
|
||||
{ name: 'config', short: 'c', type: 'string', description: 'Path to config file' },
|
||||
{ name: 'profile', short: 'p', type: 'string', description: 'Profile name to use' },
|
||||
{ name: 'output', short: 'o', type: 'string', enum: ['json', 'text', 'pretty'], default: 'pretty', description: 'Output format' },
|
||||
{ name: 'json', type: 'boolean', default: false, description: 'Shorthand for --output json' }
|
||||
];
|
||||
|
||||
export const EXIT_CODES = {
|
||||
SUCCESS: 0,
|
||||
RUNTIME_ERROR: 1,
|
||||
INVALID_ARGUMENTS: 2,
|
||||
CONFIG_ERROR: 3,
|
||||
NETWORK_ERROR: 4,
|
||||
AUTH_ERROR: 5
|
||||
} as const;
|
||||
|
||||
export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
|
||||
|
||||
export interface SuccessOutput<T = unknown> {
|
||||
success: true;
|
||||
data: T;
|
||||
meta?: {
|
||||
duration_ms?: number;
|
||||
model?: string;
|
||||
tokens?: {
|
||||
input?: number;
|
||||
output?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export type CLIOutput<T = unknown> = SuccessOutput<T> | ErrorOutput;
|
||||
|
||||
export const ENV_VARS = {
|
||||
PRAISONAI_MODEL: 'PRAISONAI_MODEL',
|
||||
PRAISONAI_PROFILE: 'PRAISONAI_PROFILE',
|
||||
PRAISONAI_VERBOSE: 'PRAISONAI_VERBOSE',
|
||||
PRAISONAI_CONFIG: 'PRAISONAI_CONFIG',
|
||||
OPENAI_API_KEY: 'OPENAI_API_KEY',
|
||||
ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY',
|
||||
GOOGLE_API_KEY: 'GOOGLE_API_KEY'
|
||||
} as const;
|
||||
|
||||
export const CONFIG_FILES = ['.praisonai.yaml', '.praisonai.json'] as const;
|
||||
|
||||
export interface ConfigSchema {
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
stream?: boolean;
|
||||
profiles?: Record<string, {
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
stream?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function validateCommand(command: string): command is keyof typeof COMMANDS {
|
||||
return command in COMMANDS;
|
||||
}
|
||||
|
||||
export function getCommandSpec(command: string): Command | undefined {
|
||||
return COMMANDS[command];
|
||||
}
|
||||
|
||||
export function getAllFlags(command: string): CommandFlag[] {
|
||||
const cmdSpec = COMMANDS[command];
|
||||
const cmdFlags = cmdSpec?.flags || [];
|
||||
return [...GLOBAL_FLAGS, ...cmdFlags];
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Event System - PubSub and Event Emitter for agent communication
|
||||
* Inspired by mastra's events module
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
topic: string;
|
||||
data: any;
|
||||
createdAt: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type EventHandler = (event: Event, ack?: () => Promise<void>) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Abstract PubSub base class
|
||||
*/
|
||||
export abstract class PubSub {
|
||||
abstract publish(topic: string, data: any, metadata?: Record<string, any>): Promise<void>;
|
||||
abstract subscribe(topic: string, handler: EventHandler): Promise<void>;
|
||||
abstract unsubscribe(topic: string, handler: EventHandler): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory EventEmitter-based PubSub implementation
|
||||
*/
|
||||
export class EventEmitterPubSub extends PubSub {
|
||||
private emitter: EventEmitter;
|
||||
private handlers: Map<string, Set<EventHandler>> = new Map();
|
||||
|
||||
constructor(existingEmitter?: EventEmitter) {
|
||||
super();
|
||||
this.emitter = existingEmitter ?? new EventEmitter();
|
||||
this.emitter.setMaxListeners(100); // Allow many listeners
|
||||
}
|
||||
|
||||
async publish(topic: string, data: any, metadata?: Record<string, any>): Promise<void> {
|
||||
const event: Event = {
|
||||
id: crypto.randomUUID(),
|
||||
topic,
|
||||
data,
|
||||
createdAt: new Date(),
|
||||
metadata
|
||||
};
|
||||
this.emitter.emit(topic, event);
|
||||
}
|
||||
|
||||
async subscribe(topic: string, handler: EventHandler): Promise<void> {
|
||||
if (!this.handlers.has(topic)) {
|
||||
this.handlers.set(topic, new Set());
|
||||
}
|
||||
this.handlers.get(topic)!.add(handler);
|
||||
this.emitter.on(topic, handler);
|
||||
}
|
||||
|
||||
async unsubscribe(topic: string, handler: EventHandler): Promise<void> {
|
||||
this.handlers.get(topic)?.delete(handler);
|
||||
this.emitter.off(topic, handler);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.emitter.removeAllListeners();
|
||||
this.handlers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific event with optional timeout
|
||||
*/
|
||||
async waitFor(topic: string, timeout?: number): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = timeout ? setTimeout(() => {
|
||||
this.emitter.off(topic, handler);
|
||||
reject(new Error(`Timeout waiting for event: ${topic}`));
|
||||
}, timeout) : null;
|
||||
|
||||
const handler = (event: Event) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
this.emitter.off(topic, handler);
|
||||
resolve(event);
|
||||
};
|
||||
|
||||
this.emitter.once(topic, handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying EventEmitter
|
||||
*/
|
||||
getEmitter(): EventEmitter {
|
||||
return this.emitter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent Event Bus - Specialized event system for agent communication
|
||||
*/
|
||||
export class AgentEventBus {
|
||||
private pubsub: PubSub;
|
||||
private agentId: string;
|
||||
|
||||
constructor(agentId: string, pubsub?: PubSub) {
|
||||
this.agentId = agentId;
|
||||
this.pubsub = pubsub ?? new EventEmitterPubSub();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an agent event
|
||||
*/
|
||||
async emit(eventType: string, data: any): Promise<void> {
|
||||
await this.pubsub.publish(`agent:${this.agentId}:${eventType}`, data, {
|
||||
agentId: this.agentId,
|
||||
eventType
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for agent events
|
||||
*/
|
||||
async on(eventType: string, handler: (data: any) => void | Promise<void>): Promise<void> {
|
||||
await this.pubsub.subscribe(`agent:${this.agentId}:${eventType}`, (event) => {
|
||||
handler(event.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast to all agents
|
||||
*/
|
||||
async broadcast(eventType: string, data: any): Promise<void> {
|
||||
await this.pubsub.publish(`broadcast:${eventType}`, data, {
|
||||
sourceAgentId: this.agentId,
|
||||
eventType
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for broadcast events
|
||||
*/
|
||||
async onBroadcast(eventType: string, handler: (data: any, sourceAgentId: string) => void | Promise<void>): Promise<void> {
|
||||
await this.pubsub.subscribe(`broadcast:${eventType}`, (event) => {
|
||||
handler(event.data, event.metadata?.sourceAgentId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to specific agent
|
||||
*/
|
||||
async sendTo(targetAgentId: string, eventType: string, data: any): Promise<void> {
|
||||
await this.pubsub.publish(`agent:${targetAgentId}:${eventType}`, data, {
|
||||
sourceAgentId: this.agentId,
|
||||
targetAgentId,
|
||||
eventType
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.pubsub.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Standard event types
|
||||
export const AgentEvents = {
|
||||
STARTED: 'started',
|
||||
COMPLETED: 'completed',
|
||||
ERROR: 'error',
|
||||
TOOL_CALLED: 'tool_called',
|
||||
TOOL_RESULT: 'tool_result',
|
||||
MESSAGE_RECEIVED: 'message_received',
|
||||
MESSAGE_SENT: 'message_sent',
|
||||
HANDOFF_INITIATED: 'handoff_initiated',
|
||||
HANDOFF_COMPLETED: 'handoff_completed'
|
||||
} as const;
|
||||
|
||||
// Factory functions
|
||||
export function createEventBus(agentId: string): AgentEventBus {
|
||||
return new AgentEventBus(agentId);
|
||||
}
|
||||
|
||||
export function createPubSub(): EventEmitterPubSub {
|
||||
return new EventEmitterPubSub();
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* LLMGuardrail - LLM-based content validation
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
|
||||
export interface LLMGuardrailConfig {
|
||||
name: string;
|
||||
criteria: string;
|
||||
llm?: string;
|
||||
threshold?: number;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export interface LLMGuardrailResult {
|
||||
status: 'passed' | 'failed' | 'warning';
|
||||
score: number;
|
||||
message?: string;
|
||||
reasoning?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLMGuardrail - Use LLM to validate content against criteria
|
||||
*/
|
||||
export class LLMGuardrail {
|
||||
readonly name: string;
|
||||
readonly criteria: string;
|
||||
private provider: LLMProvider;
|
||||
private threshold: number;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: LLMGuardrailConfig) {
|
||||
this.name = config.name;
|
||||
this.criteria = config.criteria;
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.threshold = config.threshold ?? 0.7;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check content against criteria
|
||||
*/
|
||||
async check(content: string): Promise<LLMGuardrailResult> {
|
||||
const prompt = `Evaluate the following content against this criteria:
|
||||
|
||||
Criteria: ${this.criteria}
|
||||
|
||||
Content: ${content}
|
||||
|
||||
Respond with a JSON object containing:
|
||||
- score: number from 0 to 1 (1 = fully meets criteria)
|
||||
- passed: boolean
|
||||
- reasoning: brief explanation
|
||||
|
||||
JSON response:`;
|
||||
|
||||
try {
|
||||
const result = await this.provider.generateText({
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const parsed = this.parseResponse(result.text);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[LLMGuardrail:${this.name}] Score: ${parsed.score}, Passed: ${parsed.status}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 'warning',
|
||||
score: 0.5,
|
||||
message: `Guardrail check failed: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run guardrail (alias for check)
|
||||
*/
|
||||
async run(content: string): Promise<LLMGuardrailResult> {
|
||||
return this.check(content);
|
||||
}
|
||||
|
||||
private parseResponse(response: string): LLMGuardrailResult {
|
||||
try {
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const score = typeof parsed.score === 'number' ? parsed.score : 0.5;
|
||||
return {
|
||||
status: score >= this.threshold ? 'passed' : 'failed',
|
||||
score,
|
||||
reasoning: parsed.reasoning
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Parse failed
|
||||
}
|
||||
|
||||
// Fallback parsing
|
||||
const hasPositive = /pass|good|valid|accept|meets/i.test(response);
|
||||
return {
|
||||
status: hasPositive ? 'passed' : 'failed',
|
||||
score: hasPositive ? 0.8 : 0.3,
|
||||
reasoning: response.substring(0, 200)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an LLMGuardrail
|
||||
*/
|
||||
export function createLLMGuardrail(config: LLMGuardrailConfig): LLMGuardrail {
|
||||
return new LLMGuardrail(config);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export * from './process';
|
||||
|
||||
// Export tools (excluding conflicting types)
|
||||
export {
|
||||
Tool, BaseTool,
|
||||
BaseTool, ToolResult, ToolValidationError, validateTool, createTool,
|
||||
FunctionTool, tool, ToolRegistry, getRegistry, registerTool, getTool,
|
||||
type ToolConfig, type ToolContext, type ToolParameters
|
||||
} from './tools';
|
||||
@@ -50,6 +50,100 @@ export { SkillManager, createSkillManager, parseSkillFile, type Skill, type Skil
|
||||
// Export CLI
|
||||
export { chat, listProviders, version, help } from './cli';
|
||||
|
||||
// Export Memory
|
||||
export { Memory, createMemory, type MemoryEntry, type MemoryConfig, type SearchResult as MemorySearchResult } from './memory/memory';
|
||||
|
||||
// Export Telemetry
|
||||
export { TelemetryCollector, getTelemetry, enableTelemetry, disableTelemetry, cleanupTelemetry, type TelemetryEvent, type TelemetryConfig } from './telemetry';
|
||||
|
||||
// Export AutoAgents
|
||||
export { AutoAgents, createAutoAgents, type AgentConfig, type TaskConfig, type TeamStructure, type AutoAgentsConfig } from './auto';
|
||||
|
||||
// Export ImageAgent
|
||||
export { ImageAgent, createImageAgent, type ImageAgentConfig, type ImageGenerationConfig, type ImageAnalysisConfig } from './agent/image';
|
||||
|
||||
// Export DeepResearchAgent
|
||||
export { DeepResearchAgent, createDeepResearchAgent, type DeepResearchConfig, type ResearchResponse, type Citation, type ReasoningStep } from './agent/research';
|
||||
|
||||
// Export QueryRewriterAgent
|
||||
export { QueryRewriterAgent, createQueryRewriterAgent, type QueryRewriterConfig, type RewriteResult, type RewriteStrategy } from './agent/query-rewriter';
|
||||
|
||||
// Export PromptExpanderAgent
|
||||
export { PromptExpanderAgent, createPromptExpanderAgent, type PromptExpanderConfig, type ExpandResult, type ExpandStrategy } from './agent/prompt-expander';
|
||||
|
||||
// Export Chunking
|
||||
export { Chunking, createChunking, type ChunkingConfig, type Chunk, type ChunkStrategy } from './knowledge/chunking';
|
||||
|
||||
// Export LLMGuardrail
|
||||
export { LLMGuardrail, createLLMGuardrail, type LLMGuardrailConfig, type LLMGuardrailResult } from './guardrails/llm-guardrail';
|
||||
|
||||
// Export Planning
|
||||
export { Plan, PlanStep, TodoList, TodoItem, PlanStorage, createPlan, createTodoList, createPlanStorage, type PlanConfig, type PlanStepConfig, type TodoItemConfig, type PlanStatus, type TodoStatus } from './planning';
|
||||
|
||||
// Export Cache
|
||||
export { BaseCache, MemoryCache, FileCache, createMemoryCache, createFileCache, type CacheConfig, type CacheEntry } from './cache';
|
||||
|
||||
// Export Events
|
||||
export { PubSub, EventEmitterPubSub, AgentEventBus, AgentEvents, createEventBus, createPubSub, type Event, type EventHandler } from './events';
|
||||
|
||||
// Export YAML Workflow Parser
|
||||
export { parseYAMLWorkflow, createWorkflowFromYAML, loadWorkflowFromFile, validateWorkflowDefinition, type YAMLWorkflowDefinition, type YAMLStepDefinition, type ParsedWorkflow } from './workflows/yaml-parser';
|
||||
|
||||
// Export SQLite Adapter
|
||||
export { SQLiteAdapter, createSQLiteAdapter, type SQLiteConfig, type DbMessage, type DbRun, type DbTrace } from './db/sqlite';
|
||||
|
||||
// Export Redis Adapter
|
||||
export {
|
||||
UpstashRedisAdapter, MemoryRedisAdapter,
|
||||
createUpstashRedis, createMemoryRedis,
|
||||
type RedisConfig, type RedisAdapter
|
||||
} from './db/redis';
|
||||
|
||||
// Export PostgreSQL Adapter
|
||||
export {
|
||||
NeonPostgresAdapter, MemoryPostgresAdapter, PostgresSessionStorage,
|
||||
createNeonPostgres, createMemoryPostgres, createPostgresSessionStorage,
|
||||
type PostgresConfig, type PostgresAdapter
|
||||
} from './db/postgres';
|
||||
|
||||
// Export Integrations - Vector Stores
|
||||
export {
|
||||
BaseVectorStore, MemoryVectorStore, createMemoryVectorStore,
|
||||
PineconeVectorStore, createPineconeStore,
|
||||
WeaviateVectorStore, createWeaviateStore,
|
||||
QdrantVectorStore, createQdrantStore,
|
||||
ChromaVectorStore, createChromaStore,
|
||||
type VectorDocument, type QueryResult as VectorQueryResult, type IndexStats
|
||||
} from './integrations/vector';
|
||||
|
||||
// Export Integrations - Observability
|
||||
export {
|
||||
BaseObservabilityProvider, ConsoleObservabilityProvider, MemoryObservabilityProvider,
|
||||
LangfuseObservabilityProvider,
|
||||
createConsoleObservability, createMemoryObservability, createLangfuseObservability,
|
||||
type Span, type TraceContext as ObservabilityTraceContext, type LogEntry, type Metric
|
||||
} from './integrations/observability';
|
||||
|
||||
// Export Integrations - Voice
|
||||
export {
|
||||
BaseVoiceProvider, OpenAIVoiceProvider, ElevenLabsVoiceProvider,
|
||||
createOpenAIVoice, createElevenLabsVoice,
|
||||
type VoiceConfig, type SpeakOptions, type ListenOptions, type Speaker
|
||||
} from './integrations/voice';
|
||||
|
||||
// Export Reranker
|
||||
export {
|
||||
BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker,
|
||||
createCohereReranker, createCrossEncoderReranker, createLLMReranker,
|
||||
type RerankResult, type RerankConfig
|
||||
} from './knowledge/reranker';
|
||||
|
||||
// Export Graph RAG
|
||||
export {
|
||||
GraphStore, GraphRAG, createGraphRAG,
|
||||
type GraphNode, type GraphEdge, type GraphQueryResult, type GraphRAGConfig
|
||||
} from './knowledge/graph-rag';
|
||||
|
||||
// Export providers with explicit names to avoid conflicts
|
||||
export {
|
||||
createProvider,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* PraisonAI Integrations
|
||||
* Extensions for vector stores, observability, and voice
|
||||
*/
|
||||
|
||||
// Vector Store Integrations
|
||||
export * from './vector';
|
||||
|
||||
// Observability Integrations
|
||||
export * from './observability';
|
||||
|
||||
// Voice Integrations
|
||||
export * from './voice';
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Base Observability - Abstract class for observability integrations
|
||||
* Supports tracing, logging, and metrics
|
||||
*/
|
||||
|
||||
export interface Span {
|
||||
id: string;
|
||||
traceId: string;
|
||||
parentId?: string;
|
||||
name: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
status: 'ok' | 'error' | 'unset';
|
||||
attributes: Record<string, any>;
|
||||
events: SpanEvent[];
|
||||
}
|
||||
|
||||
export interface SpanEvent {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
attributes?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface TraceContext {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
parentSpanId?: string;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
attributes?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
name: string;
|
||||
value: number;
|
||||
type: 'counter' | 'gauge' | 'histogram';
|
||||
timestamp: number;
|
||||
tags?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for observability providers
|
||||
*/
|
||||
export abstract class BaseObservabilityProvider {
|
||||
readonly name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new span
|
||||
*/
|
||||
abstract startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span;
|
||||
|
||||
/**
|
||||
* End a span
|
||||
*/
|
||||
abstract endSpan(span: Span, status?: 'ok' | 'error', error?: Error): void;
|
||||
|
||||
/**
|
||||
* Add event to span
|
||||
*/
|
||||
abstract addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
*/
|
||||
abstract log(entry: LogEntry): void;
|
||||
|
||||
/**
|
||||
* Record a metric
|
||||
*/
|
||||
abstract recordMetric(metric: Metric): void;
|
||||
|
||||
/**
|
||||
* Flush all pending data
|
||||
*/
|
||||
abstract flush(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Shutdown the provider
|
||||
*/
|
||||
abstract shutdown(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Helper to create trace context
|
||||
*/
|
||||
protected createTraceContext(span: Span): TraceContext {
|
||||
return {
|
||||
traceId: span.traceId,
|
||||
spanId: span.id,
|
||||
parentSpanId: span.parentId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID
|
||||
*/
|
||||
protected generateId(): string {
|
||||
return crypto.randomUUID().replace(/-/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Console-based observability provider for development
|
||||
*/
|
||||
export class ConsoleObservabilityProvider extends BaseObservabilityProvider {
|
||||
private spans: Map<string, Span> = new Map();
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: { verbose?: boolean } = {}) {
|
||||
super('ConsoleObservability');
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
|
||||
const span: Span = {
|
||||
id: this.generateId(),
|
||||
traceId: parentContext?.traceId || this.generateId(),
|
||||
parentId: parentContext?.spanId,
|
||||
name,
|
||||
startTime: Date.now(),
|
||||
status: 'unset',
|
||||
attributes: attributes || {},
|
||||
events: []
|
||||
};
|
||||
|
||||
this.spans.set(span.id, span);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[SPAN START] ${name}`, { traceId: span.traceId, spanId: span.id });
|
||||
}
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
|
||||
span.endTime = Date.now();
|
||||
span.status = status;
|
||||
|
||||
if (error) {
|
||||
span.attributes.error = error.message;
|
||||
span.attributes.errorStack = error.stack;
|
||||
}
|
||||
|
||||
const duration = span.endTime - span.startTime;
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[SPAN END] ${span.name}`, {
|
||||
duration: `${duration}ms`,
|
||||
status,
|
||||
...(error && { error: error.message })
|
||||
});
|
||||
}
|
||||
|
||||
this.spans.delete(span.id);
|
||||
}
|
||||
|
||||
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
|
||||
span.events.push({
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
attributes
|
||||
});
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[SPAN EVENT] ${span.name} -> ${name}`, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
log(entry: LogEntry): void {
|
||||
const prefix = `[${entry.level.toUpperCase()}]`;
|
||||
const context = entry.traceId ? ` [trace:${entry.traceId.slice(0, 8)}]` : '';
|
||||
|
||||
switch (entry.level) {
|
||||
case 'debug':
|
||||
console.debug(`${prefix}${context} ${entry.message}`, entry.attributes);
|
||||
break;
|
||||
case 'info':
|
||||
console.info(`${prefix}${context} ${entry.message}`, entry.attributes);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(`${prefix}${context} ${entry.message}`, entry.attributes);
|
||||
break;
|
||||
case 'error':
|
||||
console.error(`${prefix}${context} ${entry.message}`, entry.attributes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
recordMetric(metric: Metric): void {
|
||||
if (this.verbose) {
|
||||
console.log(`[METRIC] ${metric.name}: ${metric.value} (${metric.type})`, metric.tags);
|
||||
}
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
// Console provider doesn't need flushing
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.spans.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory-based observability provider for testing
|
||||
*/
|
||||
export class MemoryObservabilityProvider extends BaseObservabilityProvider {
|
||||
spans: Span[] = [];
|
||||
logs: LogEntry[] = [];
|
||||
metrics: Metric[] = [];
|
||||
|
||||
constructor() {
|
||||
super('MemoryObservability');
|
||||
}
|
||||
|
||||
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
|
||||
const span: Span = {
|
||||
id: this.generateId(),
|
||||
traceId: parentContext?.traceId || this.generateId(),
|
||||
parentId: parentContext?.spanId,
|
||||
name,
|
||||
startTime: Date.now(),
|
||||
status: 'unset',
|
||||
attributes: attributes || {},
|
||||
events: []
|
||||
};
|
||||
|
||||
this.spans.push(span);
|
||||
return span;
|
||||
}
|
||||
|
||||
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
|
||||
span.endTime = Date.now();
|
||||
span.status = status;
|
||||
if (error) {
|
||||
span.attributes.error = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
|
||||
span.events.push({ name, timestamp: Date.now(), attributes });
|
||||
}
|
||||
|
||||
log(entry: LogEntry): void {
|
||||
this.logs.push(entry);
|
||||
}
|
||||
|
||||
recordMetric(metric: Metric): void {
|
||||
this.metrics.push(metric);
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.spans = [];
|
||||
this.logs = [];
|
||||
this.metrics = [];
|
||||
}
|
||||
|
||||
// Helper methods for testing
|
||||
getSpansByName(name: string): Span[] {
|
||||
return this.spans.filter(s => s.name === name);
|
||||
}
|
||||
|
||||
getLogsByLevel(level: LogEntry['level']): LogEntry[] {
|
||||
return this.logs.filter(l => l.level === level);
|
||||
}
|
||||
|
||||
getMetricsByName(name: string): Metric[] {
|
||||
return this.metrics.filter(m => m.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createConsoleObservability(config?: { verbose?: boolean }): ConsoleObservabilityProvider {
|
||||
return new ConsoleObservabilityProvider(config);
|
||||
}
|
||||
|
||||
export function createMemoryObservability(): MemoryObservabilityProvider {
|
||||
return new MemoryObservabilityProvider();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Observability Integrations
|
||||
* Provides adapters for various observability platforms
|
||||
*/
|
||||
|
||||
export * from './base';
|
||||
export * from './langfuse';
|
||||
|
||||
// Re-export factory functions
|
||||
export { createConsoleObservability, createMemoryObservability } from './base';
|
||||
export { createLangfuseObservability } from './langfuse';
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Langfuse Observability Integration
|
||||
*/
|
||||
|
||||
import { BaseObservabilityProvider, Span, TraceContext, LogEntry, Metric } from './base';
|
||||
|
||||
export interface LangfuseConfig {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
baseUrl?: string;
|
||||
flushAt?: number;
|
||||
flushInterval?: number;
|
||||
}
|
||||
|
||||
export class LangfuseObservabilityProvider extends BaseObservabilityProvider {
|
||||
private publicKey: string;
|
||||
private secretKey: string;
|
||||
private baseUrl: string;
|
||||
private queue: any[] = [];
|
||||
private flushAt: number;
|
||||
private flushInterval: number;
|
||||
private flushTimer?: NodeJS.Timeout;
|
||||
|
||||
constructor(config: LangfuseConfig) {
|
||||
super('LangfuseObservability');
|
||||
this.publicKey = config.publicKey;
|
||||
this.secretKey = config.secretKey;
|
||||
this.baseUrl = config.baseUrl || 'https://cloud.langfuse.com';
|
||||
this.flushAt = config.flushAt || 20;
|
||||
this.flushInterval = config.flushInterval || 10000;
|
||||
|
||||
this.startFlushTimer();
|
||||
}
|
||||
|
||||
private startFlushTimer(): void {
|
||||
this.flushTimer = setInterval(() => {
|
||||
if (this.queue.length > 0) {
|
||||
this.flush();
|
||||
}
|
||||
}, this.flushInterval);
|
||||
}
|
||||
|
||||
private async request(path: string, body: any): Promise<any> {
|
||||
const auth = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString('base64');
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Langfuse API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private enqueue(event: any): void {
|
||||
this.queue.push(event);
|
||||
if (this.queue.length >= this.flushAt) {
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
startSpan(name: string, attributes?: Record<string, any>, parentContext?: TraceContext): Span {
|
||||
const span: Span = {
|
||||
id: this.generateId(),
|
||||
traceId: parentContext?.traceId || this.generateId(),
|
||||
parentId: parentContext?.spanId,
|
||||
name,
|
||||
startTime: Date.now(),
|
||||
status: 'unset',
|
||||
attributes: attributes || {},
|
||||
events: []
|
||||
};
|
||||
|
||||
// Create trace if this is root span
|
||||
if (!parentContext) {
|
||||
this.enqueue({
|
||||
type: 'trace-create',
|
||||
body: {
|
||||
id: span.traceId,
|
||||
name,
|
||||
metadata: attributes,
|
||||
timestamp: new Date(span.startTime).toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create span/generation
|
||||
this.enqueue({
|
||||
type: 'span-create',
|
||||
body: {
|
||||
id: span.id,
|
||||
traceId: span.traceId,
|
||||
parentObservationId: span.parentId,
|
||||
name,
|
||||
startTime: new Date(span.startTime).toISOString(),
|
||||
metadata: attributes
|
||||
}
|
||||
});
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
endSpan(span: Span, status: 'ok' | 'error' = 'ok', error?: Error): void {
|
||||
span.endTime = Date.now();
|
||||
span.status = status;
|
||||
|
||||
const updateBody: any = {
|
||||
spanId: span.id,
|
||||
endTime: new Date(span.endTime).toISOString(),
|
||||
level: status === 'error' ? 'ERROR' : 'DEFAULT'
|
||||
};
|
||||
|
||||
if (error) {
|
||||
updateBody.statusMessage = error.message;
|
||||
}
|
||||
|
||||
this.enqueue({
|
||||
type: 'span-update',
|
||||
body: updateBody
|
||||
});
|
||||
}
|
||||
|
||||
addSpanEvent(span: Span, name: string, attributes?: Record<string, any>): void {
|
||||
span.events.push({
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
attributes
|
||||
});
|
||||
|
||||
this.enqueue({
|
||||
type: 'event-create',
|
||||
body: {
|
||||
traceId: span.traceId,
|
||||
observationId: span.id,
|
||||
name,
|
||||
metadata: attributes,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log(entry: LogEntry): void {
|
||||
// Langfuse doesn't have direct log support, use events instead
|
||||
if (entry.traceId) {
|
||||
this.enqueue({
|
||||
type: 'event-create',
|
||||
body: {
|
||||
traceId: entry.traceId,
|
||||
observationId: entry.spanId,
|
||||
name: `log:${entry.level}`,
|
||||
metadata: {
|
||||
message: entry.message,
|
||||
...entry.attributes
|
||||
},
|
||||
timestamp: new Date(entry.timestamp).toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
recordMetric(metric: Metric): void {
|
||||
// Langfuse uses scores for metrics
|
||||
this.enqueue({
|
||||
type: 'score-create',
|
||||
body: {
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
timestamp: new Date(metric.timestamp).toISOString(),
|
||||
comment: JSON.stringify(metric.tags)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
if (this.queue.length === 0) return;
|
||||
|
||||
const batch = this.queue.splice(0, this.queue.length);
|
||||
|
||||
try {
|
||||
await this.request('/api/public/ingestion', {
|
||||
batch
|
||||
});
|
||||
} catch (error) {
|
||||
// Re-add failed items to queue
|
||||
this.queue.unshift(...batch);
|
||||
console.error('Langfuse flush error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.flushTimer) {
|
||||
clearInterval(this.flushTimer);
|
||||
}
|
||||
await this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function createLangfuseObservability(config: LangfuseConfig): LangfuseObservabilityProvider {
|
||||
return new LangfuseObservabilityProvider(config);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Base Vector Store - Abstract class for vector database integrations
|
||||
* Matches mastra's MastraVector pattern
|
||||
*/
|
||||
|
||||
export interface VectorDocument {
|
||||
id: string;
|
||||
vector: number[];
|
||||
metadata?: Record<string, any>;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
id: string;
|
||||
score: number;
|
||||
metadata?: Record<string, any>;
|
||||
content?: string;
|
||||
vector?: number[];
|
||||
}
|
||||
|
||||
export interface IndexStats {
|
||||
dimension: number;
|
||||
count: number;
|
||||
metric: 'cosine' | 'euclidean' | 'dotProduct';
|
||||
}
|
||||
|
||||
export interface CreateIndexParams {
|
||||
indexName: string;
|
||||
dimension: number;
|
||||
metric?: 'cosine' | 'euclidean' | 'dotProduct';
|
||||
}
|
||||
|
||||
export interface UpsertParams {
|
||||
indexName: string;
|
||||
vectors: VectorDocument[];
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
indexName: string;
|
||||
vector: number[];
|
||||
topK?: number;
|
||||
filter?: Record<string, any>;
|
||||
includeMetadata?: boolean;
|
||||
includeVectors?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteParams {
|
||||
indexName: string;
|
||||
ids?: string[];
|
||||
filter?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for vector store implementations
|
||||
*/
|
||||
export abstract class BaseVectorStore {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
|
||||
constructor(config: { id: string; name?: string }) {
|
||||
this.id = config.id;
|
||||
this.name = config.name || 'VectorStore';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new index
|
||||
*/
|
||||
abstract createIndex(params: CreateIndexParams): Promise<void>;
|
||||
|
||||
/**
|
||||
* List all indexes
|
||||
*/
|
||||
abstract listIndexes(): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Get index statistics
|
||||
*/
|
||||
abstract describeIndex(indexName: string): Promise<IndexStats>;
|
||||
|
||||
/**
|
||||
* Delete an index
|
||||
*/
|
||||
abstract deleteIndex(indexName: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Upsert vectors into an index
|
||||
*/
|
||||
abstract upsert(params: UpsertParams): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Query vectors by similarity
|
||||
*/
|
||||
abstract query(params: QueryParams): Promise<QueryResult[]>;
|
||||
|
||||
/**
|
||||
* Delete vectors by ID or filter
|
||||
*/
|
||||
abstract delete(params: DeleteParams): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update vector metadata
|
||||
*/
|
||||
abstract update(indexName: string, id: string, metadata: Record<string, any>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory vector store for testing and development
|
||||
*/
|
||||
export class MemoryVectorStore extends BaseVectorStore {
|
||||
private indexes: Map<string, {
|
||||
dimension: number;
|
||||
metric: 'cosine' | 'euclidean' | 'dotProduct';
|
||||
vectors: Map<string, VectorDocument>;
|
||||
}> = new Map();
|
||||
|
||||
constructor(config: { id: string } = { id: 'memory' }) {
|
||||
super({ ...config, name: 'MemoryVectorStore' });
|
||||
}
|
||||
|
||||
async createIndex(params: CreateIndexParams): Promise<void> {
|
||||
if (this.indexes.has(params.indexName)) {
|
||||
return; // Index already exists
|
||||
}
|
||||
this.indexes.set(params.indexName, {
|
||||
dimension: params.dimension,
|
||||
metric: params.metric || 'cosine',
|
||||
vectors: new Map()
|
||||
});
|
||||
}
|
||||
|
||||
async listIndexes(): Promise<string[]> {
|
||||
return Array.from(this.indexes.keys());
|
||||
}
|
||||
|
||||
async describeIndex(indexName: string): Promise<IndexStats> {
|
||||
const index = this.indexes.get(indexName);
|
||||
if (!index) {
|
||||
throw new Error(`Index ${indexName} not found`);
|
||||
}
|
||||
return {
|
||||
dimension: index.dimension,
|
||||
count: index.vectors.size,
|
||||
metric: index.metric
|
||||
};
|
||||
}
|
||||
|
||||
async deleteIndex(indexName: string): Promise<void> {
|
||||
this.indexes.delete(indexName);
|
||||
}
|
||||
|
||||
async upsert(params: UpsertParams): Promise<string[]> {
|
||||
const index = this.indexes.get(params.indexName);
|
||||
if (!index) {
|
||||
throw new Error(`Index ${params.indexName} not found`);
|
||||
}
|
||||
|
||||
const ids: string[] = [];
|
||||
for (const doc of params.vectors) {
|
||||
index.vectors.set(doc.id, doc);
|
||||
ids.push(doc.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
async query(params: QueryParams): Promise<QueryResult[]> {
|
||||
const index = this.indexes.get(params.indexName);
|
||||
if (!index) {
|
||||
throw new Error(`Index ${params.indexName} not found`);
|
||||
}
|
||||
|
||||
const results: QueryResult[] = [];
|
||||
|
||||
for (const [id, doc] of index.vectors) {
|
||||
// Apply filter if provided
|
||||
if (params.filter && !this.matchesFilter(doc.metadata || {}, params.filter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const score = this.calculateSimilarity(params.vector, doc.vector, index.metric);
|
||||
results.push({
|
||||
id,
|
||||
score,
|
||||
metadata: params.includeMetadata ? doc.metadata : undefined,
|
||||
content: doc.content,
|
||||
vector: params.includeVectors ? doc.vector : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by score descending and limit
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
return results.slice(0, params.topK || 10);
|
||||
}
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
const index = this.indexes.get(params.indexName);
|
||||
if (!index) {
|
||||
throw new Error(`Index ${params.indexName} not found`);
|
||||
}
|
||||
|
||||
if (params.ids) {
|
||||
for (const id of params.ids) {
|
||||
index.vectors.delete(id);
|
||||
}
|
||||
} else if (params.filter) {
|
||||
for (const [id, doc] of index.vectors) {
|
||||
if (this.matchesFilter(doc.metadata || {}, params.filter)) {
|
||||
index.vectors.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
|
||||
const index = this.indexes.get(indexName);
|
||||
if (!index) {
|
||||
throw new Error(`Index ${indexName} not found`);
|
||||
}
|
||||
|
||||
const doc = index.vectors.get(id);
|
||||
if (doc) {
|
||||
doc.metadata = { ...doc.metadata, ...metadata };
|
||||
}
|
||||
}
|
||||
|
||||
private calculateSimilarity(a: number[], b: number[], metric: string): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Vector dimensions must match');
|
||||
}
|
||||
|
||||
switch (metric) {
|
||||
case 'cosine':
|
||||
return this.cosineSimilarity(a, b);
|
||||
case 'euclidean':
|
||||
return 1 / (1 + this.euclideanDistance(a, b));
|
||||
case 'dotProduct':
|
||||
return this.dotProduct(a, b);
|
||||
default:
|
||||
return this.cosineSimilarity(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private cosineSimilarity(a: number[], b: number[]): number {
|
||||
const dot = this.dotProduct(a, b);
|
||||
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
|
||||
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
|
||||
return dot / (normA * normB);
|
||||
}
|
||||
|
||||
private euclideanDistance(a: number[], b: number[]): number {
|
||||
return Math.sqrt(a.reduce((sum, x, i) => sum + Math.pow(x - b[i], 2), 0));
|
||||
}
|
||||
|
||||
private dotProduct(a: number[], b: number[]): number {
|
||||
return a.reduce((sum, x, i) => sum + x * b[i], 0);
|
||||
}
|
||||
|
||||
private matchesFilter(metadata: Record<string, any>, filter: Record<string, any>): boolean {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (metadata[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function
|
||||
export function createMemoryVectorStore(id?: string): MemoryVectorStore {
|
||||
return new MemoryVectorStore({ id: id || 'memory' });
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Chroma Vector Store Integration
|
||||
*/
|
||||
|
||||
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
|
||||
|
||||
export interface ChromaConfig {
|
||||
host?: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export class ChromaVectorStore extends BaseVectorStore {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: ChromaConfig & { id?: string } = {}) {
|
||||
super({ id: config.id || 'chroma', name: 'ChromaVectorStore' });
|
||||
const host = config.host || 'localhost';
|
||||
const port = config.port || 8000;
|
||||
this.baseUrl = config.path || `http://${host}:${port}`;
|
||||
}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}): Promise<any> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers as Record<string, string>
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Chroma API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async createIndex(params: CreateIndexParams): Promise<void> {
|
||||
const metadataMap: Record<string, string> = {
|
||||
cosine: 'cosine',
|
||||
euclidean: 'l2',
|
||||
dotProduct: 'ip'
|
||||
};
|
||||
|
||||
await this.request('/api/v1/collections', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: params.indexName,
|
||||
metadata: {
|
||||
'hnsw:space': metadataMap[params.metric || 'cosine'],
|
||||
dimension: params.dimension
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async listIndexes(): Promise<string[]> {
|
||||
const response = await this.request('/api/v1/collections');
|
||||
return (response || []).map((c: any) => c.name);
|
||||
}
|
||||
|
||||
async describeIndex(indexName: string): Promise<IndexStats> {
|
||||
const response = await this.request(`/api/v1/collections/${indexName}`);
|
||||
|
||||
const spaceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
|
||||
'cosine': 'cosine',
|
||||
'l2': 'euclidean',
|
||||
'ip': 'dotProduct'
|
||||
};
|
||||
|
||||
return {
|
||||
dimension: response.metadata?.dimension || 0,
|
||||
count: response.count || 0,
|
||||
metric: spaceMap[response.metadata?.['hnsw:space']] || 'cosine'
|
||||
};
|
||||
}
|
||||
|
||||
async deleteIndex(indexName: string): Promise<void> {
|
||||
await this.request(`/api/v1/collections/${indexName}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async upsert(params: UpsertParams): Promise<string[]> {
|
||||
const ids = params.vectors.map(v => v.id);
|
||||
const embeddings = params.vectors.map(v => v.vector);
|
||||
const metadatas = params.vectors.map(v => v.metadata || {});
|
||||
const documents = params.vectors.map(v => v.content || '');
|
||||
|
||||
await this.request(`/api/v1/collections/${params.indexName}/upsert`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents
|
||||
})
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async query(params: QueryParams): Promise<QueryResult[]> {
|
||||
let where: any = undefined;
|
||||
if (params.filter) {
|
||||
where = params.filter;
|
||||
}
|
||||
|
||||
const response = await this.request(`/api/v1/collections/${params.indexName}/query`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query_embeddings: [params.vector],
|
||||
n_results: params.topK || 10,
|
||||
where,
|
||||
include: [
|
||||
'documents',
|
||||
'metadatas',
|
||||
'distances',
|
||||
...(params.includeVectors ? ['embeddings'] : [])
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const ids = response.ids?.[0] || [];
|
||||
const distances = response.distances?.[0] || [];
|
||||
const metadatas = response.metadatas?.[0] || [];
|
||||
const documents = response.documents?.[0] || [];
|
||||
const embeddings = response.embeddings?.[0] || [];
|
||||
|
||||
return ids.map((id: string, i: number) => ({
|
||||
id,
|
||||
score: 1 - (distances[i] || 0), // Convert distance to similarity
|
||||
metadata: metadatas[i],
|
||||
content: documents[i],
|
||||
vector: embeddings[i]
|
||||
}));
|
||||
}
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
if (params.ids) {
|
||||
await this.request(`/api/v1/collections/${params.indexName}/delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: params.ids })
|
||||
});
|
||||
} else if (params.filter) {
|
||||
await this.request(`/api/v1/collections/${params.indexName}/delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ where: params.filter })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
|
||||
await this.request(`/api/v1/collections/${indexName}/update`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
ids: [id],
|
||||
metadatas: [metadata]
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createChromaStore(config?: ChromaConfig): ChromaVectorStore {
|
||||
return new ChromaVectorStore(config);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Vector Store Integrations
|
||||
* Provides adapters for various vector databases
|
||||
*/
|
||||
|
||||
export * from './base';
|
||||
export * from './pinecone';
|
||||
export * from './weaviate';
|
||||
export * from './qdrant';
|
||||
export * from './chroma';
|
||||
|
||||
// Re-export factory functions for convenience
|
||||
export { createMemoryVectorStore } from './base';
|
||||
export { createPineconeStore } from './pinecone';
|
||||
export { createWeaviateStore } from './weaviate';
|
||||
export { createQdrantStore } from './qdrant';
|
||||
export { createChromaStore } from './chroma';
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Pinecone Vector Store Integration
|
||||
*/
|
||||
|
||||
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
|
||||
|
||||
export interface PineconeConfig {
|
||||
apiKey: string;
|
||||
environment?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export class PineconeVectorStore extends BaseVectorStore {
|
||||
private apiKey: string;
|
||||
private baseUrl: string;
|
||||
private indexHosts: Map<string, string> = new Map();
|
||||
|
||||
constructor(config: PineconeConfig & { id?: string }) {
|
||||
super({ id: config.id || 'pinecone', name: 'PineconeVectorStore' });
|
||||
this.apiKey = config.apiKey;
|
||||
this.baseUrl = 'https://api.pinecone.io';
|
||||
}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}): Promise<any> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Api-Key': this.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Pinecone API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
private async indexRequest(indexName: string, path: string, options: RequestInit = {}): Promise<any> {
|
||||
let host = this.indexHosts.get(indexName);
|
||||
if (!host) {
|
||||
const info = await this.request(`/indexes/${indexName}`);
|
||||
host = info.host as string;
|
||||
this.indexHosts.set(indexName, host);
|
||||
}
|
||||
|
||||
const response = await fetch(`https://${host}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Api-Key': this.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Pinecone Index API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async createIndex(params: CreateIndexParams): Promise<void> {
|
||||
const metricMap: Record<string, string> = {
|
||||
cosine: 'cosine',
|
||||
euclidean: 'euclidean',
|
||||
dotProduct: 'dotproduct'
|
||||
};
|
||||
|
||||
await this.request('/indexes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: params.indexName,
|
||||
dimension: params.dimension,
|
||||
metric: metricMap[params.metric || 'cosine'],
|
||||
spec: {
|
||||
serverless: {
|
||||
cloud: 'aws',
|
||||
region: 'us-east-1'
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Wait for index to be ready
|
||||
let ready = false;
|
||||
let attempts = 0;
|
||||
while (!ready && attempts < 60) {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
try {
|
||||
const info = await this.request(`/indexes/${params.indexName}`);
|
||||
ready = info.status?.ready === true;
|
||||
} catch {
|
||||
// Index not ready yet
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
async listIndexes(): Promise<string[]> {
|
||||
const response = await this.request('/indexes');
|
||||
return (response.indexes || []).map((idx: any) => idx.name);
|
||||
}
|
||||
|
||||
async describeIndex(indexName: string): Promise<IndexStats> {
|
||||
const info = await this.request(`/indexes/${indexName}`);
|
||||
const stats = await this.indexRequest(indexName, '/describe_index_stats', { method: 'POST', body: '{}' });
|
||||
|
||||
const metricMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
|
||||
cosine: 'cosine',
|
||||
euclidean: 'euclidean',
|
||||
dotproduct: 'dotProduct'
|
||||
};
|
||||
|
||||
return {
|
||||
dimension: info.dimension,
|
||||
count: stats.totalVectorCount || 0,
|
||||
metric: metricMap[info.metric] || 'cosine'
|
||||
};
|
||||
}
|
||||
|
||||
async deleteIndex(indexName: string): Promise<void> {
|
||||
await this.request(`/indexes/${indexName}`, { method: 'DELETE' });
|
||||
this.indexHosts.delete(indexName);
|
||||
}
|
||||
|
||||
async upsert(params: UpsertParams): Promise<string[]> {
|
||||
const vectors = params.vectors.map(v => ({
|
||||
id: v.id,
|
||||
values: v.vector,
|
||||
metadata: v.metadata
|
||||
}));
|
||||
|
||||
// Batch upsert in chunks of 100
|
||||
const batchSize = 100;
|
||||
const ids: string[] = [];
|
||||
|
||||
for (let i = 0; i < vectors.length; i += batchSize) {
|
||||
const batch = vectors.slice(i, i + batchSize);
|
||||
await this.indexRequest(params.indexName, '/vectors/upsert', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ vectors: batch })
|
||||
});
|
||||
ids.push(...batch.map(v => v.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async query(params: QueryParams): Promise<QueryResult[]> {
|
||||
const response = await this.indexRequest(params.indexName, '/query', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
vector: params.vector,
|
||||
topK: params.topK || 10,
|
||||
filter: params.filter,
|
||||
includeMetadata: params.includeMetadata !== false,
|
||||
includeValues: params.includeVectors
|
||||
})
|
||||
});
|
||||
|
||||
return (response.matches || []).map((match: any) => ({
|
||||
id: match.id,
|
||||
score: match.score,
|
||||
metadata: match.metadata,
|
||||
vector: match.values
|
||||
}));
|
||||
}
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
if (params.ids) {
|
||||
await this.indexRequest(params.indexName, '/vectors/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: params.ids })
|
||||
});
|
||||
} else if (params.filter) {
|
||||
await this.indexRequest(params.indexName, '/vectors/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ filter: params.filter })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
|
||||
await this.indexRequest(indexName, '/vectors/update', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id, setMetadata: metadata })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createPineconeStore(config: PineconeConfig): PineconeVectorStore {
|
||||
return new PineconeVectorStore(config);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Qdrant Vector Store Integration
|
||||
*/
|
||||
|
||||
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
|
||||
|
||||
export interface QdrantConfig {
|
||||
url: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class QdrantVectorStore extends BaseVectorStore {
|
||||
private url: string;
|
||||
private apiKey?: string;
|
||||
|
||||
constructor(config: QdrantConfig & { id?: string }) {
|
||||
super({ id: config.id || 'qdrant', name: 'QdrantVectorStore' });
|
||||
this.url = config.url.replace(/\/$/, '');
|
||||
this.apiKey = config.apiKey;
|
||||
}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}): Promise<any> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['api-key'] = this.apiKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.url}${path}`, {
|
||||
...options,
|
||||
headers: { ...headers, ...options.headers as Record<string, string> }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Qdrant API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async createIndex(params: CreateIndexParams): Promise<void> {
|
||||
const distanceMap: Record<string, string> = {
|
||||
cosine: 'Cosine',
|
||||
euclidean: 'Euclid',
|
||||
dotProduct: 'Dot'
|
||||
};
|
||||
|
||||
await this.request(`/collections/${params.indexName}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
vectors: {
|
||||
size: params.dimension,
|
||||
distance: distanceMap[params.metric || 'cosine']
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async listIndexes(): Promise<string[]> {
|
||||
const response = await this.request('/collections');
|
||||
return (response.result?.collections || []).map((c: any) => c.name);
|
||||
}
|
||||
|
||||
async describeIndex(indexName: string): Promise<IndexStats> {
|
||||
const response = await this.request(`/collections/${indexName}`);
|
||||
const config = response.result?.config?.params?.vectors;
|
||||
|
||||
const distanceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
|
||||
'Cosine': 'cosine',
|
||||
'Euclid': 'euclidean',
|
||||
'Dot': 'dotProduct'
|
||||
};
|
||||
|
||||
return {
|
||||
dimension: config?.size || 0,
|
||||
count: response.result?.points_count || 0,
|
||||
metric: distanceMap[config?.distance] || 'cosine'
|
||||
};
|
||||
}
|
||||
|
||||
async deleteIndex(indexName: string): Promise<void> {
|
||||
await this.request(`/collections/${indexName}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async upsert(params: UpsertParams): Promise<string[]> {
|
||||
const points = params.vectors.map(v => ({
|
||||
id: v.id,
|
||||
vector: v.vector,
|
||||
payload: {
|
||||
...v.metadata,
|
||||
_content: v.content
|
||||
}
|
||||
}));
|
||||
|
||||
await this.request(`/collections/${params.indexName}/points`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ points })
|
||||
});
|
||||
|
||||
return params.vectors.map(v => v.id);
|
||||
}
|
||||
|
||||
async query(params: QueryParams): Promise<QueryResult[]> {
|
||||
let filter: any = undefined;
|
||||
if (params.filter) {
|
||||
filter = {
|
||||
must: Object.entries(params.filter).map(([key, value]) => ({
|
||||
key,
|
||||
match: { value }
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.request(`/collections/${params.indexName}/points/search`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
vector: params.vector,
|
||||
limit: params.topK || 10,
|
||||
filter,
|
||||
with_payload: params.includeMetadata !== false,
|
||||
with_vector: params.includeVectors
|
||||
})
|
||||
});
|
||||
|
||||
return (response.result || []).map((r: any) => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
metadata: r.payload ? { ...r.payload, _content: undefined } : undefined,
|
||||
content: r.payload?._content,
|
||||
vector: r.vector
|
||||
}));
|
||||
}
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
if (params.ids) {
|
||||
await this.request(`/collections/${params.indexName}/points/delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ points: params.ids })
|
||||
});
|
||||
} else if (params.filter) {
|
||||
const filter = {
|
||||
must: Object.entries(params.filter).map(([key, value]) => ({
|
||||
key,
|
||||
match: { value }
|
||||
}))
|
||||
};
|
||||
await this.request(`/collections/${params.indexName}/points/delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ filter })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
|
||||
await this.request(`/collections/${indexName}/points/payload`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
points: [id],
|
||||
payload: metadata
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createQdrantStore(config: QdrantConfig): QdrantVectorStore {
|
||||
return new QdrantVectorStore(config);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Weaviate Vector Store Integration
|
||||
*/
|
||||
|
||||
import { BaseVectorStore, CreateIndexParams, UpsertParams, QueryParams, DeleteParams, QueryResult, IndexStats } from './base';
|
||||
|
||||
export interface WeaviateConfig {
|
||||
host: string;
|
||||
apiKey?: string;
|
||||
scheme?: 'http' | 'https';
|
||||
}
|
||||
|
||||
export class WeaviateVectorStore extends BaseVectorStore {
|
||||
private host: string;
|
||||
private apiKey?: string;
|
||||
private scheme: string;
|
||||
|
||||
constructor(config: WeaviateConfig & { id?: string }) {
|
||||
super({ id: config.id || 'weaviate', name: 'WeaviateVectorStore' });
|
||||
this.host = config.host;
|
||||
this.apiKey = config.apiKey;
|
||||
this.scheme = config.scheme || 'https';
|
||||
}
|
||||
|
||||
private get baseUrl(): string {
|
||||
return `${this.scheme}://${this.host}`;
|
||||
}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}): Promise<any> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: { ...headers, ...options.headers as Record<string, string> }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Weaviate API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
private async graphql(query: string, variables?: Record<string, any>): Promise<any> {
|
||||
const response = await this.request('/v1/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query, variables })
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
private toClassName(indexName: string): string {
|
||||
// Weaviate class names must start with uppercase
|
||||
return indexName.charAt(0).toUpperCase() + indexName.slice(1);
|
||||
}
|
||||
|
||||
async createIndex(params: CreateIndexParams): Promise<void> {
|
||||
const className = this.toClassName(params.indexName);
|
||||
|
||||
const vectorIndexConfig: Record<string, any> = {
|
||||
distance: params.metric === 'euclidean' ? 'l2-squared' :
|
||||
params.metric === 'dotProduct' ? 'dot' : 'cosine'
|
||||
};
|
||||
|
||||
await this.request('/v1/schema', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
class: className,
|
||||
vectorIndexConfig,
|
||||
properties: [
|
||||
{
|
||||
name: 'content',
|
||||
dataType: ['text']
|
||||
},
|
||||
{
|
||||
name: 'metadata',
|
||||
dataType: ['object']
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async listIndexes(): Promise<string[]> {
|
||||
const response = await this.request('/v1/schema');
|
||||
return (response.classes || []).map((c: any) => c.class.toLowerCase());
|
||||
}
|
||||
|
||||
async describeIndex(indexName: string): Promise<IndexStats> {
|
||||
const className = this.toClassName(indexName);
|
||||
const response = await this.request(`/v1/schema/${className}`);
|
||||
|
||||
// Get count via aggregate
|
||||
const countQuery = `{
|
||||
Aggregate {
|
||||
${className} {
|
||||
meta { count }
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const countResponse = await this.graphql(countQuery);
|
||||
const count = countResponse.data?.Aggregate?.[className]?.[0]?.meta?.count || 0;
|
||||
|
||||
const distanceMap: Record<string, 'cosine' | 'euclidean' | 'dotProduct'> = {
|
||||
'cosine': 'cosine',
|
||||
'l2-squared': 'euclidean',
|
||||
'dot': 'dotProduct'
|
||||
};
|
||||
|
||||
return {
|
||||
dimension: response.vectorIndexConfig?.dimensions || 0,
|
||||
count,
|
||||
metric: distanceMap[response.vectorIndexConfig?.distance] || 'cosine'
|
||||
};
|
||||
}
|
||||
|
||||
async deleteIndex(indexName: string): Promise<void> {
|
||||
const className = this.toClassName(indexName);
|
||||
await this.request(`/v1/schema/${className}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async upsert(params: UpsertParams): Promise<string[]> {
|
||||
const className = this.toClassName(params.indexName);
|
||||
const ids: string[] = [];
|
||||
|
||||
// Batch upsert
|
||||
const objects = params.vectors.map(v => ({
|
||||
class: className,
|
||||
id: v.id,
|
||||
vector: v.vector,
|
||||
properties: {
|
||||
content: v.content || '',
|
||||
metadata: v.metadata || {}
|
||||
}
|
||||
}));
|
||||
|
||||
// Batch in chunks of 100
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
await this.request('/v1/batch/objects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ objects: batch })
|
||||
});
|
||||
ids.push(...batch.map(o => o.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async query(params: QueryParams): Promise<QueryResult[]> {
|
||||
const className = this.toClassName(params.indexName);
|
||||
const topK = params.topK || 10;
|
||||
|
||||
let whereClause = '';
|
||||
if (params.filter) {
|
||||
const conditions = Object.entries(params.filter).map(([key, value]) => {
|
||||
return `{ path: ["metadata", "${key}"], operator: Equal, valueString: "${value}" }`;
|
||||
});
|
||||
if (conditions.length > 0) {
|
||||
whereClause = `where: { operator: And, operands: [${conditions.join(', ')}] }`;
|
||||
}
|
||||
}
|
||||
|
||||
const query = `{
|
||||
Get {
|
||||
${className}(
|
||||
nearVector: { vector: [${params.vector.join(',')}] }
|
||||
limit: ${topK}
|
||||
${whereClause}
|
||||
) {
|
||||
_additional {
|
||||
id
|
||||
distance
|
||||
${params.includeVectors ? 'vector' : ''}
|
||||
}
|
||||
content
|
||||
metadata
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await this.graphql(query);
|
||||
const results = response.data?.Get?.[className] || [];
|
||||
|
||||
return results.map((r: any) => ({
|
||||
id: r._additional.id,
|
||||
score: 1 - (r._additional.distance || 0), // Convert distance to similarity
|
||||
metadata: r.metadata,
|
||||
content: r.content,
|
||||
vector: r._additional.vector
|
||||
}));
|
||||
}
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
const className = this.toClassName(params.indexName);
|
||||
|
||||
if (params.ids) {
|
||||
for (const id of params.ids) {
|
||||
await this.request(`/v1/objects/${className}/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
} else if (params.filter) {
|
||||
// Delete by filter using batch delete
|
||||
const conditions = Object.entries(params.filter).map(([key, value]) => ({
|
||||
path: ['metadata', key],
|
||||
operator: 'Equal',
|
||||
valueString: String(value)
|
||||
}));
|
||||
|
||||
await this.request('/v1/batch/objects', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
match: {
|
||||
class: className,
|
||||
where: { operator: 'And', operands: conditions }
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async update(indexName: string, id: string, metadata: Record<string, any>): Promise<void> {
|
||||
const className = this.toClassName(indexName);
|
||||
await this.request(`/v1/objects/${className}/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
properties: { metadata }
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createWeaviateStore(config: WeaviateConfig): WeaviateVectorStore {
|
||||
return new WeaviateVectorStore(config);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Base Voice - Abstract class for voice/TTS integrations
|
||||
* Matches mastra's MastraVoice pattern
|
||||
*/
|
||||
|
||||
export interface VoiceConfig {
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
voice?: string;
|
||||
}
|
||||
|
||||
export interface SpeakOptions {
|
||||
voice?: string;
|
||||
speed?: number;
|
||||
pitch?: number;
|
||||
format?: 'mp3' | 'wav' | 'ogg' | 'opus';
|
||||
}
|
||||
|
||||
export interface ListenOptions {
|
||||
language?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface Speaker {
|
||||
id: string;
|
||||
name: string;
|
||||
language?: string;
|
||||
gender?: 'male' | 'female' | 'neutral';
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for voice providers
|
||||
*/
|
||||
export abstract class BaseVoiceProvider {
|
||||
readonly name: string;
|
||||
protected apiKey?: string;
|
||||
protected defaultVoice?: string;
|
||||
|
||||
constructor(config: VoiceConfig & { name?: string }) {
|
||||
this.name = config.name || 'VoiceProvider';
|
||||
this.apiKey = config.apiKey;
|
||||
this.defaultVoice = config.voice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert text to speech
|
||||
*/
|
||||
abstract speak(text: string, options?: SpeakOptions): Promise<Buffer | ReadableStream>;
|
||||
|
||||
/**
|
||||
* Convert speech to text
|
||||
*/
|
||||
abstract listen(audio: Buffer | ReadableStream, options?: ListenOptions): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get available voices/speakers
|
||||
*/
|
||||
abstract getSpeakers(): Promise<Speaker[]>;
|
||||
|
||||
/**
|
||||
* Check if provider is available
|
||||
*/
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Voice Provider (TTS and Whisper)
|
||||
*/
|
||||
export class OpenAIVoiceProvider extends BaseVoiceProvider {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: VoiceConfig = {}) {
|
||||
super({ ...config, name: 'OpenAIVoice' });
|
||||
this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
this.defaultVoice = config.voice || 'alloy';
|
||||
this.baseUrl = 'https://api.openai.com/v1';
|
||||
}
|
||||
|
||||
async speak(text: string, options?: SpeakOptions): Promise<Buffer> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('OpenAI API key required for TTS');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/audio/speech`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
input: text,
|
||||
voice: options?.voice || this.defaultVoice,
|
||||
speed: options?.speed || 1.0,
|
||||
response_format: options?.format || 'mp3'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenAI TTS error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
}
|
||||
|
||||
async listen(audio: Buffer | ReadableStream, options?: ListenOptions): Promise<string> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('OpenAI API key required for Whisper');
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Convert Buffer to Blob
|
||||
const audioBlob = audio instanceof Buffer
|
||||
? new Blob([new Uint8Array(audio)], { type: 'audio/mp3' })
|
||||
: audio;
|
||||
|
||||
formData.append('file', audioBlob as Blob, 'audio.mp3');
|
||||
formData.append('model', options?.model || 'whisper-1');
|
||||
if (options?.language) {
|
||||
formData.append('language', options.language);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenAI Whisper error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.text;
|
||||
}
|
||||
|
||||
async getSpeakers(): Promise<Speaker[]> {
|
||||
return [
|
||||
{ id: 'alloy', name: 'Alloy', gender: 'neutral' },
|
||||
{ id: 'echo', name: 'Echo', gender: 'male' },
|
||||
{ id: 'fable', name: 'Fable', gender: 'neutral' },
|
||||
{ id: 'onyx', name: 'Onyx', gender: 'male' },
|
||||
{ id: 'nova', name: 'Nova', gender: 'female' },
|
||||
{ id: 'shimmer', name: 'Shimmer', gender: 'female' }
|
||||
];
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return !!this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ElevenLabs Voice Provider
|
||||
*/
|
||||
export class ElevenLabsVoiceProvider extends BaseVoiceProvider {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: VoiceConfig = {}) {
|
||||
super({ ...config, name: 'ElevenLabsVoice' });
|
||||
this.apiKey = config.apiKey || process.env.ELEVENLABS_API_KEY;
|
||||
this.baseUrl = 'https://api.elevenlabs.io/v1';
|
||||
}
|
||||
|
||||
async speak(text: string, options?: SpeakOptions): Promise<Buffer> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('ElevenLabs API key required');
|
||||
}
|
||||
|
||||
const voiceId = options?.voice || this.defaultVoice || '21m00Tcm4TlvDq8ikWAM'; // Rachel
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/text-to-speech/${voiceId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': this.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
model_id: 'eleven_monolingual_v1',
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.5
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`ElevenLabs error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
}
|
||||
|
||||
async listen(_audio: Buffer | ReadableStream, _options?: ListenOptions): Promise<string> {
|
||||
throw new Error('ElevenLabs does not support speech-to-text');
|
||||
}
|
||||
|
||||
async getSpeakers(): Promise<Speaker[]> {
|
||||
if (!this.apiKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/voices`, {
|
||||
headers: {
|
||||
'xi-api-key': this.apiKey
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return (data.voices || []).map((v: any) => ({
|
||||
id: v.voice_id,
|
||||
name: v.name,
|
||||
gender: v.labels?.gender
|
||||
}));
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return !!this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createOpenAIVoice(config?: VoiceConfig): OpenAIVoiceProvider {
|
||||
return new OpenAIVoiceProvider(config);
|
||||
}
|
||||
|
||||
export function createElevenLabsVoice(config?: VoiceConfig): ElevenLabsVoiceProvider {
|
||||
return new ElevenLabsVoiceProvider(config);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Voice Integrations
|
||||
* Provides adapters for TTS and STT services
|
||||
*/
|
||||
|
||||
export * from './base';
|
||||
|
||||
// Re-export factory functions
|
||||
export { createOpenAIVoice, createElevenLabsVoice } from './base';
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Chunking - Text chunking utilities for RAG
|
||||
*/
|
||||
|
||||
export type ChunkStrategy = 'size' | 'sentence' | 'paragraph' | 'semantic';
|
||||
|
||||
export interface ChunkingConfig {
|
||||
chunkSize?: number;
|
||||
overlap?: number;
|
||||
strategy?: ChunkStrategy;
|
||||
separator?: string;
|
||||
}
|
||||
|
||||
export interface Chunk {
|
||||
content: string;
|
||||
index: number;
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunking class for splitting text into chunks
|
||||
*/
|
||||
export class Chunking {
|
||||
private chunkSize: number;
|
||||
private overlap: number;
|
||||
private strategy: ChunkStrategy;
|
||||
private separator: string;
|
||||
|
||||
constructor(config: ChunkingConfig = {}) {
|
||||
this.chunkSize = config.chunkSize ?? 500;
|
||||
this.overlap = config.overlap ?? 50;
|
||||
this.strategy = config.strategy ?? 'size';
|
||||
this.separator = config.separator ?? ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk text based on configured strategy
|
||||
*/
|
||||
chunk(text: string): Chunk[] {
|
||||
switch (this.strategy) {
|
||||
case 'sentence':
|
||||
return this.chunkBySentence(text);
|
||||
case 'paragraph':
|
||||
return this.chunkByParagraph(text);
|
||||
case 'semantic':
|
||||
return this.chunkBySemantic(text);
|
||||
default:
|
||||
return this.chunkBySize(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk by fixed size with overlap
|
||||
*/
|
||||
chunkBySize(text: string): Chunk[] {
|
||||
const chunks: Chunk[] = [];
|
||||
let startOffset = 0;
|
||||
let index = 0;
|
||||
|
||||
while (startOffset < text.length) {
|
||||
const endOffset = Math.min(startOffset + this.chunkSize, text.length);
|
||||
const content = text.slice(startOffset, endOffset);
|
||||
|
||||
chunks.push({
|
||||
content,
|
||||
index,
|
||||
startOffset,
|
||||
endOffset
|
||||
});
|
||||
|
||||
startOffset = endOffset - this.overlap;
|
||||
if (startOffset >= text.length - this.overlap) break;
|
||||
index++;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk by sentences
|
||||
*/
|
||||
chunkBySentence(text: string): Chunk[] {
|
||||
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
||||
return sentences.map((content, index) => {
|
||||
const startOffset = text.indexOf(content);
|
||||
return {
|
||||
content: content.trim(),
|
||||
index,
|
||||
startOffset,
|
||||
endOffset: startOffset + content.length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk by paragraphs
|
||||
*/
|
||||
chunkByParagraph(text: string): Chunk[] {
|
||||
const paragraphs = text.split(/\n\n+/).filter(p => p.trim().length > 0);
|
||||
let offset = 0;
|
||||
|
||||
return paragraphs.map((content, index) => {
|
||||
const startOffset = text.indexOf(content, offset);
|
||||
offset = startOffset + content.length;
|
||||
return {
|
||||
content: content.trim(),
|
||||
index,
|
||||
startOffset,
|
||||
endOffset: offset
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk by semantic boundaries (simplified)
|
||||
*/
|
||||
chunkBySemantic(text: string): Chunk[] {
|
||||
// Simplified semantic chunking - split on headers, lists, etc.
|
||||
const patterns = [
|
||||
/^#{1,6}\s+.+$/gm, // Headers
|
||||
/^\s*[-*]\s+/gm, // List items
|
||||
/^\d+\.\s+/gm // Numbered lists
|
||||
];
|
||||
|
||||
let chunks: Chunk[] = [];
|
||||
let lastEnd = 0;
|
||||
let index = 0;
|
||||
|
||||
// Find semantic boundaries
|
||||
const boundaries: number[] = [0];
|
||||
for (const pattern of patterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
boundaries.push(match.index);
|
||||
}
|
||||
}
|
||||
boundaries.push(text.length);
|
||||
boundaries.sort((a, b) => a - b);
|
||||
|
||||
// Create chunks from boundaries
|
||||
for (let i = 0; i < boundaries.length - 1; i++) {
|
||||
const start = boundaries[i];
|
||||
const end = boundaries[i + 1];
|
||||
const content = text.slice(start, end).trim();
|
||||
|
||||
if (content.length > 0) {
|
||||
chunks.push({
|
||||
content,
|
||||
index: index++,
|
||||
startOffset: start,
|
||||
endOffset: end
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunks.length > 0 ? chunks : this.chunkBySize(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge small chunks
|
||||
*/
|
||||
mergeSmallChunks(chunks: Chunk[], minSize: number = 100): Chunk[] {
|
||||
const merged: Chunk[] = [];
|
||||
let current: Chunk | null = null;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (!current) {
|
||||
current = { ...chunk };
|
||||
} else if (current.content.length < minSize) {
|
||||
current.content += '\n' + chunk.content;
|
||||
current.endOffset = chunk.endOffset;
|
||||
} else {
|
||||
merged.push(current);
|
||||
current = { ...chunk };
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
merged.push(current);
|
||||
}
|
||||
|
||||
return merged.map((c, i) => ({ ...c, index: i }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Chunking instance
|
||||
*/
|
||||
export function createChunking(config?: ChunkingConfig): Chunking {
|
||||
return new Chunking(config);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Graph RAG - Graph-based Retrieval Augmented Generation
|
||||
* Inspired by mastra's graph-rag module
|
||||
*/
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
type: string;
|
||||
content: string;
|
||||
metadata?: Record<string, any>;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
weight?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphQueryResult {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
paths: GraphNode[][];
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface GraphRAGConfig {
|
||||
maxDepth?: number;
|
||||
maxNodes?: number;
|
||||
minScore?: number;
|
||||
includeEdges?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory Graph Store for Graph RAG
|
||||
*/
|
||||
export class GraphStore {
|
||||
private nodes: Map<string, GraphNode> = new Map();
|
||||
private edges: Map<string, GraphEdge> = new Map();
|
||||
private adjacencyList: Map<string, Set<string>> = new Map();
|
||||
private reverseAdjacencyList: Map<string, Set<string>> = new Map();
|
||||
|
||||
/**
|
||||
* Add a node to the graph
|
||||
*/
|
||||
addNode(node: GraphNode): void {
|
||||
this.nodes.set(node.id, node);
|
||||
if (!this.adjacencyList.has(node.id)) {
|
||||
this.adjacencyList.set(node.id, new Set());
|
||||
}
|
||||
if (!this.reverseAdjacencyList.has(node.id)) {
|
||||
this.reverseAdjacencyList.set(node.id, new Set());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an edge to the graph
|
||||
*/
|
||||
addEdge(edge: GraphEdge): void {
|
||||
this.edges.set(edge.id, edge);
|
||||
|
||||
// Update adjacency lists
|
||||
if (!this.adjacencyList.has(edge.source)) {
|
||||
this.adjacencyList.set(edge.source, new Set());
|
||||
}
|
||||
this.adjacencyList.get(edge.source)!.add(edge.target);
|
||||
|
||||
if (!this.reverseAdjacencyList.has(edge.target)) {
|
||||
this.reverseAdjacencyList.set(edge.target, new Set());
|
||||
}
|
||||
this.reverseAdjacencyList.get(edge.target)!.add(edge.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by ID
|
||||
*/
|
||||
getNode(id: string): GraphNode | undefined {
|
||||
return this.nodes.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes
|
||||
*/
|
||||
getAllNodes(): GraphNode[] {
|
||||
return Array.from(this.nodes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get neighbors of a node
|
||||
*/
|
||||
getNeighbors(nodeId: string, direction: 'outgoing' | 'incoming' | 'both' = 'both'): GraphNode[] {
|
||||
const neighborIds = new Set<string>();
|
||||
|
||||
if (direction === 'outgoing' || direction === 'both') {
|
||||
const outgoing = this.adjacencyList.get(nodeId);
|
||||
if (outgoing) {
|
||||
outgoing.forEach(id => neighborIds.add(id));
|
||||
}
|
||||
}
|
||||
|
||||
if (direction === 'incoming' || direction === 'both') {
|
||||
const incoming = this.reverseAdjacencyList.get(nodeId);
|
||||
if (incoming) {
|
||||
incoming.forEach(id => neighborIds.add(id));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(neighborIds)
|
||||
.map(id => this.nodes.get(id))
|
||||
.filter((n): n is GraphNode => n !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edges between two nodes
|
||||
*/
|
||||
getEdgesBetween(sourceId: string, targetId: string): GraphEdge[] {
|
||||
return Array.from(this.edges.values()).filter(
|
||||
e => (e.source === sourceId && e.target === targetId) ||
|
||||
(e.source === targetId && e.target === sourceId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find paths between two nodes using BFS
|
||||
*/
|
||||
findPaths(startId: string, endId: string, maxDepth: number = 3): GraphNode[][] {
|
||||
const paths: GraphNode[][] = [];
|
||||
const queue: Array<{ path: string[]; visited: Set<string> }> = [
|
||||
{ path: [startId], visited: new Set([startId]) }
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { path, visited } = queue.shift()!;
|
||||
const currentId = path[path.length - 1];
|
||||
|
||||
if (currentId === endId) {
|
||||
paths.push(path.map(id => this.nodes.get(id)!).filter(n => n));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.length >= maxDepth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const neighbors = this.adjacencyList.get(currentId) || new Set();
|
||||
for (const neighborId of neighbors) {
|
||||
if (!visited.has(neighborId)) {
|
||||
const newVisited = new Set(visited);
|
||||
newVisited.add(neighborId);
|
||||
queue.push({ path: [...path, neighborId], visited: newVisited });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subgraph around a node
|
||||
*/
|
||||
getSubgraph(nodeId: string, depth: number = 2): { nodes: GraphNode[]; edges: GraphEdge[] } {
|
||||
const visitedNodes = new Set<string>();
|
||||
const resultEdges: GraphEdge[] = [];
|
||||
const queue: Array<{ id: string; currentDepth: number }> = [{ id: nodeId, currentDepth: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { id, currentDepth } = queue.shift()!;
|
||||
|
||||
if (visitedNodes.has(id) || currentDepth > depth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visitedNodes.add(id);
|
||||
|
||||
if (currentDepth < depth) {
|
||||
const neighbors = this.getNeighbors(id);
|
||||
for (const neighbor of neighbors) {
|
||||
if (!visitedNodes.has(neighbor.id)) {
|
||||
queue.push({ id: neighbor.id, currentDepth: currentDepth + 1 });
|
||||
|
||||
// Add edges
|
||||
const edges = this.getEdgesBetween(id, neighbor.id);
|
||||
resultEdges.push(...edges);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resultNodes = Array.from(visitedNodes)
|
||||
.map(id => this.nodes.get(id))
|
||||
.filter((n): n is GraphNode => n !== undefined);
|
||||
|
||||
return { nodes: resultNodes, edges: resultEdges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the graph
|
||||
*/
|
||||
clear(): void {
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
this.adjacencyList.clear();
|
||||
this.reverseAdjacencyList.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get graph statistics
|
||||
*/
|
||||
getStats(): { nodeCount: number; edgeCount: number } {
|
||||
return {
|
||||
nodeCount: this.nodes.size,
|
||||
edgeCount: this.edges.size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph RAG - Combines graph traversal with vector similarity
|
||||
*/
|
||||
export class GraphRAG {
|
||||
private graphStore: GraphStore;
|
||||
private embedFn?: (text: string) => Promise<number[]>;
|
||||
|
||||
constructor(config: { embedFn?: (text: string) => Promise<number[]> } = {}) {
|
||||
this.graphStore = new GraphStore();
|
||||
this.embedFn = config.embedFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a document as a node
|
||||
*/
|
||||
async addDocument(doc: { id: string; content: string; type?: string; metadata?: Record<string, any> }): Promise<void> {
|
||||
const embedding = this.embedFn ? await this.embedFn(doc.content) : undefined;
|
||||
|
||||
this.graphStore.addNode({
|
||||
id: doc.id,
|
||||
type: doc.type || 'document',
|
||||
content: doc.content,
|
||||
metadata: doc.metadata,
|
||||
embedding
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship between documents
|
||||
*/
|
||||
addRelationship(sourceId: string, targetId: string, type: string, weight?: number): void {
|
||||
this.graphStore.addEdge({
|
||||
id: `${sourceId}-${type}-${targetId}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type,
|
||||
weight
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the graph with a natural language query
|
||||
*/
|
||||
async query(query: string, config: GraphRAGConfig = {}): Promise<GraphQueryResult> {
|
||||
const { maxDepth = 2, maxNodes = 10, minScore = 0, includeEdges = true } = config;
|
||||
|
||||
// Get all nodes and score them
|
||||
const allNodes = this.graphStore.getAllNodes();
|
||||
|
||||
let scoredNodes: Array<{ node: GraphNode; score: number }>;
|
||||
|
||||
if (this.embedFn) {
|
||||
// Use embedding similarity
|
||||
const queryEmbedding = await this.embedFn(query);
|
||||
scoredNodes = allNodes
|
||||
.filter(n => n.embedding)
|
||||
.map(node => ({
|
||||
node,
|
||||
score: this.cosineSimilarity(queryEmbedding, node.embedding!)
|
||||
}));
|
||||
} else {
|
||||
// Use keyword matching
|
||||
const queryTerms = query.toLowerCase().split(/\s+/);
|
||||
scoredNodes = allNodes.map(node => {
|
||||
const content = node.content.toLowerCase();
|
||||
let score = 0;
|
||||
for (const term of queryTerms) {
|
||||
if (content.includes(term)) score += 1;
|
||||
}
|
||||
return { node, score: score / queryTerms.length };
|
||||
});
|
||||
}
|
||||
|
||||
// Filter and sort by score
|
||||
scoredNodes = scoredNodes
|
||||
.filter(s => s.score >= minScore)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Get top nodes and expand graph
|
||||
const topNodes = scoredNodes.slice(0, Math.min(3, maxNodes));
|
||||
const resultNodes: GraphNode[] = [];
|
||||
const resultEdges: GraphEdge[] = [];
|
||||
const seenNodeIds = new Set<string>();
|
||||
|
||||
for (const { node } of topNodes) {
|
||||
const subgraph = this.graphStore.getSubgraph(node.id, maxDepth);
|
||||
|
||||
for (const n of subgraph.nodes) {
|
||||
if (!seenNodeIds.has(n.id) && resultNodes.length < maxNodes) {
|
||||
seenNodeIds.add(n.id);
|
||||
resultNodes.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeEdges) {
|
||||
resultEdges.push(...subgraph.edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Find paths between top nodes
|
||||
const paths: GraphNode[][] = [];
|
||||
if (topNodes.length >= 2) {
|
||||
for (let i = 0; i < topNodes.length - 1; i++) {
|
||||
const foundPaths = this.graphStore.findPaths(
|
||||
topNodes[i].node.id,
|
||||
topNodes[i + 1].node.id,
|
||||
maxDepth
|
||||
);
|
||||
paths.push(...foundPaths.slice(0, 3));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: resultNodes,
|
||||
edges: resultEdges,
|
||||
paths,
|
||||
score: topNodes.length > 0 ? topNodes[0].score : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context string from query results
|
||||
*/
|
||||
getContext(result: GraphQueryResult): string {
|
||||
const nodeContents = result.nodes.map(n => n.content);
|
||||
return nodeContents.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying graph store
|
||||
*/
|
||||
getGraphStore(): GraphStore {
|
||||
return this.graphStore;
|
||||
}
|
||||
|
||||
private cosineSimilarity(a: number[], b: number[]): number {
|
||||
const dot = a.reduce((sum, x, i) => sum + x * b[i], 0);
|
||||
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
|
||||
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
|
||||
return dot / (normA * normB);
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function
|
||||
export function createGraphRAG(config?: { embedFn?: (text: string) => Promise<number[]> }): GraphRAG {
|
||||
return new GraphRAG(config);
|
||||
}
|
||||
@@ -47,3 +47,9 @@ export class BaseKnowledgeBase implements KnowledgeBase {
|
||||
return this.knowledge.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Export Reranker
|
||||
export * from './reranker';
|
||||
|
||||
// Export Graph RAG
|
||||
export * from './graph-rag';
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Reranker - Rerank search results for improved relevance
|
||||
* Inspired by mastra's rerank module
|
||||
*/
|
||||
|
||||
export interface RerankResult {
|
||||
id: string;
|
||||
score: number;
|
||||
content: string;
|
||||
metadata?: Record<string, any>;
|
||||
originalRank: number;
|
||||
newRank: number;
|
||||
}
|
||||
|
||||
export interface RerankConfig {
|
||||
model?: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for rerankers
|
||||
*/
|
||||
export abstract class BaseReranker {
|
||||
readonly name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
abstract rerank(
|
||||
query: string,
|
||||
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
|
||||
config?: RerankConfig
|
||||
): Promise<RerankResult[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cohere Reranker - Uses Cohere's rerank API
|
||||
*/
|
||||
export class CohereReranker extends BaseReranker {
|
||||
private apiKey: string;
|
||||
private model: string;
|
||||
|
||||
constructor(config: { apiKey?: string; model?: string } = {}) {
|
||||
super('CohereReranker');
|
||||
this.apiKey = config.apiKey || process.env.COHERE_API_KEY || '';
|
||||
this.model = config.model || 'rerank-english-v3.0';
|
||||
}
|
||||
|
||||
async rerank(
|
||||
query: string,
|
||||
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
|
||||
config?: RerankConfig
|
||||
): Promise<RerankResult[]> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('Cohere API key required for reranking');
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.cohere.ai/v1/rerank', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config?.model || this.model,
|
||||
query,
|
||||
documents: documents.map(d => d.content),
|
||||
top_n: config?.topK || documents.length,
|
||||
return_documents: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Cohere rerank error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const threshold = config?.threshold || 0;
|
||||
|
||||
return (data.results || [])
|
||||
.filter((r: any) => r.relevance_score >= threshold)
|
||||
.map((r: any, newRank: number) => ({
|
||||
id: documents[r.index].id,
|
||||
score: r.relevance_score,
|
||||
content: documents[r.index].content,
|
||||
metadata: documents[r.index].metadata,
|
||||
originalRank: r.index,
|
||||
newRank
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-Encoder Reranker - Uses sentence similarity for reranking
|
||||
* This is a simple implementation using cosine similarity
|
||||
*/
|
||||
export class CrossEncoderReranker extends BaseReranker {
|
||||
private embedFn?: (texts: string[]) => Promise<number[][]>;
|
||||
|
||||
constructor(config: { embedFn?: (texts: string[]) => Promise<number[][]> } = {}) {
|
||||
super('CrossEncoderReranker');
|
||||
this.embedFn = config.embedFn;
|
||||
}
|
||||
|
||||
async rerank(
|
||||
query: string,
|
||||
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
|
||||
config?: RerankConfig
|
||||
): Promise<RerankResult[]> {
|
||||
if (!this.embedFn) {
|
||||
// Fallback to simple keyword matching
|
||||
return this.keywordRerank(query, documents, config);
|
||||
}
|
||||
|
||||
// Get embeddings for query and all documents
|
||||
const texts = [query, ...documents.map(d => d.content)];
|
||||
const embeddings = await this.embedFn(texts);
|
||||
|
||||
const queryEmbedding = embeddings[0];
|
||||
const docEmbeddings = embeddings.slice(1);
|
||||
|
||||
// Calculate similarity scores
|
||||
const scores = docEmbeddings.map((docEmb, i) => ({
|
||||
index: i,
|
||||
score: this.cosineSimilarity(queryEmbedding, docEmb)
|
||||
}));
|
||||
|
||||
// Sort by score descending
|
||||
scores.sort((a, b) => b.score - a.score);
|
||||
|
||||
const threshold = config?.threshold || 0;
|
||||
const topK = config?.topK || documents.length;
|
||||
|
||||
return scores
|
||||
.filter(s => s.score >= threshold)
|
||||
.slice(0, topK)
|
||||
.map((s, newRank) => ({
|
||||
id: documents[s.index].id,
|
||||
score: s.score,
|
||||
content: documents[s.index].content,
|
||||
metadata: documents[s.index].metadata,
|
||||
originalRank: s.index,
|
||||
newRank
|
||||
}));
|
||||
}
|
||||
|
||||
private keywordRerank(
|
||||
query: string,
|
||||
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
|
||||
config?: RerankConfig
|
||||
): RerankResult[] {
|
||||
const queryTerms = query.toLowerCase().split(/\s+/);
|
||||
|
||||
const scores = documents.map((doc, i) => {
|
||||
const content = doc.content.toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
for (const term of queryTerms) {
|
||||
if (content.includes(term)) {
|
||||
score += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize by query length
|
||||
score = score / queryTerms.length;
|
||||
|
||||
return { index: i, score };
|
||||
});
|
||||
|
||||
scores.sort((a, b) => b.score - a.score);
|
||||
|
||||
const threshold = config?.threshold || 0;
|
||||
const topK = config?.topK || documents.length;
|
||||
|
||||
return scores
|
||||
.filter(s => s.score >= threshold)
|
||||
.slice(0, topK)
|
||||
.map((s, newRank) => ({
|
||||
id: documents[s.index].id,
|
||||
score: s.score,
|
||||
content: documents[s.index].content,
|
||||
metadata: documents[s.index].metadata,
|
||||
originalRank: s.index,
|
||||
newRank
|
||||
}));
|
||||
}
|
||||
|
||||
private cosineSimilarity(a: number[], b: number[]): number {
|
||||
const dot = a.reduce((sum, x, i) => sum + x * b[i], 0);
|
||||
const normA = Math.sqrt(a.reduce((sum, x) => sum + x * x, 0));
|
||||
const normB = Math.sqrt(b.reduce((sum, x) => sum + x * x, 0));
|
||||
return dot / (normA * normB);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM Reranker - Uses an LLM to score relevance
|
||||
*/
|
||||
export class LLMReranker extends BaseReranker {
|
||||
private generateFn: (prompt: string) => Promise<string>;
|
||||
|
||||
constructor(config: { generateFn: (prompt: string) => Promise<string> }) {
|
||||
super('LLMReranker');
|
||||
this.generateFn = config.generateFn;
|
||||
}
|
||||
|
||||
async rerank(
|
||||
query: string,
|
||||
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>,
|
||||
config?: RerankConfig
|
||||
): Promise<RerankResult[]> {
|
||||
const scores: Array<{ index: number; score: number }> = [];
|
||||
|
||||
// Score each document
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const doc = documents[i];
|
||||
const prompt = `Rate the relevance of the following document to the query on a scale of 0 to 10.
|
||||
Query: ${query}
|
||||
Document: ${doc.content.slice(0, 500)}
|
||||
|
||||
Respond with only a number between 0 and 10.`;
|
||||
|
||||
try {
|
||||
const response = await this.generateFn(prompt);
|
||||
const score = parseFloat(response.trim()) / 10;
|
||||
scores.push({ index: i, score: isNaN(score) ? 0 : Math.min(1, Math.max(0, score)) });
|
||||
} catch {
|
||||
scores.push({ index: i, score: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
scores.sort((a, b) => b.score - a.score);
|
||||
|
||||
const threshold = config?.threshold || 0;
|
||||
const topK = config?.topK || documents.length;
|
||||
|
||||
return scores
|
||||
.filter(s => s.score >= threshold)
|
||||
.slice(0, topK)
|
||||
.map((s, newRank) => ({
|
||||
id: documents[s.index].id,
|
||||
score: s.score,
|
||||
content: documents[s.index].content,
|
||||
metadata: documents[s.index].metadata,
|
||||
originalRank: s.index,
|
||||
newRank
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createCohereReranker(config?: { apiKey?: string; model?: string }): CohereReranker {
|
||||
return new CohereReranker(config);
|
||||
}
|
||||
|
||||
export function createCrossEncoderReranker(config?: { embedFn?: (texts: string[]) => Promise<number[][]> }): CrossEncoderReranker {
|
||||
return new CrossEncoderReranker(config);
|
||||
}
|
||||
|
||||
export function createLLMReranker(config: { generateFn: (prompt: string) => Promise<string> }): LLMReranker {
|
||||
return new LLMReranker(config);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Memory System - Conversation and context memory management
|
||||
*/
|
||||
|
||||
export interface MemoryEntry {
|
||||
id: string;
|
||||
content: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
timestamp: number;
|
||||
metadata?: Record<string, any>;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
export interface MemoryConfig {
|
||||
maxEntries?: number;
|
||||
maxTokens?: number;
|
||||
embeddingProvider?: EmbeddingProvider;
|
||||
}
|
||||
|
||||
export interface EmbeddingProvider {
|
||||
embed(text: string): Promise<number[]>;
|
||||
embedBatch(texts: string[]): Promise<number[][]>;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
entry: MemoryEntry;
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory class for managing conversation history and context
|
||||
*/
|
||||
export class Memory {
|
||||
private entries: Map<string, MemoryEntry> = new Map();
|
||||
private maxEntries: number;
|
||||
private maxTokens: number;
|
||||
private embeddingProvider?: EmbeddingProvider;
|
||||
|
||||
constructor(config: MemoryConfig = {}) {
|
||||
this.maxEntries = config.maxEntries ?? 1000;
|
||||
this.maxTokens = config.maxTokens ?? 100000;
|
||||
this.embeddingProvider = config.embeddingProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a memory entry
|
||||
*/
|
||||
async add(content: string, role: 'user' | 'assistant' | 'system', metadata?: Record<string, any>): Promise<MemoryEntry> {
|
||||
const id = this.generateId();
|
||||
const entry: MemoryEntry = {
|
||||
id,
|
||||
content,
|
||||
role,
|
||||
timestamp: Date.now(),
|
||||
metadata
|
||||
};
|
||||
|
||||
if (this.embeddingProvider) {
|
||||
entry.embedding = await this.embeddingProvider.embed(content);
|
||||
}
|
||||
|
||||
this.entries.set(id, entry);
|
||||
this.enforceLimit();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a memory entry by ID
|
||||
*/
|
||||
get(id: string): MemoryEntry | undefined {
|
||||
return this.entries.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entries
|
||||
*/
|
||||
getAll(): MemoryEntry[] {
|
||||
return Array.from(this.entries.values())
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent entries
|
||||
*/
|
||||
getRecent(count: number): MemoryEntry[] {
|
||||
return this.getAll().slice(-count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memory by text similarity
|
||||
*/
|
||||
async search(query: string, limit: number = 5): Promise<SearchResult[]> {
|
||||
if (!this.embeddingProvider) {
|
||||
// Fallback to simple text matching
|
||||
return this.textSearch(query, limit);
|
||||
}
|
||||
|
||||
const queryEmbedding = await this.embeddingProvider.embed(query);
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
for (const entry of this.entries.values()) {
|
||||
if (entry.embedding) {
|
||||
const score = this.cosineSimilarity(queryEmbedding, entry.embedding);
|
||||
results.push({ entry, score });
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple text search fallback
|
||||
*/
|
||||
private textSearch(query: string, limit: number): SearchResult[] {
|
||||
const queryLower = query.toLowerCase();
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
for (const entry of this.entries.values()) {
|
||||
const contentLower = entry.content.toLowerCase();
|
||||
if (contentLower.includes(queryLower)) {
|
||||
const score = queryLower.length / contentLower.length;
|
||||
results.push({ entry, score: Math.min(score * 10, 1) });
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cosine similarity
|
||||
*/
|
||||
private cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) return 0;
|
||||
|
||||
let dotProduct = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
|
||||
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denominator === 0 ? 0 : dotProduct / denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a memory entry
|
||||
*/
|
||||
delete(id: string): boolean {
|
||||
return this.entries.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all memory
|
||||
*/
|
||||
clear(): void {
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory size
|
||||
*/
|
||||
get size(): number {
|
||||
return this.entries.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build context string from recent memory
|
||||
*/
|
||||
buildContext(count?: number): string {
|
||||
const entries = count ? this.getRecent(count) : this.getAll();
|
||||
return entries
|
||||
.map(e => `${e.role}: ${e.content}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export memory to JSON
|
||||
*/
|
||||
toJSON(): MemoryEntry[] {
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import memory from JSON
|
||||
*/
|
||||
fromJSON(entries: MemoryEntry[]): void {
|
||||
this.entries.clear();
|
||||
for (const entry of entries) {
|
||||
this.entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private enforceLimit(): void {
|
||||
while (this.entries.size > this.maxEntries) {
|
||||
const oldest = this.getAll()[0];
|
||||
if (oldest) {
|
||||
this.entries.delete(oldest.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory instance
|
||||
*/
|
||||
export function createMemory(config?: MemoryConfig): Memory {
|
||||
return new Memory(config);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Planning System - Plans, Steps, and TodoLists
|
||||
*/
|
||||
|
||||
export type PlanStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
|
||||
export type TodoStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
export interface PlanConfig {
|
||||
name: string;
|
||||
description?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface PlanStepConfig {
|
||||
description: string;
|
||||
status?: PlanStatus;
|
||||
order?: number;
|
||||
dependencies?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface TodoItemConfig {
|
||||
content: string;
|
||||
priority?: 'low' | 'medium' | 'high';
|
||||
status?: TodoStatus;
|
||||
dueDate?: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanStep - A single step in a plan
|
||||
*/
|
||||
export class PlanStep {
|
||||
readonly id: string;
|
||||
description: string;
|
||||
status: PlanStatus;
|
||||
order: number;
|
||||
dependencies: string[];
|
||||
metadata: Record<string, any>;
|
||||
startedAt?: number;
|
||||
completedAt?: number;
|
||||
|
||||
constructor(config: PlanStepConfig) {
|
||||
this.id = `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.description = config.description;
|
||||
this.status = config.status ?? 'pending';
|
||||
this.order = config.order ?? 0;
|
||||
this.dependencies = config.dependencies ?? [];
|
||||
this.metadata = config.metadata ?? {};
|
||||
}
|
||||
|
||||
start(): this {
|
||||
this.status = 'in_progress';
|
||||
this.startedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
complete(): this {
|
||||
this.status = 'completed';
|
||||
this.completedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
fail(): this {
|
||||
this.status = 'failed';
|
||||
this.completedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
cancel(): this {
|
||||
this.status = 'cancelled';
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan - A collection of steps to accomplish a goal
|
||||
*/
|
||||
export class Plan {
|
||||
readonly id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: PlanStep[] = [];
|
||||
status: PlanStatus = 'pending';
|
||||
metadata: Record<string, any>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
|
||||
constructor(config: PlanConfig) {
|
||||
this.id = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.name = config.name;
|
||||
this.description = config.description;
|
||||
this.metadata = config.metadata ?? {};
|
||||
this.createdAt = Date.now();
|
||||
this.updatedAt = Date.now();
|
||||
}
|
||||
|
||||
addStep(step: PlanStep): this {
|
||||
step.order = this.steps.length;
|
||||
this.steps.push(step);
|
||||
this.updatedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
removeStep(stepId: string): boolean {
|
||||
const index = this.steps.findIndex(s => s.id === stepId);
|
||||
if (index >= 0) {
|
||||
this.steps.splice(index, 1);
|
||||
this.updatedAt = Date.now();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getStep(stepId: string): PlanStep | undefined {
|
||||
return this.steps.find(s => s.id === stepId);
|
||||
}
|
||||
|
||||
getNextStep(): PlanStep | undefined {
|
||||
return this.steps.find(s => s.status === 'pending');
|
||||
}
|
||||
|
||||
getProgress(): { completed: number; total: number; percentage: number } {
|
||||
const completed = this.steps.filter(s => s.status === 'completed').length;
|
||||
const total = this.steps.length;
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
percentage: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
};
|
||||
}
|
||||
|
||||
start(): this {
|
||||
this.status = 'in_progress';
|
||||
this.updatedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
complete(): this {
|
||||
this.status = 'completed';
|
||||
this.updatedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TodoItem - A single todo item
|
||||
*/
|
||||
export class TodoItem {
|
||||
readonly id: string;
|
||||
content: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
status: TodoStatus;
|
||||
dueDate?: Date;
|
||||
metadata: Record<string, any>;
|
||||
createdAt: number;
|
||||
completedAt?: number;
|
||||
|
||||
constructor(config: TodoItemConfig) {
|
||||
this.id = `todo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.content = config.content;
|
||||
this.priority = config.priority ?? 'medium';
|
||||
this.status = config.status ?? 'pending';
|
||||
this.dueDate = config.dueDate;
|
||||
this.metadata = config.metadata ?? {};
|
||||
this.createdAt = Date.now();
|
||||
}
|
||||
|
||||
start(): this {
|
||||
this.status = 'in_progress';
|
||||
return this;
|
||||
}
|
||||
|
||||
complete(): this {
|
||||
this.status = 'completed';
|
||||
this.completedAt = Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
this.status = 'pending';
|
||||
this.completedAt = undefined;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TodoList - A collection of todo items
|
||||
*/
|
||||
export class TodoList {
|
||||
readonly id: string;
|
||||
name: string;
|
||||
items: TodoItem[] = [];
|
||||
createdAt: number;
|
||||
|
||||
constructor(name: string = 'Todo List') {
|
||||
this.id = `todolist_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.name = name;
|
||||
this.createdAt = Date.now();
|
||||
}
|
||||
|
||||
add(item: TodoItem): this {
|
||||
this.items.push(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
remove(itemId: string): boolean {
|
||||
const index = this.items.findIndex(i => i.id === itemId);
|
||||
if (index >= 0) {
|
||||
this.items.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get(itemId: string): TodoItem | undefined {
|
||||
return this.items.find(i => i.id === itemId);
|
||||
}
|
||||
|
||||
getPending(): TodoItem[] {
|
||||
return this.items.filter(i => i.status === 'pending');
|
||||
}
|
||||
|
||||
getCompleted(): TodoItem[] {
|
||||
return this.items.filter(i => i.status === 'completed');
|
||||
}
|
||||
|
||||
getByPriority(priority: 'low' | 'medium' | 'high'): TodoItem[] {
|
||||
return this.items.filter(i => i.priority === priority);
|
||||
}
|
||||
|
||||
getProgress(): { completed: number; total: number; percentage: number } {
|
||||
const completed = this.items.filter(i => i.status === 'completed').length;
|
||||
const total = this.items.length;
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
percentage: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
};
|
||||
}
|
||||
|
||||
clear(): this {
|
||||
this.items = [];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanStorage - Persist plans (in-memory implementation)
|
||||
*/
|
||||
export class PlanStorage {
|
||||
private plans: Map<string, Plan> = new Map();
|
||||
private todoLists: Map<string, TodoList> = new Map();
|
||||
|
||||
savePlan(plan: Plan): void {
|
||||
this.plans.set(plan.id, plan);
|
||||
}
|
||||
|
||||
getPlan(planId: string): Plan | undefined {
|
||||
return this.plans.get(planId);
|
||||
}
|
||||
|
||||
deletePlan(planId: string): boolean {
|
||||
return this.plans.delete(planId);
|
||||
}
|
||||
|
||||
listPlans(): Plan[] {
|
||||
return Array.from(this.plans.values());
|
||||
}
|
||||
|
||||
saveTodoList(list: TodoList): void {
|
||||
this.todoLists.set(list.id, list);
|
||||
}
|
||||
|
||||
getTodoList(listId: string): TodoList | undefined {
|
||||
return this.todoLists.get(listId);
|
||||
}
|
||||
|
||||
deleteTodoList(listId: string): boolean {
|
||||
return this.todoLists.delete(listId);
|
||||
}
|
||||
|
||||
listTodoLists(): TodoList[] {
|
||||
return Array.from(this.todoLists.values());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Plan
|
||||
*/
|
||||
export function createPlan(config: PlanConfig): Plan {
|
||||
return new Plan(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TodoList
|
||||
*/
|
||||
export function createTodoList(name?: string): TodoList {
|
||||
return new TodoList(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PlanStorage
|
||||
*/
|
||||
export function createPlanStorage(): PlanStorage {
|
||||
return new PlanStorage();
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Telemetry - Usage tracking and analytics
|
||||
*/
|
||||
|
||||
export interface TelemetryEvent {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
properties?: Record<string, any>;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
enabled?: boolean;
|
||||
endpoint?: string;
|
||||
batchSize?: number;
|
||||
flushInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telemetry collector for tracking usage
|
||||
*/
|
||||
export class TelemetryCollector {
|
||||
private events: TelemetryEvent[] = [];
|
||||
private enabled: boolean;
|
||||
private endpoint?: string;
|
||||
private batchSize: number;
|
||||
private flushInterval: number;
|
||||
private flushTimer?: NodeJS.Timeout;
|
||||
private userId?: string;
|
||||
private sessionId: string;
|
||||
|
||||
constructor(config: TelemetryConfig = {}) {
|
||||
this.enabled = config.enabled ?? this.checkEnabled();
|
||||
this.endpoint = config.endpoint;
|
||||
this.batchSize = config.batchSize ?? 100;
|
||||
this.flushInterval = config.flushInterval ?? 60000;
|
||||
this.sessionId = this.generateSessionId();
|
||||
|
||||
if (this.enabled && this.flushInterval > 0) {
|
||||
this.startFlushTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private checkEnabled(): boolean {
|
||||
const disabled = process.env.PRAISONAI_TELEMETRY_DISABLED === 'true' ||
|
||||
process.env.PRAISONAI_DISABLE_TELEMETRY === 'true' ||
|
||||
process.env.DO_NOT_TRACK === 'true';
|
||||
return !disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an event
|
||||
*/
|
||||
track(name: string, properties?: Record<string, any>): void {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const event: TelemetryEvent = {
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
properties,
|
||||
userId: this.userId,
|
||||
sessionId: this.sessionId
|
||||
};
|
||||
|
||||
this.events.push(event);
|
||||
|
||||
if (this.events.length >= this.batchSize) {
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track feature usage
|
||||
*/
|
||||
trackFeatureUsage(feature: string, metadata?: Record<string, any>): void {
|
||||
this.track('feature_usage', { feature, ...metadata });
|
||||
}
|
||||
|
||||
/**
|
||||
* Track agent execution
|
||||
*/
|
||||
trackAgentExecution(agentName: string, duration: number, success: boolean): void {
|
||||
this.track('agent_execution', { agentName, duration, success });
|
||||
}
|
||||
|
||||
/**
|
||||
* Track tool call
|
||||
*/
|
||||
trackToolCall(toolName: string, duration: number, success: boolean): void {
|
||||
this.track('tool_call', { toolName, duration, success });
|
||||
}
|
||||
|
||||
/**
|
||||
* Track LLM call
|
||||
*/
|
||||
trackLLMCall(provider: string, model: string, tokens: number, duration: number): void {
|
||||
this.track('llm_call', { provider, model, tokens, duration });
|
||||
}
|
||||
|
||||
/**
|
||||
* Track error
|
||||
*/
|
||||
trackError(error: string, context?: Record<string, any>): void {
|
||||
this.track('error', { error, ...context });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user ID
|
||||
*/
|
||||
setUserId(userId: string): void {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush events
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
if (this.events.length === 0) return;
|
||||
|
||||
const eventsToSend = [...this.events];
|
||||
this.events = [];
|
||||
|
||||
if (this.endpoint) {
|
||||
try {
|
||||
await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ events: eventsToSend })
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail - never break user applications
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable telemetry
|
||||
*/
|
||||
enable(): void {
|
||||
this.enabled = true;
|
||||
this.startFlushTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable telemetry
|
||||
*/
|
||||
disable(): void {
|
||||
this.enabled = false;
|
||||
this.stopFlushTimer();
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending events count
|
||||
*/
|
||||
getPendingCount(): number {
|
||||
return this.events.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.stopFlushTimer();
|
||||
this.flush();
|
||||
}
|
||||
|
||||
private startFlushTimer(): void {
|
||||
if (this.flushTimer) return;
|
||||
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
||||
}
|
||||
|
||||
private stopFlushTimer(): void {
|
||||
if (this.flushTimer) {
|
||||
clearInterval(this.flushTimer);
|
||||
this.flushTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private generateSessionId(): string {
|
||||
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Global telemetry instance
|
||||
let globalTelemetry: TelemetryCollector | null = null;
|
||||
|
||||
/**
|
||||
* Get global telemetry instance
|
||||
*/
|
||||
export function getTelemetry(): TelemetryCollector {
|
||||
if (!globalTelemetry) {
|
||||
globalTelemetry = new TelemetryCollector();
|
||||
}
|
||||
return globalTelemetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable telemetry
|
||||
*/
|
||||
export function enableTelemetry(): void {
|
||||
getTelemetry().enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable telemetry
|
||||
*/
|
||||
export function disableTelemetry(): void {
|
||||
getTelemetry().disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup telemetry resources
|
||||
*/
|
||||
export function cleanupTelemetry(): void {
|
||||
if (globalTelemetry) {
|
||||
globalTelemetry.cleanup();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { Tool, BaseTool } from './index';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import { BaseTool } from './base';
|
||||
|
||||
export interface ArxivPaper {
|
||||
id: string;
|
||||
@@ -12,21 +12,21 @@ export interface ArxivPaper {
|
||||
link: string;
|
||||
}
|
||||
|
||||
export class ArxivSearchTool extends BaseTool {
|
||||
export class ArxivSearchTool extends BaseTool<{ query: string; maxResults?: number }, ArxivPaper[]> {
|
||||
name = 'arxiv-search';
|
||||
description = 'Search for academic papers on arXiv';
|
||||
private parser: XMLParser;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
'arxiv-search',
|
||||
'Search for academic papers on arXiv'
|
||||
);
|
||||
super();
|
||||
this.parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_'
|
||||
});
|
||||
}
|
||||
|
||||
async execute(query: string, maxResults: number = 10): Promise<ArxivPaper[]> {
|
||||
async run(params: { query: string; maxResults?: number }): Promise<ArxivPaper[]> {
|
||||
const { query, maxResults = 10 } = params;
|
||||
try {
|
||||
const response = await axios.get(`http://export.arxiv.org/api/query`, {
|
||||
params: {
|
||||
@@ -67,15 +67,12 @@ export class ArxivSearchTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
export class ArxivDownloadTool extends BaseTool {
|
||||
constructor() {
|
||||
super(
|
||||
'arxiv-download',
|
||||
'Download PDF of an arXiv paper'
|
||||
);
|
||||
}
|
||||
export class ArxivDownloadTool extends BaseTool<{ paperId: string }, Buffer> {
|
||||
name = 'arxiv-download';
|
||||
description = 'Download PDF of an arXiv paper';
|
||||
|
||||
async execute(paperId: string): Promise<Buffer> {
|
||||
async run(params: { paperId: string }): Promise<Buffer> {
|
||||
const { paperId } = params;
|
||||
try {
|
||||
const response = await axios.get(`https://arxiv.org/pdf/${paperId}.pdf`, {
|
||||
responseType: 'arraybuffer'
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* BaseTool - Abstract base class for creating custom tools (plugins)
|
||||
*
|
||||
* This provides the same extensibility pattern as Python's BaseTool.
|
||||
* External developers can create plugins by extending BaseTool.
|
||||
*
|
||||
* Usage:
|
||||
* import { BaseTool } from 'praisonai';
|
||||
*
|
||||
* class MyTool extends BaseTool {
|
||||
* name = 'my_tool';
|
||||
* description = 'Does something useful';
|
||||
*
|
||||
* async run(params: { query: string }): Promise<string> {
|
||||
* return `Result for ${params.query}`;
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
export interface ToolResult<T = any> {
|
||||
output: T;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class ToolValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ToolValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ToolParameters {
|
||||
type: 'object';
|
||||
properties: Record<string, {
|
||||
type: string;
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
default?: any;
|
||||
}>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for all PraisonAI tools.
|
||||
*
|
||||
* Subclass this to create custom tools that can be:
|
||||
* - Used directly by agents
|
||||
* - Distributed as npm packages (plugins)
|
||||
* - Auto-discovered via package.json
|
||||
*/
|
||||
export abstract class BaseTool<TParams = any, TResult = any> {
|
||||
/** Unique identifier for the tool */
|
||||
abstract name: string;
|
||||
|
||||
/** Human-readable description (used by LLM) */
|
||||
abstract description: string;
|
||||
|
||||
/** Tool version string */
|
||||
version: string = '1.0.0';
|
||||
|
||||
/** JSON Schema for parameters */
|
||||
parameters?: ToolParameters;
|
||||
|
||||
/**
|
||||
* Execute the tool with given arguments.
|
||||
* This method must be implemented by subclasses.
|
||||
*/
|
||||
abstract run(params: TParams): Promise<TResult> | TResult;
|
||||
|
||||
/**
|
||||
* Allow tool to be called directly like a function.
|
||||
*/
|
||||
async execute(params: TParams): Promise<TResult> {
|
||||
return this.run(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute tool with error handling, returning ToolResult.
|
||||
*/
|
||||
async safeRun(params: TParams): Promise<ToolResult<TResult>> {
|
||||
try {
|
||||
const output = await this.run(params);
|
||||
return { output, success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
output: null as any,
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenAI-compatible function schema for this tool.
|
||||
*/
|
||||
getSchema(): { type: 'function'; function: { name: string; description: string; parameters: any } } {
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
parameters: this.parameters || { type: 'object', properties: {}, required: [] }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the tool configuration.
|
||||
*/
|
||||
validate(): boolean {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!this.name || typeof this.name !== 'string') {
|
||||
errors.push("Tool must have a non-empty string 'name'");
|
||||
}
|
||||
|
||||
if (!this.description || typeof this.description !== 'string') {
|
||||
errors.push("Tool must have a non-empty string 'description'");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new ToolValidationError(`Tool '${this.name}' validation failed: ${errors.join('; ')}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.constructor.name}(name='${this.name}')`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate any tool-like object.
|
||||
*/
|
||||
export function validateTool(tool: any): boolean {
|
||||
if (tool instanceof BaseTool) {
|
||||
return tool.validate();
|
||||
}
|
||||
|
||||
if (typeof tool === 'function' || (tool && typeof tool.run === 'function')) {
|
||||
const name = tool.name || tool.__name__;
|
||||
if (!name) {
|
||||
throw new ToolValidationError('Tool must have a name');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ToolValidationError(`Invalid tool type: ${typeof tool}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple tool from a function (alternative to class-based approach)
|
||||
*/
|
||||
export function createTool<TParams = any, TResult = any>(config: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: ToolParameters;
|
||||
run: (params: TParams) => Promise<TResult> | TResult;
|
||||
}): BaseTool<TParams, TResult> {
|
||||
return {
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
version: '1.0.0',
|
||||
parameters: config.parameters,
|
||||
run: config.run,
|
||||
execute: async (params: TParams) => config.run(params),
|
||||
safeRun: async (params: TParams) => {
|
||||
try {
|
||||
const output = await config.run(params);
|
||||
return { output, success: true };
|
||||
} catch (error: any) {
|
||||
return { output: null as any, success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
getSchema: () => ({
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
parameters: config.parameters || { type: 'object', properties: {}, required: [] }
|
||||
}
|
||||
}),
|
||||
validate: () => true,
|
||||
toString: () => `Tool(name='${config.name}')`
|
||||
} as BaseTool<TParams, TResult>;
|
||||
}
|
||||
@@ -1,23 +1,5 @@
|
||||
// Export base tool interfaces
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
execute(...args: any[]): Promise<any>;
|
||||
}
|
||||
|
||||
export class BaseTool implements Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
constructor(name: string, description: string) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
async execute(...args: any[]): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
// Export base tool interfaces and classes
|
||||
export { BaseTool, ToolResult, ToolValidationError, validateTool, createTool, type ToolParameters } from './base';
|
||||
|
||||
// Export decorator and registry
|
||||
export * from './decorator';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { BaseTool } from './index';
|
||||
import { BaseTool } from './base';
|
||||
|
||||
export interface MCPToolInfo {
|
||||
name: string;
|
||||
@@ -8,12 +8,16 @@ export interface MCPToolInfo {
|
||||
inputSchema?: any;
|
||||
}
|
||||
|
||||
export class MCPTool extends BaseTool {
|
||||
export class MCPTool extends BaseTool<any, any> {
|
||||
name: string;
|
||||
description: string;
|
||||
private client: Client;
|
||||
private inputSchema: any;
|
||||
|
||||
constructor(info: MCPToolInfo, client: Client) {
|
||||
super(info.name, info.description || `Call the ${info.name} tool`);
|
||||
super();
|
||||
this.name = info.name;
|
||||
this.description = info.description || `Call the ${info.name} tool`;
|
||||
this.client = client;
|
||||
this.inputSchema = info.inputSchema || { type: 'object', properties: {}, required: [] };
|
||||
}
|
||||
@@ -22,7 +26,7 @@ export class MCPTool extends BaseTool {
|
||||
return this.inputSchema?.properties;
|
||||
}
|
||||
|
||||
async execute(args: any = {}): Promise<any> {
|
||||
async run(args: any = {}): Promise<any> {
|
||||
try {
|
||||
const result: any = await this.client.callTool({ name: this.name, arguments: args });
|
||||
if (result.structuredContent) {
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* YAML Workflow Parser
|
||||
* Parse YAML workflow definitions into executable workflows
|
||||
*/
|
||||
|
||||
import { Workflow, WorkflowStep, WorkflowStepConfig } from './index';
|
||||
|
||||
export interface YAMLWorkflowDefinition {
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
steps: YAMLStepDefinition[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface YAMLStepDefinition {
|
||||
name: string;
|
||||
type: 'agent' | 'tool' | 'condition' | 'parallel' | 'loop';
|
||||
agent?: string;
|
||||
tool?: string;
|
||||
input?: string | Record<string, any>;
|
||||
output?: string;
|
||||
condition?: string;
|
||||
onError?: 'fail' | 'skip' | 'retry';
|
||||
maxRetries?: number;
|
||||
timeout?: number;
|
||||
steps?: YAMLStepDefinition[]; // For parallel/loop
|
||||
loopCondition?: string;
|
||||
maxIterations?: number;
|
||||
}
|
||||
|
||||
export interface ParsedWorkflow {
|
||||
workflow: Workflow;
|
||||
definition: YAMLWorkflowDefinition;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML string into workflow definition
|
||||
*/
|
||||
export function parseYAMLWorkflow(yamlContent: string): YAMLWorkflowDefinition {
|
||||
// Simple YAML parser for workflow definitions
|
||||
// For production, use js-yaml package
|
||||
const lines = yamlContent.split('\n');
|
||||
const result: YAMLWorkflowDefinition = {
|
||||
name: '',
|
||||
steps: []
|
||||
};
|
||||
|
||||
let currentStep: Partial<YAMLStepDefinition> | null = null;
|
||||
let indent = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
const currentIndent = line.search(/\S/);
|
||||
|
||||
// Parse key-value pairs
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = trimmed.substring(0, colonIndex).trim();
|
||||
const value = trimmed.substring(colonIndex + 1).trim();
|
||||
|
||||
if (currentIndent === 0) {
|
||||
// Top-level keys
|
||||
if (key === 'name') result.name = value;
|
||||
else if (key === 'description') result.description = value;
|
||||
else if (key === 'version') result.version = value;
|
||||
else if (key === 'steps') {
|
||||
// Steps array starts
|
||||
indent = currentIndent;
|
||||
}
|
||||
} else if (trimmed.startsWith('- name:')) {
|
||||
// New step
|
||||
if (currentStep && currentStep.name) {
|
||||
result.steps.push(currentStep as YAMLStepDefinition);
|
||||
}
|
||||
currentStep = {
|
||||
name: value,
|
||||
type: 'agent'
|
||||
};
|
||||
} else if (currentStep) {
|
||||
// Step properties
|
||||
if (key === 'type') currentStep.type = value as any;
|
||||
else if (key === 'agent') currentStep.agent = value;
|
||||
else if (key === 'tool') currentStep.tool = value;
|
||||
else if (key === 'input') currentStep.input = value;
|
||||
else if (key === 'output') currentStep.output = value;
|
||||
else if (key === 'condition') currentStep.condition = value;
|
||||
else if (key === 'onError') currentStep.onError = value as any;
|
||||
else if (key === 'maxRetries') currentStep.maxRetries = parseInt(value);
|
||||
else if (key === 'timeout') currentStep.timeout = parseInt(value);
|
||||
else if (key === 'loopCondition') currentStep.loopCondition = value;
|
||||
else if (key === 'maxIterations') currentStep.maxIterations = parseInt(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add last step
|
||||
if (currentStep && currentStep.name) {
|
||||
result.steps.push(currentStep as YAMLStepDefinition);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create executable workflow from YAML definition
|
||||
*/
|
||||
export function createWorkflowFromYAML(
|
||||
definition: YAMLWorkflowDefinition,
|
||||
agents: Record<string, any> = {},
|
||||
tools: Record<string, any> = {}
|
||||
): ParsedWorkflow {
|
||||
const errors: string[] = [];
|
||||
const workflow = new Workflow(definition.name);
|
||||
|
||||
for (const stepDef of definition.steps) {
|
||||
try {
|
||||
const stepConfig = createStepConfig(stepDef, agents, tools, errors);
|
||||
if (stepConfig) {
|
||||
workflow.addStep(stepConfig);
|
||||
}
|
||||
} catch (error: any) {
|
||||
errors.push(`Error creating step ${stepDef.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { workflow, definition, errors };
|
||||
}
|
||||
|
||||
function createStepConfig(
|
||||
stepDef: YAMLStepDefinition,
|
||||
agents: Record<string, any>,
|
||||
tools: Record<string, any>,
|
||||
errors: string[]
|
||||
): WorkflowStepConfig | null {
|
||||
const { name, type, agent, tool, onError, maxRetries, timeout, condition } = stepDef;
|
||||
|
||||
let execute: any;
|
||||
|
||||
switch (type) {
|
||||
case 'agent':
|
||||
if (!agent) {
|
||||
errors.push(`Step ${name}: agent type requires 'agent' field`);
|
||||
return null;
|
||||
}
|
||||
const agentInstance = agents[agent];
|
||||
if (!agentInstance) {
|
||||
errors.push(`Step ${name}: agent '${agent}' not found`);
|
||||
return null;
|
||||
}
|
||||
execute = async (input: any) => {
|
||||
if (typeof agentInstance.chat === 'function') {
|
||||
return agentInstance.chat(typeof input === 'string' ? input : JSON.stringify(input));
|
||||
}
|
||||
return agentInstance(input);
|
||||
};
|
||||
break;
|
||||
|
||||
case 'tool':
|
||||
if (!tool) {
|
||||
errors.push(`Step ${name}: tool type requires 'tool' field`);
|
||||
return null;
|
||||
}
|
||||
const toolInstance = tools[tool];
|
||||
if (!toolInstance) {
|
||||
errors.push(`Step ${name}: tool '${tool}' not found`);
|
||||
return null;
|
||||
}
|
||||
execute = async (input: any) => {
|
||||
if (typeof toolInstance.execute === 'function') {
|
||||
return toolInstance.execute(input);
|
||||
}
|
||||
if (typeof toolInstance.run === 'function') {
|
||||
return toolInstance.run(input);
|
||||
}
|
||||
return toolInstance(input);
|
||||
};
|
||||
break;
|
||||
|
||||
case 'condition':
|
||||
execute = async (input: any, context: any) => {
|
||||
// Evaluate condition and return input or skip
|
||||
if (condition) {
|
||||
// Simple condition evaluation
|
||||
const result = evaluateCondition(condition, input, context);
|
||||
return result ? input : null;
|
||||
}
|
||||
return input;
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
execute = async (input: any) => input;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
execute,
|
||||
onError,
|
||||
maxRetries,
|
||||
timeout,
|
||||
condition: condition ? (context) => evaluateCondition(condition, null, context) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function evaluateCondition(condition: string, input: any, context: any): boolean {
|
||||
// Simple condition evaluation
|
||||
// Supports: "result.success", "input.length > 0", etc.
|
||||
try {
|
||||
// Create a safe evaluation context
|
||||
const evalContext = {
|
||||
input,
|
||||
context,
|
||||
result: context?.get?.('lastResult'),
|
||||
...context?.metadata
|
||||
};
|
||||
|
||||
// Simple expression evaluation
|
||||
if (condition.includes('===')) {
|
||||
const [left, right] = condition.split('===').map(s => s.trim());
|
||||
return getNestedValue(evalContext, left) === parseValue(right);
|
||||
}
|
||||
if (condition.includes('!==')) {
|
||||
const [left, right] = condition.split('!==').map(s => s.trim());
|
||||
return getNestedValue(evalContext, left) !== parseValue(right);
|
||||
}
|
||||
if (condition.includes('>')) {
|
||||
const [left, right] = condition.split('>').map(s => s.trim());
|
||||
return getNestedValue(evalContext, left) > parseValue(right);
|
||||
}
|
||||
if (condition.includes('<')) {
|
||||
const [left, right] = condition.split('<').map(s => s.trim());
|
||||
return getNestedValue(evalContext, left) < parseValue(right);
|
||||
}
|
||||
|
||||
// Boolean check
|
||||
return !!getNestedValue(evalContext, condition);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getNestedValue(obj: any, path: string): any {
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||
}
|
||||
|
||||
function parseValue(value: string): any {
|
||||
value = value.trim();
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
if (value === 'null') return null;
|
||||
if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
||||
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
||||
const num = Number(value);
|
||||
if (!isNaN(num)) return num;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load workflow from YAML file
|
||||
*/
|
||||
export async function loadWorkflowFromFile(
|
||||
filePath: string,
|
||||
agents: Record<string, any> = {},
|
||||
tools: Record<string, any> = {}
|
||||
): Promise<ParsedWorkflow> {
|
||||
const fs = await import('fs/promises');
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const definition = parseYAMLWorkflow(content);
|
||||
return createWorkflowFromYAML(definition, agents, tools);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate YAML workflow definition
|
||||
*/
|
||||
export function validateWorkflowDefinition(definition: YAMLWorkflowDefinition): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!definition.name) {
|
||||
errors.push('Workflow must have a name');
|
||||
}
|
||||
|
||||
if (!definition.steps || definition.steps.length === 0) {
|
||||
errors.push('Workflow must have at least one step');
|
||||
}
|
||||
|
||||
const stepNames = new Set<string>();
|
||||
for (const step of definition.steps) {
|
||||
if (!step.name) {
|
||||
errors.push('Each step must have a name');
|
||||
} else if (stepNames.has(step.name)) {
|
||||
errors.push(`Duplicate step name: ${step.name}`);
|
||||
} else {
|
||||
stepNames.add(step.name);
|
||||
}
|
||||
|
||||
if (!step.type) {
|
||||
errors.push(`Step ${step.name}: must have a type`);
|
||||
}
|
||||
|
||||
if (step.type === 'agent' && !step.agent) {
|
||||
errors.push(`Step ${step.name}: agent type requires 'agent' field`);
|
||||
}
|
||||
|
||||
if (step.type === 'tool' && !step.tool) {
|
||||
errors.push(`Step ${step.name}: tool type requires 'tool' field`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -385,6 +385,141 @@ Do something`;
|
||||
if (h.name !== 'support_handoff') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
// 13. Memory Tests
|
||||
console.log('\n--- 13. Memory System ---');
|
||||
|
||||
await test('Memory add/get', async () => {
|
||||
const { Memory } = await import('../../src/memory/memory');
|
||||
const mem = new Memory();
|
||||
await mem.add('Hello', 'user');
|
||||
await mem.add('Hi there', 'assistant');
|
||||
if (mem.size !== 2) throw new Error('Wrong size');
|
||||
});
|
||||
|
||||
await test('Memory search', async () => {
|
||||
const { Memory } = await import('../../src/memory/memory');
|
||||
const mem = new Memory();
|
||||
await mem.add('TypeScript is great', 'user');
|
||||
await mem.add('Python is also good', 'user');
|
||||
const results = await mem.search('TypeScript');
|
||||
if (results.length === 0) throw new Error('No results');
|
||||
});
|
||||
|
||||
// 14. Telemetry Tests
|
||||
console.log('\n--- 14. Telemetry ---');
|
||||
|
||||
await test('Telemetry collector', async () => {
|
||||
const { TelemetryCollector } = await import('../../src/telemetry');
|
||||
const collector = new TelemetryCollector({ enabled: true });
|
||||
collector.track('test_event', { key: 'value' });
|
||||
if (collector.getPendingCount() !== 1) throw new Error('Event not tracked');
|
||||
collector.disable();
|
||||
});
|
||||
|
||||
// 15. AutoAgents Tests
|
||||
console.log('\n--- 15. AutoAgents ---');
|
||||
|
||||
await test('AutoAgents creation', async () => {
|
||||
const { AutoAgents } = await import('../../src/auto');
|
||||
const auto = new AutoAgents({ singleAgent: true });
|
||||
const complexity = auto.analyzeComplexity('Simple task');
|
||||
if (complexity !== 'simple') throw new Error('Wrong complexity');
|
||||
});
|
||||
|
||||
await test('AutoAgents pattern recommendation', async () => {
|
||||
const { AutoAgents } = await import('../../src/auto');
|
||||
const auto = new AutoAgents();
|
||||
const pattern = auto.recommendPattern('Run tasks in parallel');
|
||||
if (pattern !== 'parallel') throw new Error('Wrong pattern');
|
||||
});
|
||||
|
||||
// 16. ImageAgent Tests
|
||||
console.log('\n--- 16. ImageAgent ---');
|
||||
|
||||
await test('ImageAgent creation', async () => {
|
||||
const { ImageAgent } = await import('../../src/agent/image');
|
||||
const agent = new ImageAgent({ name: 'TestImageAgent' });
|
||||
if (agent.name !== 'TestImageAgent') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
// 17. DeepResearchAgent Tests
|
||||
console.log('\n--- 17. DeepResearchAgent ---');
|
||||
|
||||
await test('DeepResearchAgent creation', async () => {
|
||||
const { DeepResearchAgent } = await import('../../src/agent/research');
|
||||
const agent = new DeepResearchAgent({ name: 'TestResearcher' });
|
||||
if (agent.name !== 'TestResearcher') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
// 18. QueryRewriterAgent Tests
|
||||
console.log('\n--- 18. QueryRewriterAgent ---');
|
||||
|
||||
await test('QueryRewriterAgent creation', async () => {
|
||||
const { QueryRewriterAgent } = await import('../../src/agent/query-rewriter');
|
||||
const agent = new QueryRewriterAgent({ name: 'TestRewriter' });
|
||||
if (agent.name !== 'TestRewriter') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
// 19. PromptExpanderAgent Tests
|
||||
console.log('\n--- 19. PromptExpanderAgent ---');
|
||||
|
||||
await test('PromptExpanderAgent creation', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent({ name: 'TestExpander' });
|
||||
if (agent.name !== 'TestExpander') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
await test('PromptExpanderAgent strategy detection', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent();
|
||||
const strategy = agent.detectStrategy('Write code');
|
||||
if (strategy !== 'detail') throw new Error('Wrong strategy');
|
||||
});
|
||||
|
||||
// 20. Chunking Tests
|
||||
console.log('\n--- 20. Chunking ---');
|
||||
|
||||
await test('Chunking by size', async () => {
|
||||
const { Chunking } = await import('../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ chunkSize: 100, overlap: 20 });
|
||||
const chunks = chunker.chunk('A'.repeat(250));
|
||||
if (chunks.length < 2) throw new Error('Should have multiple chunks');
|
||||
});
|
||||
|
||||
await test('Chunking by sentence', async () => {
|
||||
const { Chunking } = await import('../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ strategy: 'sentence' });
|
||||
const chunks = chunker.chunkBySentence('First. Second. Third.');
|
||||
if (chunks.length !== 3) throw new Error('Should have 3 chunks');
|
||||
});
|
||||
|
||||
// 21. LLMGuardrail Tests
|
||||
console.log('\n--- 21. LLMGuardrail ---');
|
||||
|
||||
await test('LLMGuardrail creation', async () => {
|
||||
const { LLMGuardrail } = await import('../../src/guardrails/llm-guardrail');
|
||||
const guard = new LLMGuardrail({ name: 'test', criteria: 'Must be polite' });
|
||||
if (guard.name !== 'test') throw new Error('Wrong name');
|
||||
});
|
||||
|
||||
// 22. Planning Tests
|
||||
console.log('\n--- 22. Planning ---');
|
||||
|
||||
await test('Plan creation', async () => {
|
||||
const { Plan, PlanStep } = await import('../../src/planning');
|
||||
const plan = new Plan({ name: 'Test Plan' });
|
||||
plan.addStep(new PlanStep({ description: 'Step 1' }));
|
||||
if (plan.steps.length !== 1) throw new Error('Should have 1 step');
|
||||
});
|
||||
|
||||
await test('TodoList', async () => {
|
||||
const { TodoList, TodoItem } = await import('../../src/planning');
|
||||
const todos = new TodoList();
|
||||
todos.add(new TodoItem({ content: 'Task 1' }));
|
||||
todos.add(new TodoItem({ content: 'Task 2' }));
|
||||
if (todos.items.length !== 2) throw new Error('Should have 2 items');
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* PromptExpanderAgent Unit Tests (TDD - Write tests first)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('PromptExpanderAgent', () => {
|
||||
describe('Creation', () => {
|
||||
it('should create with default config', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent();
|
||||
expect(agent.name).toContain('PromptExpander');
|
||||
});
|
||||
|
||||
it('should create with custom name', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent({ name: 'CustomExpander' });
|
||||
expect(agent.name).toBe('CustomExpander');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Strategy Detection', () => {
|
||||
it('should detect detail strategy for short prompts', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent();
|
||||
// @ts-ignore - accessing private method for testing
|
||||
const strategy = agent.detectStrategy('Write code');
|
||||
expect(strategy).toBe('detail');
|
||||
});
|
||||
|
||||
it('should detect context strategy for prompts needing background', async () => {
|
||||
const { PromptExpanderAgent } = await import('../../../src/agent/prompt-expander');
|
||||
const agent = new PromptExpanderAgent();
|
||||
// @ts-ignore
|
||||
const strategy = agent.detectStrategy('Explain why this matters for the project');
|
||||
expect(strategy).toBe('context');
|
||||
});
|
||||
});
|
||||
});
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Cache System Unit Tests
|
||||
*/
|
||||
|
||||
import { MemoryCache, FileCache, createMemoryCache, createFileCache } from '../../../src/cache';
|
||||
|
||||
describe('MemoryCache', () => {
|
||||
let cache: MemoryCache;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new MemoryCache();
|
||||
});
|
||||
|
||||
describe('basic operations', () => {
|
||||
it('should set and get values', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
const result = await cache.get('key1');
|
||||
expect(result).toBe('value1');
|
||||
});
|
||||
|
||||
it('should return undefined for missing keys', async () => {
|
||||
const result = await cache.get('nonexistent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete values', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
const deleted = await cache.delete('key1');
|
||||
expect(deleted).toBe(true);
|
||||
expect(await cache.get('key1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should check if key exists', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
expect(await cache.has('key1')).toBe(true);
|
||||
expect(await cache.has('nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('should clear all values', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.set('key2', 'value2');
|
||||
await cache.clear();
|
||||
expect(await cache.size()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return all keys', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.set('key2', 'value2');
|
||||
const keys = await cache.keys();
|
||||
expect(keys).toContain('key1');
|
||||
expect(keys).toContain('key2');
|
||||
});
|
||||
|
||||
it('should return size', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.set('key2', 'value2');
|
||||
expect(await cache.size()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TTL (time to live)', () => {
|
||||
it('should expire values after TTL', async () => {
|
||||
const shortCache = new MemoryCache({ ttl: 50 });
|
||||
await shortCache.set('key1', 'value1');
|
||||
|
||||
// Should exist immediately
|
||||
expect(await shortCache.get('key1')).toBe('value1');
|
||||
|
||||
// Wait for expiration
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
expect(await shortCache.get('key1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow per-key TTL override', async () => {
|
||||
await cache.set('key1', 'value1', 50);
|
||||
expect(await cache.get('key1')).toBe('value1');
|
||||
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
expect(await cache.get('key1')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('max size', () => {
|
||||
it('should enforce max size by removing oldest', async () => {
|
||||
const limitedCache = new MemoryCache({ maxSize: 2 });
|
||||
await limitedCache.set('key1', 'value1');
|
||||
await limitedCache.set('key2', 'value2');
|
||||
await limitedCache.set('key3', 'value3');
|
||||
|
||||
expect(await limitedCache.size()).toBe(2);
|
||||
expect(await limitedCache.get('key1')).toBeUndefined();
|
||||
expect(await limitedCache.get('key2')).toBe('value2');
|
||||
expect(await limitedCache.get('key3')).toBe('value3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list operations', () => {
|
||||
it('should push to list', async () => {
|
||||
await cache.listPush('mylist', 'item1');
|
||||
await cache.listPush('mylist', 'item2');
|
||||
expect(await cache.listLength('mylist')).toBe(2);
|
||||
});
|
||||
|
||||
it('should get list range', async () => {
|
||||
await cache.listPush('mylist', 'a');
|
||||
await cache.listPush('mylist', 'b');
|
||||
await cache.listPush('mylist', 'c');
|
||||
|
||||
const range = await cache.listRange('mylist', 0, 2);
|
||||
expect(range).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('should return empty array for nonexistent list', async () => {
|
||||
expect(await cache.listLength('nonexistent')).toBe(0);
|
||||
expect(await cache.listRange('nonexistent', 0)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileCache', () => {
|
||||
let cache: FileCache;
|
||||
const testDir = '/tmp/praisonai-test-cache-' + Date.now();
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new FileCache({ cacheDir: testDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cache.clear();
|
||||
});
|
||||
|
||||
it('should set and get values', async () => {
|
||||
await cache.set('key1', { data: 'test' });
|
||||
const result = await cache.get<{ data: string }>('key1');
|
||||
expect(result?.data).toBe('test');
|
||||
});
|
||||
|
||||
it('should return undefined for missing keys', async () => {
|
||||
const result = await cache.get('nonexistent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete values', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.delete('key1');
|
||||
expect(await cache.get('key1')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Factory functions', () => {
|
||||
it('should create MemoryCache', () => {
|
||||
const cache = createMemoryCache({ ttl: 1000 });
|
||||
expect(cache).toBeInstanceOf(MemoryCache);
|
||||
});
|
||||
|
||||
it('should create FileCache', () => {
|
||||
const cache = createFileCache({ cacheDir: '/tmp/test' });
|
||||
expect(cache).toBeInstanceOf(FileCache);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Events System Unit Tests
|
||||
*/
|
||||
|
||||
import { EventEmitterPubSub, AgentEventBus, AgentEvents, createEventBus, createPubSub } from '../../../src/events';
|
||||
|
||||
describe('EventEmitterPubSub', () => {
|
||||
let pubsub: EventEmitterPubSub;
|
||||
|
||||
beforeEach(() => {
|
||||
pubsub = new EventEmitterPubSub();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await pubsub.close();
|
||||
});
|
||||
|
||||
it('should publish and subscribe to events', async () => {
|
||||
const received: any[] = [];
|
||||
|
||||
await pubsub.subscribe('test-topic', (event) => {
|
||||
received.push(event);
|
||||
});
|
||||
|
||||
await pubsub.publish('test-topic', { message: 'hello' });
|
||||
|
||||
expect(received.length).toBe(1);
|
||||
expect(received[0].data.message).toBe('hello');
|
||||
expect(received[0].topic).toBe('test-topic');
|
||||
expect(received[0].id).toBeDefined();
|
||||
expect(received[0].createdAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should support multiple subscribers', async () => {
|
||||
let count = 0;
|
||||
|
||||
await pubsub.subscribe('topic', () => { count++; });
|
||||
await pubsub.subscribe('topic', () => { count++; });
|
||||
|
||||
await pubsub.publish('topic', {});
|
||||
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it('should unsubscribe handlers', async () => {
|
||||
let count = 0;
|
||||
const handler = () => { count++; };
|
||||
|
||||
await pubsub.subscribe('topic', handler);
|
||||
await pubsub.publish('topic', {});
|
||||
expect(count).toBe(1);
|
||||
|
||||
await pubsub.unsubscribe('topic', handler);
|
||||
await pubsub.publish('topic', {});
|
||||
expect(count).toBe(1); // Should not increment
|
||||
});
|
||||
|
||||
it('should include metadata in events', async () => {
|
||||
let receivedEvent: any;
|
||||
|
||||
await pubsub.subscribe('topic', (event) => {
|
||||
receivedEvent = event;
|
||||
});
|
||||
|
||||
await pubsub.publish('topic', { data: 'test' }, { source: 'unit-test' });
|
||||
|
||||
expect(receivedEvent.metadata?.source).toBe('unit-test');
|
||||
});
|
||||
|
||||
it('should wait for specific event with timeout', async () => {
|
||||
const promise = pubsub.waitFor('delayed-topic', 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
pubsub.publish('delayed-topic', { value: 42 });
|
||||
}, 50);
|
||||
|
||||
const event = await promise;
|
||||
expect(event.data.value).toBe(42);
|
||||
});
|
||||
|
||||
it('should timeout when waiting for event', async () => {
|
||||
await expect(pubsub.waitFor('never-topic', 50)).rejects.toThrow('Timeout');
|
||||
});
|
||||
|
||||
it('should close and remove all listeners', async () => {
|
||||
let count = 0;
|
||||
await pubsub.subscribe('topic', () => { count++; });
|
||||
|
||||
await pubsub.close();
|
||||
|
||||
// After close, publishing should not trigger handlers
|
||||
const emitter = pubsub.getEmitter();
|
||||
expect(emitter.listenerCount('topic')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentEventBus', () => {
|
||||
let bus: AgentEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
bus = new AgentEventBus('agent-1');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await bus.close();
|
||||
});
|
||||
|
||||
it('should emit and receive agent events', async () => {
|
||||
let received: any;
|
||||
|
||||
await bus.on('message', (data) => {
|
||||
received = data;
|
||||
});
|
||||
|
||||
await bus.emit('message', { text: 'Hello' });
|
||||
|
||||
expect(received.text).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should broadcast to all agents', async () => {
|
||||
const bus2 = new AgentEventBus('agent-2');
|
||||
let received: any;
|
||||
let sourceAgent: string | undefined;
|
||||
|
||||
// Both buses share the same underlying pubsub for this test
|
||||
const sharedPubSub = createPubSub();
|
||||
const busA = new AgentEventBus('agent-A', sharedPubSub);
|
||||
const busB = new AgentEventBus('agent-B', sharedPubSub);
|
||||
|
||||
await busB.onBroadcast('announcement', (data, source) => {
|
||||
received = data;
|
||||
sourceAgent = source;
|
||||
});
|
||||
|
||||
await busA.broadcast('announcement', { info: 'Important!' });
|
||||
|
||||
expect(received.info).toBe('Important!');
|
||||
expect(sourceAgent).toBe('agent-A');
|
||||
|
||||
await busA.close();
|
||||
await busB.close();
|
||||
await bus2.close();
|
||||
});
|
||||
|
||||
it('should send message to specific agent', async () => {
|
||||
const sharedPubSub = createPubSub();
|
||||
const busA = new AgentEventBus('agent-A', sharedPubSub);
|
||||
const busB = new AgentEventBus('agent-B', sharedPubSub);
|
||||
|
||||
let receivedByB: any;
|
||||
|
||||
await busB.on('direct', (data) => {
|
||||
receivedByB = data;
|
||||
});
|
||||
|
||||
await busA.sendTo('agent-B', 'direct', { secret: 'message' });
|
||||
|
||||
expect(receivedByB.secret).toBe('message');
|
||||
|
||||
await busA.close();
|
||||
await busB.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentEvents constants', () => {
|
||||
it('should have standard event types', () => {
|
||||
expect(AgentEvents.STARTED).toBe('started');
|
||||
expect(AgentEvents.COMPLETED).toBe('completed');
|
||||
expect(AgentEvents.ERROR).toBe('error');
|
||||
expect(AgentEvents.TOOL_CALLED).toBe('tool_called');
|
||||
expect(AgentEvents.TOOL_RESULT).toBe('tool_result');
|
||||
expect(AgentEvents.MESSAGE_RECEIVED).toBe('message_received');
|
||||
expect(AgentEvents.MESSAGE_SENT).toBe('message_sent');
|
||||
expect(AgentEvents.HANDOFF_INITIATED).toBe('handoff_initiated');
|
||||
expect(AgentEvents.HANDOFF_COMPLETED).toBe('handoff_completed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Factory functions', () => {
|
||||
it('should create EventBus', () => {
|
||||
const bus = createEventBus('test-agent');
|
||||
expect(bus).toBeInstanceOf(AgentEventBus);
|
||||
});
|
||||
|
||||
it('should create PubSub', () => {
|
||||
const pubsub = createPubSub();
|
||||
expect(pubsub).toBeInstanceOf(EventEmitterPubSub);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* LLMGuardrail Unit Tests (TDD - Write tests first)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('LLMGuardrail', () => {
|
||||
describe('Creation', () => {
|
||||
it('should create with criteria', async () => {
|
||||
const { LLMGuardrail } = await import('../../../src/guardrails/llm-guardrail');
|
||||
const guard = new LLMGuardrail({
|
||||
name: 'safety_check',
|
||||
criteria: 'Content must be safe and appropriate'
|
||||
});
|
||||
expect(guard.name).toBe('safety_check');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should have check method', async () => {
|
||||
const { LLMGuardrail } = await import('../../../src/guardrails/llm-guardrail');
|
||||
const guard = new LLMGuardrail({
|
||||
name: 'test',
|
||||
criteria: 'Must be polite'
|
||||
});
|
||||
expect(typeof guard.check).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Chunking Unit Tests (TDD - Write tests first)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Chunking', () => {
|
||||
describe('Text Chunking', () => {
|
||||
it('should chunk text by size', async () => {
|
||||
const { Chunking } = await import('../../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ chunkSize: 100, overlap: 20 });
|
||||
const text = 'A'.repeat(250);
|
||||
const chunks = chunker.chunk(text);
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('should respect overlap', async () => {
|
||||
const { Chunking } = await import('../../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ chunkSize: 50, overlap: 10 });
|
||||
const text = 'Word '.repeat(30);
|
||||
const chunks = chunker.chunk(text);
|
||||
expect(chunks.length).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
it('should chunk by sentences', async () => {
|
||||
const { Chunking } = await import('../../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ strategy: 'sentence' });
|
||||
const text = 'First sentence. Second sentence. Third sentence.';
|
||||
const chunks = chunker.chunkBySentence(text);
|
||||
expect(chunks.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should chunk by paragraphs', async () => {
|
||||
const { Chunking } = await import('../../../src/knowledge/chunking');
|
||||
const chunker = new Chunking({ strategy: 'paragraph' });
|
||||
const text = 'Paragraph one.\n\nParagraph two.\n\nParagraph three.';
|
||||
const chunks = chunker.chunkByParagraph(text);
|
||||
expect(chunks.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Planning System Unit Tests (TDD - Write tests first)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Planning System', () => {
|
||||
describe('Plan', () => {
|
||||
it('should create a plan', async () => {
|
||||
const { Plan } = await import('../../../src/planning');
|
||||
const plan = new Plan({ name: 'Test Plan' });
|
||||
expect(plan.name).toBe('Test Plan');
|
||||
});
|
||||
|
||||
it('should add steps', async () => {
|
||||
const { Plan, PlanStep } = await import('../../../src/planning');
|
||||
const plan = new Plan({ name: 'Test' });
|
||||
plan.addStep(new PlanStep({ description: 'Step 1' }));
|
||||
plan.addStep(new PlanStep({ description: 'Step 2' }));
|
||||
expect(plan.steps.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TodoList', () => {
|
||||
it('should create a todo list', async () => {
|
||||
const { TodoList } = await import('../../../src/planning');
|
||||
const todos = new TodoList();
|
||||
expect(todos.items.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should add items', async () => {
|
||||
const { TodoList, TodoItem } = await import('../../../src/planning');
|
||||
const todos = new TodoList();
|
||||
todos.add(new TodoItem({ content: 'Task 1' }));
|
||||
todos.add(new TodoItem({ content: 'Task 2' }));
|
||||
expect(todos.items.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should mark items complete', async () => {
|
||||
const { TodoList, TodoItem } = await import('../../../src/planning');
|
||||
const todos = new TodoList();
|
||||
const item = new TodoItem({ content: 'Task 1' });
|
||||
todos.add(item);
|
||||
item.complete();
|
||||
expect(item.status).toBe('completed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* BaseTool Unit Tests
|
||||
* Tests for the abstract BaseTool class and plugin extensibility
|
||||
*/
|
||||
|
||||
import { BaseTool, ToolResult, ToolValidationError, validateTool, createTool } from '../../../src/tools/base';
|
||||
|
||||
// Test implementation of BaseTool
|
||||
class WeatherTool extends BaseTool<{ location: string }, { temp: number; condition: string }> {
|
||||
name = 'get_weather';
|
||||
description = 'Get current weather for a location';
|
||||
|
||||
async run(params: { location: string }): Promise<{ temp: number; condition: string }> {
|
||||
return { temp: 22, condition: 'sunny' };
|
||||
}
|
||||
}
|
||||
|
||||
class CalculatorTool extends BaseTool<{ expression: string }, number> {
|
||||
name = 'calculator';
|
||||
description = 'Evaluate a math expression';
|
||||
parameters = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
expression: { type: 'string', description: 'Math expression to evaluate' }
|
||||
},
|
||||
required: ['expression']
|
||||
};
|
||||
|
||||
run(params: { expression: string }): number {
|
||||
return eval(params.expression);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseTool', () => {
|
||||
describe('Basic functionality', () => {
|
||||
it('should create a tool with name and description', () => {
|
||||
const tool = new WeatherTool();
|
||||
expect(tool.name).toBe('get_weather');
|
||||
expect(tool.description).toBe('Get current weather for a location');
|
||||
});
|
||||
|
||||
it('should have default version', () => {
|
||||
const tool = new WeatherTool();
|
||||
expect(tool.version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('should execute run method', async () => {
|
||||
const tool = new WeatherTool();
|
||||
const result = await tool.run({ location: 'New York' });
|
||||
expect(result.temp).toBe(22);
|
||||
expect(result.condition).toBe('sunny');
|
||||
});
|
||||
|
||||
it('should execute via execute method', async () => {
|
||||
const tool = new WeatherTool();
|
||||
const result = await tool.execute({ location: 'London' });
|
||||
expect(result.temp).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeRun', () => {
|
||||
it('should return ToolResult on success', async () => {
|
||||
const tool = new WeatherTool();
|
||||
const result = await tool.safeRun({ location: 'Paris' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output.temp).toBe(22);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should catch errors and return ToolResult', async () => {
|
||||
class FailingTool extends BaseTool<{}, string> {
|
||||
name = 'failing';
|
||||
description = 'Always fails';
|
||||
run(): string {
|
||||
throw new Error('Intentional failure');
|
||||
}
|
||||
}
|
||||
|
||||
const tool = new FailingTool();
|
||||
const result = await tool.safeRun({});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Intentional failure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSchema', () => {
|
||||
it('should return OpenAI-compatible schema', () => {
|
||||
const tool = new CalculatorTool();
|
||||
const schema = tool.getSchema();
|
||||
|
||||
expect(schema.type).toBe('function');
|
||||
expect(schema.function.name).toBe('calculator');
|
||||
expect(schema.function.description).toBe('Evaluate a math expression');
|
||||
expect(schema.function.parameters.properties.expression.type).toBe('string');
|
||||
});
|
||||
|
||||
it('should return empty parameters if not defined', () => {
|
||||
const tool = new WeatherTool();
|
||||
const schema = tool.getSchema();
|
||||
|
||||
expect(schema.function.parameters.type).toBe('object');
|
||||
expect(schema.function.parameters.properties).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
it('should pass validation for valid tool', () => {
|
||||
const tool = new WeatherTool();
|
||||
expect(tool.validate()).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw ToolValidationError for invalid tool', () => {
|
||||
class InvalidTool extends BaseTool<{}, string> {
|
||||
name = '';
|
||||
description = '';
|
||||
run(): string {
|
||||
return 'test';
|
||||
}
|
||||
}
|
||||
|
||||
const tool = new InvalidTool();
|
||||
expect(() => tool.validate()).toThrow(ToolValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return string representation', () => {
|
||||
const tool = new WeatherTool();
|
||||
expect(tool.toString()).toBe("WeatherTool(name='get_weather')");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTool', () => {
|
||||
it('should validate BaseTool instance', () => {
|
||||
const tool = new WeatherTool();
|
||||
expect(validateTool(tool)).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate callable with name', () => {
|
||||
const fn = function myTool() { return 'test'; };
|
||||
expect(validateTool(fn)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw for invalid tool', () => {
|
||||
expect(() => validateTool({})).toThrow(ToolValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTool', () => {
|
||||
it('should create a tool from config', async () => {
|
||||
const tool = createTool({
|
||||
name: 'greeter',
|
||||
description: 'Greet someone',
|
||||
run: (params: { name: string }) => `Hello, ${params.name}!`
|
||||
});
|
||||
|
||||
expect(tool.name).toBe('greeter');
|
||||
expect(tool.description).toBe('Greet someone');
|
||||
|
||||
const result = await tool.run({ name: 'World' });
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should support async run function', async () => {
|
||||
const tool = createTool({
|
||||
name: 'async_tool',
|
||||
description: 'Async tool',
|
||||
run: async (params: { delay: number }) => {
|
||||
await new Promise(r => setTimeout(r, params.delay));
|
||||
return 'done';
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.run({ delay: 10 });
|
||||
expect(result).toBe('done');
|
||||
});
|
||||
|
||||
it('should have safeRun method', async () => {
|
||||
const tool = createTool({
|
||||
name: 'safe_tool',
|
||||
description: 'Safe tool',
|
||||
run: () => 'success'
|
||||
});
|
||||
|
||||
const result = await tool.safeRun({});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toBe('success');
|
||||
});
|
||||
|
||||
it('should have getSchema method', () => {
|
||||
const tool = createTool({
|
||||
name: 'schema_tool',
|
||||
description: 'Tool with schema',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: { type: 'string' }
|
||||
},
|
||||
required: ['input']
|
||||
},
|
||||
run: (params: { input: string }) => params.input
|
||||
});
|
||||
|
||||
const schema = tool.getSchema();
|
||||
expect(schema.function.name).toBe('schema_tool');
|
||||
expect(schema.function.parameters.properties.input.type).toBe('string');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* YAML Workflow Parser Unit Tests
|
||||
*/
|
||||
|
||||
import {
|
||||
parseYAMLWorkflow,
|
||||
createWorkflowFromYAML,
|
||||
validateWorkflowDefinition,
|
||||
YAMLWorkflowDefinition
|
||||
} from '../../../src/workflows/yaml-parser';
|
||||
|
||||
describe('parseYAMLWorkflow', () => {
|
||||
it('should parse basic workflow definition', () => {
|
||||
const yaml = `
|
||||
name: test-workflow
|
||||
description: A test workflow
|
||||
version: 1.0.0
|
||||
steps:
|
||||
- name: step1
|
||||
type: agent
|
||||
agent: researcher
|
||||
- name: step2
|
||||
type: tool
|
||||
tool: search
|
||||
`;
|
||||
|
||||
const result = parseYAMLWorkflow(yaml);
|
||||
|
||||
expect(result.name).toBe('test-workflow');
|
||||
expect(result.description).toBe('A test workflow');
|
||||
expect(result.version).toBe('1.0.0');
|
||||
expect(result.steps.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should parse step properties', () => {
|
||||
const yaml = `
|
||||
name: workflow
|
||||
steps:
|
||||
- name: process
|
||||
type: agent
|
||||
agent: processor
|
||||
onError: retry
|
||||
maxRetries: 3
|
||||
timeout: 5000
|
||||
`;
|
||||
|
||||
const result = parseYAMLWorkflow(yaml);
|
||||
const step = result.steps[0];
|
||||
|
||||
expect(step.name).toBe('process');
|
||||
expect(step.type).toBe('agent');
|
||||
expect(step.agent).toBe('processor');
|
||||
expect(step.onError).toBe('retry');
|
||||
expect(step.maxRetries).toBe(3);
|
||||
expect(step.timeout).toBe(5000);
|
||||
});
|
||||
|
||||
it('should handle condition steps', () => {
|
||||
const yaml = `
|
||||
name: conditional
|
||||
steps:
|
||||
- name: check
|
||||
type: condition
|
||||
condition: result.success === true
|
||||
`;
|
||||
|
||||
const result = parseYAMLWorkflow(yaml);
|
||||
|
||||
expect(result.steps[0].type).toBe('condition');
|
||||
expect(result.steps[0].condition).toBe('result.success === true');
|
||||
});
|
||||
|
||||
it('should ignore comments', () => {
|
||||
const yaml = `
|
||||
# This is a comment
|
||||
name: workflow
|
||||
# Another comment
|
||||
steps:
|
||||
- name: step1
|
||||
type: agent
|
||||
agent: test
|
||||
`;
|
||||
|
||||
const result = parseYAMLWorkflow(yaml);
|
||||
expect(result.name).toBe('workflow');
|
||||
expect(result.steps.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateWorkflowDefinition', () => {
|
||||
it('should pass for valid workflow', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'valid-workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'agent', agent: 'test' }
|
||||
]
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should fail for missing name', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: '',
|
||||
steps: [{ name: 'step1', type: 'agent', agent: 'test' }]
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors).toContain('Workflow must have a name');
|
||||
});
|
||||
|
||||
it('should fail for empty steps', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: []
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors).toContain('Workflow must have at least one step');
|
||||
});
|
||||
|
||||
it('should fail for duplicate step names', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'agent', agent: 'a' },
|
||||
{ name: 'step1', type: 'agent', agent: 'b' }
|
||||
]
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors).toContain('Duplicate step name: step1');
|
||||
});
|
||||
|
||||
it('should fail for agent type without agent field', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'agent' }
|
||||
]
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors.some(e => e.includes('agent type requires'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail for tool type without tool field', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'tool' }
|
||||
]
|
||||
};
|
||||
|
||||
const errors = validateWorkflowDefinition(definition);
|
||||
expect(errors.some(e => e.includes('tool type requires'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWorkflowFromYAML', () => {
|
||||
it('should create executable workflow', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'test-workflow',
|
||||
steps: [
|
||||
{ name: 'greet', type: 'agent', agent: 'greeter' }
|
||||
]
|
||||
};
|
||||
|
||||
const mockAgent = {
|
||||
chat: async (input: string) => `Hello, ${input}!`
|
||||
};
|
||||
|
||||
const { workflow, errors } = createWorkflowFromYAML(
|
||||
definition,
|
||||
{ greeter: mockAgent },
|
||||
{}
|
||||
);
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
expect(workflow.name).toBe('test-workflow');
|
||||
expect(workflow.stepCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should report errors for missing agents', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'agent', agent: 'nonexistent' }
|
||||
]
|
||||
};
|
||||
|
||||
const { errors } = createWorkflowFromYAML(definition, {}, {});
|
||||
|
||||
expect(errors.some(e => e.includes('not found'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should report errors for missing tools', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'workflow',
|
||||
steps: [
|
||||
{ name: 'step1', type: 'tool', tool: 'nonexistent' }
|
||||
]
|
||||
};
|
||||
|
||||
const { errors } = createWorkflowFromYAML(definition, {}, {});
|
||||
|
||||
expect(errors.some(e => e.includes('not found'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should create workflow with tool steps', async () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'tool-workflow',
|
||||
steps: [
|
||||
{ name: 'search', type: 'tool', tool: 'searcher' }
|
||||
]
|
||||
};
|
||||
|
||||
const mockTool = {
|
||||
execute: async (input: any) => `Found: ${input}`
|
||||
};
|
||||
|
||||
const { workflow, errors } = createWorkflowFromYAML(
|
||||
definition,
|
||||
{},
|
||||
{ searcher: mockTool }
|
||||
);
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
expect(workflow.stepCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle condition steps', () => {
|
||||
const definition: YAMLWorkflowDefinition = {
|
||||
name: 'conditional-workflow',
|
||||
steps: [
|
||||
{ name: 'check', type: 'condition', condition: 'input.valid === true' }
|
||||
]
|
||||
};
|
||||
|
||||
const { workflow, errors } = createWorkflowFromYAML(definition, {}, {});
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
expect(workflow.stepCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Auto-generated API server for PraisonAI agents.
|
||||
"""
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from praisonai import PraisonAI
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# CORS configuration
|
||||
CORS(app)
|
||||
|
||||
# Authentication
|
||||
AUTH_ENABLED = False
|
||||
AUTH_TOKEN = None
|
||||
|
||||
def check_auth():
|
||||
"""Check authentication if enabled."""
|
||||
if not AUTH_ENABLED:
|
||||
return True
|
||||
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
return token == AUTH_TOKEN
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({"status": "ok", "service": "praisonai-api"})
|
||||
|
||||
@app.route('/chat', methods=['POST'])
|
||||
def chat():
|
||||
"""Chat endpoint for agent interaction."""
|
||||
if not check_auth():
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'message' not in data:
|
||||
return jsonify({"error": "Message required"}), 400
|
||||
|
||||
try:
|
||||
praisonai = PraisonAI(agent_file="agents.yaml")
|
||||
result = praisonai.run()
|
||||
|
||||
return jsonify({
|
||||
"response": result,
|
||||
"status": "success"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"error": str(e),
|
||||
"status": "error"
|
||||
}), 500
|
||||
|
||||
@app.route('/agents', methods=['GET'])
|
||||
def list_agents():
|
||||
"""List available agents."""
|
||||
if not check_auth():
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
return jsonify({
|
||||
"agents": ["default"],
|
||||
"agent_file": "agents.yaml"
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=8080,
|
||||
debug=False
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
# Example: API Server Deployment
|
||||
# Deploy agents as a local API server
|
||||
|
||||
name: Customer Support Agent
|
||||
description: AI-powered customer support system
|
||||
framework: praisonai
|
||||
|
||||
agents:
|
||||
support_agent:
|
||||
name: Support Agent
|
||||
role: Customer Support Specialist
|
||||
goal: Provide helpful and accurate customer support
|
||||
backstory: Experienced customer service professional with deep product knowledge
|
||||
instructions: |
|
||||
Help customers with their questions and issues.
|
||||
Be polite, professional, and solution-oriented.
|
||||
Escalate complex issues when necessary.
|
||||
tools:
|
||||
- search_tool
|
||||
|
||||
deploy:
|
||||
type: api
|
||||
api:
|
||||
host: 0.0.0.0
|
||||
port: 8005
|
||||
workers: 2
|
||||
cors_enabled: true
|
||||
auth_enabled: false
|
||||
reload: false
|
||||
@@ -0,0 +1,32 @@
|
||||
# Example: AWS Cloud Deployment
|
||||
# Deploy agents to AWS ECS Fargate
|
||||
|
||||
name: Research Agent
|
||||
description: AI research assistant
|
||||
framework: praisonai
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Research Assistant
|
||||
role: Research Specialist
|
||||
goal: Conduct thorough research on topics
|
||||
backstory: Academic researcher with broad knowledge
|
||||
instructions: |
|
||||
Research topics thoroughly using available tools.
|
||||
Synthesize information from multiple sources.
|
||||
Provide well-structured summaries.
|
||||
tools:
|
||||
- search_tool
|
||||
|
||||
deploy:
|
||||
type: cloud
|
||||
cloud:
|
||||
provider: aws
|
||||
region: us-east-1
|
||||
service_name: praisonai-research
|
||||
cpu: "512"
|
||||
memory: "1024"
|
||||
min_instances: 1
|
||||
max_instances: 5
|
||||
# image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/praisonai-research:latest
|
||||
# cluster_name: praisonai-cluster
|
||||
@@ -0,0 +1,31 @@
|
||||
# Example: Azure Cloud Deployment
|
||||
# Deploy agents to Azure Container Apps
|
||||
|
||||
name: Content Generator
|
||||
description: AI content generation system
|
||||
framework: praisonai
|
||||
|
||||
agents:
|
||||
writer:
|
||||
name: Content Writer
|
||||
role: Content Creation Specialist
|
||||
goal: Generate high-quality content
|
||||
backstory: Professional writer with diverse experience
|
||||
instructions: |
|
||||
Create engaging and well-structured content.
|
||||
Adapt tone and style to the target audience.
|
||||
Ensure accuracy and clarity.
|
||||
|
||||
deploy:
|
||||
type: cloud
|
||||
cloud:
|
||||
provider: azure
|
||||
region: eastus
|
||||
service_name: praisonai-writer
|
||||
resource_group: praisonai-rg
|
||||
subscription_id: your-subscription-id-here
|
||||
cpu: "0.5"
|
||||
memory: "1.0"
|
||||
min_instances: 1
|
||||
max_instances: 10
|
||||
# image: myregistry.azurecr.io/praisonai-writer:latest
|
||||
@@ -0,0 +1,28 @@
|
||||
# Example: Docker Deployment
|
||||
# Build and run agents in a Docker container
|
||||
|
||||
name: Data Analysis Agent
|
||||
description: AI agent for data analysis tasks
|
||||
framework: praisonai
|
||||
|
||||
agents:
|
||||
analyst:
|
||||
name: Data Analyst
|
||||
role: Data Analysis Expert
|
||||
goal: Analyze data and provide insights
|
||||
backstory: Skilled data scientist with expertise in statistical analysis
|
||||
instructions: |
|
||||
Analyze datasets and generate insights.
|
||||
Create visualizations when helpful.
|
||||
Explain findings clearly.
|
||||
|
||||
deploy:
|
||||
type: docker
|
||||
docker:
|
||||
image_name: praisonai-analyst
|
||||
tag: v1.0.0
|
||||
base_image: python:3.11-slim
|
||||
expose:
|
||||
- 8005
|
||||
registry: ghcr.io/myorg
|
||||
push: false
|
||||
@@ -0,0 +1,30 @@
|
||||
# Example: GCP Cloud Deployment
|
||||
# Deploy agents to Google Cloud Run
|
||||
|
||||
name: Translation Agent
|
||||
description: AI-powered translation service
|
||||
framework: praisonai
|
||||
|
||||
agents:
|
||||
translator:
|
||||
name: Translator
|
||||
role: Language Translation Expert
|
||||
goal: Provide accurate translations
|
||||
backstory: Multilingual expert with cultural awareness
|
||||
instructions: |
|
||||
Translate text accurately between languages.
|
||||
Preserve meaning and context.
|
||||
Adapt for cultural nuances when appropriate.
|
||||
|
||||
deploy:
|
||||
type: cloud
|
||||
cloud:
|
||||
provider: gcp
|
||||
region: us-central1
|
||||
service_name: praisonai-translator
|
||||
project_id: your-project-id-here
|
||||
cpu: "1"
|
||||
memory: "512"
|
||||
min_instances: 0
|
||||
max_instances: 10
|
||||
# image: gcr.io/your-project-id/praisonai-translator:latest
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.5.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.5.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.7.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.7.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -14,6 +14,10 @@ from .version import __version__
|
||||
__all__ = [
|
||||
'PraisonAI',
|
||||
'__version__',
|
||||
'Deploy',
|
||||
'DeployConfig',
|
||||
'DeployType',
|
||||
'CloudProvider',
|
||||
]
|
||||
|
||||
# Lazy loading for heavy imports
|
||||
@@ -22,6 +26,18 @@ def __getattr__(name):
|
||||
if name == 'PraisonAI':
|
||||
from .cli import PraisonAI
|
||||
return PraisonAI
|
||||
elif name == 'Deploy':
|
||||
from .deploy import Deploy
|
||||
return Deploy
|
||||
elif name == 'DeployConfig':
|
||||
from .deploy import DeployConfig
|
||||
return DeployConfig
|
||||
elif name == 'DeployType':
|
||||
from .deploy import DeployType
|
||||
return DeployType
|
||||
elif name == 'CloudProvider':
|
||||
from .deploy import CloudProvider
|
||||
return CloudProvider
|
||||
|
||||
# Try praisonaiagents exports
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
"""
|
||||
CLI handler for deploy commands.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich import print as rprint
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class DeployHandler:
|
||||
"""Handler for deploy CLI commands."""
|
||||
|
||||
def handle_deploy(self, args):
|
||||
"""
|
||||
Handle deploy command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy import Deploy, DeployConfig, DeployType, CloudProvider
|
||||
from praisonai.deploy.models import APIConfig, DockerConfig, CloudConfig
|
||||
|
||||
# Determine if loading from YAML or using explicit flags
|
||||
if args.type:
|
||||
# Explicit type specified via CLI flags
|
||||
deploy_type = DeployType(args.type)
|
||||
|
||||
if deploy_type == DeployType.API:
|
||||
config = DeployConfig(
|
||||
type=deploy_type,
|
||||
api=APIConfig(
|
||||
host=getattr(args, 'host', '127.0.0.1'),
|
||||
port=getattr(args, 'port', 8005),
|
||||
workers=getattr(args, 'workers', 1)
|
||||
)
|
||||
)
|
||||
elif deploy_type == DeployType.DOCKER:
|
||||
config = DeployConfig(
|
||||
type=deploy_type,
|
||||
docker=DockerConfig(
|
||||
image_name=getattr(args, 'image_name', 'praisonai-app'),
|
||||
tag=getattr(args, 'tag', 'latest'),
|
||||
push=getattr(args, 'push', False),
|
||||
registry=getattr(args, 'registry', None)
|
||||
)
|
||||
)
|
||||
elif deploy_type == DeployType.CLOUD:
|
||||
provider = CloudProvider(args.provider)
|
||||
config = DeployConfig(
|
||||
type=deploy_type,
|
||||
cloud=CloudConfig(
|
||||
provider=provider,
|
||||
region=getattr(args, 'region', 'us-east-1'),
|
||||
service_name=getattr(args, 'service_name', 'praisonai-service'),
|
||||
project_id=getattr(args, 'project_id', None),
|
||||
resource_group=getattr(args, 'resource_group', None),
|
||||
subscription_id=getattr(args, 'subscription_id', None)
|
||||
)
|
||||
)
|
||||
|
||||
deploy = Deploy(config, args.file)
|
||||
else:
|
||||
# Load from YAML
|
||||
deploy = Deploy.from_yaml(args.file)
|
||||
|
||||
# Execute deployment
|
||||
console.print(f"\n[bold blue]🚀 Starting deployment...[/bold blue]\n")
|
||||
|
||||
background = getattr(args, 'background', False)
|
||||
result = deploy.deploy(background=background)
|
||||
|
||||
if args.json:
|
||||
self._print_json({
|
||||
"success": result.success,
|
||||
"message": result.message,
|
||||
"url": result.url,
|
||||
"error": result.error,
|
||||
"metadata": result.metadata
|
||||
})
|
||||
else:
|
||||
if result.success:
|
||||
console.print(f"\n[bold green]✅ {result.message}[/bold green]")
|
||||
if result.url:
|
||||
console.print(f"[bold cyan]🔗 URL:[/bold cyan] {result.url}")
|
||||
if result.metadata:
|
||||
console.print(f"\n[bold]Metadata:[/bold]")
|
||||
for key, value in result.metadata.items():
|
||||
console.print(f" • {key}: {value}")
|
||||
else:
|
||||
console.print(f"\n[bold red]❌ {result.message}[/bold red]")
|
||||
if result.error:
|
||||
console.print(f"[red]Error: {result.error}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
if getattr(args, 'json', False):
|
||||
self._print_json({"success": False, "error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Deployment failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_doctor(self, args):
|
||||
"""
|
||||
Handle deploy doctor command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy.doctor import (
|
||||
run_all_checks, run_local_checks,
|
||||
run_aws_checks, run_azure_checks, run_gcp_checks
|
||||
)
|
||||
|
||||
console.print("\n[bold blue]🏥 Running deployment health checks...[/bold blue]\n")
|
||||
|
||||
# Determine which checks to run
|
||||
if args.all:
|
||||
report = run_all_checks(args.file)
|
||||
elif args.provider:
|
||||
if args.provider == 'aws':
|
||||
report = run_aws_checks()
|
||||
elif args.provider == 'azure':
|
||||
report = run_azure_checks()
|
||||
elif args.provider == 'gcp':
|
||||
report = run_gcp_checks()
|
||||
else:
|
||||
console.print(f"[red]Unknown provider: {args.provider}[/red]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
report = run_local_checks(agents_file=args.file)
|
||||
|
||||
if args.json:
|
||||
self._print_json({
|
||||
"total_checks": report.total_checks,
|
||||
"passed": report.passed_checks,
|
||||
"failed": report.failed_checks,
|
||||
"all_passed": report.all_passed,
|
||||
"checks": [
|
||||
{
|
||||
"name": c.name,
|
||||
"passed": c.passed,
|
||||
"message": c.message,
|
||||
"fix_suggestion": c.fix_suggestion
|
||||
}
|
||||
for c in report.checks
|
||||
]
|
||||
})
|
||||
else:
|
||||
# Create results table
|
||||
table = Table(title="Health Check Results")
|
||||
table.add_column("Check", style="cyan")
|
||||
table.add_column("Status", style="bold")
|
||||
table.add_column("Message")
|
||||
|
||||
for check in report.checks:
|
||||
status = "[green]✅ PASS[/green]" if check.passed else "[red]❌ FAIL[/red]"
|
||||
table.add_row(check.name, status, check.message)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Summary
|
||||
console.print(f"\n[bold]Summary:[/bold]")
|
||||
console.print(f" Total checks: {report.total_checks}")
|
||||
console.print(f" Passed: [green]{report.passed_checks}[/green]")
|
||||
console.print(f" Failed: [red]{report.failed_checks}[/red]")
|
||||
|
||||
# Show fix suggestions for failed checks
|
||||
failed_checks = [c for c in report.checks if not c.passed and c.fix_suggestion]
|
||||
if failed_checks:
|
||||
console.print(f"\n[bold yellow]💡 Fix Suggestions:[/bold yellow]")
|
||||
for check in failed_checks:
|
||||
console.print(f"\n[bold]{check.name}:[/bold]")
|
||||
console.print(f" {check.fix_suggestion}")
|
||||
|
||||
if not report.all_passed:
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
if getattr(args, 'json', False):
|
||||
self._print_json({"error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Doctor check failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_init(self, args):
|
||||
"""
|
||||
Handle deploy init command - generate sample agents.yaml.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy.schema import save_sample_yaml
|
||||
from praisonai.deploy.models import DeployType, CloudProvider
|
||||
|
||||
deploy_type = DeployType(args.type) if args.type else DeployType.API
|
||||
provider = CloudProvider(args.provider) if hasattr(args, 'provider') and args.provider else None
|
||||
|
||||
console.print(f"\n[bold blue]📝 Generating sample agents.yaml...[/bold blue]\n")
|
||||
|
||||
save_sample_yaml(args.file, deploy_type, provider)
|
||||
|
||||
console.print(f"[bold green]✅ Created {args.file}[/bold green]")
|
||||
console.print(f"\n[bold]Next steps:[/bold]")
|
||||
console.print(f" 1. Edit {args.file} to configure your agents")
|
||||
console.print(f" 2. Run: praisonai deploy validate --file {args.file}")
|
||||
console.print(f" 3. Run: praisonai deploy --file {args.file}")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]❌ Failed to create sample YAML: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_validate(self, args):
|
||||
"""
|
||||
Handle deploy validate command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy.schema import validate_agents_yaml
|
||||
|
||||
console.print(f"\n[bold blue]🔍 Validating {args.file}...[/bold blue]\n")
|
||||
|
||||
config = validate_agents_yaml(args.file)
|
||||
|
||||
if config is None:
|
||||
if args.json:
|
||||
self._print_json({"valid": False, "error": "No deploy section found"})
|
||||
else:
|
||||
console.print("[yellow]⚠️ No deploy configuration found in agents.yaml[/yellow]")
|
||||
console.print("\nRun: praisonai deploy init --file agents.yaml")
|
||||
sys.exit(1)
|
||||
|
||||
if args.json:
|
||||
self._print_json({
|
||||
"valid": True,
|
||||
"type": config.type.value,
|
||||
"config": config.model_dump()
|
||||
})
|
||||
else:
|
||||
console.print("[bold green]✅ Configuration is valid![/bold green]")
|
||||
console.print(f"\n[bold]Deploy Type:[/bold] {config.type.value}")
|
||||
|
||||
if config.type.value == "api":
|
||||
console.print(f" Host: {config.api.host}")
|
||||
console.print(f" Port: {config.api.port}")
|
||||
console.print(f" Workers: {config.api.workers}")
|
||||
elif config.type.value == "docker":
|
||||
console.print(f" Image: {config.docker.image_name}:{config.docker.tag}")
|
||||
console.print(f" Ports: {config.docker.expose}")
|
||||
elif config.type.value == "cloud":
|
||||
console.print(f" Provider: {config.cloud.provider.value}")
|
||||
console.print(f" Region: {config.cloud.region}")
|
||||
console.print(f" Service: {config.cloud.service_name}")
|
||||
|
||||
except ValueError as e:
|
||||
if args.json:
|
||||
self._print_json({"valid": False, "error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Validation failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
if args.json:
|
||||
self._print_json({"valid": False, "error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Error: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_plan(self, args):
|
||||
"""
|
||||
Handle deploy plan command - show deployment plan without executing.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy import Deploy
|
||||
|
||||
console.print(f"\n[bold blue]📋 Generating deployment plan...[/bold blue]\n")
|
||||
|
||||
deploy = Deploy.from_yaml(args.file)
|
||||
plan = deploy.plan()
|
||||
|
||||
if args.json:
|
||||
self._print_json(plan)
|
||||
else:
|
||||
console.print(Panel.fit(
|
||||
self._format_plan(plan),
|
||||
title="[bold]Deployment Plan[/bold]",
|
||||
border_style="blue"
|
||||
))
|
||||
|
||||
console.print("\n[bold]To execute this plan, run:[/bold]")
|
||||
console.print(f" praisonai deploy --file {args.file}")
|
||||
|
||||
except Exception as e:
|
||||
if args.json:
|
||||
self._print_json({"error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Failed to generate plan: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def _format_plan(self, plan: dict) -> str:
|
||||
"""Format plan dictionary as readable text."""
|
||||
lines = []
|
||||
for key, value in plan.items():
|
||||
if key == "steps" and isinstance(value, list):
|
||||
lines.append(f"\n[bold]Steps:[/bold]")
|
||||
for step in value:
|
||||
lines.append(f" {step}")
|
||||
elif isinstance(value, dict):
|
||||
lines.append(f"\n[bold]{key}:[/bold]")
|
||||
for k, v in value.items():
|
||||
lines.append(f" {k}: {v}")
|
||||
else:
|
||||
lines.append(f"[bold]{key}:[/bold] {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def handle_status(self, args):
|
||||
"""
|
||||
Handle deploy status command - show current deployment status.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy import Deploy
|
||||
|
||||
console.print(f"\n[bold blue]📊 Checking deployment status...[/bold blue]\n")
|
||||
|
||||
deploy = Deploy.from_yaml(args.file)
|
||||
status = deploy.status()
|
||||
|
||||
if args.json:
|
||||
self._print_json(status.to_dict())
|
||||
else:
|
||||
# Create status table
|
||||
table = Table(title="Deployment Status")
|
||||
table.add_column("Property", style="cyan")
|
||||
table.add_column("Value", style="bold")
|
||||
|
||||
# State with color
|
||||
state_color = {
|
||||
"running": "green",
|
||||
"stopped": "yellow",
|
||||
"pending": "blue",
|
||||
"failed": "red",
|
||||
"not_found": "dim",
|
||||
"unknown": "dim"
|
||||
}.get(status.state.value, "white")
|
||||
|
||||
table.add_row("State", f"[{state_color}]{status.state.value.upper()}[/{state_color}]")
|
||||
table.add_row("Service Name", status.service_name or "N/A")
|
||||
table.add_row("Provider", status.provider or "N/A")
|
||||
table.add_row("Region", status.region or "N/A")
|
||||
|
||||
if status.url:
|
||||
table.add_row("URL", f"[cyan]{status.url}[/cyan]")
|
||||
|
||||
table.add_row("Healthy", "✅ Yes" if status.healthy else "❌ No")
|
||||
table.add_row("Instances", f"{status.instances_running}/{status.instances_desired}")
|
||||
|
||||
if status.created_at:
|
||||
table.add_row("Created", status.created_at)
|
||||
if status.updated_at:
|
||||
table.add_row("Updated", status.updated_at)
|
||||
|
||||
console.print(table)
|
||||
|
||||
if status.message:
|
||||
console.print(f"\n[bold]Message:[/bold] {status.message}")
|
||||
|
||||
# Show metadata if verbose
|
||||
if getattr(args, 'verbose', False) and status.metadata:
|
||||
console.print(f"\n[bold]Metadata:[/bold]")
|
||||
for key, value in status.metadata.items():
|
||||
console.print(f" • {key}: {value}")
|
||||
|
||||
except Exception as e:
|
||||
if getattr(args, 'json', False):
|
||||
self._print_json({"error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Failed to get status: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_destroy(self, args):
|
||||
"""
|
||||
Handle deploy destroy command - destroy/delete deployment.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
try:
|
||||
from praisonai.deploy import Deploy
|
||||
|
||||
deploy = Deploy.from_yaml(args.file)
|
||||
|
||||
# Confirmation check unless --yes is provided
|
||||
if not args.yes:
|
||||
console.print(f"\n[bold yellow]⚠️ Warning: This will destroy the deployment![/bold yellow]")
|
||||
console.print(f"[bold]File:[/bold] {args.file}")
|
||||
|
||||
# Show what will be destroyed
|
||||
status = deploy.status()
|
||||
if status.service_name:
|
||||
console.print(f"[bold]Service:[/bold] {status.service_name}")
|
||||
if status.provider:
|
||||
console.print(f"[bold]Provider:[/bold] {status.provider}")
|
||||
|
||||
confirm = input("\nType 'yes' to confirm destruction: ")
|
||||
if confirm.lower() != 'yes':
|
||||
console.print("[yellow]Destruction cancelled.[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"\n[bold red]🗑️ Destroying deployment...[/bold red]\n")
|
||||
|
||||
force = getattr(args, 'force', False)
|
||||
result = deploy.destroy(force=force)
|
||||
|
||||
if args.json:
|
||||
self._print_json({
|
||||
"success": result.success,
|
||||
"message": result.message,
|
||||
"resources_deleted": result.resources_deleted,
|
||||
"error": result.error,
|
||||
"metadata": result.metadata
|
||||
})
|
||||
else:
|
||||
if result.success:
|
||||
console.print(f"[bold green]✅ {result.message}[/bold green]")
|
||||
if result.resources_deleted:
|
||||
console.print(f"\n[bold]Deleted resources:[/bold]")
|
||||
for resource in result.resources_deleted:
|
||||
console.print(f" • {resource}")
|
||||
else:
|
||||
console.print(f"[bold red]❌ {result.message}[/bold red]")
|
||||
if result.error:
|
||||
console.print(f"[red]Error: {result.error}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
if getattr(args, 'json', False):
|
||||
self._print_json({"success": False, "error": str(e)})
|
||||
else:
|
||||
console.print(f"[bold red]❌ Destruction failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
def _print_json(self, data: dict):
|
||||
"""Print data as JSON."""
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
def add_deploy_subcommands(subparsers):
|
||||
"""
|
||||
Add deploy subcommands to CLI parser.
|
||||
|
||||
Args:
|
||||
subparsers: Subparsers from argparse
|
||||
"""
|
||||
# Main deploy command
|
||||
deploy_parser = subparsers.add_parser('deploy', help='Deploy agents')
|
||||
deploy_subparsers = deploy_parser.add_subparsers(dest='deploy_command')
|
||||
|
||||
# deploy (main deployment)
|
||||
deploy_main = deploy_subparsers.add_parser('run', help='Deploy agents')
|
||||
deploy_main.add_argument('--file', '-f', default='agents.yaml', help='Path to agents.yaml')
|
||||
deploy_main.add_argument('--type', choices=['api', 'docker', 'cloud'], help='Deployment type')
|
||||
deploy_main.add_argument('--provider', choices=['aws', 'azure', 'gcp'], help='Cloud provider (for cloud type)')
|
||||
deploy_main.add_argument('--background', action='store_true', help='Run in background (API only)')
|
||||
deploy_main.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# deploy doctor
|
||||
doctor_parser = deploy_subparsers.add_parser('doctor', help='Check deployment readiness')
|
||||
doctor_parser.add_argument('--file', '-f', help='Path to agents.yaml')
|
||||
doctor_parser.add_argument('--provider', choices=['aws', 'azure', 'gcp'], help='Check specific provider')
|
||||
doctor_parser.add_argument('--all', action='store_true', help='Run all checks')
|
||||
doctor_parser.add_argument('--verbose', action='store_true', help='Verbose output')
|
||||
doctor_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# deploy init
|
||||
init_parser = deploy_subparsers.add_parser('init', help='Generate sample agents.yaml')
|
||||
init_parser.add_argument('--file', '-f', default='agents.yaml', help='Output file path')
|
||||
init_parser.add_argument('--type', choices=['api', 'docker', 'cloud'], default='api', help='Deployment type')
|
||||
init_parser.add_argument('--provider', choices=['aws', 'azure', 'gcp'], help='Cloud provider (for cloud type)')
|
||||
|
||||
# deploy validate
|
||||
validate_parser = deploy_subparsers.add_parser('validate', help='Validate agents.yaml deploy config')
|
||||
validate_parser.add_argument('--file', '-f', default='agents.yaml', help='Path to agents.yaml')
|
||||
validate_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# deploy plan
|
||||
plan_parser = deploy_subparsers.add_parser('plan', help='Show deployment plan')
|
||||
plan_parser.add_argument('--file', '-f', default='agents.yaml', help='Path to agents.yaml')
|
||||
plan_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
# deploy status
|
||||
status_parser = deploy_subparsers.add_parser('status', help='Show deployment status')
|
||||
status_parser.add_argument('--file', '-f', default='agents.yaml', help='Path to agents.yaml')
|
||||
status_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
status_parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed metadata')
|
||||
|
||||
# deploy destroy
|
||||
destroy_parser = deploy_subparsers.add_parser('destroy', help='Destroy/delete deployment')
|
||||
destroy_parser.add_argument('--file', '-f', default='agents.yaml', help='Path to agents.yaml')
|
||||
destroy_parser.add_argument('--yes', '-y', action='store_true', help='Skip confirmation prompt')
|
||||
destroy_parser.add_argument('--force', action='store_true', help='Force deletion and remove all related resources')
|
||||
destroy_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
||||
|
||||
return deploy_parser
|
||||
@@ -416,10 +416,20 @@ class PraisonAI:
|
||||
print("Failed to start scheduler")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# One-time deployment (backward compatible)
|
||||
from praisonai.deploy import CloudDeployer
|
||||
deployer = CloudDeployer()
|
||||
deployer.run_commands()
|
||||
# One-time deployment using new deploy system
|
||||
from praisonai.cli.features.deploy import DeployHandler
|
||||
handler = DeployHandler()
|
||||
|
||||
# Create args object for handler
|
||||
class DeployArgs:
|
||||
def __init__(self):
|
||||
self.file = "agents.yaml"
|
||||
self.type = None
|
||||
self.provider = args.provider if hasattr(args, 'provider') else 'gcp'
|
||||
self.json = False
|
||||
self.background = False
|
||||
|
||||
handler.handle_deploy(DeployArgs())
|
||||
return
|
||||
|
||||
if getattr(args, 'chat', False):
|
||||
@@ -668,7 +678,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -1180,6 +1190,99 @@ class PraisonAI:
|
||||
from .features.output_style import handle_output_command
|
||||
exit_code = handle_output_command(unknown_args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
elif args.command == 'deploy':
|
||||
# Deploy command - deploy agents as API/Docker/Cloud
|
||||
from .features.deploy import DeployHandler
|
||||
handler = DeployHandler()
|
||||
|
||||
# Parse deploy subcommand from unknown_args
|
||||
if unknown_args:
|
||||
subcommand = unknown_args[0] if unknown_args else None
|
||||
sub_args = unknown_args[1:] if len(unknown_args) > 1 else []
|
||||
|
||||
# Create args namespace for handler
|
||||
import argparse as ap
|
||||
deploy_args = ap.Namespace()
|
||||
# Use file from main args if provided, otherwise default
|
||||
deploy_args.file = getattr(args, 'file', None) or 'agents.yaml'
|
||||
deploy_args.all = False
|
||||
deploy_args.verbose = False
|
||||
deploy_args.background = False
|
||||
deploy_args.yes = False
|
||||
deploy_args.force = False
|
||||
deploy_args.provider = getattr(args, 'provider', None)
|
||||
deploy_args.type = None
|
||||
deploy_args.background = False
|
||||
|
||||
# Parse sub_args (may override main args)
|
||||
i = 0
|
||||
while i < len(sub_args):
|
||||
arg = sub_args[i]
|
||||
if arg in ['--file', '-f'] and i + 1 < len(sub_args):
|
||||
deploy_args.file = sub_args[i + 1]
|
||||
i += 2
|
||||
elif arg == '--json':
|
||||
deploy_args.json = True
|
||||
i += 1
|
||||
elif arg == '--verbose':
|
||||
deploy_args.verbose = True
|
||||
i += 1
|
||||
elif arg == '--all':
|
||||
deploy_args.all = True
|
||||
i += 1
|
||||
elif arg == '--provider' and i + 1 < len(sub_args):
|
||||
deploy_args.provider = sub_args[i + 1]
|
||||
i += 2
|
||||
elif arg == '--type' and i + 1 < len(sub_args):
|
||||
deploy_args.type = sub_args[i + 1]
|
||||
i += 2
|
||||
elif arg == '--background':
|
||||
deploy_args.background = True
|
||||
i += 1
|
||||
elif arg in ['--yes', '-y']:
|
||||
deploy_args.yes = True
|
||||
i += 1
|
||||
elif arg == '--force':
|
||||
deploy_args.force = True
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if subcommand == 'doctor':
|
||||
handler.handle_doctor(deploy_args)
|
||||
elif subcommand == 'init':
|
||||
handler.handle_init(deploy_args)
|
||||
elif subcommand == 'validate':
|
||||
handler.handle_validate(deploy_args)
|
||||
elif subcommand == 'plan':
|
||||
handler.handle_plan(deploy_args)
|
||||
elif subcommand == 'status':
|
||||
handler.handle_status(deploy_args)
|
||||
elif subcommand == 'destroy':
|
||||
handler.handle_destroy(deploy_args)
|
||||
elif subcommand == 'run' or subcommand in ['api', 'docker', 'cloud']:
|
||||
if subcommand in ['api', 'docker', 'cloud']:
|
||||
deploy_args.type = subcommand
|
||||
handler.handle_deploy(deploy_args)
|
||||
else:
|
||||
print(f"Unknown deploy subcommand: {subcommand}")
|
||||
print("Available: doctor, init, validate, plan, status, destroy, run, api, docker, cloud")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# No subcommand - show help
|
||||
print("Deploy commands:")
|
||||
print(" praisonai deploy doctor [--all] [--provider aws|azure|gcp] [--json]")
|
||||
print(" praisonai deploy init [--file FILE] [--type api|docker|cloud]")
|
||||
print(" praisonai deploy validate [--file FILE] [--json]")
|
||||
print(" praisonai deploy plan [--file FILE] [--json]")
|
||||
print(" praisonai deploy status [--file FILE] [--json] [--verbose]")
|
||||
print(" praisonai deploy destroy [--file FILE] [--yes] [--force] [--json]")
|
||||
print(" praisonai deploy run [--file FILE] [--type api|docker|cloud] [--provider aws|azure|gcp]")
|
||||
print(" praisonai deploy api [--file FILE]")
|
||||
print(" praisonai deploy docker [--file FILE]")
|
||||
print(" praisonai deploy cloud --provider aws|azure|gcp [--file FILE]")
|
||||
sys.exit(0)
|
||||
|
||||
# Only check framework availability for agent-related operations
|
||||
if not args.command and (args.init or args.auto or args.framework):
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.5.5 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.5.7 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Deploy module for PraisonAI - API, Docker, and Cloud deployments.
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import DeployConfig, DeployResult, DeployType, CloudProvider
|
||||
from .schema import validate_agents_yaml, generate_sample_yaml
|
||||
from .doctor import DoctorReport, run_all_checks
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load deploy modules."""
|
||||
if name == 'Deploy':
|
||||
from .main import Deploy
|
||||
return Deploy
|
||||
elif name == 'DeployConfig':
|
||||
from .models import DeployConfig
|
||||
return DeployConfig
|
||||
elif name == 'DeployType':
|
||||
from .models import DeployType
|
||||
return DeployType
|
||||
elif name == 'CloudProvider':
|
||||
from .models import CloudProvider
|
||||
return CloudProvider
|
||||
elif name == 'DeployResult':
|
||||
from .models import DeployResult
|
||||
return DeployResult
|
||||
elif name == 'DeployStatus':
|
||||
from .models import DeployStatus
|
||||
return DeployStatus
|
||||
elif name == 'DestroyResult':
|
||||
from .models import DestroyResult
|
||||
return DestroyResult
|
||||
elif name == 'ServiceState':
|
||||
from .models import ServiceState
|
||||
return ServiceState
|
||||
elif name == 'validate_agents_yaml':
|
||||
from .schema import validate_agents_yaml
|
||||
return validate_agents_yaml
|
||||
elif name == 'generate_sample_yaml':
|
||||
from .schema import generate_sample_yaml
|
||||
return generate_sample_yaml
|
||||
elif name == 'run_all_checks':
|
||||
from .doctor import run_all_checks
|
||||
return run_all_checks
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Deploy',
|
||||
'DeployConfig',
|
||||
'DeployType',
|
||||
'CloudProvider',
|
||||
'DeployResult',
|
||||
'DeployStatus',
|
||||
'DestroyResult',
|
||||
'ServiceState',
|
||||
'validate_agents_yaml',
|
||||
'generate_sample_yaml',
|
||||
'run_all_checks'
|
||||
]
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
API server deployment functionality.
|
||||
"""
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from typing import Optional
|
||||
from .models import APIConfig, DeployResult
|
||||
|
||||
|
||||
def generate_api_server_code(agents_file: str, config: Optional[APIConfig] = None) -> str:
|
||||
"""
|
||||
Generate API server code for serving agents.
|
||||
|
||||
Args:
|
||||
agents_file: Path to agents.yaml file
|
||||
config: API configuration
|
||||
|
||||
Returns:
|
||||
Python code for API server
|
||||
"""
|
||||
if config is None:
|
||||
config = APIConfig()
|
||||
|
||||
code = f'''"""
|
||||
Auto-generated API server for PraisonAI agents.
|
||||
"""
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from praisonai import PraisonAI
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# CORS configuration
|
||||
{"CORS(app)" if config.cors_enabled else "# CORS disabled"}
|
||||
|
||||
# Authentication
|
||||
AUTH_ENABLED = {config.auth_enabled}
|
||||
AUTH_TOKEN = {repr(config.auth_token)}
|
||||
|
||||
def check_auth():
|
||||
"""Check authentication if enabled."""
|
||||
if not AUTH_ENABLED:
|
||||
return True
|
||||
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
return token == AUTH_TOKEN
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({{"status": "ok", "service": "praisonai-api"}})
|
||||
|
||||
@app.route('/chat', methods=['POST'])
|
||||
def chat():
|
||||
"""Chat endpoint for agent interaction."""
|
||||
if not check_auth():
|
||||
return jsonify({{"error": "Unauthorized"}}), 401
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'message' not in data:
|
||||
return jsonify({{"error": "Message required"}}), 400
|
||||
|
||||
try:
|
||||
praisonai = PraisonAI(agent_file="{agents_file}")
|
||||
result = praisonai.run()
|
||||
|
||||
return jsonify({{
|
||||
"response": result,
|
||||
"status": "success"
|
||||
}})
|
||||
except Exception as e:
|
||||
return jsonify({{
|
||||
"error": str(e),
|
||||
"status": "error"
|
||||
}}), 500
|
||||
|
||||
@app.route('/agents', methods=['GET'])
|
||||
def list_agents():
|
||||
"""List available agents."""
|
||||
if not check_auth():
|
||||
return jsonify({{"error": "Unauthorized"}}), 401
|
||||
|
||||
return jsonify({{
|
||||
"agents": ["default"],
|
||||
"agent_file": "{agents_file}"
|
||||
}})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host='{config.host}',
|
||||
port={config.port},
|
||||
debug={config.reload}
|
||||
)
|
||||
'''
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def start_api_server(
|
||||
agents_file: str,
|
||||
config: Optional[APIConfig] = None,
|
||||
background: bool = False
|
||||
) -> DeployResult:
|
||||
"""
|
||||
Start API server for agents.
|
||||
|
||||
Args:
|
||||
agents_file: Path to agents.yaml file
|
||||
config: API configuration
|
||||
background: Run in background mode
|
||||
|
||||
Returns:
|
||||
DeployResult with server information
|
||||
"""
|
||||
if config is None:
|
||||
config = APIConfig()
|
||||
|
||||
try:
|
||||
# Generate server code
|
||||
server_code = generate_api_server_code(agents_file, config)
|
||||
|
||||
# Write to temporary file
|
||||
server_file = "api_server.py"
|
||||
with open(server_file, 'w') as f:
|
||||
f.write(server_code)
|
||||
|
||||
# Install flask and flask-cors if needed
|
||||
try:
|
||||
subprocess.run(
|
||||
['pip', 'install', '-q', 'flask', 'flask-cors'],
|
||||
check=False,
|
||||
capture_output=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Start server
|
||||
if background:
|
||||
process = subprocess.Popen(
|
||||
['python', server_file],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
start_new_session=True
|
||||
)
|
||||
|
||||
# Wait a bit to check if it started successfully
|
||||
time.sleep(2)
|
||||
|
||||
if process.poll() is None:
|
||||
url = f"http://{config.host}:{config.port}"
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"API server started in background (PID: {process.pid})",
|
||||
url=url,
|
||||
metadata={"pid": process.pid, "server_file": server_file}
|
||||
)
|
||||
else:
|
||||
stderr = process.stderr.read().decode() if process.stderr else "Unknown error"
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Failed to start API server",
|
||||
error=stderr
|
||||
)
|
||||
else:
|
||||
# Run in foreground
|
||||
url = f"http://{config.host}:{config.port}"
|
||||
print(f"\n🚀 Starting API server at {url}")
|
||||
print(f"📁 Serving agents from: {agents_file}")
|
||||
print(f"🔗 Health check: {url}/health")
|
||||
print(f"💬 Chat endpoint: {url}/chat")
|
||||
print("\nPress Ctrl+C to stop the server\n")
|
||||
|
||||
process = subprocess.Popen(['python', server_file])
|
||||
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"API server running at {url}",
|
||||
url=url,
|
||||
metadata={"pid": process.pid, "server_file": server_file}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Failed to start API server",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
def check_api_health(url: str, timeout: int = 5) -> bool:
|
||||
"""
|
||||
Check if API server is healthy.
|
||||
|
||||
Args:
|
||||
url: Base URL of API server
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
True if healthy, False otherwise
|
||||
"""
|
||||
try:
|
||||
import urllib.request
|
||||
health_url = f"{url}/health"
|
||||
|
||||
req = urllib.request.Request(health_url)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
return response.status == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def stop_api_server(pid: int) -> bool:
|
||||
"""
|
||||
Stop API server by PID.
|
||||
|
||||
Args:
|
||||
pid: Process ID of server
|
||||
|
||||
Returns:
|
||||
True if stopped successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
Docker deployment functionality.
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Optional, Dict
|
||||
from pathlib import Path
|
||||
from .models import DockerConfig, DeployResult, DeployStatus, DestroyResult, ServiceState
|
||||
|
||||
|
||||
def generate_dockerfile(agents_file: str, config: Optional[DockerConfig] = None) -> str:
|
||||
"""
|
||||
Generate Dockerfile for agents.
|
||||
|
||||
Args:
|
||||
agents_file: Path to agents.yaml file
|
||||
config: Docker configuration
|
||||
|
||||
Returns:
|
||||
Dockerfile content as string
|
||||
"""
|
||||
if config is None:
|
||||
config = DockerConfig()
|
||||
|
||||
# Build expose statements
|
||||
expose_lines = "\n".join([f"EXPOSE {port}" for port in config.expose])
|
||||
|
||||
dockerfile = f"""FROM {config.base_image}
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application files
|
||||
COPY {agents_file} /app/{agents_file}
|
||||
COPY . /app/
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir praisonai flask flask-cors gunicorn
|
||||
|
||||
# Expose ports
|
||||
{expose_lines}
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:{config.expose[0]}/health')" || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:{config.expose[0]}", "-w", "1", "api_server:app"]
|
||||
"""
|
||||
|
||||
return dockerfile
|
||||
|
||||
|
||||
def check_docker_installed() -> bool:
|
||||
"""
|
||||
Check if Docker is installed and available.
|
||||
|
||||
Returns:
|
||||
True if Docker is available, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', '--version'],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
|
||||
def build_docker_image(config: DockerConfig, build_context: str = ".") -> DeployResult:
|
||||
"""
|
||||
Build Docker image.
|
||||
|
||||
Args:
|
||||
config: Docker configuration
|
||||
build_context: Build context directory
|
||||
|
||||
Returns:
|
||||
DeployResult with build information
|
||||
"""
|
||||
try:
|
||||
if not check_docker_installed():
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Docker not installed",
|
||||
error="Docker is required for Docker deployment"
|
||||
)
|
||||
|
||||
# Build image tag
|
||||
image_tag = f"{config.image_name}:{config.tag}"
|
||||
if config.registry:
|
||||
image_tag = f"{config.registry}/{image_tag}"
|
||||
|
||||
# Build command
|
||||
cmd = ['docker', 'build', '-t', image_tag]
|
||||
|
||||
# Add build args if provided
|
||||
if config.build_args:
|
||||
for key, value in config.build_args.items():
|
||||
cmd.extend(['--build-arg', f"{key}={value}"])
|
||||
|
||||
cmd.append(build_context)
|
||||
|
||||
print(f"🐳 Building Docker image: {image_tag}")
|
||||
|
||||
# Run build
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"Docker image built successfully: {image_tag}",
|
||||
metadata={"image": image_tag, "tag": config.tag}
|
||||
)
|
||||
else:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Docker build failed",
|
||||
error=result.stderr
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Docker build failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
def run_docker_container(
|
||||
config: DockerConfig,
|
||||
env_vars: Optional[Dict[str, str]] = None,
|
||||
detached: bool = True
|
||||
) -> DeployResult:
|
||||
"""
|
||||
Run Docker container.
|
||||
|
||||
Args:
|
||||
config: Docker configuration
|
||||
env_vars: Environment variables to pass to container
|
||||
detached: Run in detached mode
|
||||
|
||||
Returns:
|
||||
DeployResult with container information
|
||||
"""
|
||||
try:
|
||||
# Build image tag
|
||||
image_tag = f"{config.image_name}:{config.tag}"
|
||||
if config.registry:
|
||||
image_tag = f"{config.registry}/{image_tag}"
|
||||
|
||||
# Build run command
|
||||
cmd = ['docker', 'run']
|
||||
|
||||
if detached:
|
||||
cmd.append('-d')
|
||||
|
||||
# Add port mappings
|
||||
for port in config.expose:
|
||||
cmd.extend(['-p', f"{port}:{port}"])
|
||||
|
||||
# Add environment variables
|
||||
if env_vars:
|
||||
for key, value in env_vars.items():
|
||||
cmd.extend(['-e', f"{key}={value}"])
|
||||
|
||||
# Add container name
|
||||
container_name = f"{config.image_name}-{config.tag}".replace(':', '-')
|
||||
cmd.extend(['--name', container_name])
|
||||
|
||||
cmd.append(image_tag)
|
||||
|
||||
print(f"🚀 Starting Docker container: {container_name}")
|
||||
|
||||
# Run container
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
container_id = result.stdout.strip()
|
||||
url = f"http://localhost:{config.expose[0]}"
|
||||
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"Container started successfully: {container_name}",
|
||||
url=url,
|
||||
metadata={
|
||||
"container_id": container_id,
|
||||
"container_name": container_name,
|
||||
"image": image_tag
|
||||
}
|
||||
)
|
||||
else:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Failed to start container",
|
||||
error=result.stderr
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Failed to start container",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
def push_docker_image(config: DockerConfig) -> DeployResult:
|
||||
"""
|
||||
Push Docker image to registry.
|
||||
|
||||
Args:
|
||||
config: Docker configuration
|
||||
|
||||
Returns:
|
||||
DeployResult with push information
|
||||
"""
|
||||
try:
|
||||
if not config.registry:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="No registry specified",
|
||||
error="Registry URL required for push operation"
|
||||
)
|
||||
|
||||
# Build image tag
|
||||
image_tag = f"{config.registry}/{config.image_name}:{config.tag}"
|
||||
|
||||
print(f"📤 Pushing Docker image to registry: {image_tag}")
|
||||
|
||||
# Push image
|
||||
result = subprocess.run(
|
||||
['docker', 'push', image_tag],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"Image pushed successfully: {image_tag}",
|
||||
metadata={"image": image_tag}
|
||||
)
|
||||
else:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Docker push failed",
|
||||
error=result.stderr
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Docker push failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
def stop_docker_container(container_id: str) -> bool:
|
||||
"""
|
||||
Stop Docker container.
|
||||
|
||||
Args:
|
||||
container_id: Container ID or name
|
||||
|
||||
Returns:
|
||||
True if stopped successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'stop', container_id],
|
||||
capture_output=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def save_dockerfile(agents_file: str, config: Optional[DockerConfig] = None, output_path: str = "Dockerfile"):
|
||||
"""
|
||||
Save generated Dockerfile to file.
|
||||
|
||||
Args:
|
||||
agents_file: Path to agents.yaml file
|
||||
config: Docker configuration
|
||||
output_path: Path to save Dockerfile
|
||||
"""
|
||||
dockerfile_content = generate_dockerfile(agents_file, config)
|
||||
|
||||
path = Path(output_path)
|
||||
with open(path, 'w') as f:
|
||||
f.write(dockerfile_content)
|
||||
|
||||
|
||||
def get_docker_container_status(config: DockerConfig) -> DeployStatus:
|
||||
"""
|
||||
Get status of a Docker container.
|
||||
|
||||
Args:
|
||||
config: Docker configuration
|
||||
|
||||
Returns:
|
||||
DeployStatus with container information
|
||||
"""
|
||||
try:
|
||||
container_name = f"{config.image_name}-{config.tag}".replace(':', '-')
|
||||
|
||||
result = subprocess.run(
|
||||
['docker', 'inspect', container_name, '--format', '{{json .}}'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return DeployStatus(
|
||||
state=ServiceState.NOT_FOUND,
|
||||
message=f"Container not found: {container_name}",
|
||||
service_name=container_name,
|
||||
provider="docker"
|
||||
)
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
state_data = data.get('State', {})
|
||||
running = state_data.get('Running', False)
|
||||
status_str = state_data.get('Status', 'unknown')
|
||||
|
||||
# Map Docker status to ServiceState
|
||||
if running:
|
||||
state = ServiceState.RUNNING
|
||||
elif status_str == 'exited':
|
||||
state = ServiceState.STOPPED
|
||||
elif status_str == 'created':
|
||||
state = ServiceState.PENDING
|
||||
elif status_str == 'dead':
|
||||
state = ServiceState.FAILED
|
||||
else:
|
||||
state = ServiceState.UNKNOWN
|
||||
|
||||
# Get port mapping
|
||||
ports = data.get('NetworkSettings', {}).get('Ports', {})
|
||||
url = None
|
||||
if ports and config.expose:
|
||||
port_key = f"{config.expose[0]}/tcp"
|
||||
if port_key in ports and ports[port_key]:
|
||||
host_port = ports[port_key][0].get('HostPort')
|
||||
if host_port:
|
||||
url = f"http://localhost:{host_port}"
|
||||
|
||||
return DeployStatus(
|
||||
state=state,
|
||||
url=url,
|
||||
message=f"Status: {status_str}",
|
||||
service_name=container_name,
|
||||
provider="docker",
|
||||
healthy=running,
|
||||
instances_running=1 if running else 0,
|
||||
instances_desired=1,
|
||||
created_at=data.get('Created'),
|
||||
metadata={
|
||||
"container_id": data.get('Id', '')[:12],
|
||||
"image": data.get('Config', {}).get('Image'),
|
||||
"status": status_str,
|
||||
"started_at": state_data.get('StartedAt'),
|
||||
"finished_at": state_data.get('FinishedAt')
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployStatus(
|
||||
state=ServiceState.UNKNOWN,
|
||||
message=f"Failed to get status: {e}",
|
||||
service_name=config.image_name,
|
||||
provider="docker"
|
||||
)
|
||||
|
||||
|
||||
def remove_docker_container(config: DockerConfig, force: bool = False) -> DestroyResult:
|
||||
"""
|
||||
Remove Docker container and optionally the image.
|
||||
|
||||
Args:
|
||||
config: Docker configuration
|
||||
force: Force removal and also remove image
|
||||
|
||||
Returns:
|
||||
DestroyResult with removal information
|
||||
"""
|
||||
try:
|
||||
container_name = f"{config.image_name}-{config.tag}".replace(':', '-')
|
||||
deleted_resources = []
|
||||
|
||||
# Stop container first
|
||||
print(f"🛑 Stopping container: {container_name}")
|
||||
subprocess.run(
|
||||
['docker', 'stop', container_name],
|
||||
capture_output=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Remove container
|
||||
print(f"🗑️ Removing container: {container_name}")
|
||||
result = subprocess.run(
|
||||
['docker', 'rm', container_name] + (['-f'] if force else []),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
deleted_resources.append(f"container:{container_name}")
|
||||
else:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message="Failed to remove container",
|
||||
error=result.stderr,
|
||||
resources_deleted=deleted_resources
|
||||
)
|
||||
|
||||
# Optionally remove image
|
||||
if force:
|
||||
image_tag = f"{config.image_name}:{config.tag}"
|
||||
print(f"🗑️ Removing image: {image_tag}")
|
||||
img_result = subprocess.run(
|
||||
['docker', 'rmi', image_tag, '-f'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if img_result.returncode == 0:
|
||||
deleted_resources.append(f"image:{image_tag}")
|
||||
|
||||
return DestroyResult(
|
||||
success=True,
|
||||
message="Successfully removed Docker container",
|
||||
resources_deleted=deleted_resources,
|
||||
metadata={"container_name": container_name}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message="Failed to remove Docker resources",
|
||||
error=str(e)
|
||||
)
|
||||
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Deploy doctor - connectivity and readiness checks.
|
||||
"""
|
||||
import subprocess
|
||||
import socket
|
||||
import sys
|
||||
from typing import Optional, List
|
||||
from dataclasses import dataclass
|
||||
from .schema import validate_agents_yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoctorCheckResult:
|
||||
"""Result of a single doctor check."""
|
||||
name: str
|
||||
passed: bool
|
||||
message: str
|
||||
fix_suggestion: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoctorReport:
|
||||
"""Aggregated report of all doctor checks."""
|
||||
checks: List[DoctorCheckResult]
|
||||
|
||||
@property
|
||||
def total_checks(self) -> int:
|
||||
return len(self.checks)
|
||||
|
||||
@property
|
||||
def passed_checks(self) -> int:
|
||||
return sum(1 for c in self.checks if c.passed)
|
||||
|
||||
@property
|
||||
def failed_checks(self) -> int:
|
||||
return sum(1 for c in self.checks if not c.passed)
|
||||
|
||||
@property
|
||||
def all_passed(self) -> bool:
|
||||
return all(c.passed for c in self.checks)
|
||||
|
||||
|
||||
def check_python_version() -> DoctorCheckResult:
|
||||
"""Check Python version is 3.9+."""
|
||||
try:
|
||||
version = sys.version_info
|
||||
version_str = f"{version.major}.{version.minor}.{version.micro}"
|
||||
|
||||
if version.major >= 3 and version.minor >= 9:
|
||||
return DoctorCheckResult(
|
||||
name="Python Version",
|
||||
passed=True,
|
||||
message=f"Python {version_str} (>= 3.9 required)"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="Python Version",
|
||||
passed=False,
|
||||
message=f"Python {version_str} found, but 3.9+ required",
|
||||
fix_suggestion="Upgrade Python to version 3.9 or higher"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="Python Version",
|
||||
passed=False,
|
||||
message=f"Failed to check Python version: {e}",
|
||||
fix_suggestion="Ensure Python is properly installed"
|
||||
)
|
||||
|
||||
|
||||
def check_docker_available() -> DoctorCheckResult:
|
||||
"""Check if Docker is installed and running."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
return DoctorCheckResult(
|
||||
name="Docker",
|
||||
passed=True,
|
||||
message=f"Docker available: {version}"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="Docker",
|
||||
passed=False,
|
||||
message="Docker command failed",
|
||||
fix_suggestion="Install Docker: https://docs.docker.com/get-docker/"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return DoctorCheckResult(
|
||||
name="Docker",
|
||||
passed=False,
|
||||
message="Docker not found",
|
||||
fix_suggestion="Install Docker: https://docs.docker.com/get-docker/"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="Docker",
|
||||
passed=False,
|
||||
message=f"Docker check failed: {e}",
|
||||
fix_suggestion="Ensure Docker is installed and running"
|
||||
)
|
||||
|
||||
|
||||
def check_port_available(port: int) -> DoctorCheckResult:
|
||||
"""Check if a port is available."""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
result = s.connect_ex(('127.0.0.1', port))
|
||||
|
||||
if result != 0:
|
||||
return DoctorCheckResult(
|
||||
name=f"Port {port}",
|
||||
passed=True,
|
||||
message=f"Port {port} is available"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name=f"Port {port}",
|
||||
passed=False,
|
||||
message=f"Port {port} is already in use",
|
||||
fix_suggestion=f"Stop the service using port {port} or choose a different port"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name=f"Port {port}",
|
||||
passed=False,
|
||||
message=f"Failed to check port {port}: {e}",
|
||||
fix_suggestion="Check network configuration"
|
||||
)
|
||||
|
||||
|
||||
def check_aws_cli() -> DoctorCheckResult:
|
||||
"""Check AWS CLI configuration."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['aws', 'sts', 'get-caller-identity', '--output', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
import json
|
||||
identity = json.loads(result.stdout)
|
||||
account = identity.get('Account', 'unknown')
|
||||
user_id = identity.get('UserId', 'unknown')
|
||||
|
||||
return DoctorCheckResult(
|
||||
name="AWS CLI",
|
||||
passed=True,
|
||||
message=f"AWS CLI configured (Account: {account[:4]}...{account[-4:]})"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="AWS CLI",
|
||||
passed=False,
|
||||
message="AWS CLI not configured or credentials invalid",
|
||||
fix_suggestion="Run: aws configure"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return DoctorCheckResult(
|
||||
name="AWS CLI",
|
||||
passed=False,
|
||||
message="AWS CLI not installed",
|
||||
fix_suggestion="Install AWS CLI: https://aws.amazon.com/cli/"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="AWS CLI",
|
||||
passed=False,
|
||||
message=f"AWS CLI check failed: {e}",
|
||||
fix_suggestion="Run: aws configure"
|
||||
)
|
||||
|
||||
|
||||
def check_azure_cli() -> DoctorCheckResult:
|
||||
"""Check Azure CLI configuration."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['az', 'account', 'show', '--output', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
import json
|
||||
account = json.loads(result.stdout)
|
||||
sub_id = account.get('id', 'unknown')
|
||||
name = account.get('name', 'unknown')
|
||||
|
||||
return DoctorCheckResult(
|
||||
name="Azure CLI",
|
||||
passed=True,
|
||||
message=f"Azure CLI logged in (Subscription: {sub_id[:8]}...)"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="Azure CLI",
|
||||
passed=False,
|
||||
message="Azure CLI not logged in",
|
||||
fix_suggestion="Run: az login"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return DoctorCheckResult(
|
||||
name="Azure CLI",
|
||||
passed=False,
|
||||
message="Azure CLI not installed",
|
||||
fix_suggestion="Install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="Azure CLI",
|
||||
passed=False,
|
||||
message=f"Azure CLI check failed: {e}",
|
||||
fix_suggestion="Run: az login"
|
||||
)
|
||||
|
||||
|
||||
def check_gcp_cli() -> DoctorCheckResult:
|
||||
"""Check GCP CLI configuration."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['gcloud', 'config', 'get-value', 'project'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
project_id = result.stdout.strip()
|
||||
|
||||
return DoctorCheckResult(
|
||||
name="GCP CLI",
|
||||
passed=True,
|
||||
message=f"GCP CLI configured (Project: {project_id})"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="GCP CLI",
|
||||
passed=False,
|
||||
message="GCP CLI not configured or no project set",
|
||||
fix_suggestion="Run: gcloud init"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return DoctorCheckResult(
|
||||
name="GCP CLI",
|
||||
passed=False,
|
||||
message="GCP CLI not installed",
|
||||
fix_suggestion="Install gcloud CLI: https://cloud.google.com/sdk/docs/install"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="GCP CLI",
|
||||
passed=False,
|
||||
message=f"GCP CLI check failed: {e}",
|
||||
fix_suggestion="Run: gcloud init"
|
||||
)
|
||||
|
||||
|
||||
def check_agents_yaml(file_path: str) -> DoctorCheckResult:
|
||||
"""Check if agents.yaml exists and has valid deploy config."""
|
||||
try:
|
||||
config = validate_agents_yaml(file_path)
|
||||
|
||||
if config:
|
||||
return DoctorCheckResult(
|
||||
name="agents.yaml",
|
||||
passed=True,
|
||||
message=f"Valid deploy configuration found (type: {config.type.value})"
|
||||
)
|
||||
else:
|
||||
return DoctorCheckResult(
|
||||
name="agents.yaml",
|
||||
passed=False,
|
||||
message="No deploy section found in agents.yaml",
|
||||
fix_suggestion="Add deploy configuration to agents.yaml or run: praisonai deploy init"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return DoctorCheckResult(
|
||||
name="agents.yaml",
|
||||
passed=False,
|
||||
message=f"File not found: {file_path}",
|
||||
fix_suggestion="Create agents.yaml or run: praisonai deploy init"
|
||||
)
|
||||
except ValueError as e:
|
||||
return DoctorCheckResult(
|
||||
name="agents.yaml",
|
||||
passed=False,
|
||||
message=f"Invalid deploy configuration: {e}",
|
||||
fix_suggestion="Fix deploy configuration in agents.yaml or run: praisonai deploy validate"
|
||||
)
|
||||
except Exception as e:
|
||||
return DoctorCheckResult(
|
||||
name="agents.yaml",
|
||||
passed=False,
|
||||
message=f"Failed to validate agents.yaml: {e}",
|
||||
fix_suggestion="Check agents.yaml syntax"
|
||||
)
|
||||
|
||||
|
||||
def run_local_checks(port: int = 8005, agents_file: Optional[str] = None) -> DoctorReport:
|
||||
"""Run local environment checks."""
|
||||
checks = [
|
||||
check_python_version(),
|
||||
check_port_available(port),
|
||||
]
|
||||
|
||||
if agents_file:
|
||||
checks.append(check_agents_yaml(agents_file))
|
||||
|
||||
return DoctorReport(checks=checks)
|
||||
|
||||
|
||||
def run_aws_checks() -> DoctorReport:
|
||||
"""Run AWS-specific checks."""
|
||||
checks = [
|
||||
check_aws_cli(),
|
||||
]
|
||||
return DoctorReport(checks=checks)
|
||||
|
||||
|
||||
def run_azure_checks() -> DoctorReport:
|
||||
"""Run Azure-specific checks."""
|
||||
checks = [
|
||||
check_azure_cli(),
|
||||
]
|
||||
return DoctorReport(checks=checks)
|
||||
|
||||
|
||||
def run_gcp_checks() -> DoctorReport:
|
||||
"""Run GCP-specific checks."""
|
||||
checks = [
|
||||
check_gcp_cli(),
|
||||
]
|
||||
return DoctorReport(checks=checks)
|
||||
|
||||
|
||||
def run_all_checks(agents_file: Optional[str] = None) -> DoctorReport:
|
||||
"""Run all available checks."""
|
||||
checks = []
|
||||
|
||||
# Local checks
|
||||
local_report = run_local_checks(agents_file=agents_file)
|
||||
checks.extend(local_report.checks)
|
||||
|
||||
# Docker check
|
||||
checks.append(check_docker_available())
|
||||
|
||||
# Cloud provider checks
|
||||
aws_report = run_aws_checks()
|
||||
checks.extend(aws_report.checks)
|
||||
|
||||
azure_report = run_azure_checks()
|
||||
checks.extend(azure_report.checks)
|
||||
|
||||
gcp_report = run_gcp_checks()
|
||||
checks.extend(gcp_report.checks)
|
||||
|
||||
return DoctorReport(checks=checks)
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Main Deploy class for unified deployment interface.
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from .models import DeployConfig, DeployResult, DeployType, DeployStatus, DestroyResult, ServiceState
|
||||
from .schema import validate_agents_yaml
|
||||
from .api import start_api_server, generate_api_server_code
|
||||
from .docker import (
|
||||
build_docker_image, run_docker_container, push_docker_image, save_dockerfile,
|
||||
get_docker_container_status, remove_docker_container
|
||||
)
|
||||
from .providers import get_provider
|
||||
|
||||
|
||||
class Deploy:
|
||||
"""
|
||||
Unified deployment interface for PraisonAI agents.
|
||||
|
||||
Supports:
|
||||
- API server deployment (local/production)
|
||||
- Docker containerization
|
||||
- Cloud deployment (AWS, Azure, GCP)
|
||||
|
||||
Examples:
|
||||
# From YAML
|
||||
deploy = Deploy.from_yaml("agents.yaml")
|
||||
result = deploy.deploy()
|
||||
|
||||
# Programmatic
|
||||
from praisonai.deploy import Deploy, DeployConfig, DeployType
|
||||
|
||||
config = DeployConfig(type=DeployType.API)
|
||||
deploy = Deploy(config, agents_file="agents.yaml")
|
||||
result = deploy.deploy()
|
||||
"""
|
||||
|
||||
def __init__(self, config: DeployConfig, agents_file: str = "agents.yaml"):
|
||||
"""
|
||||
Initialize Deploy with configuration.
|
||||
|
||||
Args:
|
||||
config: Deployment configuration
|
||||
agents_file: Path to agents.yaml file
|
||||
"""
|
||||
self.config = config
|
||||
self.agents_file = agents_file
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, agents_file: str = "agents.yaml") -> 'Deploy':
|
||||
"""
|
||||
Create Deploy instance from agents.yaml file.
|
||||
|
||||
Args:
|
||||
agents_file: Path to agents.yaml file
|
||||
|
||||
Returns:
|
||||
Deploy instance
|
||||
|
||||
Raises:
|
||||
ValueError: If no deploy configuration found in YAML
|
||||
"""
|
||||
config = validate_agents_yaml(agents_file)
|
||||
|
||||
if config is None:
|
||||
raise ValueError(f"No deploy configuration found in {agents_file}")
|
||||
|
||||
return cls(config, agents_file)
|
||||
|
||||
def deploy(self, background: bool = False) -> DeployResult:
|
||||
"""
|
||||
Execute deployment based on configuration.
|
||||
|
||||
Args:
|
||||
background: Run in background mode (for API deployments)
|
||||
|
||||
Returns:
|
||||
DeployResult with deployment information
|
||||
"""
|
||||
if self.config.type == DeployType.API:
|
||||
return self._deploy_api(background)
|
||||
elif self.config.type == DeployType.DOCKER:
|
||||
return self._deploy_docker()
|
||||
elif self.config.type == DeployType.CLOUD:
|
||||
return self._deploy_cloud()
|
||||
else:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message=f"Unsupported deployment type: {self.config.type}",
|
||||
error="Invalid deployment type"
|
||||
)
|
||||
|
||||
def _deploy_api(self, background: bool = False) -> DeployResult:
|
||||
"""Deploy as API server."""
|
||||
return start_api_server(
|
||||
self.agents_file,
|
||||
self.config.api,
|
||||
background=background
|
||||
)
|
||||
|
||||
def _deploy_docker(self) -> DeployResult:
|
||||
"""Deploy as Docker container."""
|
||||
# Generate and save Dockerfile
|
||||
save_dockerfile(self.agents_file, self.config.docker)
|
||||
|
||||
# Generate API server code
|
||||
api_code = generate_api_server_code(self.agents_file, None)
|
||||
with open("api_server.py", 'w') as f:
|
||||
f.write(api_code)
|
||||
|
||||
# Build image
|
||||
build_result = build_docker_image(self.config.docker)
|
||||
|
||||
if not build_result.success:
|
||||
return build_result
|
||||
|
||||
# Push if configured
|
||||
if self.config.docker.push and self.config.docker.registry:
|
||||
push_result = push_docker_image(self.config.docker)
|
||||
if not push_result.success:
|
||||
return push_result
|
||||
|
||||
# Run container
|
||||
return run_docker_container(self.config.docker)
|
||||
|
||||
def _deploy_cloud(self) -> DeployResult:
|
||||
"""Deploy to cloud provider."""
|
||||
provider = get_provider(self.config.cloud)
|
||||
return provider.deploy()
|
||||
|
||||
def plan(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate deployment plan without executing.
|
||||
|
||||
Returns:
|
||||
Dictionary with planned deployment configuration
|
||||
"""
|
||||
if self.config.type == DeployType.API:
|
||||
return {
|
||||
"type": "api",
|
||||
"host": self.config.api.host,
|
||||
"port": self.config.api.port,
|
||||
"workers": self.config.api.workers,
|
||||
"agents_file": self.agents_file
|
||||
}
|
||||
elif self.config.type == DeployType.DOCKER:
|
||||
return {
|
||||
"type": "docker",
|
||||
"image": f"{self.config.docker.image_name}:{self.config.docker.tag}",
|
||||
"registry": self.config.docker.registry,
|
||||
"push": self.config.docker.push,
|
||||
"ports": self.config.docker.expose
|
||||
}
|
||||
elif self.config.type == DeployType.CLOUD:
|
||||
provider = get_provider(self.config.cloud)
|
||||
return provider.plan()
|
||||
else:
|
||||
return {"error": "Invalid deployment type"}
|
||||
|
||||
def doctor(self):
|
||||
"""
|
||||
Run health checks for deployment.
|
||||
|
||||
Returns:
|
||||
DoctorReport with check results
|
||||
"""
|
||||
from .doctor import run_local_checks, run_all_checks, DoctorReport
|
||||
|
||||
if self.config.type == DeployType.API:
|
||||
return run_local_checks(
|
||||
port=self.config.api.port,
|
||||
agents_file=self.agents_file
|
||||
)
|
||||
elif self.config.type == DeployType.DOCKER:
|
||||
from .doctor import check_docker_available, DoctorReport
|
||||
checks = run_local_checks(agents_file=self.agents_file).checks
|
||||
checks.append(check_docker_available())
|
||||
return DoctorReport(checks=checks)
|
||||
elif self.config.type == DeployType.CLOUD:
|
||||
provider = get_provider(self.config.cloud)
|
||||
return provider.doctor()
|
||||
else:
|
||||
return run_all_checks(self.agents_file)
|
||||
|
||||
def status(self) -> DeployStatus:
|
||||
"""
|
||||
Get current deployment status.
|
||||
|
||||
Returns:
|
||||
DeployStatus with current state and information
|
||||
"""
|
||||
if self.config.type == DeployType.API:
|
||||
return self._status_api()
|
||||
elif self.config.type == DeployType.DOCKER:
|
||||
return self._status_docker()
|
||||
elif self.config.type == DeployType.CLOUD:
|
||||
return self._status_cloud()
|
||||
else:
|
||||
return DeployStatus(
|
||||
state=ServiceState.UNKNOWN,
|
||||
message=f"Unsupported deployment type: {self.config.type}"
|
||||
)
|
||||
|
||||
def _status_api(self) -> DeployStatus:
|
||||
"""Get status of local API server."""
|
||||
import socket
|
||||
port = self.config.api.port if self.config.api else 8005
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex(('127.0.0.1', port))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
return DeployStatus(
|
||||
state=ServiceState.RUNNING,
|
||||
url=f"http://127.0.0.1:{port}",
|
||||
message=f"API server running on port {port}",
|
||||
service_name="praisonai-api",
|
||||
provider="api",
|
||||
healthy=True,
|
||||
instances_running=1,
|
||||
instances_desired=1
|
||||
)
|
||||
else:
|
||||
return DeployStatus(
|
||||
state=ServiceState.STOPPED,
|
||||
message=f"No service running on port {port}",
|
||||
service_name="praisonai-api",
|
||||
provider="api",
|
||||
healthy=False,
|
||||
instances_running=0,
|
||||
instances_desired=1
|
||||
)
|
||||
except Exception as e:
|
||||
return DeployStatus(
|
||||
state=ServiceState.UNKNOWN,
|
||||
message=f"Failed to check status: {e}",
|
||||
service_name="praisonai-api",
|
||||
provider="api"
|
||||
)
|
||||
|
||||
def _status_docker(self) -> DeployStatus:
|
||||
"""Get status of Docker container."""
|
||||
return get_docker_container_status(self.config.docker)
|
||||
|
||||
def _status_cloud(self) -> DeployStatus:
|
||||
"""Get status of cloud deployment."""
|
||||
provider = get_provider(self.config.cloud)
|
||||
return provider.status()
|
||||
|
||||
def destroy(self, force: bool = False) -> DestroyResult:
|
||||
"""
|
||||
Destroy/delete the deployment.
|
||||
|
||||
Args:
|
||||
force: Force deletion without confirmation
|
||||
|
||||
Returns:
|
||||
DestroyResult with deletion information
|
||||
"""
|
||||
if self.config.type == DeployType.API:
|
||||
return self._destroy_api()
|
||||
elif self.config.type == DeployType.DOCKER:
|
||||
return self._destroy_docker(force)
|
||||
elif self.config.type == DeployType.CLOUD:
|
||||
return self._destroy_cloud(force)
|
||||
else:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message=f"Unsupported deployment type: {self.config.type}",
|
||||
error="Invalid deployment type"
|
||||
)
|
||||
|
||||
def _destroy_api(self) -> DestroyResult:
|
||||
"""Stop local API server."""
|
||||
import subprocess
|
||||
import signal
|
||||
import os
|
||||
|
||||
port = self.config.api.port if self.config.api else 8005
|
||||
|
||||
try:
|
||||
# Find process using the port
|
||||
result = subprocess.run(
|
||||
['lsof', '-ti', f':{port}'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pids = result.stdout.strip().split('\n')
|
||||
deleted_resources = []
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGTERM)
|
||||
deleted_resources.append(f"process:{pid}")
|
||||
except (ProcessLookupError, ValueError):
|
||||
pass
|
||||
|
||||
return DestroyResult(
|
||||
success=True,
|
||||
message=f"Stopped API server on port {port}",
|
||||
resources_deleted=deleted_resources
|
||||
)
|
||||
else:
|
||||
return DestroyResult(
|
||||
success=True,
|
||||
message=f"No API server running on port {port}",
|
||||
resources_deleted=[]
|
||||
)
|
||||
except Exception as e:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message="Failed to stop API server",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def _destroy_docker(self, force: bool = False) -> DestroyResult:
|
||||
"""Remove Docker container."""
|
||||
return remove_docker_container(self.config.docker, force)
|
||||
|
||||
def _destroy_cloud(self, force: bool = False) -> DestroyResult:
|
||||
"""Destroy cloud deployment."""
|
||||
provider = get_provider(self.config.cloud)
|
||||
return provider.destroy(force)
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Deploy configuration models using Pydantic.
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, List, Any
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class DeployType(str, Enum):
|
||||
"""Deployment type enum."""
|
||||
API = "api"
|
||||
DOCKER = "docker"
|
||||
CLOUD = "cloud"
|
||||
|
||||
|
||||
class CloudProvider(str, Enum):
|
||||
"""Cloud provider enum."""
|
||||
AWS = "aws"
|
||||
AZURE = "azure"
|
||||
GCP = "gcp"
|
||||
|
||||
|
||||
class APIConfig(BaseModel):
|
||||
"""Configuration for API server deployment."""
|
||||
host: str = Field(default="127.0.0.1", description="Server host")
|
||||
port: int = Field(default=8005, description="Server port")
|
||||
workers: int = Field(default=1, description="Number of worker processes")
|
||||
cors_enabled: bool = Field(default=True, description="Enable CORS")
|
||||
auth_enabled: bool = Field(default=False, description="Enable authentication")
|
||||
auth_token: Optional[str] = Field(default=None, description="Authentication token")
|
||||
reload: bool = Field(default=False, description="Enable auto-reload for development")
|
||||
|
||||
|
||||
class DockerConfig(BaseModel):
|
||||
"""Configuration for Docker deployment."""
|
||||
image_name: str = Field(default="praisonai-app", description="Docker image name")
|
||||
tag: str = Field(default="latest", description="Docker image tag")
|
||||
base_image: str = Field(default="python:3.11-slim", description="Base Docker image")
|
||||
expose: List[int] = Field(default=[8005], description="Ports to expose")
|
||||
registry: Optional[str] = Field(default=None, description="Docker registry URL")
|
||||
push: bool = Field(default=False, description="Push image to registry after build")
|
||||
build_args: Optional[Dict[str, str]] = Field(default=None, description="Docker build arguments")
|
||||
|
||||
|
||||
class CloudConfig(BaseModel):
|
||||
"""Configuration for cloud deployment."""
|
||||
provider: CloudProvider = Field(..., description="Cloud provider")
|
||||
region: str = Field(..., description="Deployment region")
|
||||
service_name: str = Field(..., description="Service/application name")
|
||||
|
||||
# Common cloud config
|
||||
image: Optional[str] = Field(default=None, description="Container image URL")
|
||||
cpu: Optional[str] = Field(default="256", description="CPU allocation")
|
||||
memory: Optional[str] = Field(default="512", description="Memory allocation (MB)")
|
||||
min_instances: int = Field(default=1, description="Minimum instances")
|
||||
max_instances: int = Field(default=10, description="Maximum instances")
|
||||
env_vars: Optional[Dict[str, str]] = Field(default=None, description="Environment variables")
|
||||
|
||||
# AWS-specific
|
||||
cluster_name: Optional[str] = Field(default=None, description="ECS cluster name (AWS)")
|
||||
task_definition: Optional[str] = Field(default=None, description="Task definition (AWS)")
|
||||
|
||||
# Azure-specific
|
||||
resource_group: Optional[str] = Field(default=None, description="Resource group (Azure)")
|
||||
subscription_id: Optional[str] = Field(default=None, description="Subscription ID (Azure)")
|
||||
|
||||
# GCP-specific
|
||||
project_id: Optional[str] = Field(default=None, description="Project ID (GCP)")
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""Configuration for an agent to deploy."""
|
||||
name: str = Field(..., description="Agent name/identifier")
|
||||
entrypoint: str = Field(..., description="Agent entrypoint file (e.g., agents.yaml)")
|
||||
env: Dict[str, str] = Field(default_factory=dict, description="Environment variables")
|
||||
secrets: Dict[str, str] = Field(default_factory=dict, description="Secret references")
|
||||
ports: Optional[List[int]] = Field(default=None, description="Ports to expose")
|
||||
resources: Optional[Dict[str, str]] = Field(default=None, description="Resource requirements")
|
||||
|
||||
|
||||
class DeployConfig(BaseModel):
|
||||
"""Main deployment configuration."""
|
||||
type: DeployType = Field(..., description="Deployment type")
|
||||
api: Optional[APIConfig] = Field(default=None, description="API server configuration")
|
||||
docker: Optional[DockerConfig] = Field(default=None, description="Docker configuration")
|
||||
cloud: Optional[CloudConfig] = Field(default=None, description="Cloud configuration")
|
||||
agents: Optional[List[AgentConfig]] = Field(default=None, description="Agent configurations")
|
||||
|
||||
@field_validator('api', 'docker', 'cloud')
|
||||
@classmethod
|
||||
def validate_config_for_type(cls, v, info):
|
||||
"""Validate that required config is present for the deployment type."""
|
||||
if info.data.get('type') == DeployType.API and info.field_name == 'api':
|
||||
return v or APIConfig()
|
||||
elif info.data.get('type') == DeployType.DOCKER and info.field_name == 'docker':
|
||||
return v or DockerConfig()
|
||||
elif info.data.get('type') == DeployType.CLOUD and info.field_name == 'cloud':
|
||||
if v is None:
|
||||
raise ValueError("cloud config required for cloud deployment type")
|
||||
return v
|
||||
return v
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""Post-initialization validation."""
|
||||
if self.type == DeployType.API and self.api is None:
|
||||
self.api = APIConfig()
|
||||
elif self.type == DeployType.DOCKER and self.docker is None:
|
||||
self.docker = DockerConfig()
|
||||
elif self.type == DeployType.CLOUD and self.cloud is None:
|
||||
raise ValueError("cloud config required for cloud deployment type")
|
||||
|
||||
|
||||
class DeployResult(BaseModel):
|
||||
"""Result of a deployment operation."""
|
||||
success: bool = Field(..., description="Whether deployment succeeded")
|
||||
message: str = Field(..., description="Result message")
|
||||
url: Optional[str] = Field(default=None, description="Deployed service URL")
|
||||
error: Optional[str] = Field(default=None, description="Error message if failed")
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
||||
|
||||
|
||||
class ServiceState(str, Enum):
|
||||
"""Service state enum."""
|
||||
RUNNING = "running"
|
||||
STOPPED = "stopped"
|
||||
PENDING = "pending"
|
||||
FAILED = "failed"
|
||||
NOT_FOUND = "not_found"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class DeployStatus(BaseModel):
|
||||
"""Status of a deployed service."""
|
||||
state: ServiceState = Field(..., description="Current service state")
|
||||
url: Optional[str] = Field(default=None, description="Service URL/endpoint")
|
||||
message: str = Field(default="", description="Status message")
|
||||
|
||||
# Resource identifiers
|
||||
service_name: Optional[str] = Field(default=None, description="Service name")
|
||||
provider: Optional[str] = Field(default=None, description="Provider (api/docker/aws/azure/gcp)")
|
||||
region: Optional[str] = Field(default=None, description="Deployment region")
|
||||
|
||||
# Health info
|
||||
healthy: bool = Field(default=False, description="Whether service is healthy")
|
||||
instances_running: int = Field(default=0, description="Number of running instances")
|
||||
instances_desired: int = Field(default=0, description="Desired number of instances")
|
||||
|
||||
# Timestamps
|
||||
created_at: Optional[str] = Field(default=None, description="Creation timestamp")
|
||||
updated_at: Optional[str] = Field(default=None, description="Last update timestamp")
|
||||
|
||||
# Provider-specific metadata
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Provider-specific metadata")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON output."""
|
||||
return {
|
||||
"state": self.state.value,
|
||||
"url": self.url,
|
||||
"message": self.message,
|
||||
"service_name": self.service_name,
|
||||
"provider": self.provider,
|
||||
"region": self.region,
|
||||
"healthy": self.healthy,
|
||||
"instances_running": self.instances_running,
|
||||
"instances_desired": self.instances_desired,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
|
||||
class DestroyResult(BaseModel):
|
||||
"""Result of a destroy operation."""
|
||||
success: bool = Field(..., description="Whether destroy succeeded")
|
||||
message: str = Field(..., description="Result message")
|
||||
resources_deleted: List[str] = Field(default_factory=list, description="List of deleted resources")
|
||||
error: Optional[str] = Field(default=None, description="Error message if failed")
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Cloud provider adapters for deployment.
|
||||
"""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import BaseProvider
|
||||
from .aws import AWSProvider
|
||||
from .azure import AzureProvider
|
||||
from .gcp import GCPProvider
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Lazy load provider modules."""
|
||||
if name == 'BaseProvider':
|
||||
from .base import BaseProvider
|
||||
return BaseProvider
|
||||
elif name == 'AWSProvider':
|
||||
from .aws import AWSProvider
|
||||
return AWSProvider
|
||||
elif name == 'AzureProvider':
|
||||
from .azure import AzureProvider
|
||||
return AzureProvider
|
||||
elif name == 'GCPProvider':
|
||||
from .gcp import GCPProvider
|
||||
return GCPProvider
|
||||
elif name == 'get_provider':
|
||||
from .base import get_provider
|
||||
return get_provider
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = ['BaseProvider', 'AWSProvider', 'AzureProvider', 'GCPProvider', 'get_provider']
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
AWS provider for cloud deployment (ECS Fargate).
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from .base import BaseProvider
|
||||
from ..models import CloudConfig, DeployResult, DeployStatus, DestroyResult, ServiceState
|
||||
from ..doctor import DoctorReport, DoctorCheckResult, check_aws_cli
|
||||
|
||||
|
||||
class AWSProvider(BaseProvider):
|
||||
"""AWS deployment provider using ECS Fargate."""
|
||||
|
||||
def doctor(self) -> DoctorReport:
|
||||
"""Run AWS-specific health checks."""
|
||||
checks = [check_aws_cli()]
|
||||
|
||||
# Check if region is configured
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['aws', 'configure', 'get', 'region'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
checks.append(DoctorCheckResult(
|
||||
name="AWS Region",
|
||||
passed=True,
|
||||
message=f"Region configured: {result.stdout.strip()}"
|
||||
))
|
||||
else:
|
||||
checks.append(DoctorCheckResult(
|
||||
name="AWS Region",
|
||||
passed=False,
|
||||
message="No default region configured",
|
||||
fix_suggestion="Run: aws configure set region us-east-1"
|
||||
))
|
||||
except Exception as e:
|
||||
checks.append(DoctorCheckResult(
|
||||
name="AWS Region",
|
||||
passed=False,
|
||||
message=f"Failed to check region: {e}",
|
||||
fix_suggestion="Run: aws configure"
|
||||
))
|
||||
|
||||
return DoctorReport(checks=checks)
|
||||
|
||||
def plan(self) -> Dict[str, Any]:
|
||||
"""Generate AWS deployment plan."""
|
||||
cluster_name = self.config.cluster_name or f"{self.config.service_name}-cluster"
|
||||
|
||||
plan = {
|
||||
"provider": "aws",
|
||||
"service_name": self.config.service_name,
|
||||
"region": self.config.region,
|
||||
"cluster_name": cluster_name,
|
||||
"launch_type": "FARGATE",
|
||||
"cpu": self.config.cpu,
|
||||
"memory": self.config.memory,
|
||||
"desired_count": self.config.min_instances,
|
||||
"image": self.config.image or f"{self.config.service_name}:latest",
|
||||
"steps": [
|
||||
"1. Create ECS cluster (if not exists)",
|
||||
"2. Create task definition",
|
||||
"3. Create or update ECS service",
|
||||
"4. Wait for service to stabilize"
|
||||
]
|
||||
}
|
||||
|
||||
return plan
|
||||
|
||||
def deploy(self) -> DeployResult:
|
||||
"""Deploy to AWS ECS Fargate."""
|
||||
try:
|
||||
cluster_name = self.config.cluster_name or f"{self.config.service_name}-cluster"
|
||||
|
||||
# Step 1: Create cluster if not exists
|
||||
print(f"📦 Creating ECS cluster: {cluster_name}")
|
||||
try:
|
||||
subprocess.run(
|
||||
['aws', 'ecs', 'create-cluster',
|
||||
'--cluster-name', cluster_name,
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
timeout=30
|
||||
)
|
||||
except Exception:
|
||||
pass # Cluster may already exist
|
||||
|
||||
# Step 2: Register task definition
|
||||
print(f"📝 Registering task definition")
|
||||
|
||||
task_def = {
|
||||
"family": self.config.service_name,
|
||||
"networkMode": "awsvpc",
|
||||
"requiresCompatibilities": ["FARGATE"],
|
||||
"cpu": self.config.cpu,
|
||||
"memory": self.config.memory,
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": self.config.service_name,
|
||||
"image": self.config.image or f"{self.config.service_name}:latest",
|
||||
"portMappings": [
|
||||
{
|
||||
"containerPort": 8005,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"environment": [
|
||||
{"name": k, "value": v}
|
||||
for k, v in (self.config.env_vars or {}).items()
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
['aws', 'ecs', 'register-task-definition',
|
||||
'--cli-input-json', json.dumps(task_def),
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Failed to register task definition",
|
||||
error=result.stderr
|
||||
)
|
||||
|
||||
# Step 3: Create or update service
|
||||
print(f"🚀 Deploying service: {self.config.service_name}")
|
||||
|
||||
# Try to update first, create if doesn't exist
|
||||
update_result = subprocess.run(
|
||||
['aws', 'ecs', 'update-service',
|
||||
'--cluster', cluster_name,
|
||||
'--service', self.config.service_name,
|
||||
'--task-definition', self.config.service_name,
|
||||
'--desired-count', str(self.config.min_instances),
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if update_result.returncode != 0:
|
||||
# Service doesn't exist, create it
|
||||
# Note: This is simplified - production would need VPC/subnet/security group config
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="Service creation requires VPC configuration",
|
||||
error="Please create the service manually with proper VPC/subnet/security group configuration, then use update"
|
||||
)
|
||||
|
||||
return DeployResult(
|
||||
success=True,
|
||||
message=f"Service deployed successfully to AWS ECS",
|
||||
url=f"https://console.aws.amazon.com/ecs/home?region={self.config.region}#/clusters/{cluster_name}/services/{self.config.service_name}",
|
||||
metadata={
|
||||
"cluster": cluster_name,
|
||||
"service": self.config.service_name,
|
||||
"region": self.config.region
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployResult(
|
||||
success=False,
|
||||
message="AWS deployment failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def status(self) -> DeployStatus:
|
||||
"""Get current AWS ECS service status."""
|
||||
try:
|
||||
cluster_name = self.config.cluster_name or f"{self.config.service_name}-cluster"
|
||||
|
||||
result = subprocess.run(
|
||||
['aws', 'ecs', 'describe-services',
|
||||
'--cluster', cluster_name,
|
||||
'--services', self.config.service_name,
|
||||
'--region', self.config.region,
|
||||
'--output', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return DeployStatus(
|
||||
state=ServiceState.NOT_FOUND,
|
||||
message=f"Service not found or error: {result.stderr}",
|
||||
service_name=self.config.service_name,
|
||||
provider="aws",
|
||||
region=self.config.region
|
||||
)
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
services = data.get('services', [])
|
||||
|
||||
if not services:
|
||||
return DeployStatus(
|
||||
state=ServiceState.NOT_FOUND,
|
||||
message="Service not found",
|
||||
service_name=self.config.service_name,
|
||||
provider="aws",
|
||||
region=self.config.region
|
||||
)
|
||||
|
||||
service = services[0]
|
||||
status_str = service.get('status', 'UNKNOWN')
|
||||
running_count = service.get('runningCount', 0)
|
||||
desired_count = service.get('desiredCount', 0)
|
||||
|
||||
# Map AWS status to ServiceState
|
||||
if status_str == 'ACTIVE' and running_count > 0:
|
||||
state = ServiceState.RUNNING
|
||||
elif status_str == 'ACTIVE' and running_count == 0:
|
||||
state = ServiceState.STOPPED
|
||||
elif status_str == 'DRAINING':
|
||||
state = ServiceState.PENDING
|
||||
elif status_str == 'INACTIVE':
|
||||
state = ServiceState.STOPPED
|
||||
else:
|
||||
state = ServiceState.UNKNOWN
|
||||
|
||||
return DeployStatus(
|
||||
state=state,
|
||||
url=f"https://console.aws.amazon.com/ecs/home?region={self.config.region}#/clusters/{cluster_name}/services/{self.config.service_name}",
|
||||
message=f"Status: {status_str}",
|
||||
service_name=self.config.service_name,
|
||||
provider="aws",
|
||||
region=self.config.region,
|
||||
healthy=running_count >= desired_count and running_count > 0,
|
||||
instances_running=running_count,
|
||||
instances_desired=desired_count,
|
||||
created_at=service.get('createdAt'),
|
||||
metadata={
|
||||
"cluster": cluster_name,
|
||||
"task_definition": service.get('taskDefinition'),
|
||||
"launch_type": service.get('launchType'),
|
||||
"deployments": len(service.get('deployments', []))
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DeployStatus(
|
||||
state=ServiceState.UNKNOWN,
|
||||
message=f"Failed to get status: {e}",
|
||||
service_name=self.config.service_name,
|
||||
provider="aws",
|
||||
region=self.config.region
|
||||
)
|
||||
|
||||
def destroy(self, force: bool = False) -> DestroyResult:
|
||||
"""Destroy AWS ECS service and related resources."""
|
||||
try:
|
||||
cluster_name = self.config.cluster_name or f"{self.config.service_name}-cluster"
|
||||
deleted_resources = []
|
||||
|
||||
# Step 1: Update service desired count to 0
|
||||
print(f"🛑 Stopping service: {self.config.service_name}")
|
||||
subprocess.run(
|
||||
['aws', 'ecs', 'update-service',
|
||||
'--cluster', cluster_name,
|
||||
'--service', self.config.service_name,
|
||||
'--desired-count', '0',
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Step 2: Delete the service
|
||||
print(f"🗑️ Deleting service: {self.config.service_name}")
|
||||
result = subprocess.run(
|
||||
['aws', 'ecs', 'delete-service',
|
||||
'--cluster', cluster_name,
|
||||
'--service', self.config.service_name,
|
||||
'--force' if force else '--no-force',
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
deleted_resources.append(f"ecs-service:{self.config.service_name}")
|
||||
else:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message="Failed to delete service",
|
||||
error=result.stderr,
|
||||
resources_deleted=deleted_resources
|
||||
)
|
||||
|
||||
# Step 3: Optionally delete cluster if empty
|
||||
if force:
|
||||
print(f"🗑️ Deleting cluster: {cluster_name}")
|
||||
cluster_result = subprocess.run(
|
||||
['aws', 'ecs', 'delete-cluster',
|
||||
'--cluster', cluster_name,
|
||||
'--region', self.config.region],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if cluster_result.returncode == 0:
|
||||
deleted_resources.append(f"ecs-cluster:{cluster_name}")
|
||||
|
||||
return DestroyResult(
|
||||
success=True,
|
||||
message=f"Successfully destroyed AWS ECS service",
|
||||
resources_deleted=deleted_resources,
|
||||
metadata={
|
||||
"cluster": cluster_name,
|
||||
"region": self.config.region
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DestroyResult(
|
||||
success=False,
|
||||
message="Failed to destroy AWS resources",
|
||||
error=str(e)
|
||||
)
|
||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user