Compare commits

...
4 Commits
Author SHA1 Message Date
Mervin Praison f0f0f653fe Release v2.5.7 2025-12-26 13:31:45 +00:00
Mervin Praison 98a4ff8450 Release v2.5.6 2025-12-26 13:11:09 +00:00
Mervin Praison d6333aaf33 Release v1.2.1 - refactor TypeScript tools to use generic BaseTool
- Bump praisonai-ts version from 1.2.0 to 1.2.1
- Refactor BaseTool to use generics for type-safe input/output parameters
- Update ArxivSearchTool and ArxivDownloadTool to extend generic BaseTool with typed params
- Update MCPTool to extend generic BaseTool
- Change tool method from execute() to run() for consistency
- Export ToolResult, ToolValidationError, validateTool, createTool from base module
- Remove Tool interface export
2025-12-26 11:47:17 +00:00
Mervin Praison 8005094267 update 2025-12-26 08:39:22 +00:00
113 changed files with 14385 additions and 135 deletions

No files matched your search

+1 -1
View File
@@ -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]"
+1 -1
View File
@@ -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]" \
+1 -1
View File
@@ -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]"
+19
View File
@@ -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);
+47
View File
@@ -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);
+29
View File
@@ -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);
+48
View File
@@ -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);
+33
View File
@@ -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);
+57
View File
@@ -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);
+59
View File
@@ -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);
+47
View File
@@ -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);
+64
View File
@@ -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);
+62
View File
@@ -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);
+42
View File
@@ -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);
+60
View File
@@ -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);
+71
View File
@@ -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);
+74
View File
@@ -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);
+61
View File
@@ -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);
+113
View File
@@ -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);
+30
View File
@@ -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);
+29
View File
@@ -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);
+105 -82
View File
@@ -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 -1
View File
@@ -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",
+266
View File
@@ -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"
+130
View File
@@ -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);
}
+162
View File
@@ -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);
}
+185
View File
@@ -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);
}
+253
View File
@@ -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);
}
+114
View File
@@ -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);
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Config utilities index
*/
export * from './schema';
export * from './load';
export * from './resolve';
+213
View File
@@ -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 || {});
}
+80
View File
@@ -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;
}
+56
View File
@@ -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;
}
}
+48
View File
@@ -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';
+56
View File
@@ -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;
}
}
+204
View File
@@ -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];
}
+184
View File
@@ -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);
}
+95 -1
View File
@@ -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';
+193
View File
@@ -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);
}
+366
View File
@@ -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);
}
+6
View File
@@ -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';
+265
View File
@@ -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);
}
+221
View File
@@ -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);
}
+307
View File
@@ -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();
}
+227
View File
@@ -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();
}
}
+12 -15
View File
@@ -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'
+189
View File
@@ -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>;
}
+2 -20
View File
@@ -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';
+8 -4
View File
@@ -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
View File
@@ -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);
});
});
+71
View File
@@ -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
+2 -2
View File
@@ -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]"
+16
View File
@@ -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
+108 -5
View File
@@ -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):
+1 -1
View File
@@ -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'
]
+231
View File
@@ -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
+454
View File
@@ -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)
)
+367
View File
@@ -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)
+327
View File
@@ -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)
+179
View File
@@ -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