mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 22:52:58 -10:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90950f8889 | ||
|
|
56f36e2593 | ||
|
|
7aa67ad27c | ||
|
|
307df443ef | ||
|
|
6434a87b60 | ||
|
|
97d5b6db0d | ||
|
|
4996a9f9fa | ||
|
|
54f4a96cba | ||
|
|
e59be8f0fd | ||
|
|
b52708f3f6 | ||
|
|
ba37ce39d3 | ||
|
|
98c5e8f175 |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai>=2.8.3" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai>=2.8.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.7.9" \
|
||||
"praisonai>=2.8.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# AI SDK Examples
|
||||
|
||||
Examples demonstrating the AI SDK integration in PraisonAI TypeScript.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
npm install praisonai ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google zod
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export GOOGLE_API_KEY=AIza...
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic-chat.ts` | Simple text generation |
|
||||
| `streaming.ts` | Streaming text output |
|
||||
| `tool-calling.ts` | Function/tool calling |
|
||||
| `multi-provider.ts` | Using multiple providers |
|
||||
| `structured-output.ts` | JSON schema output |
|
||||
| `multi-agent.ts` | Multi-agent attribution |
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
npx ts-node basic-chat.ts
|
||||
npx ts-node streaming.ts
|
||||
npx ts-node tool-calling.ts
|
||||
```
|
||||
|
||||
## More Information
|
||||
|
||||
- [AI SDK Documentation](https://docs.praisonai.com/docs/js/ai-sdk)
|
||||
- [AI SDK CLI](https://docs.praisonai.com/docs/js/ai-sdk-cli)
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* AI SDK Basic Chat Example
|
||||
*
|
||||
* Demonstrates simple text generation using the AI SDK backend.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node basic-chat.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create backend with model string
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini');
|
||||
|
||||
console.log('Sending message to OpenAI...\n');
|
||||
|
||||
// Generate text
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'What are the three primary colors?' }
|
||||
],
|
||||
maxTokens: 200,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
console.log('Response:', result.text);
|
||||
console.log('\nFinish reason:', result.finishReason);
|
||||
|
||||
if (result.usage) {
|
||||
console.log('Token usage:', result.usage);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Generate Text Example
|
||||
*
|
||||
* Demonstrates basic text generation using AI SDK v6 via praisonai-ts.
|
||||
*
|
||||
* Run: npx ts-node generate-text.ts
|
||||
* Required: OPENAI_API_KEY
|
||||
*/
|
||||
|
||||
// Import from local praisonai-ts package
|
||||
// In production: import { Agent } from 'praisonai';
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
// Simple text generation with Agent
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant that provides concise answers.',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
});
|
||||
|
||||
console.log('=== Generate Text Example ===\n');
|
||||
|
||||
// Basic chat
|
||||
const response = await agent.chat('What is the capital of France?');
|
||||
console.log('Response:', response);
|
||||
|
||||
// With context
|
||||
const response2 = await agent.chat('What is its population?');
|
||||
console.log('\nFollow-up:', response2);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* MCP Client Example (AI SDK v6 Parity)
|
||||
*
|
||||
* Demonstrates Model Context Protocol client with HTTP/SSE transports.
|
||||
*
|
||||
* Run: npx ts-node mcp-client.ts
|
||||
* Note: Requires an MCP server running
|
||||
*/
|
||||
|
||||
import {
|
||||
createMCP,
|
||||
closeMCPClient,
|
||||
type MCPConfig,
|
||||
} from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== MCP Client Example ===\n');
|
||||
|
||||
// Example configurations for different transports
|
||||
const configs: Record<string, MCPConfig> = {
|
||||
// HTTP transport (recommended for production)
|
||||
http: {
|
||||
transport: {
|
||||
type: 'http',
|
||||
url: 'https://mcp-server.example.com/mcp',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
},
|
||||
name: 'http-client',
|
||||
},
|
||||
// SSE transport
|
||||
sse: {
|
||||
transport: {
|
||||
type: 'sse',
|
||||
url: 'https://mcp-server.example.com/sse',
|
||||
},
|
||||
name: 'sse-client',
|
||||
},
|
||||
// Stdio transport (for local servers)
|
||||
stdio: {
|
||||
transport: {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-everything'],
|
||||
},
|
||||
name: 'stdio-client',
|
||||
},
|
||||
};
|
||||
|
||||
console.log('MCP Transport Configurations:');
|
||||
console.log('');
|
||||
console.log('1. HTTP Transport (recommended):');
|
||||
console.log(` ${JSON.stringify(configs.http.transport, null, 2)}`);
|
||||
console.log('');
|
||||
console.log('2. SSE Transport:');
|
||||
console.log(` ${JSON.stringify(configs.sse.transport, null, 2)}`);
|
||||
console.log('');
|
||||
console.log('3. Stdio Transport (local):');
|
||||
console.log(` ${JSON.stringify(configs.stdio.transport, null, 2)}`);
|
||||
|
||||
console.log('\n--- Example Usage ---\n');
|
||||
console.log(`
|
||||
// Create MCP client
|
||||
const client = await createMCP({
|
||||
transport: {
|
||||
type: 'http',
|
||||
url: 'https://your-mcp-server.com/mcp',
|
||||
headers: { Authorization: 'Bearer your-token' },
|
||||
}
|
||||
});
|
||||
|
||||
// Get available tools
|
||||
const tools = await client.tools();
|
||||
console.log('Available tools:', Object.keys(tools));
|
||||
|
||||
// List resources
|
||||
const resources = await client.listResources();
|
||||
console.log('Resources:', resources);
|
||||
|
||||
// List prompts
|
||||
const prompts = await client.listPrompts();
|
||||
console.log('Prompts:', prompts);
|
||||
|
||||
// Close when done
|
||||
await closeMCPClient('default');
|
||||
`);
|
||||
|
||||
console.log('\n✅ MCP Client demo completed!');
|
||||
console.log('Note: To run with a real MCP server, uncomment the connection code.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* AI SDK Multi-Agent Attribution Example
|
||||
*
|
||||
* Demonstrates how to propagate attribution context (agentId, runId, traceId)
|
||||
* to LLM calls for multi-agent observability and safety.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node multi-agent.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from 'praisonai';
|
||||
|
||||
// Simulate a multi-agent system with attribution
|
||||
interface AgentContext {
|
||||
agentId: string;
|
||||
runId: string;
|
||||
traceId: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
async function runAgent(name: string, task: string, context: AgentContext) {
|
||||
console.log(`\n[${name}] Starting task: ${task}`);
|
||||
console.log(` Attribution: agentId=${context.agentId}, runId=${context.runId}`);
|
||||
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
attribution: context,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: `You are ${name}, a specialized AI agent.` },
|
||||
{ role: 'user', content: task }
|
||||
],
|
||||
maxTokens: 100,
|
||||
});
|
||||
|
||||
console.log(` Response: ${result.text.slice(0, 100)}...`);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create a shared trace ID for the entire workflow
|
||||
const traceId = `trace-${Date.now().toString(36)}`;
|
||||
const sessionId = 'session-user123';
|
||||
|
||||
console.log('Multi-Agent Workflow with Attribution');
|
||||
console.log('=====================================');
|
||||
console.log(`Trace ID: ${traceId}`);
|
||||
console.log(`Session ID: ${sessionId}`);
|
||||
|
||||
// Agent 1: Research Agent
|
||||
const researchResult = await runAgent(
|
||||
'ResearchAgent',
|
||||
'What are the key benefits of TypeScript?',
|
||||
{
|
||||
agentId: 'agent-research',
|
||||
runId: `run-${Date.now().toString(36)}-1`,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
);
|
||||
|
||||
// Agent 2: Summary Agent
|
||||
const summaryResult = await runAgent(
|
||||
'SummaryAgent',
|
||||
`Summarize this in one sentence: ${researchResult}`,
|
||||
{
|
||||
agentId: 'agent-summary',
|
||||
runId: `run-${Date.now().toString(36)}-2`,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
);
|
||||
|
||||
// Agent 3: Review Agent
|
||||
await runAgent(
|
||||
'ReviewAgent',
|
||||
`Review this summary for accuracy: ${summaryResult}`,
|
||||
{
|
||||
agentId: 'agent-review',
|
||||
runId: `run-${Date.now().toString(36)}-3`,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
);
|
||||
|
||||
console.log('\n\nWorkflow complete!');
|
||||
console.log('All requests were tagged with the same trace ID for observability.');
|
||||
console.log('Check your LLM provider dashboard to see the correlated requests.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* AI SDK Multi-Provider Example
|
||||
*
|
||||
* Demonstrates using multiple LLM providers with the same interface.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node multi-provider.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
* ANTHROPIC_API_KEY - Your Anthropic API key
|
||||
* GOOGLE_API_KEY - Your Google API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from 'praisonai';
|
||||
|
||||
const PROMPT = 'Explain what an API is in one sentence.';
|
||||
|
||||
async function testProvider(modelString: string) {
|
||||
try {
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await backend.generateText({
|
||||
messages: [{ role: 'user', content: PROMPT }],
|
||||
maxTokens: 100,
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`\n${modelString}:`);
|
||||
console.log(` Response: ${result.text}`);
|
||||
console.log(` Duration: ${duration}ms`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.log(`\n${modelString}:`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Testing multiple providers with the same prompt...');
|
||||
console.log(`Prompt: "${PROMPT}"`);
|
||||
|
||||
const providers = [
|
||||
'openai/gpt-4o-mini',
|
||||
'anthropic/claude-3-haiku-20240307',
|
||||
'google/gemini-1.5-flash',
|
||||
];
|
||||
|
||||
const results = await Promise.all(providers.map(testProvider));
|
||||
|
||||
const successful = results.filter(Boolean).length;
|
||||
console.log(`\n\nSummary: ${successful}/${providers.length} providers succeeded`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Speech & Transcription Example (AI SDK v6 Parity)
|
||||
*
|
||||
* Demonstrates speech generation and transcription wrappers.
|
||||
*
|
||||
* Run: npx ts-node speech-transcription.ts
|
||||
* Required: OPENAI_API_KEY or ELEVENLABS_API_KEY
|
||||
*/
|
||||
|
||||
import {
|
||||
generateSpeech,
|
||||
transcribe,
|
||||
SPEECH_MODELS,
|
||||
TRANSCRIPTION_MODELS,
|
||||
} from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Speech & Transcription Example ===\n');
|
||||
|
||||
// Show available models
|
||||
console.log('1. Available Speech Models:');
|
||||
Object.entries(SPEECH_MODELS).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
|
||||
console.log('\n2. Available Transcription Models:');
|
||||
Object.entries(TRANSCRIPTION_MODELS).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
|
||||
// Example: Generate speech (requires API key)
|
||||
console.log('\n3. Speech Generation Example:');
|
||||
console.log(`
|
||||
// Generate speech from text
|
||||
const result = await generateSpeech({
|
||||
text: 'Hello, this is a test of speech synthesis.',
|
||||
model: 'openai/tts-1',
|
||||
voice: 'alloy',
|
||||
});
|
||||
|
||||
// Save to file
|
||||
await fs.writeFile('output.mp3', result.audio);
|
||||
console.log('Audio saved to output.mp3');
|
||||
`);
|
||||
|
||||
// Example: Transcribe audio (requires API key)
|
||||
console.log('4. Transcription Example:');
|
||||
console.log(`
|
||||
// Transcribe audio file
|
||||
const result = await transcribe({
|
||||
audio: await fs.readFile('audio.mp3'),
|
||||
model: 'openai/whisper-1',
|
||||
});
|
||||
|
||||
console.log('Transcription:', result.text);
|
||||
console.log('Language:', result.language);
|
||||
console.log('Duration:', result.durationInSeconds, 'seconds');
|
||||
|
||||
// With segments
|
||||
if (result.segments) {
|
||||
result.segments.forEach(seg => {
|
||||
console.log(\`[\${seg.start}s - \${seg.end}s]: \${seg.text}\`);
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
// Live test if API key is available
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
console.log('\n5. Live Speech Generation Test:');
|
||||
try {
|
||||
const result = await generateSpeech({
|
||||
text: 'Hello from PraisonAI!',
|
||||
model: 'openai/tts-1',
|
||||
voice: 'alloy',
|
||||
});
|
||||
console.log(` ✅ Generated ${result.audio.length} bytes of audio`);
|
||||
} catch (error: any) {
|
||||
console.log(` ⚠️ Speech generation failed: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('\n5. Live Test: Skipped (OPENAI_API_KEY not set)');
|
||||
}
|
||||
|
||||
console.log('\n✅ Speech & Transcription demo completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Stream Text Example
|
||||
*
|
||||
* Demonstrates streaming text generation using AI SDK v6 via praisonai-ts.
|
||||
*
|
||||
* Run: npx ts-node stream-text.ts
|
||||
* Required: OPENAI_API_KEY
|
||||
*/
|
||||
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant that provides detailed answers.',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
stream: true,
|
||||
});
|
||||
|
||||
console.log('=== Stream Text Example ===\n');
|
||||
console.log('Streaming response:\n');
|
||||
|
||||
// Stream the response
|
||||
const response = await agent.chat('Explain quantum computing in 3 paragraphs.');
|
||||
console.log('\n\nFinal response:', response);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* AI SDK Streaming Example
|
||||
*
|
||||
* Demonstrates streaming text generation with real-time output.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node streaming.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini');
|
||||
|
||||
console.log('Streaming response from OpenAI...\n');
|
||||
|
||||
const stream = await backend.streamText({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Write a short poem about coding.' }
|
||||
],
|
||||
maxTokens: 200,
|
||||
});
|
||||
|
||||
// Stream chunks as they arrive
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.text) {
|
||||
process.stdout.write(chunk.text);
|
||||
}
|
||||
|
||||
if (chunk.finishReason) {
|
||||
console.log('\n\nFinish reason:', chunk.finishReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* AI SDK Structured Output Example
|
||||
*
|
||||
* Demonstrates generating typed JSON objects with Zod schemas.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node structured-output.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { generateObject } from 'ai';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define schema for a person profile
|
||||
const PersonSchema = z.object({
|
||||
name: z.string().describe('Full name'),
|
||||
age: z.number().describe('Age in years'),
|
||||
occupation: z.string().describe('Job title'),
|
||||
city: z.string().describe('City of residence'),
|
||||
interests: z.array(z.string()).describe('List of hobbies/interests'),
|
||||
bio: z.string().describe('Short biography'),
|
||||
});
|
||||
|
||||
// Define schema for a recipe
|
||||
const RecipeSchema = z.object({
|
||||
name: z.string().describe('Recipe name'),
|
||||
cuisine: z.string().describe('Type of cuisine'),
|
||||
prepTime: z.number().describe('Preparation time in minutes'),
|
||||
cookTime: z.number().describe('Cooking time in minutes'),
|
||||
servings: z.number().describe('Number of servings'),
|
||||
ingredients: z.array(z.object({
|
||||
item: z.string(),
|
||||
amount: z.string(),
|
||||
})).describe('List of ingredients'),
|
||||
instructions: z.array(z.string()).describe('Step-by-step instructions'),
|
||||
});
|
||||
|
||||
async function main() {
|
||||
console.log('Generating structured output with Zod schemas...\n');
|
||||
|
||||
// Generate a person profile
|
||||
console.log('1. Generating a fictional person profile...');
|
||||
const personResult = await generateObject({
|
||||
model: openai('gpt-4o-mini'),
|
||||
schema: PersonSchema,
|
||||
prompt: 'Generate a fictional software engineer profile.',
|
||||
});
|
||||
|
||||
console.log('Person:', JSON.stringify(personResult.object, null, 2));
|
||||
|
||||
// Generate a recipe
|
||||
console.log('\n2. Generating a recipe...');
|
||||
const recipeResult = await generateObject({
|
||||
model: openai('gpt-4o-mini'),
|
||||
schema: RecipeSchema,
|
||||
prompt: 'Generate a simple pasta recipe.',
|
||||
});
|
||||
|
||||
console.log('Recipe:', JSON.stringify(recipeResult.object, null, 2));
|
||||
|
||||
// Type-safe access
|
||||
console.log('\n3. Type-safe access:');
|
||||
console.log(` Person name: ${personResult.object.name}`);
|
||||
console.log(` Person age: ${personResult.object.age}`);
|
||||
console.log(` Recipe name: ${recipeResult.object.name}`);
|
||||
console.log(` Total time: ${recipeResult.object.prepTime + recipeResult.object.cookTime} minutes`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Telemetry Example (AI SDK v6 Parity)
|
||||
*
|
||||
* Demonstrates telemetry and tracing with optional OpenTelemetry integration.
|
||||
*
|
||||
* Run: npx ts-node telemetry.ts
|
||||
* Required: OPENAI_API_KEY
|
||||
*/
|
||||
|
||||
import {
|
||||
configureTelemetry,
|
||||
isTelemetryEnabled,
|
||||
createAISpan,
|
||||
recordEvent,
|
||||
getEvents,
|
||||
clearEvents,
|
||||
Agent,
|
||||
} from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Telemetry Example ===\n');
|
||||
|
||||
// Configure telemetry
|
||||
configureTelemetry({
|
||||
isEnabled: true,
|
||||
functionId: 'telemetry-demo',
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
environment: 'development',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('1. Telemetry Status:');
|
||||
console.log(` Enabled: ${isTelemetryEnabled()}`);
|
||||
|
||||
// Create a span for an operation
|
||||
console.log('\n2. Creating AI Span:');
|
||||
const span = createAISpan('generateText', {
|
||||
model: 'gpt-4o-mini',
|
||||
provider: 'openai',
|
||||
});
|
||||
|
||||
try {
|
||||
// Simulate AI operation
|
||||
console.log(' Performing AI operation...');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
span.setAttribute('tokens', 150);
|
||||
span.setStatus({ code: 'ok' });
|
||||
console.log(' Span completed successfully');
|
||||
} catch (error: any) {
|
||||
span.recordException(error);
|
||||
span.setStatus({ code: 'error', message: error.message });
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
|
||||
// Record custom events
|
||||
console.log('\n3. Recording Events:');
|
||||
recordEvent('agent.started', { agentName: 'demo-agent' });
|
||||
recordEvent('tool.called', { toolName: 'search', duration: 250 });
|
||||
recordEvent('agent.completed', { success: true });
|
||||
|
||||
const events = getEvents();
|
||||
console.log(` Recorded ${events.length} events:`);
|
||||
events.forEach(e => {
|
||||
console.log(` - ${e.name}: ${JSON.stringify(e.attributes)}`);
|
||||
});
|
||||
|
||||
// Clear events
|
||||
console.log('\n4. Clearing Events:');
|
||||
clearEvents();
|
||||
console.log(` Events after clear: ${getEvents().length}`);
|
||||
|
||||
console.log('\n✅ Telemetry demo completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Tool Approval Example (AI SDK v6 Parity)
|
||||
*
|
||||
* Demonstrates human-in-the-loop tool approval using needsApproval.
|
||||
*
|
||||
* Run: npx ts-node tool-approval.ts
|
||||
* Required: OPENAI_API_KEY
|
||||
*/
|
||||
|
||||
import {
|
||||
Agent,
|
||||
withApproval,
|
||||
getApprovalManager,
|
||||
createDangerousPatternChecker,
|
||||
} from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Tool Approval Example ===\n');
|
||||
|
||||
// Set up approval handler
|
||||
const manager = getApprovalManager();
|
||||
manager.onApprovalRequest(async (request) => {
|
||||
console.log(`\n🔐 APPROVAL REQUESTED`);
|
||||
console.log(` Tool: ${request.toolName}`);
|
||||
console.log(` Input: ${JSON.stringify(request.input)}`);
|
||||
console.log(` Reason: ${request.reason || 'No reason provided'}`);
|
||||
|
||||
// Auto-approve for demo (in production, prompt user)
|
||||
console.log(` ✅ Auto-approved for demo\n`);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Create a tool that always requires approval
|
||||
const deleteFile = withApproval({
|
||||
name: 'deleteFile',
|
||||
description: 'Delete a file from the filesystem',
|
||||
needsApproval: true, // Always require approval
|
||||
execute: async (input: { path: string }) => {
|
||||
console.log(` Executing: Would delete ${input.path}`);
|
||||
return { success: true, deleted: input.path };
|
||||
},
|
||||
});
|
||||
|
||||
// Create a tool with conditional approval (dangerous patterns only)
|
||||
const runCommand = withApproval({
|
||||
name: 'runCommand',
|
||||
description: 'Run a shell command',
|
||||
needsApproval: createDangerousPatternChecker(),
|
||||
execute: async (input: { command: string }) => {
|
||||
console.log(` Executing: ${input.command}`);
|
||||
return { output: 'command executed' };
|
||||
},
|
||||
});
|
||||
|
||||
// Test the tools
|
||||
console.log('1. Testing deleteFile (always requires approval):');
|
||||
const result1 = await deleteFile({ path: '/tmp/test.txt' });
|
||||
console.log(' Result:', result1);
|
||||
|
||||
console.log('\n2. Testing runCommand with safe command (no approval needed):');
|
||||
const result2 = await runCommand({ command: 'ls -la' });
|
||||
console.log(' Result:', result2);
|
||||
|
||||
console.log('\n3. Testing runCommand with dangerous command (requires approval):');
|
||||
const result3 = await runCommand({ command: 'rm -rf /tmp/test' });
|
||||
console.log(' Result:', result3);
|
||||
|
||||
console.log('\n✅ Tool approval demo completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* AI SDK Tool Calling Example
|
||||
*
|
||||
* Demonstrates function/tool calling with Zod schemas.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node tool-calling.ts
|
||||
*
|
||||
* Environment:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { generateText, tool } from 'ai';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define tools with Zod schemas
|
||||
const tools = {
|
||||
get_weather: tool({
|
||||
description: 'Get the current weather for a city',
|
||||
inputSchema: z.object({
|
||||
city: z.string().describe('The city name'),
|
||||
unit: z.enum(['celsius', 'fahrenheit']).optional().describe('Temperature unit'),
|
||||
}),
|
||||
execute: async ({ city, unit = 'celsius' }) => {
|
||||
// Mock weather data
|
||||
const weather = {
|
||||
city,
|
||||
temperature: unit === 'celsius' ? 22 : 72,
|
||||
unit,
|
||||
condition: 'sunny',
|
||||
humidity: 45,
|
||||
};
|
||||
return weather;
|
||||
},
|
||||
}),
|
||||
|
||||
get_time: tool({
|
||||
description: 'Get the current time in a timezone',
|
||||
inputSchema: z.object({
|
||||
timezone: z.string().describe('Timezone like "America/New_York"'),
|
||||
}),
|
||||
execute: async ({ timezone }) => {
|
||||
const time = new Date().toLocaleString('en-US', { timeZone: timezone });
|
||||
return { timezone, time };
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('Asking about weather with tool calling...\n');
|
||||
|
||||
const result = await generateText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: [
|
||||
{ role: 'user', content: 'What is the weather in Paris and what time is it in New York?' }
|
||||
],
|
||||
tools,
|
||||
maxSteps: 5, // Allow multiple tool calls
|
||||
});
|
||||
|
||||
console.log('Tool Calls:');
|
||||
for (const toolCall of result.toolCalls) {
|
||||
console.log(` - ${toolCall.toolName}:`, toolCall.args);
|
||||
}
|
||||
|
||||
console.log('\nTool Results:');
|
||||
for (const toolResult of result.toolResults) {
|
||||
console.log(` - ${toolResult.toolName}:`, toolResult.result);
|
||||
}
|
||||
|
||||
console.log('\nFinal Response:', result.text);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* UI Message Example (AI SDK v6 Parity)
|
||||
*
|
||||
* Demonstrates UIMessage protocol for Next.js/React integration.
|
||||
*
|
||||
* Run: npx ts-node ui-message.ts
|
||||
* Required: OPENAI_API_KEY
|
||||
*/
|
||||
|
||||
import {
|
||||
createTextMessage,
|
||||
createSystemMessage,
|
||||
convertToModelMessages,
|
||||
convertToUIMessages,
|
||||
validateUIMessages,
|
||||
hasPendingApprovals,
|
||||
type UIMessage,
|
||||
} from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== UI Message Example ===\n');
|
||||
|
||||
// Create UI messages
|
||||
const messages: UIMessage[] = [
|
||||
createSystemMessage('You are a helpful assistant.'),
|
||||
createTextMessage('user', 'What is the capital of France?'),
|
||||
createTextMessage('assistant', 'The capital of France is Paris.'),
|
||||
createTextMessage('user', 'What is its population?'),
|
||||
];
|
||||
|
||||
console.log('1. Created UI Messages:');
|
||||
messages.forEach((msg, i) => {
|
||||
const text = msg.parts.find(p => p.type === 'text');
|
||||
console.log(` [${i}] ${msg.role}: ${text ? (text as any).text.substring(0, 50) : '...'}`);
|
||||
});
|
||||
|
||||
// Validate messages
|
||||
console.log('\n2. Validating messages:');
|
||||
const isValid = validateUIMessages(messages);
|
||||
console.log(` Valid: ${isValid}`);
|
||||
|
||||
// Convert to model messages (for AI SDK)
|
||||
console.log('\n3. Converting to Model Messages:');
|
||||
const modelMessages = await convertToModelMessages(messages);
|
||||
modelMessages.forEach((msg, i) => {
|
||||
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
||||
console.log(` [${i}] ${msg.role}: ${content.substring(0, 50)}...`);
|
||||
});
|
||||
|
||||
// Check for pending approvals
|
||||
console.log('\n4. Checking for pending approvals:');
|
||||
const hasPending = messages.some(m => hasPendingApprovals(m));
|
||||
console.log(` Has pending approvals: ${hasPending}`);
|
||||
|
||||
// Convert model messages back to UI messages
|
||||
console.log('\n5. Round-trip conversion:');
|
||||
const uiMessages = convertToUIMessages(modelMessages);
|
||||
console.log(` Converted ${uiMessages.length} messages back to UI format`);
|
||||
|
||||
console.log('\n✅ UI Message demo completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Basic Embedding Example
|
||||
*
|
||||
* Demonstrates using Agent.embed() for text embeddings
|
||||
*
|
||||
* Run: npx ts-node examples/js/embeddings/embed-basic.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../../src/praisonai-ts/dist/agent/simple';
|
||||
import { embed, embedMany, cosineSimilarity } from '../../../src/praisonai-ts/dist/llm/embeddings';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Basic Embedding Example ===\n');
|
||||
|
||||
// Method 1: Using Agent.embed()
|
||||
console.log('1. Agent.embed() - Single text');
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant',
|
||||
llm: 'openai/gpt-4o-mini',
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const singleEmbedding = await agent.embed('Hello world');
|
||||
console.log(` Dimensions: ${(singleEmbedding as number[]).length}`);
|
||||
console.log(` First 5 values: [${(singleEmbedding as number[]).slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]`);
|
||||
|
||||
// Method 2: Using Agent.embed() with multiple texts
|
||||
console.log('\n2. Agent.embed() - Multiple texts');
|
||||
const multiEmbeddings = await agent.embed(['Hello', 'World', 'How are you?']);
|
||||
console.log(` Count: ${(multiEmbeddings as number[][]).length}`);
|
||||
console.log(` Each has ${(multiEmbeddings as number[][])[0].length} dimensions`);
|
||||
|
||||
// Method 3: Direct embed function
|
||||
console.log('\n3. Direct embed() function');
|
||||
const result = await embed('PraisonAI is an AI agent framework');
|
||||
console.log(` Dimensions: ${result.embedding.length}`);
|
||||
console.log(` Tokens used: ${result.usage?.tokens || 'N/A'}`);
|
||||
|
||||
// Method 4: Batch embeddings
|
||||
console.log('\n4. Batch embedMany() function');
|
||||
const texts = [
|
||||
'Machine learning is a subset of AI',
|
||||
'Deep learning uses neural networks',
|
||||
'Natural language processing handles text'
|
||||
];
|
||||
const batchResult = await embedMany(texts);
|
||||
console.log(` Embedded ${batchResult.embeddings.length} texts`);
|
||||
console.log(` Total tokens: ${batchResult.usage?.tokens || 'N/A'}`);
|
||||
|
||||
// Method 5: Similarity comparison
|
||||
console.log('\n5. Cosine Similarity');
|
||||
const emb1 = await embed('I love programming');
|
||||
const emb2 = await embed('Coding is my passion');
|
||||
const emb3 = await embed('The weather is nice today');
|
||||
|
||||
const sim12 = cosineSimilarity(emb1.embedding, emb2.embedding);
|
||||
const sim13 = cosineSimilarity(emb1.embedding, emb3.embedding);
|
||||
|
||||
console.log(` "I love programming" vs "Coding is my passion": ${(sim12 * 100).toFixed(1)}%`);
|
||||
console.log(` "I love programming" vs "The weather is nice": ${(sim13 * 100).toFixed(1)}%`);
|
||||
|
||||
console.log('\n=== Complete ===');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Document Embedding Example
|
||||
*
|
||||
* Demonstrates chunking and embedding documents for retrieval
|
||||
*
|
||||
* Run: npx ts-node examples/js/embeddings/embed-docs.ts
|
||||
*/
|
||||
|
||||
import { embedMany, cosineSimilarity } from '../../../src/praisonai-ts/dist/llm/embeddings';
|
||||
|
||||
// Sample documents
|
||||
const documents = [
|
||||
{
|
||||
id: 'doc1',
|
||||
title: 'Introduction to AI',
|
||||
content: 'Artificial Intelligence (AI) is the simulation of human intelligence by machines. It includes machine learning, natural language processing, and computer vision.'
|
||||
},
|
||||
{
|
||||
id: 'doc2',
|
||||
title: 'Machine Learning Basics',
|
||||
content: 'Machine learning is a subset of AI that enables systems to learn from data. It includes supervised learning, unsupervised learning, and reinforcement learning.'
|
||||
},
|
||||
{
|
||||
id: 'doc3',
|
||||
title: 'Deep Learning',
|
||||
content: 'Deep learning uses neural networks with multiple layers. It excels at tasks like image recognition, speech processing, and natural language understanding.'
|
||||
},
|
||||
{
|
||||
id: 'doc4',
|
||||
title: 'Natural Language Processing',
|
||||
content: 'NLP enables computers to understand human language. Applications include chatbots, translation, sentiment analysis, and text summarization.'
|
||||
},
|
||||
{
|
||||
id: 'doc5',
|
||||
title: 'Computer Vision',
|
||||
content: 'Computer vision allows machines to interpret visual information. It powers applications like facial recognition, autonomous vehicles, and medical imaging.'
|
||||
}
|
||||
];
|
||||
|
||||
interface EmbeddedDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
embedding: number[];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Document Embedding Example ===\n');
|
||||
|
||||
// Step 1: Embed all documents
|
||||
console.log('1. Embedding documents...');
|
||||
const contents = documents.map(d => d.content);
|
||||
const { embeddings } = await embedMany(contents);
|
||||
|
||||
const embeddedDocs: EmbeddedDocument[] = documents.map((doc, i) => ({
|
||||
...doc,
|
||||
embedding: embeddings[i]
|
||||
}));
|
||||
|
||||
console.log(` Embedded ${embeddedDocs.length} documents`);
|
||||
console.log(` Embedding dimensions: ${embeddings[0].length}`);
|
||||
|
||||
// Step 2: Search function
|
||||
async function search(query: string, topK: number = 3): Promise<Array<{ doc: EmbeddedDocument; score: number }>> {
|
||||
const { embedding: queryEmbedding } = await (await import('../../../src/praisonai-ts/dist/llm/embeddings')).embed(query);
|
||||
|
||||
const results = embeddedDocs.map(doc => ({
|
||||
doc,
|
||||
score: cosineSimilarity(queryEmbedding, doc.embedding)
|
||||
}));
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, topK);
|
||||
}
|
||||
|
||||
// Step 3: Test queries
|
||||
console.log('\n2. Testing search queries...\n');
|
||||
|
||||
const queries = [
|
||||
'How do machines learn from data?',
|
||||
'What is image recognition?',
|
||||
'Tell me about chatbots and translation'
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
console.log(`Query: "${query}"`);
|
||||
const results = await search(query);
|
||||
|
||||
console.log('Top results:');
|
||||
results.forEach((r, i) => {
|
||||
console.log(` ${i + 1}. [${(r.score * 100).toFixed(1)}%] ${r.doc.title}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Step 4: Show document similarity matrix
|
||||
console.log('3. Document Similarity Matrix');
|
||||
console.log('');
|
||||
|
||||
// Header
|
||||
process.stdout.write(' ');
|
||||
documents.forEach((_, i) => process.stdout.write(`Doc${i + 1} `));
|
||||
console.log('');
|
||||
|
||||
// Matrix
|
||||
for (let i = 0; i < embeddedDocs.length; i++) {
|
||||
process.stdout.write(`Doc${i + 1} `);
|
||||
for (let j = 0; j < embeddedDocs.length; j++) {
|
||||
const sim = cosineSimilarity(embeddedDocs[i].embedding, embeddedDocs[j].embedding);
|
||||
process.stdout.write(`${(sim * 100).toFixed(0).padStart(3)}% `);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('\n=== Complete ===');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Retrieval Example - Simple Vector Search
|
||||
*
|
||||
* Demonstrates building a simple retrieval system with embeddings
|
||||
*
|
||||
* Run: npx ts-node examples/js/embeddings/retrieval.ts
|
||||
*/
|
||||
|
||||
import { embed, embedMany, cosineSimilarity } from '../../../src/praisonai-ts/dist/llm/embeddings';
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
content: string;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
document: Document;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// Simple in-memory vector store
|
||||
class SimpleVectorStore {
|
||||
private documents: Document[] = [];
|
||||
|
||||
async add(doc: Document): Promise<void> {
|
||||
const { embedding } = await embed(doc.content);
|
||||
this.documents.push({ ...doc, embedding });
|
||||
}
|
||||
|
||||
async addBatch(docs: Document[]): Promise<void> {
|
||||
const contents = docs.map(d => d.content);
|
||||
const { embeddings } = await embedMany(contents);
|
||||
docs.forEach((doc, i) => {
|
||||
this.documents.push({ ...doc, embedding: embeddings[i] });
|
||||
});
|
||||
}
|
||||
|
||||
async search(query: string, topK: number = 3): Promise<SearchResult[]> {
|
||||
const { embedding: queryEmb } = await embed(query);
|
||||
|
||||
const results = this.documents
|
||||
.filter(doc => doc.embedding)
|
||||
.map(doc => ({
|
||||
document: doc,
|
||||
score: cosineSimilarity(queryEmb, doc.embedding!)
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, topK);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
get(id: string): Document | undefined {
|
||||
return this.documents.find(d => d.id === id);
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
const idx = this.documents.findIndex(d => d.id === id);
|
||||
if (idx >= 0) {
|
||||
this.documents.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Simple Vector Retrieval ===\n');
|
||||
|
||||
// Create vector store
|
||||
console.log('1. Creating vector store...');
|
||||
const store = new SimpleVectorStore();
|
||||
|
||||
// Add documents
|
||||
console.log('2. Adding documents...');
|
||||
const docs: Document[] = [
|
||||
{ id: '1', content: 'PraisonAI is an AI agent framework for building intelligent applications.' },
|
||||
{ id: '2', content: 'Agents can use tools to interact with external systems and APIs.' },
|
||||
{ id: '3', content: 'Workflows allow orchestrating multiple agents in sequence or parallel.' },
|
||||
{ id: '4', content: 'Memory systems help agents maintain context across conversations.' },
|
||||
{ id: '5', content: 'The AI SDK provides a unified interface for multiple LLM providers.' },
|
||||
{ id: '6', content: 'Embeddings enable semantic search and similarity matching.' },
|
||||
{ id: '7', content: 'Guardrails ensure agent outputs meet safety and quality standards.' },
|
||||
{ id: '8', content: 'Sessions persist conversation state for multi-turn interactions.' }
|
||||
];
|
||||
|
||||
await store.addBatch(docs);
|
||||
console.log(` Added ${docs.length} documents`);
|
||||
|
||||
// Search queries
|
||||
console.log('\n3. Running search queries...\n');
|
||||
|
||||
const queries = [
|
||||
'How do I build an AI application?',
|
||||
'Can agents call external APIs?',
|
||||
'How to maintain conversation context?',
|
||||
'What providers are supported?'
|
||||
];
|
||||
|
||||
for (const query of queries) {
|
||||
console.log(`Query: "${query}"`);
|
||||
const results = await store.search(query);
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log('Results:');
|
||||
results.forEach((r, i) => {
|
||||
console.log(` ${i + 1}. [${(r.score * 100).toFixed(1)}%] ${r.document.content.substring(0, 60)}...`);
|
||||
});
|
||||
} else {
|
||||
console.log(' No results found');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Get specific document
|
||||
console.log('4. Get document by ID...');
|
||||
const doc = store.get('5');
|
||||
if (doc) {
|
||||
console.log(` Found: ${doc.content}`);
|
||||
}
|
||||
|
||||
// Delete document
|
||||
console.log('\n5. Delete document...');
|
||||
const deleted = store.delete('8');
|
||||
console.log(` Deleted doc 8: ${deleted}`);
|
||||
|
||||
console.log('\n=== Complete ===');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* MCP Filesystem Server Example
|
||||
*
|
||||
* Demonstrates using MCP to connect to a filesystem server.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai-ts
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*
|
||||
* Run:
|
||||
* npx ts-node filesystem-mcp.ts
|
||||
*/
|
||||
|
||||
import { Agent, createMCP } from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== MCP Filesystem Server Example ===\n');
|
||||
|
||||
// Connect to MCP filesystem server
|
||||
const mcp = await createMCP({
|
||||
transport: {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Connected to MCP filesystem server\n');
|
||||
|
||||
// Get tools from MCP server
|
||||
const tools = await mcp.tools();
|
||||
console.log('Available tools:', Object.keys(tools));
|
||||
|
||||
// Create agent with MCP tools
|
||||
const agent = new Agent({
|
||||
name: 'FileAgent',
|
||||
instructions: 'You help manage files using the filesystem tools.',
|
||||
tools: Object.values(tools),
|
||||
});
|
||||
|
||||
// Ask the agent to list files
|
||||
const response = await agent.chat('List all TypeScript files in the current directory');
|
||||
console.log('\nResponse:', response);
|
||||
|
||||
// Cleanup
|
||||
await mcp.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Multimodal Image Analysis Example
|
||||
*
|
||||
* Demonstrates analyzing images with a vision-capable model.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai-ts
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*
|
||||
* Run:
|
||||
* npx ts-node image-analysis.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Multimodal Image Analysis Example ===\n');
|
||||
|
||||
// Create an agent with a vision-capable model
|
||||
const agent = new Agent({
|
||||
name: 'VisionAgent',
|
||||
llm: 'gpt-4o', // Vision-capable model
|
||||
instructions: 'You analyze images and describe what you see in detail.',
|
||||
});
|
||||
|
||||
// For multimodal, we describe the image in the prompt
|
||||
// The agent will use its vision capabilities
|
||||
const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/300px-PNG_transparency_demonstration_1.png';
|
||||
|
||||
console.log('Analyzing image...\n');
|
||||
console.log('Image URL:', imageUrl, '\n');
|
||||
|
||||
// Ask the agent to analyze (in production, use multimodal message format)
|
||||
const response = await agent.chat(
|
||||
`Please analyze this image: ${imageUrl}\nDescribe what you see in detail.`
|
||||
);
|
||||
|
||||
console.log('Analysis:', response);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,50 @@
|
||||
# Observability Examples
|
||||
|
||||
Examples demonstrating how to use observability integrations with PraisonAI TypeScript.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd /path/to/praisonai-ts
|
||||
pnpm install
|
||||
|
||||
# Set API keys for observability tools
|
||||
export LANGFUSE_SECRET_KEY=sk-...
|
||||
export LANGFUSE_PUBLIC_KEY=pk-...
|
||||
# Or use built-in tools (no API key needed)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic-tracing.ts` | Basic tracing with memory adapter |
|
||||
| `langfuse-integration.ts` | Langfuse observability integration |
|
||||
| `multi-agent-attribution.ts` | Track agent_id/run_id across agents |
|
||||
| `switchable-tools.ts` | Switch between observability tools via env |
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Run with ts-node or tsx
|
||||
npx tsx examples/js/observability/basic-tracing.ts
|
||||
npx tsx examples/js/observability/multi-agent-attribution.ts
|
||||
|
||||
# With Langfuse (requires API keys)
|
||||
LANGFUSE_SECRET_KEY=sk-... npx tsx examples/js/observability/langfuse-integration.ts
|
||||
```
|
||||
|
||||
## Supported Tools
|
||||
|
||||
| Tool | Env Variable | Package |
|
||||
|------|--------------|---------|
|
||||
| Langfuse | `LANGFUSE_SECRET_KEY` | `langfuse` |
|
||||
| LangSmith | `LANGCHAIN_API_KEY` | `langsmith` |
|
||||
| LangWatch | `LANGWATCH_API_KEY` | `langwatch` |
|
||||
| Arize | `ARIZE_API_KEY` | `arize-phoenix` |
|
||||
| Axiom | `AXIOM_TOKEN` | `@axiomhq/js` |
|
||||
| Braintrust | `BRAINTRUST_API_KEY` | `braintrust` |
|
||||
| Helicone | `HELICONE_API_KEY` | `@helicone/helicone` |
|
||||
| Console | (none) | built-in |
|
||||
| Memory | (none) | built-in |
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Basic Tracing Example
|
||||
*
|
||||
* Demonstrates basic observability with the built-in memory adapter.
|
||||
* No external API keys required.
|
||||
*/
|
||||
|
||||
import { Agent, MemoryObservabilityAdapter, setObservabilityAdapter } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create memory adapter (stores traces in memory)
|
||||
const obs = new MemoryObservabilityAdapter();
|
||||
setObservabilityAdapter(obs);
|
||||
|
||||
// Create agent with observability enabled
|
||||
const agent = new Agent({
|
||||
name: 'TracedAgent',
|
||||
instructions: 'You are a helpful assistant. Answer questions concisely.'
|
||||
});
|
||||
|
||||
console.log('Running agent with tracing...\n');
|
||||
|
||||
// Run some interactions
|
||||
await agent.chat('What is 2 + 2?');
|
||||
await agent.chat('What is the capital of France?');
|
||||
|
||||
// View traces
|
||||
const traces = obs.getAllTraces();
|
||||
|
||||
console.log('\n=== Trace Summary ===');
|
||||
console.log(`Total traces: ${traces.length}`);
|
||||
|
||||
for (const trace of traces) {
|
||||
console.log(`\nTrace: ${trace.name}`);
|
||||
console.log(` Status: ${trace.status}`);
|
||||
console.log(` Spans: ${trace.spans.length}`);
|
||||
|
||||
for (const span of trace.spans) {
|
||||
const duration = span.endTime ? span.endTime - span.startTime : 'running';
|
||||
console.log(` - ${span.kind}: ${span.name} (${duration}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Multi-Agent Attribution Example
|
||||
*
|
||||
* Demonstrates tracking agent_id, run_id, and trace_id across
|
||||
* multi-agent workflows for proper attribution.
|
||||
*/
|
||||
|
||||
import { Agent, Agents, MemoryObservabilityAdapter, setObservabilityAdapter } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Create memory adapter
|
||||
const obs = new MemoryObservabilityAdapter();
|
||||
setObservabilityAdapter(obs);
|
||||
|
||||
// Create agents
|
||||
const researcher = new Agent({
|
||||
name: 'Researcher',
|
||||
instructions: 'Research topics and gather information.'
|
||||
});
|
||||
|
||||
const writer = new Agent({
|
||||
name: 'Writer',
|
||||
instructions: 'Write clear summaries based on research.'
|
||||
});
|
||||
|
||||
// Create multi-agent workflow
|
||||
const agents = new Agents({
|
||||
agents: [researcher, writer],
|
||||
tasks: [
|
||||
{ agent: researcher, description: 'Research: {topic}' },
|
||||
{ agent: writer, description: 'Summarize the research findings' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Running multi-agent workflow with attribution...\n');
|
||||
|
||||
await agents.start({ topic: 'TypeScript best practices' });
|
||||
|
||||
// View traces with attribution
|
||||
const traces = obs.getAllTraces();
|
||||
|
||||
console.log('\n=== Attribution Report ===');
|
||||
for (const trace of traces) {
|
||||
console.log(`\nTrace ID: ${trace.id}`);
|
||||
console.log(` Name: ${trace.name}`);
|
||||
|
||||
if (trace.attribution) {
|
||||
console.log(` Agent ID: ${trace.attribution.agentId || 'N/A'}`);
|
||||
console.log(` Run ID: ${trace.attribution.runId || 'N/A'}`);
|
||||
console.log(` Session ID: ${trace.attribution.sessionId || 'N/A'}`);
|
||||
}
|
||||
|
||||
console.log(` Spans: ${trace.spans.length}`);
|
||||
for (const span of trace.spans) {
|
||||
console.log(` - [${span.kind}] ${span.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Switchable Observability Tools Example
|
||||
*
|
||||
* Demonstrates switching between observability tools via environment variable.
|
||||
* Set OBS_TOOL env var to: langfuse, langsmith, console, memory, noop
|
||||
*
|
||||
* Usage:
|
||||
* OBS_TOOL=console npx tsx switchable-tools.ts
|
||||
* OBS_TOOL=langfuse LANGFUSE_SECRET_KEY=sk-... npx tsx switchable-tools.ts
|
||||
*/
|
||||
|
||||
import { Agent, createObservabilityAdapter, setObservabilityAdapter } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Get tool from environment (default to memory)
|
||||
const toolName = process.env.OBS_TOOL || 'memory';
|
||||
|
||||
console.log(`Using observability tool: ${toolName}\n`);
|
||||
|
||||
// Create adapter dynamically
|
||||
const obs = await createObservabilityAdapter(toolName as any);
|
||||
setObservabilityAdapter(obs);
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
name: 'ObservedAgent',
|
||||
instructions: 'You are a helpful assistant.'
|
||||
});
|
||||
|
||||
// Run interaction
|
||||
console.log('Running agent...\n');
|
||||
const response = await agent.chat('Hello! What is 2 + 2?');
|
||||
console.log('Response:', response);
|
||||
|
||||
// Flush traces to backend
|
||||
await obs.flush();
|
||||
console.log('\nTraces flushed to', toolName);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
# Provider Examples
|
||||
|
||||
Examples demonstrating how to use different LLM providers with PraisonAI TypeScript.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd /path/to/praisonai-ts
|
||||
pnpm install
|
||||
|
||||
# Set API keys
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export GOOGLE_API_KEY=AIza...
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `multi-provider.ts` | Use multiple providers in one workflow |
|
||||
| `openai-compatible.ts` | Use OpenAI-compatible APIs (LM Studio, etc.) |
|
||||
| `local-ollama.ts` | Use local Ollama models |
|
||||
| `provider-selection.ts` | Dynamic provider selection based on task |
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Run with ts-node or tsx
|
||||
npx tsx examples/js/providers/multi-provider.ts
|
||||
npx tsx examples/js/providers/openai-compatible.ts
|
||||
npx tsx examples/js/providers/local-ollama.ts
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Provider | Variable | Example |
|
||||
|----------|----------|---------|
|
||||
| OpenAI | `OPENAI_API_KEY` | `sk-...` |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `sk-ant-...` |
|
||||
| Google | `GOOGLE_API_KEY` | `AIza...` |
|
||||
| xAI | `XAI_API_KEY` | `xai-...` |
|
||||
| Groq | `GROQ_API_KEY` | `gsk_...` |
|
||||
| Mistral | `MISTRAL_API_KEY` | `...` |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` | `...` |
|
||||
| Ollama | `OLLAMA_BASE_URL` | `http://localhost:11434` |
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Local Ollama Provider Example
|
||||
*
|
||||
* Demonstrates using local Ollama models with PraisonAI.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Install Ollama: https://ollama.ai
|
||||
* 2. Pull a model: ollama pull llama3.2
|
||||
* 3. Run this example
|
||||
*
|
||||
* Optional env var:
|
||||
* - OLLAMA_BASE_URL (default: http://localhost:11434)
|
||||
*/
|
||||
|
||||
import { Agent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Ollama with llama3.2
|
||||
const ollamaAgent = new Agent({
|
||||
name: 'LocalLlama',
|
||||
instructions: 'You are a helpful local AI assistant running on Ollama.',
|
||||
llm: 'ollama/llama3.2',
|
||||
llmConfig: {
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Testing Ollama local model...\n');
|
||||
|
||||
try {
|
||||
const response = await ollamaAgent.chat('Hello! What model are you running?');
|
||||
console.log('Response:', response);
|
||||
} catch (error: any) {
|
||||
console.log('Note: Ollama not running or model not pulled.');
|
||||
console.log('Start Ollama and run: ollama pull llama3.2');
|
||||
console.log('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Multi-Provider Example
|
||||
*
|
||||
* Demonstrates using multiple LLM providers in a single workflow.
|
||||
* Each agent uses a different provider optimized for its task.
|
||||
*
|
||||
* Required env vars:
|
||||
* - OPENAI_API_KEY
|
||||
* - ANTHROPIC_API_KEY
|
||||
* - GOOGLE_API_KEY
|
||||
*/
|
||||
|
||||
import { Agent, Agents } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// Fast model for quick categorization
|
||||
const triageAgent = new Agent({
|
||||
name: 'Triage',
|
||||
instructions: 'Quickly categorize the user request into: technical, creative, or general.',
|
||||
llm: 'openai/gpt-4o-mini' // Fast and cheap
|
||||
});
|
||||
|
||||
// Powerful model for complex reasoning
|
||||
const analysisAgent = new Agent({
|
||||
name: 'Analyst',
|
||||
instructions: 'Analyze the request in depth and provide detailed insights.',
|
||||
llm: 'anthropic/claude-3-5-sonnet' // Best reasoning
|
||||
});
|
||||
|
||||
// Creative model for content generation
|
||||
const writerAgent = new Agent({
|
||||
name: 'Writer',
|
||||
instructions: 'Write a clear, engaging response based on the analysis.',
|
||||
llm: 'google/gemini-2.0-flash' // Good for creative tasks
|
||||
});
|
||||
|
||||
// Create multi-agent workflow
|
||||
const agents = new Agents({
|
||||
agents: [triageAgent, analysisAgent, writerAgent],
|
||||
tasks: [
|
||||
{ agent: triageAgent, description: 'Categorize: {input}' },
|
||||
{ agent: analysisAgent, description: 'Analyze the categorized request' },
|
||||
{ agent: writerAgent, description: 'Write the final response' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Starting multi-provider workflow...\n');
|
||||
|
||||
const result = await agents.start({
|
||||
input: 'Explain how neural networks learn'
|
||||
});
|
||||
|
||||
console.log('\n=== Final Result ===');
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* OpenAI-Compatible Provider Example
|
||||
*
|
||||
* Demonstrates using any OpenAI-compatible API endpoint.
|
||||
* Works with LM Studio, vLLM, LocalAI, and other compatible servers.
|
||||
*
|
||||
* Required env vars:
|
||||
* - OPENAI_COMPATIBLE_BASE_URL (e.g., http://localhost:1234/v1)
|
||||
* - OPENAI_COMPATIBLE_API_KEY (optional, use 'not-needed' for local)
|
||||
*/
|
||||
|
||||
import { Agent } from 'praisonai';
|
||||
|
||||
async function main() {
|
||||
// LM Studio example (local)
|
||||
const lmStudioAgent = new Agent({
|
||||
name: 'LocalAssistant',
|
||||
instructions: 'You are a helpful local AI assistant.',
|
||||
llm: 'openai-compatible/local-model',
|
||||
llmConfig: {
|
||||
baseUrl: process.env.LM_STUDIO_BASE_URL || 'http://localhost:1234/v1',
|
||||
apiKey: 'not-needed'
|
||||
}
|
||||
});
|
||||
|
||||
// Custom OpenAI-compatible endpoint
|
||||
const customAgent = new Agent({
|
||||
name: 'CustomAssistant',
|
||||
instructions: 'You are a helpful assistant.',
|
||||
llm: 'openai-compatible/my-model',
|
||||
llmConfig: {
|
||||
baseUrl: process.env.OPENAI_COMPATIBLE_BASE_URL || 'http://localhost:8000/v1',
|
||||
apiKey: process.env.OPENAI_COMPATIBLE_API_KEY || 'not-needed'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Testing OpenAI-compatible endpoint...\n');
|
||||
|
||||
try {
|
||||
const response = await lmStudioAgent.chat('Hello! What model are you?');
|
||||
console.log('Response:', response);
|
||||
} catch (error: any) {
|
||||
console.log('Note: LM Studio not running. Start it with a model loaded.');
|
||||
console.log('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Basic RAG Agent Example
|
||||
*
|
||||
* Demonstrates building a RAG agent with context injection.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai-ts
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*
|
||||
* Run:
|
||||
* npx ts-node basic-rag.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
|
||||
// Simulated knowledge base (in production, use a vector store)
|
||||
const knowledgeBase = [
|
||||
{ id: '1', content: 'PraisonAI is an AI agent framework for building intelligent applications.' },
|
||||
{ id: '2', content: 'Agents can use tools to accomplish tasks like web search and calculations.' },
|
||||
{ id: '3', content: 'RAG combines retrieval with generation for accurate, grounded responses.' },
|
||||
];
|
||||
|
||||
// Simple search function (in production, use vector similarity)
|
||||
function searchKnowledge(query: string): string[] {
|
||||
const queryLower = query.toLowerCase();
|
||||
return knowledgeBase
|
||||
.filter(doc => doc.content.toLowerCase().includes(queryLower.split(' ')[0]))
|
||||
.map(doc => doc.content);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Basic RAG Agent Example ===\n');
|
||||
|
||||
// Create an agent with RAG-style instructions
|
||||
const agent = new Agent({
|
||||
name: 'RAGAgent',
|
||||
instructions: `You are a helpful assistant that answers questions using provided context.
|
||||
Always base your answers on the context provided.
|
||||
If the context doesn't contain relevant information, say so.`,
|
||||
});
|
||||
|
||||
// Query with context injection
|
||||
const query = 'What is PraisonAI?';
|
||||
console.log(`Q: ${query}`);
|
||||
|
||||
// Retrieve relevant context
|
||||
const context = searchKnowledge(query).join('\n\n');
|
||||
|
||||
// Ask the agent with context
|
||||
const response = await agent.chat(
|
||||
`Context:\n${context}\n\nQuestion: ${query}`
|
||||
);
|
||||
|
||||
console.log(`A: ${response}\n`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Express Server Adapter Example
|
||||
*
|
||||
* Demonstrates deploying an AI agent as an Express HTTP server.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai-ts express
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*
|
||||
* Run:
|
||||
* npx ts-node express-server.ts
|
||||
*
|
||||
* Test:
|
||||
* curl -X POST http://localhost:3000/api/chat \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{"message": "Hello!"}'
|
||||
*/
|
||||
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
|
||||
// Simple HTTP server without Express dependency for this example
|
||||
import http from 'http';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Express Server Adapter Example ===\n');
|
||||
|
||||
// Create an agent
|
||||
const agent = new Agent({
|
||||
name: 'APIAgent',
|
||||
instructions: 'You are a helpful API assistant. Keep responses concise.',
|
||||
});
|
||||
|
||||
// Create HTTP server
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/api/chat') {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { message } = JSON.parse(body);
|
||||
const response = await agent.chat(message);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ response }));
|
||||
} catch (error: any) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
console.log('\nTest with:');
|
||||
console.log(` curl -X POST http://localhost:${PORT}/api/chat \\`);
|
||||
console.log(' -H "Content-Type: application/json" \\');
|
||||
console.log(' -d \'{"message": "Hello!"}\'');
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Isolated Registry Example - Multi-Agent Safety
|
||||
|
||||
This example demonstrates how to use isolated registries to prevent
|
||||
provider registration collisions in multi-agent scenarios.
|
||||
|
||||
Key concepts:
|
||||
- Each agent/context can have its own registry
|
||||
- No global state mutation
|
||||
- Safe for concurrent/parallel runs
|
||||
- Different agents can use different provider implementations
|
||||
|
||||
Requirements:
|
||||
- pip install praisonai
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '../../src/praisonai')
|
||||
|
||||
from praisonai.llm import (
|
||||
LLMProviderRegistry,
|
||||
create_llm_provider,
|
||||
register_llm_provider,
|
||||
get_default_llm_registry,
|
||||
_reset_default_registry
|
||||
)
|
||||
|
||||
|
||||
# Define two different provider implementations with the same name
|
||||
class Agent1Provider:
|
||||
"""Provider implementation for Agent 1."""
|
||||
provider_id = "custom"
|
||||
|
||||
def __init__(self, model_id, config=None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
self.agent_name = "Agent1"
|
||||
|
||||
def generate(self, prompt):
|
||||
return f"[{self.agent_name}] Response to: {prompt}"
|
||||
|
||||
|
||||
class Agent2Provider:
|
||||
"""Provider implementation for Agent 2 - different behavior."""
|
||||
provider_id = "custom"
|
||||
|
||||
def __init__(self, model_id, config=None):
|
||||
self.model_id = model_id
|
||||
self.config = config or {}
|
||||
self.agent_name = "Agent2"
|
||||
|
||||
def generate(self, prompt):
|
||||
return f"[{self.agent_name}] Different response to: {prompt}"
|
||||
|
||||
|
||||
def demonstrate_collision_problem():
|
||||
"""Show what happens with global registry collisions."""
|
||||
print("=== Problem: Global Registry Collision ===\n")
|
||||
|
||||
# Reset to clean state
|
||||
_reset_default_registry()
|
||||
|
||||
# Agent 1 registers "custom" provider
|
||||
register_llm_provider("custom", Agent1Provider)
|
||||
print("Agent 1 registered 'custom' provider")
|
||||
|
||||
# Agent 2 tries to register same name - ERROR!
|
||||
try:
|
||||
register_llm_provider("custom", Agent2Provider)
|
||||
print("Agent 2 registered 'custom' provider")
|
||||
except ValueError as e:
|
||||
print(f"Agent 2 FAILED: {e}")
|
||||
|
||||
print()
|
||||
print("Problem: Both agents want to use 'custom' but with different implementations.")
|
||||
print("Solution: Use isolated registries.\n")
|
||||
|
||||
|
||||
def demonstrate_isolated_solution():
|
||||
"""Show how isolated registries solve the collision problem."""
|
||||
print("=== Solution: Isolated Registries ===\n")
|
||||
|
||||
# Create isolated registries for each agent
|
||||
agent1_registry = LLMProviderRegistry()
|
||||
agent2_registry = LLMProviderRegistry()
|
||||
|
||||
# Each agent registers its own "custom" provider
|
||||
agent1_registry.register("custom", Agent1Provider)
|
||||
agent2_registry.register("custom", Agent2Provider)
|
||||
|
||||
print("Agent 1 registry providers:", agent1_registry.list())
|
||||
print("Agent 2 registry providers:", agent2_registry.list())
|
||||
print()
|
||||
|
||||
# Create providers from each registry
|
||||
provider1 = create_llm_provider("custom/model-v1", registry=agent1_registry)
|
||||
provider2 = create_llm_provider("custom/model-v1", registry=agent2_registry)
|
||||
|
||||
# They have different implementations!
|
||||
print("Provider 1 response:", provider1.generate("Hello"))
|
||||
print("Provider 2 response:", provider2.generate("Hello"))
|
||||
print()
|
||||
|
||||
# Verify isolation
|
||||
print("Verification:")
|
||||
print(f" provider1.agent_name = {provider1.agent_name}")
|
||||
print(f" provider2.agent_name = {provider2.agent_name}")
|
||||
print(f" Same class? {type(provider1) == type(provider2)}")
|
||||
print()
|
||||
|
||||
|
||||
def demonstrate_parallel_agents():
|
||||
"""Simulate parallel agent execution with isolated registries."""
|
||||
print("=== Parallel Agent Simulation ===\n")
|
||||
|
||||
def run_agent(agent_id, registry):
|
||||
"""Simulate an agent run with its own registry."""
|
||||
provider = create_llm_provider("custom/model", registry=registry)
|
||||
result = provider.generate(f"Query from agent {agent_id}")
|
||||
return result
|
||||
|
||||
# Setup registries
|
||||
registries = {}
|
||||
for i in range(3):
|
||||
reg = LLMProviderRegistry()
|
||||
# Each agent gets a slightly different provider
|
||||
class DynamicProvider:
|
||||
provider_id = "custom"
|
||||
def __init__(self, model_id, config=None):
|
||||
self.model_id = model_id
|
||||
self.agent_id = i # Capture agent ID
|
||||
def generate(self, prompt):
|
||||
return f"Agent-{self.agent_id} processed: {prompt}"
|
||||
|
||||
reg.register("custom", DynamicProvider)
|
||||
registries[i] = reg
|
||||
|
||||
# Simulate parallel execution
|
||||
print("Simulating parallel agent runs:")
|
||||
for agent_id, registry in registries.items():
|
||||
result = run_agent(agent_id, registry)
|
||||
print(f" {result}")
|
||||
|
||||
print()
|
||||
print("Each agent used its own isolated registry - no collisions!")
|
||||
print()
|
||||
|
||||
|
||||
def demonstrate_shared_vs_isolated():
|
||||
"""Compare shared (global) vs isolated registry patterns."""
|
||||
print("=== Shared vs Isolated Patterns ===\n")
|
||||
|
||||
# Reset global registry
|
||||
_reset_default_registry()
|
||||
|
||||
# Pattern 1: Shared (global) registry
|
||||
print("Pattern 1: Shared Registry (use for app-wide providers)")
|
||||
register_llm_provider("shared-provider", Agent1Provider)
|
||||
|
||||
# All code sees the same provider
|
||||
global_reg = get_default_llm_registry()
|
||||
print(f" Global registry has 'shared-provider': {global_reg.has('shared-provider')}")
|
||||
|
||||
# Pattern 2: Isolated registry
|
||||
print("\nPattern 2: Isolated Registry (use for per-agent/per-run providers)")
|
||||
isolated = LLMProviderRegistry()
|
||||
isolated.register("isolated-provider", Agent2Provider)
|
||||
|
||||
# Only code with reference to this registry sees it
|
||||
print(f" Isolated registry has 'isolated-provider': {isolated.has('isolated-provider')}")
|
||||
print(f" Global registry has 'isolated-provider': {global_reg.has('isolated-provider')}")
|
||||
|
||||
print()
|
||||
print("Recommendation:")
|
||||
print(" - Use global registry for app-wide, stable providers")
|
||||
print(" - Use isolated registries for per-agent or per-run customization")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Isolated Registry Example - Multi-Agent Safety")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
demonstrate_collision_problem()
|
||||
demonstrate_isolated_solution()
|
||||
demonstrate_parallel_agents()
|
||||
demonstrate_shared_vs_isolated()
|
||||
|
||||
print("=" * 60)
|
||||
print("Example Complete")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Exa Web Search Tool Example
|
||||
*
|
||||
* Demonstrates semantic web search with Exa AI.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai-ts @exalabs/ai-sdk
|
||||
* export EXA_API_KEY=your-api-key
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*/
|
||||
|
||||
// For local development, use relative imports
|
||||
// For production, use: import { Agent, exaSearch } from 'praisonai-ts'
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
import { exaSearch } from '../../../src/praisonai-ts/src/tools/builtins';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Exa Web Search Tool Example ===\n');
|
||||
|
||||
// Example 1: Basic Semantic Search
|
||||
console.log('1. Basic Semantic Search');
|
||||
console.log('------------------------');
|
||||
|
||||
const searchAgent = new Agent({
|
||||
name: 'SemanticSearcher',
|
||||
instructions: 'You search the web using semantic search and provide detailed summaries.',
|
||||
tools: [exaSearch({ numResults: 5, type: 'auto' })],
|
||||
});
|
||||
|
||||
const searchResult = await searchAgent.chat('Find the latest AI research breakthroughs');
|
||||
console.log('Search Result:', searchResult);
|
||||
console.log();
|
||||
|
||||
// Example 2: Company Research
|
||||
console.log('2. Company Research');
|
||||
console.log('-------------------');
|
||||
|
||||
const companyAgent = new Agent({
|
||||
name: 'CompanyResearcher',
|
||||
instructions: 'You research companies and provide analysis.',
|
||||
tools: [
|
||||
exaSearch({
|
||||
type: 'auto',
|
||||
numResults: 5,
|
||||
category: 'company',
|
||||
contents: {
|
||||
text: { maxCharacters: 1000 },
|
||||
summary: true,
|
||||
livecrawl: 'preferred',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const companyResult = await companyAgent.chat(
|
||||
'Find AI startups in Europe founded after 2020'
|
||||
);
|
||||
console.log('Company Result:', companyResult);
|
||||
console.log();
|
||||
|
||||
// Example 3: Academic Paper Search
|
||||
console.log('3. Academic Paper Search');
|
||||
console.log('------------------------');
|
||||
|
||||
const academicAgent = new Agent({
|
||||
name: 'AcademicResearcher',
|
||||
instructions: 'You search for academic papers and summarize findings.',
|
||||
tools: [
|
||||
exaSearch({
|
||||
type: 'neural',
|
||||
category: 'research paper',
|
||||
numResults: 5,
|
||||
includeDomains: ['arxiv.org', 'nature.com', 'science.org'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const academicResult = await academicAgent.chat(
|
||||
'Find recent papers on transformer architectures and attention mechanisms'
|
||||
);
|
||||
console.log('Academic Result:', academicResult);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Tavily Search Tool Example
|
||||
*
|
||||
* Demonstrates web search, content extraction, and crawling with Tavily.
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install praisonai @tavily/ai-sdk
|
||||
* export TAVILY_API_KEY=your-api-key
|
||||
* export OPENAI_API_KEY=your-api-key
|
||||
*/
|
||||
|
||||
// For local development, use relative imports
|
||||
// For production, use: import { Agent, tavilySearch } from 'praisonai-ts'
|
||||
import { Agent } from '../../../src/praisonai-ts/src';
|
||||
import { tavilySearch, tavilyExtract, tavilyCrawl } from '../../../src/praisonai-ts/src/tools/builtins';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Tavily Search Tool Example ===\n');
|
||||
|
||||
// Example 1: Basic Web Search
|
||||
console.log('1. Basic Web Search');
|
||||
console.log('-------------------');
|
||||
|
||||
const searchAgent = new Agent({
|
||||
name: 'WebSearcher',
|
||||
instructions: 'You search the web for information and provide concise summaries with sources.',
|
||||
tools: [tavilySearch({ maxResults: 5, includeAnswer: true })],
|
||||
});
|
||||
|
||||
const searchResult = await searchAgent.chat('What are the latest developments in AI agents?');
|
||||
console.log('Search Result:', searchResult);
|
||||
console.log();
|
||||
|
||||
// Example 2: News Search with Time Filter
|
||||
console.log('2. News Search with Time Filter');
|
||||
console.log('-------------------------------');
|
||||
|
||||
const newsAgent = new Agent({
|
||||
name: 'NewsSearcher',
|
||||
instructions: 'You search for recent news and summarize key points.',
|
||||
tools: [
|
||||
tavilySearch({
|
||||
topic: 'news',
|
||||
timeRange: 'week',
|
||||
maxResults: 5,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const newsResult = await newsAgent.chat('Latest news about OpenAI');
|
||||
console.log('News Result:', newsResult);
|
||||
console.log();
|
||||
|
||||
// Example 3: Content Extraction
|
||||
console.log('3. Content Extraction');
|
||||
console.log('--------------------');
|
||||
|
||||
const extractAgent = new Agent({
|
||||
name: 'ContentExtractor',
|
||||
instructions: 'You extract and summarize content from web pages.',
|
||||
tools: [tavilyExtract()],
|
||||
});
|
||||
|
||||
const extractResult = await extractAgent.chat(
|
||||
'Extract the main content from https://docs.tavily.com and summarize it'
|
||||
);
|
||||
console.log('Extract Result:', extractResult);
|
||||
console.log();
|
||||
|
||||
// Example 4: Website Crawling
|
||||
console.log('4. Website Crawling');
|
||||
console.log('------------------');
|
||||
|
||||
const crawlAgent = new Agent({
|
||||
name: 'WebCrawler',
|
||||
instructions: 'You crawl websites and analyze their structure and content.',
|
||||
tools: [tavilyCrawl({ maxDepth: 2 })],
|
||||
});
|
||||
|
||||
const crawlResult = await crawlAgent.chat(
|
||||
'Crawl https://docs.tavily.com and list the main documentation sections'
|
||||
);
|
||||
console.log('Crawl Result:', crawlResult);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -11,8 +11,15 @@ import platform
|
||||
from typing import Any, List, Optional, Callable, Iterable, Union
|
||||
from functools import wraps, partial
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
ClientSession = None
|
||||
StdioServerParameters = None
|
||||
stdio_client = None
|
||||
|
||||
class MCPToolRunner(threading.Thread):
|
||||
"""A dedicated thread for running MCP operations."""
|
||||
@@ -196,10 +203,17 @@ class MCP:
|
||||
debug: Enable debug logging for MCP operations (default: False)
|
||||
**kwargs: Additional parameters for StdioServerParameters
|
||||
"""
|
||||
# Check if MCP is available
|
||||
if not MCP_AVAILABLE:
|
||||
raise ImportError(
|
||||
"MCP (Model Context Protocol) package is not installed. "
|
||||
"Install it with: pip install praisonaiagents[mcp]"
|
||||
)
|
||||
|
||||
# Handle backward compatibility with named parameter 'command'
|
||||
if command_or_string is None and command is not None:
|
||||
command_or_string = command
|
||||
|
||||
|
||||
# Set up logging - default to WARNING level to hide INFO messages
|
||||
if debug:
|
||||
logging.getLogger("mcp-wrapper").setLevel(logging.DEBUG)
|
||||
|
||||
@@ -16,11 +16,17 @@ import weakref
|
||||
from typing import List, Dict, Any, Optional, Callable, Iterable, Union
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from mcp import ClientSession
|
||||
try:
|
||||
from mcp import ClientSession
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
ClientSession = None
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
raise ImportError("aiohttp is required for HTTP Stream transport. Install with: pip install praisonaiagents[mcp]")
|
||||
aiohttp = None
|
||||
|
||||
logger = logging.getLogger("mcp-http-stream")
|
||||
|
||||
@@ -507,9 +513,23 @@ class HTTPStreamMCPClient:
|
||||
timeout: Timeout in seconds for operations (default: 60)
|
||||
options: Additional configuration options for the transport
|
||||
"""
|
||||
# Check if MCP is available
|
||||
if not MCP_AVAILABLE:
|
||||
raise ImportError(
|
||||
"MCP (Model Context Protocol) package is not installed. "
|
||||
"Install it with: pip install praisonaiagents[mcp]"
|
||||
)
|
||||
|
||||
# Check if aiohttp is available
|
||||
if aiohttp is None:
|
||||
raise ImportError(
|
||||
"aiohttp is required for HTTP Stream transport. "
|
||||
"Install it with: pip install praisonaiagents[mcp]"
|
||||
)
|
||||
|
||||
# Parse URL to extract base URL and endpoint
|
||||
parsed = urlparse(server_url)
|
||||
|
||||
|
||||
# If the URL already has a path, use it; otherwise use default /mcp endpoint
|
||||
if parsed.path and parsed.path != '/':
|
||||
self.base_url = server_url
|
||||
|
||||
@@ -11,8 +11,14 @@ import inspect
|
||||
import json
|
||||
from typing import List, Dict, Any, Optional, Callable, Iterable
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.sse import sse_client
|
||||
try:
|
||||
from mcp import ClientSession
|
||||
from mcp.client.sse import sse_client
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
ClientSession = None
|
||||
sse_client = None
|
||||
|
||||
logger = logging.getLogger("mcp-sse")
|
||||
|
||||
@@ -169,12 +175,19 @@ class SSEMCPClient:
|
||||
debug: Whether to enable debug logging
|
||||
timeout: Timeout in seconds for operations (default: 60)
|
||||
"""
|
||||
# Check if MCP is available
|
||||
if not MCP_AVAILABLE:
|
||||
raise ImportError(
|
||||
"MCP (Model Context Protocol) package is not installed. "
|
||||
"Install it with: pip install praisonaiagents[mcp]"
|
||||
)
|
||||
|
||||
self.server_url = server_url
|
||||
self.debug = debug
|
||||
self.timeout = timeout
|
||||
self.session = None
|
||||
self.tools = []
|
||||
|
||||
|
||||
# Set up logging
|
||||
if debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Test that MCP module imports are optional and don't break basic functionality.
|
||||
This test verifies the fix for issue #1091.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestMCPOptionalImport:
|
||||
"""Test that MCP is properly optional."""
|
||||
|
||||
def test_praisonaiagents_import_without_mcp(self):
|
||||
"""Test that praisonaiagents can be imported without mcp installed."""
|
||||
# This should not raise any ImportError
|
||||
import praisonaiagents
|
||||
assert praisonaiagents is not None
|
||||
|
||||
def test_agent_import_without_mcp(self):
|
||||
"""Test that Agent can be imported without mcp installed."""
|
||||
from praisonaiagents import Agent
|
||||
assert Agent is not None
|
||||
|
||||
def test_mcp_lazy_loading(self):
|
||||
"""Test that MCP is lazily loaded."""
|
||||
from praisonaiagents import MCP # noqa: F401
|
||||
# MCP should either be available or None (not raise ImportError on access)
|
||||
# The actual ImportError should only be raised when trying to instantiate
|
||||
# if mcp package is not installed
|
||||
|
||||
def test_mcp_module_import_no_crash(self):
|
||||
"""Test that mcp module can be imported without crashing."""
|
||||
# This tests that the try/except in mcp.py works correctly
|
||||
try:
|
||||
from praisonaiagents.mcp import mcp as mcp_module
|
||||
assert hasattr(mcp_module, 'MCP_AVAILABLE')
|
||||
except ImportError:
|
||||
# If import fails, it should be because of missing mcp,
|
||||
# not because of a syntax error
|
||||
pass
|
||||
|
||||
def test_mcp_sse_module_import_no_crash(self):
|
||||
"""Test that mcp_sse module can be imported without crashing."""
|
||||
try:
|
||||
from praisonaiagents.mcp import mcp_sse
|
||||
assert hasattr(mcp_sse, 'MCP_AVAILABLE')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def test_mcp_http_stream_module_import_no_crash(self):
|
||||
"""Test that mcp_http_stream module can be imported without crashing."""
|
||||
try:
|
||||
from praisonaiagents.mcp import mcp_http_stream
|
||||
assert hasattr(mcp_http_stream, 'MCP_AVAILABLE')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def test_basic_agent_creation_without_mcp_tools(self):
|
||||
"""Test that agents can be created without MCP tools."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Creating an agent without MCP tools should work
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
instructions="You are a test agent.",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
assert agent is not None
|
||||
|
||||
def test_mcp_raises_import_error_when_unavailable(self):
|
||||
"""Test that MCP raises ImportError with install instructions when mcp package unavailable."""
|
||||
import praisonaiagents.mcp.mcp as mcp_module
|
||||
|
||||
# Save original value
|
||||
original_available = mcp_module.MCP_AVAILABLE
|
||||
|
||||
# Simulate MCP not installed
|
||||
mcp_module.MCP_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp import MCP
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
MCP('test-command')
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert 'pip install praisonaiagents[mcp]' in error_msg
|
||||
assert 'MCP' in error_msg or 'Model Context Protocol' in error_msg
|
||||
finally:
|
||||
# Restore original value
|
||||
mcp_module.MCP_AVAILABLE = original_available
|
||||
|
||||
def test_sse_client_raises_import_error_when_unavailable(self):
|
||||
"""Test that SSEMCPClient raises ImportError with install instructions when mcp unavailable."""
|
||||
import praisonaiagents.mcp.mcp_sse as sse_module
|
||||
|
||||
original_available = sse_module.MCP_AVAILABLE
|
||||
sse_module.MCP_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp_sse import SSEMCPClient
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
SSEMCPClient('http://test.com/sse')
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert 'pip install praisonaiagents[mcp]' in error_msg
|
||||
finally:
|
||||
sse_module.MCP_AVAILABLE = original_available
|
||||
|
||||
def test_http_stream_client_raises_import_error_when_mcp_unavailable(self):
|
||||
"""Test that HTTPStreamMCPClient raises ImportError when mcp unavailable."""
|
||||
import praisonaiagents.mcp.mcp_http_stream as http_module
|
||||
|
||||
original_available = http_module.MCP_AVAILABLE
|
||||
http_module.MCP_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp_http_stream import HTTPStreamMCPClient
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
HTTPStreamMCPClient('http://test.com/mcp')
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert 'pip install praisonaiagents[mcp]' in error_msg
|
||||
finally:
|
||||
http_module.MCP_AVAILABLE = original_available
|
||||
|
||||
def test_http_stream_client_raises_import_error_when_aiohttp_unavailable(self):
|
||||
"""Test that HTTPStreamMCPClient raises ImportError when aiohttp unavailable."""
|
||||
import praisonaiagents.mcp.mcp_http_stream as http_module
|
||||
|
||||
# Ensure MCP is available but aiohttp is not
|
||||
original_available = http_module.MCP_AVAILABLE
|
||||
original_aiohttp = http_module.aiohttp
|
||||
|
||||
http_module.MCP_AVAILABLE = True
|
||||
http_module.aiohttp = None
|
||||
|
||||
try:
|
||||
from praisonaiagents.mcp.mcp_http_stream import HTTPStreamMCPClient
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
HTTPStreamMCPClient('http://test.com/mcp')
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert 'aiohttp' in error_msg.lower()
|
||||
assert 'pip install praisonaiagents[mcp]' in error_msg
|
||||
finally:
|
||||
http_module.MCP_AVAILABLE = original_available
|
||||
http_module.aiohttp = original_aiohttp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
{
|
||||
"version": "1.2.1",
|
||||
"lastUpdated": "2024-12-26",
|
||||
"implementationStatus": "Phase 4 Complete - Full Feature Parity",
|
||||
"version": "1.3.0",
|
||||
"lastUpdated": "2025-01-15",
|
||||
"implementationStatus": "Phase 5 Complete - AI SDK Wrapper + Integrations",
|
||||
"summary": {
|
||||
"pythonCoreFeatures": 85,
|
||||
"pythonWrapperFeatures": 45,
|
||||
"typescriptFeatures": 75,
|
||||
"gapCount": 10,
|
||||
"typescriptFeatures": 95,
|
||||
"gapCount": 5,
|
||||
"priorityP0": 0,
|
||||
"priorityP1": 3,
|
||||
"priorityP2": 5,
|
||||
"priorityP3": 2
|
||||
"priorityP1": 1,
|
||||
"priorityP2": 2,
|
||||
"priorityP3": 2,
|
||||
"newInV1_3": {
|
||||
"aiSdkWrappers": ["generateText", "streamText", "generateObject", "streamObject", "generateImage", "embed", "embedMany"],
|
||||
"serverAdapters": ["http", "express", "hono", "fastify", "nest"],
|
||||
"nextjsIntegration": ["createRouteHandler", "createPagesHandler"],
|
||||
"mcpClient": ["createMCP", "getMCPClient", "mcpToolsToAITools"],
|
||||
"integrations": ["createSlackBot", "createNLPostgres", "createComputerUse"],
|
||||
"agentLoop": ["createAgentLoop", "stopAfterSteps", "stopWhenNoToolCalls"],
|
||||
"multimodal": ["createImagePart", "createFilePart", "createPdfPart", "createMultimodalMessage"],
|
||||
"middleware": ["createCachingMiddleware", "createLoggingMiddleware", "wrapModel"]
|
||||
}
|
||||
},
|
||||
"pythonCoreSDK": {
|
||||
"path": "/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents",
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* AI SDK v6 Parity - Basic Usage Examples
|
||||
*
|
||||
* This example demonstrates the new AI SDK v6 compatible features in praisonai-ts.
|
||||
*/
|
||||
|
||||
import {
|
||||
// Core Agent
|
||||
Agent,
|
||||
// Tool Approval
|
||||
ApprovalManager,
|
||||
getApprovalManager,
|
||||
withApproval,
|
||||
createDangerousPatternChecker,
|
||||
// UI Messages
|
||||
UIMessage,
|
||||
convertToModelMessages,
|
||||
createTextMessage,
|
||||
toUIMessageStreamResponse,
|
||||
// Speech & Transcription
|
||||
generateSpeech,
|
||||
transcribe,
|
||||
// DevTools
|
||||
enableDevTools,
|
||||
isDevToolsEnabled,
|
||||
// Telemetry
|
||||
configureTelemetry,
|
||||
createAISpan,
|
||||
// MCP
|
||||
createMCP,
|
||||
} from '../../src';
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Tool with Approval
|
||||
// ============================================================================
|
||||
|
||||
async function toolApprovalExample() {
|
||||
console.log('\n=== Tool Approval Example ===\n');
|
||||
|
||||
// Create a tool that requires approval
|
||||
const deleteFile = withApproval({
|
||||
name: 'deleteFile',
|
||||
description: 'Delete a file from the filesystem',
|
||||
needsApproval: true, // Always require approval
|
||||
execute: async (input: { path: string }) => {
|
||||
console.log(`Would delete: ${input.path}`);
|
||||
return { success: true, path: input.path };
|
||||
},
|
||||
onDenied: (input) => {
|
||||
console.log(`Deletion denied for: ${input.path}`);
|
||||
return { success: false, path: input.path };
|
||||
},
|
||||
});
|
||||
|
||||
// Create a tool with conditional approval
|
||||
const runCommand = withApproval({
|
||||
name: 'runCommand',
|
||||
description: 'Run a shell command',
|
||||
needsApproval: createDangerousPatternChecker(), // Only approve dangerous commands
|
||||
execute: async (input: { command: string }) => {
|
||||
console.log(`Would run: ${input.command}`);
|
||||
return { output: 'command output' };
|
||||
},
|
||||
});
|
||||
|
||||
// Set up approval handler
|
||||
const manager = getApprovalManager();
|
||||
manager.onApprovalRequest(async (request) => {
|
||||
console.log(`\n🔐 Approval requested for: ${request.toolName}`);
|
||||
console.log(` Input: ${JSON.stringify(request.input)}`);
|
||||
// Auto-approve for demo
|
||||
return true;
|
||||
});
|
||||
|
||||
// Test the tools
|
||||
try {
|
||||
const result1 = await deleteFile({ path: '/tmp/test.txt' });
|
||||
console.log('Delete result:', result1);
|
||||
|
||||
const result2 = await runCommand({ command: 'ls -la' });
|
||||
console.log('Command result:', result2);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: UI Messages
|
||||
// ============================================================================
|
||||
|
||||
async function uiMessageExample() {
|
||||
console.log('\n=== UI Message Example ===\n');
|
||||
|
||||
// Create UI messages
|
||||
const messages: UIMessage[] = [
|
||||
createTextMessage('user', 'Hello, how are you?'),
|
||||
createTextMessage('assistant', 'I am doing well, thank you for asking!'),
|
||||
];
|
||||
|
||||
console.log('UI Messages:', JSON.stringify(messages, null, 2));
|
||||
|
||||
// Convert to model messages
|
||||
const modelMessages = await convertToModelMessages(messages);
|
||||
console.log('\nModel Messages:', JSON.stringify(modelMessages, null, 2));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: DevTools
|
||||
// ============================================================================
|
||||
|
||||
async function devToolsExample() {
|
||||
console.log('\n=== DevTools Example ===\n');
|
||||
|
||||
// Check if DevTools is enabled
|
||||
console.log('DevTools enabled:', isDevToolsEnabled());
|
||||
|
||||
// Enable DevTools (in development)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
await enableDevTools({
|
||||
port: 3001,
|
||||
projectName: 'PraisonAI Demo',
|
||||
});
|
||||
console.log('DevTools enabled:', isDevToolsEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Telemetry
|
||||
// ============================================================================
|
||||
|
||||
async function telemetryExample() {
|
||||
console.log('\n=== Telemetry Example ===\n');
|
||||
|
||||
// Configure telemetry
|
||||
configureTelemetry({
|
||||
isEnabled: true,
|
||||
functionId: 'demo-app',
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
environment: 'development',
|
||||
},
|
||||
});
|
||||
|
||||
// Create a span
|
||||
const span = createAISpan('generateText', {
|
||||
model: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
});
|
||||
|
||||
try {
|
||||
// Simulate some work
|
||||
console.log('Performing AI operation...');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
span.setAttribute('tokens', 150);
|
||||
span.setStatus({ code: 'ok' });
|
||||
} catch (error: any) {
|
||||
span.recordException(error);
|
||||
span.setStatus({ code: 'error', message: error.message });
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
|
||||
console.log('Telemetry span completed');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: MCP with HTTP Transport
|
||||
// ============================================================================
|
||||
|
||||
async function mcpExample() {
|
||||
console.log('\n=== MCP Example ===\n');
|
||||
|
||||
// Note: This requires an MCP server to be running
|
||||
console.log('MCP HTTP transport configuration example:');
|
||||
console.log(`
|
||||
const client = await createMCP({
|
||||
transport: {
|
||||
type: 'http',
|
||||
url: 'https://mcp-server.example.com/mcp',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
}
|
||||
});
|
||||
|
||||
const tools = await client.tools();
|
||||
const resources = await client.listResources();
|
||||
const prompts = await client.listPrompts();
|
||||
`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 AI SDK v6 Parity Examples\n');
|
||||
console.log('=' .repeat(50));
|
||||
|
||||
await toolApprovalExample();
|
||||
await uiMessageExample();
|
||||
await devToolsExample();
|
||||
await telemetryExample();
|
||||
await mcpExample();
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('✅ All examples completed!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* AI SDK Wrapper Demo
|
||||
*
|
||||
* Demonstrates the new AI SDK wrapper functions in praisonai-ts.
|
||||
* These wrappers provide a stable interface over the AI SDK primitives.
|
||||
*/
|
||||
|
||||
import {
|
||||
// Core text generation
|
||||
aiGenerateText,
|
||||
aiStreamText,
|
||||
// Object generation
|
||||
aiGenerateObject,
|
||||
// Image generation
|
||||
aiGenerateImage,
|
||||
// Embeddings
|
||||
aiEmbed,
|
||||
// Tools
|
||||
defineTool,
|
||||
createToolSet,
|
||||
// Models
|
||||
getModel,
|
||||
MODEL_ALIASES,
|
||||
// Middleware
|
||||
createCachingMiddleware,
|
||||
// Multimodal
|
||||
createImagePart,
|
||||
createMultimodalMessage,
|
||||
// MCP
|
||||
createMCP,
|
||||
// Server adapters
|
||||
createExpressHandler,
|
||||
// Next.js
|
||||
createRouteHandler,
|
||||
// Agent loop
|
||||
createAgentLoop,
|
||||
stopWhenNoToolCalls,
|
||||
} from '../../src';
|
||||
|
||||
// Also import the Agent class for comparison
|
||||
import { Agent } from '../../src';
|
||||
|
||||
async function main() {
|
||||
console.log('=== PraisonAI TypeScript - AI SDK Wrapper Demo ===\n');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Simple Text Generation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('1. Simple Text Generation');
|
||||
console.log('-------------------------');
|
||||
|
||||
try {
|
||||
const result = await aiGenerateText({
|
||||
model: 'gpt-4o-mini',
|
||||
prompt: 'What is 2 + 2? Answer in one word.',
|
||||
});
|
||||
console.log('Result:', result.text);
|
||||
console.log('Usage:', result.usage);
|
||||
} catch (error: any) {
|
||||
console.log('Skipped (AI SDK not installed):', error.message);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Streaming Text Generation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n2. Streaming Text Generation');
|
||||
console.log('----------------------------');
|
||||
|
||||
try {
|
||||
const stream = await aiStreamText({
|
||||
model: 'gpt-4o-mini',
|
||||
prompt: 'Count from 1 to 5.',
|
||||
});
|
||||
|
||||
process.stdout.write('Streaming: ');
|
||||
for await (const chunk of stream.textStream) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
console.log('\n');
|
||||
} catch (error: any) {
|
||||
console.log('Skipped (AI SDK not installed):', error.message);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Structured Object Generation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('3. Structured Object Generation');
|
||||
console.log('-------------------------------');
|
||||
|
||||
try {
|
||||
// Using a simple JSON schema (Zod would be better in production)
|
||||
const result = await aiGenerateObject({
|
||||
model: 'gpt-4o-mini',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
required: ['name', 'age', 'city'],
|
||||
},
|
||||
prompt: 'Generate a fictional person.',
|
||||
});
|
||||
console.log('Generated object:', result.object);
|
||||
} catch (error: any) {
|
||||
console.log('Skipped (AI SDK not installed):', error.message);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Tool Definition
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n4. Tool Definition');
|
||||
console.log('------------------');
|
||||
|
||||
const weatherTool = defineTool({
|
||||
description: 'Get weather for a city',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: { type: 'string', description: 'City name' },
|
||||
},
|
||||
required: ['city'],
|
||||
},
|
||||
execute: async ({ city }) => {
|
||||
return { temperature: 20, unit: 'celsius', city };
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Weather tool defined:', weatherTool.description);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Tool Set Creation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n5. Tool Set Creation');
|
||||
console.log('--------------------');
|
||||
|
||||
const tools = createToolSet({
|
||||
weather: weatherTool,
|
||||
calculator: defineTool({
|
||||
description: 'Perform calculations',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expression: { type: 'string' },
|
||||
},
|
||||
required: ['expression'],
|
||||
},
|
||||
execute: async ({ expression }) => {
|
||||
return { result: eval(expression) };
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
console.log('Tools created:', Object.keys(tools));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Model Aliases
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n6. Model Aliases');
|
||||
console.log('----------------');
|
||||
console.log('Available aliases:', Object.keys(MODEL_ALIASES).slice(0, 10), '...');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. Multimodal Message Creation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n7. Multimodal Message Creation');
|
||||
console.log('------------------------------');
|
||||
|
||||
const imagePart = createImagePart('https://example.com/image.png');
|
||||
console.log('Image part type:', imagePart.type);
|
||||
|
||||
const multimodalMsg = createMultimodalMessage(
|
||||
'What is in this image?',
|
||||
['https://example.com/image.png']
|
||||
);
|
||||
console.log('Multimodal message role:', multimodalMsg.role);
|
||||
console.log('Content parts:', multimodalMsg.content.length);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. Agent Loop (Manual Control)
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n8. Agent Loop (Manual Control)');
|
||||
console.log('------------------------------');
|
||||
|
||||
const loop = createAgentLoop({
|
||||
model: 'gpt-4o-mini',
|
||||
system: 'You are a helpful assistant.',
|
||||
tools: {
|
||||
greet: defineTool({
|
||||
description: 'Greet someone',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name'],
|
||||
},
|
||||
execute: async ({ name }) => `Hello, ${name}!`,
|
||||
}),
|
||||
},
|
||||
maxSteps: 3,
|
||||
stopWhen: stopWhenNoToolCalls(),
|
||||
});
|
||||
|
||||
console.log('Agent loop created with max 3 steps');
|
||||
console.log('Stop condition: when no tool calls');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Server Handler Creation
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n9. Server Handler Creation');
|
||||
console.log('--------------------------');
|
||||
|
||||
const expressHandler = createExpressHandler({
|
||||
handler: async (input) => {
|
||||
return { text: `Echo: ${input.message}` };
|
||||
},
|
||||
streaming: false,
|
||||
cors: true,
|
||||
});
|
||||
console.log('Express handler created');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. Next.js Route Handler
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n10. Next.js Route Handler');
|
||||
console.log('-------------------------');
|
||||
|
||||
const routeHandler = createRouteHandler({
|
||||
handler: async (input) => {
|
||||
return { text: `Response: ${input.prompt}` };
|
||||
},
|
||||
streaming: true,
|
||||
});
|
||||
console.log('Next.js route handler created');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 11. Comparison with Agent Class
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n11. Comparison with Agent Class');
|
||||
console.log('-------------------------------');
|
||||
console.log('The Agent class provides a higher-level abstraction:');
|
||||
console.log('- Automatic tool calling');
|
||||
console.log('- Session persistence');
|
||||
console.log('- Multi-agent orchestration');
|
||||
console.log('');
|
||||
console.log('The AI SDK wrappers provide lower-level control:');
|
||||
console.log('- Direct access to generateText/streamText');
|
||||
console.log('- Manual agent loops');
|
||||
console.log('- Server adapters for various frameworks');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Summary
|
||||
// -------------------------------------------------------------------------
|
||||
console.log('\n=== Summary ===');
|
||||
console.log('New AI SDK wrapper exports:');
|
||||
console.log('- aiGenerateText, aiStreamText');
|
||||
console.log('- aiGenerateObject, aiStreamObject');
|
||||
console.log('- aiGenerateImage');
|
||||
console.log('- aiEmbed, aiEmbedMany');
|
||||
console.log('- defineTool, createToolSet');
|
||||
console.log('- createAgentLoop');
|
||||
console.log('- createHttpHandler, createExpressHandler, createHonoHandler, createFastifyHandler, createNestHandler');
|
||||
console.log('- createRouteHandler (Next.js)');
|
||||
console.log('- createMCP (MCP client)');
|
||||
console.log('- createSlackBot, createNLPostgres, createComputerUse (integrations)');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* AI SDK Basic Chat Example
|
||||
*
|
||||
* Demonstrates basic text generation using the AI SDK backend.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/basic-chat.ts
|
||||
*
|
||||
* Required environment variables:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from '../../src/llm/providers/ai-sdk';
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Basic Chat Example\n');
|
||||
|
||||
// Create a backend for OpenAI
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
timeout: 30000,
|
||||
maxRetries: 2,
|
||||
});
|
||||
|
||||
console.log(`Provider: ${backend.providerId}`);
|
||||
console.log(`Model: ${backend.modelId}\n`);
|
||||
|
||||
// Generate text
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'What is the capital of France? Answer in one sentence.' }
|
||||
],
|
||||
temperature: 0.7,
|
||||
maxTokens: 100,
|
||||
});
|
||||
|
||||
console.log('Response:', result.text);
|
||||
console.log('\nUsage:', result.usage);
|
||||
console.log('Finish Reason:', result.finishReason);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* AI SDK Multi-Agent Example
|
||||
*
|
||||
* Demonstrates multi-agent safety with attribution context.
|
||||
* Each agent has its own ID and the context is propagated to LLM calls.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/multi-agent.ts
|
||||
*
|
||||
* Required environment variables:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from '../../src/llm/providers/ai-sdk';
|
||||
import type { AttributionContext } from '../../src/llm/providers/ai-sdk/types';
|
||||
|
||||
// Simulate multiple agents running in parallel
|
||||
async function runAgent(
|
||||
agentId: string,
|
||||
task: string,
|
||||
attribution: AttributionContext
|
||||
): Promise<{ agentId: string; result: string; duration: number }> {
|
||||
console.log(`[${agentId}] Starting task: "${task}"`);
|
||||
|
||||
// Create backend with attribution context
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
timeout: 30000,
|
||||
attribution,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: `You are agent ${agentId}. Be concise.` },
|
||||
{ role: 'user', content: task }
|
||||
],
|
||||
temperature: 0.7,
|
||||
maxTokens: 100,
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`[${agentId}] Completed in ${duration}ms`);
|
||||
|
||||
return {
|
||||
agentId,
|
||||
result: result.text,
|
||||
duration
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Multi-Agent Example\n');
|
||||
console.log('Running multiple agents in parallel with attribution tracking...\n');
|
||||
|
||||
// Shared run context
|
||||
const runId = `run-${Date.now()}`;
|
||||
const traceId = `trace-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const sessionId = 'session-demo';
|
||||
|
||||
console.log(`Run ID: ${runId}`);
|
||||
console.log(`Trace ID: ${traceId}`);
|
||||
console.log(`Session ID: ${sessionId}\n`);
|
||||
console.log('---\n');
|
||||
|
||||
// Define agents with their tasks
|
||||
const agents = [
|
||||
{
|
||||
id: 'researcher',
|
||||
task: 'What is the population of Tokyo? Just the number.',
|
||||
attribution: {
|
||||
agentId: 'researcher',
|
||||
runId,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'analyst',
|
||||
task: 'What is 15% of 1000? Just the number.',
|
||||
attribution: {
|
||||
agentId: 'analyst',
|
||||
runId,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'writer',
|
||||
task: 'Write a one-sentence tagline for a coffee shop.',
|
||||
attribution: {
|
||||
agentId: 'writer',
|
||||
runId,
|
||||
traceId,
|
||||
sessionId,
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Run all agents in parallel
|
||||
const startTime = Date.now();
|
||||
const results = await Promise.all(
|
||||
agents.map(agent => runAgent(agent.id, agent.task, agent.attribution))
|
||||
);
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
console.log('\n---\n');
|
||||
console.log('Results:\n');
|
||||
|
||||
for (const result of results) {
|
||||
console.log(`[${result.agentId}] (${result.duration}ms)`);
|
||||
console.log(` ${result.result.trim()}\n`);
|
||||
}
|
||||
|
||||
console.log('---');
|
||||
console.log(`Total parallel execution time: ${totalDuration}ms`);
|
||||
console.log(`Sum of individual times: ${results.reduce((sum, r) => sum + r.duration, 0)}ms`);
|
||||
console.log('\nNote: Attribution headers (X-Agent-Id, X-Run-Id, X-Trace-Id, X-Session-Id)');
|
||||
console.log('are automatically injected into each LLM request for tracing and debugging.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* AI SDK Multi-Provider Example
|
||||
*
|
||||
* Demonstrates using multiple AI providers with the same interface.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/multi-provider.ts
|
||||
*
|
||||
* Required environment variables (set at least one):
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
* ANTHROPIC_API_KEY - Your Anthropic API key
|
||||
* GOOGLE_API_KEY - Your Google API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend, validateProviderApiKey } from '../../src/llm/providers/ai-sdk';
|
||||
|
||||
const PROVIDERS = [
|
||||
{ id: 'openai', model: 'gpt-4o-mini' },
|
||||
{ id: 'anthropic', model: 'claude-3-haiku-20240307' },
|
||||
{ id: 'google', model: 'gemini-1.5-flash' },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Multi-Provider Example\n');
|
||||
console.log('Testing multiple providers with the same prompt...\n');
|
||||
|
||||
const prompt = 'What is 2 + 2? Answer with just the number.';
|
||||
console.log(`Prompt: "${prompt}"\n`);
|
||||
console.log('---\n');
|
||||
|
||||
for (const provider of PROVIDERS) {
|
||||
// Check if API key is available
|
||||
if (!validateProviderApiKey(provider.id)) {
|
||||
console.log(`${provider.id}: Skipped (no API key)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const modelString = `${provider.id}/${provider.model}`;
|
||||
console.log(`${provider.id} (${provider.model}):`);
|
||||
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
timeout: 30000,
|
||||
maxRetries: 1,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
temperature: 0,
|
||||
maxTokens: 10,
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(` Response: ${result.text.trim()}`);
|
||||
console.log(` Duration: ${duration}ms`);
|
||||
console.log(` Tokens: ${result.usage?.totalTokens || 'N/A'}`);
|
||||
console.log();
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* AI SDK Streaming Example
|
||||
*
|
||||
* Demonstrates streaming text generation using the AI SDK backend.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/streaming.ts
|
||||
*
|
||||
* Required environment variables:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from '../../src/llm/providers/ai-sdk';
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Streaming Example\n');
|
||||
|
||||
// Create a backend for OpenAI
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
console.log(`Provider: ${backend.providerId}`);
|
||||
console.log(`Model: ${backend.modelId}\n`);
|
||||
console.log('Streaming response:\n');
|
||||
|
||||
// Stream text
|
||||
const stream = await backend.streamText({
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Write a short poem about programming.' }
|
||||
],
|
||||
temperature: 0.8,
|
||||
maxTokens: 200,
|
||||
onToken: (token) => {
|
||||
// This callback is called for each token
|
||||
process.stdout.write(token);
|
||||
},
|
||||
});
|
||||
|
||||
// Consume the stream
|
||||
let totalTokens = 0;
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.text) {
|
||||
// Text is already printed via onToken callback
|
||||
}
|
||||
if (chunk.usage) {
|
||||
totalTokens = chunk.usage.totalTokens || 0;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n\n---');
|
||||
console.log(`Total tokens: ${totalTokens}`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* AI SDK Structured Output Example
|
||||
*
|
||||
* Demonstrates generating structured JSON output using schemas.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/structured-output.ts
|
||||
*
|
||||
* Required environment variables:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from '../../src/llm/providers/ai-sdk';
|
||||
|
||||
// Define the schema for structured output
|
||||
const recipeSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the recipe'
|
||||
},
|
||||
ingredients: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
amount: { type: 'string' },
|
||||
unit: { type: 'string' }
|
||||
},
|
||||
required: ['name', 'amount']
|
||||
},
|
||||
description: 'List of ingredients'
|
||||
},
|
||||
steps: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Cooking steps'
|
||||
},
|
||||
prepTime: {
|
||||
type: 'number',
|
||||
description: 'Preparation time in minutes'
|
||||
},
|
||||
cookTime: {
|
||||
type: 'number',
|
||||
description: 'Cooking time in minutes'
|
||||
},
|
||||
servings: {
|
||||
type: 'number',
|
||||
description: 'Number of servings'
|
||||
}
|
||||
},
|
||||
required: ['name', 'ingredients', 'steps']
|
||||
};
|
||||
|
||||
interface Recipe {
|
||||
name: string;
|
||||
ingredients: Array<{
|
||||
name: string;
|
||||
amount: string;
|
||||
unit?: string;
|
||||
}>;
|
||||
steps: string[];
|
||||
prepTime?: number;
|
||||
cookTime?: number;
|
||||
servings?: number;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Structured Output Example\n');
|
||||
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
console.log(`Provider: ${backend.providerId}`);
|
||||
console.log(`Model: ${backend.modelId}\n`);
|
||||
|
||||
console.log('Generating a recipe in structured JSON format...\n');
|
||||
|
||||
const result = await backend.generateObject<Recipe>({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a helpful cooking assistant. Generate recipes in the requested format.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Give me a simple recipe for chocolate chip cookies.'
|
||||
}
|
||||
],
|
||||
schema: recipeSchema,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
console.log('Generated Recipe:');
|
||||
console.log('================\n');
|
||||
|
||||
const recipe = result.object;
|
||||
|
||||
console.log(`📝 ${recipe.name}`);
|
||||
console.log();
|
||||
|
||||
if (recipe.prepTime || recipe.cookTime) {
|
||||
console.log(`⏱️ Prep: ${recipe.prepTime || 'N/A'} min | Cook: ${recipe.cookTime || 'N/A'} min`);
|
||||
}
|
||||
if (recipe.servings) {
|
||||
console.log(`🍽️ Servings: ${recipe.servings}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('Ingredients:');
|
||||
for (const ing of recipe.ingredients) {
|
||||
const unit = ing.unit ? ` ${ing.unit}` : '';
|
||||
console.log(` • ${ing.amount}${unit} ${ing.name}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log('Steps:');
|
||||
recipe.steps.forEach((step, i) => {
|
||||
console.log(` ${i + 1}. ${step}`);
|
||||
});
|
||||
|
||||
console.log('\n---');
|
||||
console.log('Usage:', result.usage);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* AI SDK Tool Calling Example
|
||||
*
|
||||
* Demonstrates tool calling using the AI SDK backend.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node examples/ai-sdk/tool-calling.ts
|
||||
*
|
||||
* Required environment variables:
|
||||
* OPENAI_API_KEY - Your OpenAI API key
|
||||
*/
|
||||
|
||||
import { createAISDKBackend } from '../../src/llm/providers/ai-sdk';
|
||||
import type { ToolDefinition, Message } from '../../src/llm/providers/types';
|
||||
|
||||
// Define tools
|
||||
const tools: ToolDefinition[] = [
|
||||
{
|
||||
name: 'get_weather',
|
||||
description: 'Get the current weather for a location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'The city and country, e.g., "London, UK"'
|
||||
},
|
||||
unit: {
|
||||
type: 'string',
|
||||
enum: ['celsius', 'fahrenheit'],
|
||||
description: 'Temperature unit'
|
||||
}
|
||||
},
|
||||
required: ['location']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_time',
|
||||
description: 'Get the current time for a timezone',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'The timezone, e.g., "America/New_York"'
|
||||
}
|
||||
},
|
||||
required: ['timezone']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Mock tool execution
|
||||
function executeTool(name: string, args: Record<string, unknown>): string {
|
||||
switch (name) {
|
||||
case 'get_weather':
|
||||
return JSON.stringify({
|
||||
location: args.location,
|
||||
temperature: 22,
|
||||
unit: args.unit || 'celsius',
|
||||
condition: 'Partly cloudy'
|
||||
});
|
||||
case 'get_time':
|
||||
return JSON.stringify({
|
||||
timezone: args.timezone,
|
||||
time: new Date().toLocaleTimeString('en-US', {
|
||||
timeZone: args.timezone as string
|
||||
})
|
||||
});
|
||||
default:
|
||||
return JSON.stringify({ error: 'Unknown tool' });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('AI SDK Tool Calling Example\n');
|
||||
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
console.log(`Provider: ${backend.providerId}`);
|
||||
console.log(`Model: ${backend.modelId}\n`);
|
||||
|
||||
// Initial conversation
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'You are a helpful assistant with access to weather and time tools.' },
|
||||
{ role: 'user', content: 'What is the weather in London and what time is it in New York?' }
|
||||
];
|
||||
|
||||
console.log('User:', messages[1].content);
|
||||
console.log('\n--- First LLM Call ---\n');
|
||||
|
||||
// First call - model should request tool calls
|
||||
let result = await backend.generateText({
|
||||
messages,
|
||||
tools,
|
||||
toolChoice: 'auto',
|
||||
});
|
||||
|
||||
console.log('Finish Reason:', result.finishReason);
|
||||
|
||||
// Handle tool calls
|
||||
if (result.toolCalls && result.toolCalls.length > 0) {
|
||||
console.log(`\nTool Calls: ${result.toolCalls.length}`);
|
||||
|
||||
// Add assistant message with tool calls
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: result.toolCalls
|
||||
});
|
||||
|
||||
// Execute each tool and add results
|
||||
for (const toolCall of result.toolCalls) {
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
console.log(`\n Executing: ${toolCall.function.name}`);
|
||||
console.log(` Arguments: ${JSON.stringify(args)}`);
|
||||
|
||||
const toolResult = executeTool(toolCall.function.name, args);
|
||||
console.log(` Result: ${toolResult}`);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: toolResult,
|
||||
tool_call_id: toolCall.id,
|
||||
name: toolCall.function.name
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n--- Second LLM Call (with tool results) ---\n');
|
||||
|
||||
// Second call with tool results
|
||||
result = await backend.generateText({
|
||||
messages,
|
||||
tools,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Final Response:', result.text);
|
||||
console.log('\nUsage:', result.usage);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* AI SDK Tools Registry Example
|
||||
*
|
||||
* Demonstrates how to use the built-in AI SDK tools with PraisonAI agents.
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src/agent';
|
||||
import { tools, registerBuiltinTools } from '../../src/tools/tools';
|
||||
import { getToolsRegistry } from '../../src/tools/registry/registry';
|
||||
import { createLoggingMiddleware, createTimeoutMiddleware, createRedactionMiddleware } from '../../src/tools/registry/middleware';
|
||||
import type { ToolExecutionContext } from '../../src/tools/registry/types';
|
||||
|
||||
// Register all built-in tools
|
||||
registerBuiltinTools();
|
||||
|
||||
// Example 1: Web Research Agent with Tavily
|
||||
async function webResearchExample() {
|
||||
console.log('\n=== Web Research Agent ===\n');
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'WebResearcher',
|
||||
instructions: 'You are a web research assistant. Search the web to find information.',
|
||||
tools: [tools.tavily()],
|
||||
});
|
||||
|
||||
// Note: Requires TAVILY_API_KEY environment variable
|
||||
// const result = await agent.run('What are the latest developments in AI?');
|
||||
// console.log(result.text);
|
||||
|
||||
console.log('Web research agent created with Tavily search tool.');
|
||||
console.log('Set TAVILY_API_KEY to use this agent.');
|
||||
}
|
||||
|
||||
// Example 2: Code Execution Agent
|
||||
async function codeExecutionExample() {
|
||||
console.log('\n=== Code Execution Agent ===\n');
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'CodeRunner',
|
||||
instructions: 'You can execute Python code to solve computational problems.',
|
||||
tools: [tools.codeExecution()],
|
||||
});
|
||||
|
||||
// Note: Requires Vercel deployment or VERCEL_OIDC_TOKEN
|
||||
// const result = await agent.run('Calculate the first 10 Fibonacci numbers');
|
||||
// console.log(result.text);
|
||||
|
||||
console.log('Code execution agent created.');
|
||||
console.log('Deploy to Vercel or set VERCEL_OIDC_TOKEN to use.');
|
||||
}
|
||||
|
||||
// Example 3: Secure Agent with Guardrails
|
||||
async function secureAgentExample() {
|
||||
console.log('\n=== Secure Agent with Guardrails ===\n');
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'SecureAssistant',
|
||||
instructions: 'You are a secure assistant that protects user data.',
|
||||
tools: [
|
||||
tools.guard(), // Check for prompt injection
|
||||
tools.redact(), // Remove PII from responses
|
||||
tools.verify(), // Verify claims
|
||||
],
|
||||
});
|
||||
|
||||
// Note: Requires SUPERAGENT_API_KEY
|
||||
console.log('Secure agent created with guard, redact, and verify tools.');
|
||||
console.log('Set SUPERAGENT_API_KEY to use security features.');
|
||||
}
|
||||
|
||||
// Example 4: Finance Research Agent with Valyu
|
||||
async function financeResearchExample() {
|
||||
console.log('\n=== Finance Research Agent ===\n');
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'FinanceAnalyst',
|
||||
instructions: 'You are a financial analyst. Research stocks, SEC filings, and economic data.',
|
||||
tools: [
|
||||
tools.valyuFinanceSearch(),
|
||||
tools.valyuSecSearch(),
|
||||
tools.valyuEconomicsSearch(),
|
||||
],
|
||||
});
|
||||
|
||||
// Note: Requires VALYU_API_KEY
|
||||
console.log('Finance research agent created with Valyu tools.');
|
||||
console.log('Set VALYU_API_KEY to use finance search features.');
|
||||
}
|
||||
|
||||
// Example 5: Multi-Tool Agent
|
||||
async function multiToolExample() {
|
||||
console.log('\n=== Multi-Tool Research Agent ===\n');
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'ResearchAssistant',
|
||||
instructions: 'You are a comprehensive research assistant with access to multiple search tools.',
|
||||
tools: [
|
||||
tools.tavily(), // General web search
|
||||
tools.exa(), // Semantic search
|
||||
tools.valyuPaperSearch(), // Academic papers
|
||||
tools.firecrawl(), // Web scraping
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Multi-tool agent created with 4 different search tools.');
|
||||
}
|
||||
|
||||
// Example 6: Using Middleware
|
||||
async function middlewareExample() {
|
||||
console.log('\n=== Registry with Middleware ===\n');
|
||||
|
||||
const registry = getToolsRegistry();
|
||||
|
||||
// Add logging middleware
|
||||
registry.use(createLoggingMiddleware());
|
||||
|
||||
// Add timeout middleware (30 second timeout)
|
||||
registry.use(createTimeoutMiddleware(30000));
|
||||
|
||||
// Add PII redaction middleware
|
||||
registry.use(createRedactionMiddleware());
|
||||
|
||||
// Set hooks for monitoring
|
||||
registry.setHooks({
|
||||
beforeToolCall: (name: string, input: unknown, ctx: ToolExecutionContext) => {
|
||||
console.log(`[Hook] Starting tool: ${name}`);
|
||||
},
|
||||
afterToolCall: (name: string, input: unknown, output: unknown, ctx: ToolExecutionContext) => {
|
||||
console.log(`[Hook] Completed tool: ${name}`);
|
||||
},
|
||||
onError: (name: string, error: Error, ctx: ToolExecutionContext) => {
|
||||
console.error(`[Hook] Error in tool ${name}:`, error.message);
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Registry configured with logging, timeout, and redaction middleware.');
|
||||
}
|
||||
|
||||
// Example 7: Custom Tool Registration
|
||||
async function customToolExample() {
|
||||
console.log('\n=== Custom Tool Registration ===\n');
|
||||
|
||||
// Register a custom tool
|
||||
const customTool = tools.custom({
|
||||
id: 'my-calculator',
|
||||
name: 'calculator',
|
||||
description: 'Perform basic arithmetic calculations',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expression: {
|
||||
type: 'string',
|
||||
description: 'Mathematical expression to evaluate',
|
||||
},
|
||||
},
|
||||
required: ['expression'],
|
||||
},
|
||||
execute: async (input: { expression: string }) => {
|
||||
// Simple eval for demo (use a proper math parser in production)
|
||||
try {
|
||||
const result = Function(`"use strict"; return (${input.expression})`)();
|
||||
return { result, expression: input.expression };
|
||||
} catch (error) {
|
||||
return { error: 'Invalid expression', expression: input.expression };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'Calculator',
|
||||
instructions: 'You can perform calculations.',
|
||||
tools: [customTool],
|
||||
});
|
||||
|
||||
console.log('Custom calculator tool registered and agent created.');
|
||||
}
|
||||
|
||||
// Example 8: List Available Tools
|
||||
async function listToolsExample() {
|
||||
console.log('\n=== Available Tools ===\n');
|
||||
|
||||
const allTools = tools.list();
|
||||
|
||||
console.log(`Total registered tools: ${allTools.length}\n`);
|
||||
|
||||
for (const tool of allTools) {
|
||||
console.log(`• ${tool.id} (${tool.displayName})`);
|
||||
console.log(` Tags: ${tool.tags.join(', ')}`);
|
||||
console.log(` Package: ${tool.packageName}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
// Run all examples
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('AI SDK Tools Registry Examples');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
await webResearchExample();
|
||||
await codeExecutionExample();
|
||||
await secureAgentExample();
|
||||
await financeResearchExample();
|
||||
await multiToolExample();
|
||||
await middlewareExample();
|
||||
await customToolExample();
|
||||
await listToolsExample();
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('Examples completed!');
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Tavily Search Tool Example
|
||||
*
|
||||
* Demonstrates using Tavily search with PraisonAI agents.
|
||||
*
|
||||
* Run with:
|
||||
* TAVILY_API_KEY=your-key node examples/tools/tavily-search.mjs
|
||||
*/
|
||||
|
||||
import { tavilySearch } from '@tavily/ai-sdk';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Tavily Search Example ===\n');
|
||||
|
||||
// Check for API key
|
||||
if (!process.env.TAVILY_API_KEY) {
|
||||
console.error('ERROR: TAVILY_API_KEY environment variable is required');
|
||||
console.error('Get your API key at: https://tavily.com');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the Tavily search tool
|
||||
const searchTool = tavilySearch({
|
||||
apiKey: process.env.TAVILY_API_KEY,
|
||||
searchDepth: 'basic',
|
||||
maxResults: 5,
|
||||
includeAnswer: true,
|
||||
});
|
||||
|
||||
console.log('Tool:', searchTool.description);
|
||||
console.log('\nSearching for: "latest AI agent frameworks 2024"\n');
|
||||
|
||||
try {
|
||||
// Execute the search
|
||||
const result = await searchTool.execute(
|
||||
{ query: 'latest AI agent frameworks 2024' },
|
||||
{ toolCallId: 'search-1', messages: [] }
|
||||
);
|
||||
|
||||
console.log('=== Results ===\n');
|
||||
console.log('Query:', result.query);
|
||||
console.log('Response Time:', result.responseTime, 'ms');
|
||||
|
||||
if (result.answer) {
|
||||
console.log('\nAI Answer:', result.answer);
|
||||
}
|
||||
|
||||
console.log('\nTop Results:');
|
||||
for (const r of result.results || []) {
|
||||
console.log(`\n• ${r.title}`);
|
||||
console.log(` URL: ${r.url}`);
|
||||
console.log(` Score: ${r.score?.toFixed(3)}`);
|
||||
console.log(` ${r.content.substring(0, 150)}...`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Search completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('❌ Search failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Tavily Search Tool Test
|
||||
*
|
||||
* Tests the Tavily search tool with a real API key.
|
||||
* Run with: TAVILY_API_KEY=your-key npx ts-node examples/tools/tavily-test.ts
|
||||
*/
|
||||
|
||||
import { tavilySearch } from '../../src/tools/builtins/tavily';
|
||||
|
||||
async function main() {
|
||||
console.log('Testing Tavily Search Tool...\n');
|
||||
|
||||
// Check for API key
|
||||
if (!process.env.TAVILY_API_KEY) {
|
||||
console.error('ERROR: TAVILY_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('API Key found: ' + process.env.TAVILY_API_KEY.substring(0, 10) + '...\n');
|
||||
|
||||
try {
|
||||
// Create the tool
|
||||
const tool = tavilySearch({ includeAnswer: true, maxResults: 3 });
|
||||
|
||||
console.log('Tool created:', tool.name);
|
||||
console.log('Description:', tool.description);
|
||||
console.log('\nExecuting search for "latest AI developments 2024"...\n');
|
||||
|
||||
// Execute the search
|
||||
const result = await tool.execute({ query: 'latest AI developments 2024' });
|
||||
|
||||
console.log('=== Search Results ===\n');
|
||||
|
||||
if (result.answer) {
|
||||
console.log('AI Answer:', result.answer.substring(0, 200) + '...\n');
|
||||
}
|
||||
|
||||
console.log(`Found ${result.results.length} results:\n`);
|
||||
|
||||
for (const r of result.results) {
|
||||
console.log(`- ${r.title}`);
|
||||
console.log(` URL: ${r.url}`);
|
||||
console.log(` Score: ${r.score}`);
|
||||
console.log(` Content: ${r.content.substring(0, 100)}...`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('Response time:', result.responseTime, 'ms');
|
||||
console.log('\n✅ Tavily search test PASSED!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test FAILED:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "praisonai",
|
||||
"version": "1.3.2",
|
||||
"version": "1.4.0",
|
||||
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -46,11 +46,15 @@
|
||||
"author": "Mervin Praison",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.1",
|
||||
"@ai-sdk/google": "^3.0.1",
|
||||
"@ai-sdk/openai": "^3.0.1",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"ai": "^6.0.3",
|
||||
"eslint": "^9.19.0",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -60,20 +64,43 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"fast-xml-parser": "^4.5.1",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openai": "^4.81.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1"
|
||||
"openai": "^4.81.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@airweave/sdk": ">=0.1.0",
|
||||
"@mendable/firecrawl-js": ">=1.0.0",
|
||||
"bedrock-agentcore": ">=0.1.0",
|
||||
"boxen": "^7.1.1",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.3",
|
||||
"figlet": "^1.7.0",
|
||||
"ora": "^5.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/anthropic": ">=1.0.0",
|
||||
"@ai-sdk/google": ">=1.0.0",
|
||||
"@ai-sdk/openai": ">=1.0.0",
|
||||
"ai": ">=4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ai": {
|
||||
"optional": true
|
||||
},
|
||||
"@ai-sdk/openai": {
|
||||
"optional": true
|
||||
},
|
||||
"@ai-sdk/anthropic": {
|
||||
"optional": true
|
||||
},
|
||||
"@ai-sdk/google": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"whatwg-url": "^14.1.0",
|
||||
"node-fetch": "^2.6.9"
|
||||
|
||||
Generated
+1597
-26
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,8 @@
|
||||
* Context Agent - Agent with enhanced context management
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
import { type LLMProvider, type Message } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
import { Session } from '../session';
|
||||
import { KnowledgeBase, type SearchResult } from '../knowledge/rag';
|
||||
|
||||
@@ -30,7 +31,9 @@ export class ContextAgent {
|
||||
readonly name: string;
|
||||
readonly instructions: string;
|
||||
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
private knowledgeBase?: KnowledgeBase;
|
||||
private contextWindow: number;
|
||||
private maxContextTokens: number;
|
||||
@@ -40,13 +43,34 @@ export class ContextAgent {
|
||||
constructor(config: ContextAgentConfig) {
|
||||
this.name = config.name || `ContextAgent_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.instructions = config.instructions;
|
||||
this.provider = createProvider(config.llm || 'gpt-4o-mini');
|
||||
this.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.knowledgeBase = config.knowledgeBase;
|
||||
this.contextWindow = config.contextWindow ?? 10;
|
||||
this.maxContextTokens = config.maxContextTokens ?? 4000;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LLM provider (lazy initialization with AI SDK backend)
|
||||
*/
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, {
|
||||
attribution: { agentId: this.name },
|
||||
});
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat with context awareness
|
||||
*/
|
||||
@@ -83,7 +107,8 @@ export class ContextAgent {
|
||||
];
|
||||
|
||||
// Generate response
|
||||
const result = await this.provider.generateText({ messages });
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({ messages });
|
||||
|
||||
// Add assistant message
|
||||
this.messages.push({
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Enhanced Agent - Agent with session management, provider abstraction, and tool support
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message, type ToolCall, type GenerateTextResult } from '../llm/providers';
|
||||
import { type LLMProvider, type Message, type ToolCall, type GenerateTextResult } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
import { Session, Run, Trace, getSessionManager } from '../session';
|
||||
import { FunctionTool, ToolRegistry, tool } from '../tools/decorator';
|
||||
import { Logger } from '../utils/logger';
|
||||
@@ -51,7 +52,9 @@ export class EnhancedAgent {
|
||||
readonly instructions: string;
|
||||
readonly sessionId: string;
|
||||
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
private session: Session;
|
||||
private toolRegistry: ToolRegistry;
|
||||
private verbose: boolean;
|
||||
@@ -71,8 +74,8 @@ export class EnhancedAgent {
|
||||
this.maxTokens = config.maxTokens;
|
||||
this.outputSchema = config.outputSchema;
|
||||
|
||||
// Initialize provider
|
||||
this.provider = createProvider(config.llm || 'gpt-4o-mini');
|
||||
// Store model string for lazy provider initialization
|
||||
this.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
|
||||
// Initialize session
|
||||
if (config.session) {
|
||||
@@ -93,6 +96,30 @@ export class EnhancedAgent {
|
||||
Logger.setVerbose(this.verbose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LLM provider (lazy initialization with AI SDK backend)
|
||||
*/
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, {
|
||||
attribution: {
|
||||
agentId: this.name,
|
||||
sessionId: this.sessionId,
|
||||
},
|
||||
});
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
private registerTools(tools: EnhancedAgentConfig['tools']): void {
|
||||
if (!tools) return;
|
||||
|
||||
@@ -176,7 +203,8 @@ export class EnhancedAgent {
|
||||
|
||||
if (options.stream && !tools && options.onToken) {
|
||||
// Streaming without tools
|
||||
const stream = await this.provider.streamText({
|
||||
const provider = await this.getProvider();
|
||||
const stream = await provider.streamText({
|
||||
messages,
|
||||
temperature: options.temperature ?? this.temperature,
|
||||
maxTokens: options.maxTokens ?? this.maxTokens,
|
||||
@@ -197,7 +225,8 @@ export class EnhancedAgent {
|
||||
break;
|
||||
} else if (options.outputSchema) {
|
||||
// Structured output
|
||||
const objResult = await this.provider.generateObject({
|
||||
const provider = await this.getProvider();
|
||||
const objResult = await provider.generateObject({
|
||||
messages,
|
||||
schema: options.outputSchema ?? this.outputSchema,
|
||||
temperature: options.temperature ?? this.temperature,
|
||||
@@ -213,7 +242,8 @@ export class EnhancedAgent {
|
||||
break;
|
||||
} else {
|
||||
// Regular generation with potential tool calls
|
||||
result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
result = await provider.generateText({
|
||||
messages,
|
||||
temperature: options.temperature ?? this.temperature,
|
||||
maxTokens: options.maxTokens ?? this.maxTokens,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* ImageAgent - Agent for image generation and analysis
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
import { type LLMProvider, type Message } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export interface ImageGenerationConfig {
|
||||
prompt: string;
|
||||
@@ -30,17 +31,40 @@ export interface ImageAgentConfig {
|
||||
*/
|
||||
export class ImageAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
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.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.imageModel = config.imageModel || 'dall-e-3';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LLM provider (lazy initialization with AI SDK backend)
|
||||
*/
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, {
|
||||
attribution: { agentId: this.name },
|
||||
});
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze an image
|
||||
*/
|
||||
@@ -63,7 +87,8 @@ export class ImageAgent {
|
||||
}
|
||||
];
|
||||
|
||||
const result = await this.provider.generateText({ messages });
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({ messages });
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`[ImageAgent] Analysis: ${result.text.substring(0, 100)}...`);
|
||||
@@ -93,7 +118,8 @@ export class ImageAgent {
|
||||
return this.analyze({ imageUrl, prompt });
|
||||
}
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
@@ -117,7 +143,8 @@ export class ImageAgent {
|
||||
}
|
||||
];
|
||||
|
||||
const result = await this.provider.generateText({ messages });
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({ messages });
|
||||
return result.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* PromptExpanderAgent - Expand and enhance prompts
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
import { type LLMProvider } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export type ExpandStrategy = 'detail' | 'context' | 'examples' | 'constraints' | 'auto';
|
||||
|
||||
@@ -25,17 +26,31 @@ export interface PromptExpanderConfig {
|
||||
*/
|
||||
export class PromptExpanderAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
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.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.defaultStrategy = config.defaultStrategy || 'auto';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) return this.provider;
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, { attribution: { agentId: this.name } });
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a prompt
|
||||
*/
|
||||
@@ -45,7 +60,8 @@ export class PromptExpanderAgent {
|
||||
|
||||
const systemPrompt = this.getSystemPrompt(actualStrategy);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Expand this prompt: ${prompt}` }
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* QueryRewriterAgent - Rewrite and optimize queries
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
import { type LLMProvider } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export type RewriteStrategy = 'expand' | 'simplify' | 'decompose' | 'rephrase' | 'auto';
|
||||
|
||||
@@ -25,17 +26,31 @@ export interface QueryRewriterConfig {
|
||||
*/
|
||||
export class QueryRewriterAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
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.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.defaultStrategy = config.defaultStrategy || 'auto';
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) return this.provider;
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, { attribution: { agentId: this.name } });
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a query
|
||||
*/
|
||||
@@ -45,7 +60,8 @@ export class QueryRewriterAgent {
|
||||
|
||||
const prompt = this.buildPrompt(query, actualStrategy);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: this.getSystemPrompt(actualStrategy) },
|
||||
{ role: 'user', content: prompt }
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* DeepResearchAgent - Agent for comprehensive research tasks
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
import { type LLMProvider, type Message } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export interface Citation {
|
||||
title: string;
|
||||
@@ -37,19 +38,33 @@ export interface DeepResearchConfig {
|
||||
*/
|
||||
export class DeepResearchAgent {
|
||||
readonly name: string;
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
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.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.maxIterations = config.maxIterations ?? 5;
|
||||
this.searchTool = config.searchTool;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) return this.provider;
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, { attribution: { agentId: this.name } });
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conduct deep research on a topic
|
||||
*/
|
||||
@@ -115,7 +130,8 @@ export class DeepResearchAgent {
|
||||
}
|
||||
|
||||
private async generateSearchQueries(query: string): Promise<string[]> {
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -133,7 +149,8 @@ export class DeepResearchAgent {
|
||||
? `\n\nRelevant sources:\n${citations.map(c => `- ${c.title}: ${c.snippet || ''}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Logger } from '../utils/logger';
|
||||
import type { ChatCompletionTool } from 'openai/resources/chat/completions';
|
||||
import type { DbAdapter, DbMessage, DbRun } from '../db/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { LLMProvider } from '../llm/providers/types';
|
||||
import type { BackendResolutionResult } from '../llm/backend-resolver';
|
||||
|
||||
/**
|
||||
* Agent Configuration
|
||||
@@ -116,6 +118,12 @@ export class Agent {
|
||||
private cacheTTL: number;
|
||||
private responseCache: Map<string, { response: string; timestamp: number }> = new Map();
|
||||
private telemetryEnabled: boolean;
|
||||
|
||||
// AI SDK backend support
|
||||
private _backend: LLMProvider | null = null;
|
||||
private _backendPromise: Promise<BackendResolutionResult> | null = null;
|
||||
private _backendSource: 'ai-sdk' | 'native' | 'custom' | 'legacy' = 'legacy';
|
||||
private _useAISDKBackend: boolean = false;
|
||||
|
||||
constructor(config: SimpleAgentConfig) {
|
||||
// Build instructions from either simple or advanced mode
|
||||
@@ -148,7 +156,16 @@ export class Agent {
|
||||
this.cache = config.cache ?? false;
|
||||
this.cacheTTL = config.cacheTTL ?? 3600;
|
||||
this.telemetryEnabled = config.telemetry ?? false;
|
||||
this.llmService = new OpenAIService(this.llm);
|
||||
|
||||
// Parse model string to extract provider and model ID
|
||||
// Format: "provider/model" or just "model"
|
||||
const providerId = this.llm.includes('/') ? this.llm.split('/')[0] : 'openai';
|
||||
const modelId = this.llm.includes('/') ? this.llm.split('/').slice(1).join('/') : this.llm;
|
||||
|
||||
// For OpenAI, use OpenAIService directly for backward compatibility
|
||||
// For other providers, we'll use the AI SDK backend via getBackend()
|
||||
this._useAISDKBackend = providerId !== 'openai';
|
||||
this.llmService = new OpenAIService(modelId);
|
||||
|
||||
// Configure logging
|
||||
Logger.setVerbose(this.verbose);
|
||||
@@ -207,13 +224,14 @@ export class Agent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session ID based on current hour and agent name (like Python SDK)
|
||||
* Generate a unique session ID based on current hour, agent name, and random suffix
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
const now = new Date();
|
||||
const hourStr = now.toISOString().slice(0, 13).replace(/[-T:]/g, '');
|
||||
const hash = this.name ? this.name.slice(0, 6) : 'agent';
|
||||
return `${hourStr}-${hash}`;
|
||||
const randomSuffix = randomUUID().slice(0, 8);
|
||||
return `${hourStr}-${hash}-${randomSuffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,8 +450,35 @@ export class Agent {
|
||||
|
||||
let finalResponse = '';
|
||||
|
||||
if (this.stream && !this.tools) {
|
||||
// Use streaming with full conversation history
|
||||
// Use AI SDK backend for non-OpenAI providers
|
||||
if (this._useAISDKBackend) {
|
||||
const backend = await this.getBackend();
|
||||
|
||||
if (this.stream && !this.tools) {
|
||||
// Streaming with AI SDK backend
|
||||
const stream = await backend.streamText({
|
||||
messages,
|
||||
temperature: 0.7
|
||||
});
|
||||
|
||||
let accumulated = '';
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.text) {
|
||||
process.stdout.write(chunk.text);
|
||||
accumulated += chunk.text;
|
||||
}
|
||||
}
|
||||
finalResponse = accumulated;
|
||||
} else {
|
||||
// Non-streaming with AI SDK backend
|
||||
const result = await backend.generateText({
|
||||
messages,
|
||||
temperature: 0.7
|
||||
});
|
||||
finalResponse = result.text;
|
||||
}
|
||||
} else if (this.stream && !this.tools) {
|
||||
// Use streaming with full conversation history (OpenAI)
|
||||
finalResponse = await this.llmService.streamChat(
|
||||
messages,
|
||||
0.7,
|
||||
@@ -606,6 +651,79 @@ export class Agent {
|
||||
clearCache(): void {
|
||||
this.responseCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved backend (AI SDK preferred, native fallback)
|
||||
* Lazy initialization - backend is only resolved on first use
|
||||
*/
|
||||
async getBackend(): Promise<LLMProvider> {
|
||||
if (this._backend) {
|
||||
return this._backend;
|
||||
}
|
||||
|
||||
if (!this._backendPromise) {
|
||||
this._backendPromise = (async () => {
|
||||
const { resolveBackend } = await import('../llm/backend-resolver');
|
||||
const result = await resolveBackend(this.llm, {
|
||||
attribution: {
|
||||
agentId: this.name,
|
||||
runId: this.runId,
|
||||
sessionId: this.sessionId,
|
||||
},
|
||||
});
|
||||
this._backend = result.provider;
|
||||
this._backendSource = result.source;
|
||||
Logger.debug(`Agent ${this.name} using ${result.source} backend for ${this.llm}`);
|
||||
return result;
|
||||
})();
|
||||
}
|
||||
|
||||
const result = await this._backendPromise;
|
||||
return result.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backend source (ai-sdk, native, custom, or legacy)
|
||||
*/
|
||||
getBackendSource(): 'ai-sdk' | 'native' | 'custom' | 'legacy' {
|
||||
return this._backendSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed text using AI SDK (preferred) or native provider
|
||||
*
|
||||
* @param text - Text to embed (string or array of strings)
|
||||
* @param options - Embedding options
|
||||
* @returns Embedding vector(s)
|
||||
*
|
||||
* @example Single text
|
||||
* ```typescript
|
||||
* const embedding = await agent.embed("Hello world");
|
||||
* ```
|
||||
*
|
||||
* @example Multiple texts
|
||||
* ```typescript
|
||||
* const embeddings = await agent.embed(["Hello", "World"]);
|
||||
* ```
|
||||
*/
|
||||
async embed(text: string | string[], options?: { model?: string }): Promise<number[] | number[][]> {
|
||||
const { embed, embedMany } = await import('../llm/embeddings');
|
||||
|
||||
if (Array.isArray(text)) {
|
||||
const result = await embedMany(text, { model: options?.model });
|
||||
return result.embeddings;
|
||||
} else {
|
||||
const result = await embed(text, { model: options?.model });
|
||||
return result.embedding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model string for this agent
|
||||
*/
|
||||
getModel(): string {
|
||||
return this.llm;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Agent Loop - Manual Agent Loop Control
|
||||
*
|
||||
* Provides utilities for manual control of agent execution loops.
|
||||
*/
|
||||
|
||||
import type { Message, ToolCallPart, ToolResultPart } from './types';
|
||||
|
||||
export interface AgentLoopConfig {
|
||||
/** Model to use */
|
||||
model: string;
|
||||
/** System prompt */
|
||||
system?: string;
|
||||
/** Available tools */
|
||||
tools?: Record<string, AgentTool>;
|
||||
/** Maximum steps (default: 10) */
|
||||
maxSteps?: number;
|
||||
/** Stop condition */
|
||||
stopWhen?: StopCondition;
|
||||
/** On step finish callback */
|
||||
onStepFinish?: (step: AgentStep) => void | Promise<void>;
|
||||
/** On tool call callback (for approval) */
|
||||
onToolCall?: (toolCall: ToolCallInfo) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface AgentTool {
|
||||
description: string;
|
||||
parameters: any;
|
||||
execute: (args: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface AgentStep {
|
||||
stepNumber: number;
|
||||
text: string;
|
||||
toolCalls: ToolCallInfo[];
|
||||
toolResults: ToolResultInfo[];
|
||||
usage: { promptTokens: number; completionTokens: number; totalTokens: number };
|
||||
finishReason: string;
|
||||
}
|
||||
|
||||
export interface ToolCallInfo {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: any;
|
||||
}
|
||||
|
||||
export interface ToolResultInfo {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: any;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export type StopCondition =
|
||||
| { type: 'stepCount'; count: number }
|
||||
| { type: 'noToolCalls' }
|
||||
| { type: 'custom'; check: (step: AgentStep) => boolean };
|
||||
|
||||
export interface AgentLoopResult {
|
||||
text: string;
|
||||
steps: AgentStep[];
|
||||
totalUsage: { promptTokens: number; completionTokens: number; totalTokens: number };
|
||||
finishReason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a manual agent loop for fine-grained control.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```typescript
|
||||
* const loop = createAgentLoop({
|
||||
* model: 'gpt-4o',
|
||||
* system: 'You are a helpful assistant',
|
||||
* tools: {
|
||||
* search: {
|
||||
* description: 'Search the web',
|
||||
* parameters: z.object({ query: z.string() }),
|
||||
* execute: async ({ query }) => searchWeb(query)
|
||||
* }
|
||||
* },
|
||||
* maxSteps: 5
|
||||
* });
|
||||
*
|
||||
* const result = await loop.run('Find information about AI');
|
||||
* ```
|
||||
*
|
||||
* @example With approval
|
||||
* ```typescript
|
||||
* const loop = createAgentLoop({
|
||||
* model: 'gpt-4o',
|
||||
* tools: { ... },
|
||||
* onToolCall: async (toolCall) => {
|
||||
* const approved = await askUserForApproval(toolCall);
|
||||
* return approved;
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Step-by-step control
|
||||
* ```typescript
|
||||
* const loop = createAgentLoop({ model: 'gpt-4o', tools: { ... } });
|
||||
*
|
||||
* // Initialize with a prompt
|
||||
* loop.addMessage({ role: 'user', content: 'Hello' });
|
||||
*
|
||||
* // Run one step at a time
|
||||
* while (!loop.isComplete()) {
|
||||
* const step = await loop.step();
|
||||
* console.log('Step:', step);
|
||||
*
|
||||
* // Optionally modify messages or tools between steps
|
||||
* if (needsMoreContext) {
|
||||
* loop.addMessage({ role: 'user', content: 'Additional context...' });
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const result = loop.getResult();
|
||||
* ```
|
||||
*/
|
||||
export function createAgentLoop(config: AgentLoopConfig): AgentLoop {
|
||||
return new AgentLoop(config);
|
||||
}
|
||||
|
||||
export class AgentLoop {
|
||||
private config: AgentLoopConfig;
|
||||
private messages: Message[] = [];
|
||||
private steps: AgentStep[] = [];
|
||||
private currentStep = 0;
|
||||
private complete = false;
|
||||
private totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
|
||||
constructor(config: AgentLoopConfig) {
|
||||
this.config = {
|
||||
maxSteps: 10,
|
||||
...config,
|
||||
};
|
||||
|
||||
// Add system message if provided
|
||||
if (config.system) {
|
||||
this.messages.push({ role: 'system', content: config.system });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the conversation.
|
||||
*/
|
||||
addMessage(message: Message): void {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages in the conversation.
|
||||
*/
|
||||
getMessages(): Message[] {
|
||||
return [...this.messages];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the loop is complete.
|
||||
*/
|
||||
isComplete(): boolean {
|
||||
return this.complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single step of the agent loop.
|
||||
*/
|
||||
async step(): Promise<AgentStep> {
|
||||
if (this.complete) {
|
||||
throw new Error('Agent loop is already complete');
|
||||
}
|
||||
|
||||
this.currentStep++;
|
||||
|
||||
// Check max steps
|
||||
if (this.currentStep > (this.config.maxSteps || 10)) {
|
||||
this.complete = true;
|
||||
throw new Error(`Maximum steps (${this.config.maxSteps}) reached`);
|
||||
}
|
||||
|
||||
// Import and call generateText
|
||||
const { generateText } = await import('./generate-text');
|
||||
|
||||
const result = await generateText({
|
||||
model: this.config.model,
|
||||
messages: this.messages as any,
|
||||
tools: this.config.tools,
|
||||
maxSteps: 1, // Single step
|
||||
});
|
||||
|
||||
// Build step result
|
||||
const step: AgentStep = {
|
||||
stepNumber: this.currentStep,
|
||||
text: result.text,
|
||||
toolCalls: result.toolCalls.map(tc => ({
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args: tc.args,
|
||||
})),
|
||||
toolResults: result.toolResults.map(tr => ({
|
||||
toolCallId: tr.toolCallId,
|
||||
toolName: tr.toolName,
|
||||
result: tr.result,
|
||||
})),
|
||||
usage: result.usage,
|
||||
finishReason: result.finishReason,
|
||||
};
|
||||
|
||||
// Update total usage
|
||||
this.totalUsage.promptTokens += result.usage.promptTokens;
|
||||
this.totalUsage.completionTokens += result.usage.completionTokens;
|
||||
this.totalUsage.totalTokens += result.usage.totalTokens;
|
||||
|
||||
// Add assistant message
|
||||
if (result.text) {
|
||||
this.messages.push({ role: 'assistant', content: result.text });
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
if (step.toolCalls.length > 0) {
|
||||
// Check for approval if callback provided
|
||||
if (this.config.onToolCall) {
|
||||
for (const toolCall of step.toolCalls) {
|
||||
const approved = await this.config.onToolCall(toolCall);
|
||||
if (!approved) {
|
||||
this.complete = true;
|
||||
step.finishReason = 'tool_rejected';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool call message
|
||||
const toolCallParts: ToolCallPart[] = step.toolCalls.map(tc => ({
|
||||
type: 'tool-call',
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args: tc.args,
|
||||
}));
|
||||
this.messages.push({ role: 'assistant', content: '', toolCalls: toolCallParts });
|
||||
|
||||
// Add tool results
|
||||
for (const tr of step.toolResults) {
|
||||
const toolResultPart: ToolResultPart = {
|
||||
type: 'tool-result',
|
||||
toolCallId: tr.toolCallId,
|
||||
toolName: tr.toolName,
|
||||
result: tr.result,
|
||||
};
|
||||
this.messages.push({
|
||||
role: 'tool',
|
||||
content: [toolResultPart],
|
||||
toolCallId: tr.toolCallId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store step
|
||||
this.steps.push(step);
|
||||
|
||||
// Call step finish callback
|
||||
if (this.config.onStepFinish) {
|
||||
await this.config.onStepFinish(step);
|
||||
}
|
||||
|
||||
// Check stop conditions
|
||||
if (this.shouldStop(step)) {
|
||||
this.complete = true;
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the full agent loop until completion.
|
||||
*/
|
||||
async run(prompt: string): Promise<AgentLoopResult> {
|
||||
// Add user message
|
||||
this.addMessage({ role: 'user', content: prompt });
|
||||
|
||||
// Run steps until complete
|
||||
while (!this.isComplete()) {
|
||||
try {
|
||||
await this.step();
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('Maximum steps')) {
|
||||
break;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the final result.
|
||||
*/
|
||||
getResult(): AgentLoopResult {
|
||||
const lastStep = this.steps[this.steps.length - 1];
|
||||
return {
|
||||
text: lastStep?.text || '',
|
||||
steps: this.steps,
|
||||
totalUsage: this.totalUsage,
|
||||
finishReason: lastStep?.finishReason || 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the loop should stop.
|
||||
*/
|
||||
private shouldStop(step: AgentStep): boolean {
|
||||
// Check custom stop condition
|
||||
if (this.config.stopWhen) {
|
||||
switch (this.config.stopWhen.type) {
|
||||
case 'stepCount':
|
||||
return this.currentStep >= this.config.stopWhen.count;
|
||||
case 'noToolCalls':
|
||||
return step.toolCalls.length === 0;
|
||||
case 'custom':
|
||||
return this.config.stopWhen.check(step);
|
||||
}
|
||||
}
|
||||
|
||||
// Default: stop when no tool calls
|
||||
return step.toolCalls.length === 0 && step.finishReason === 'stop';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the agent loop.
|
||||
*/
|
||||
reset(): void {
|
||||
this.messages = [];
|
||||
this.steps = [];
|
||||
this.currentStep = 0;
|
||||
this.complete = false;
|
||||
this.totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
|
||||
if (this.config.system) {
|
||||
this.messages.push({ role: 'system', content: this.config.system });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stop condition that stops after N steps.
|
||||
*/
|
||||
export function stopAfterSteps(count: number): StopCondition {
|
||||
return { type: 'stepCount', count };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stop condition that stops when no tool calls are made.
|
||||
*/
|
||||
export function stopWhenNoToolCalls(): StopCondition {
|
||||
return { type: 'noToolCalls' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom stop condition.
|
||||
*/
|
||||
export function stopWhen(check: (step: AgentStep) => boolean): StopCondition {
|
||||
return { type: 'custom', check };
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* DevTools - AI SDK v6 DevTools Integration
|
||||
*
|
||||
* Provides optional DevTools integration for debugging AI SDK calls.
|
||||
* This is an optional dependency that provides visibility into LLM calls.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface DevToolsConfig {
|
||||
/** Enable DevTools (default: auto-detect from NODE_ENV) */
|
||||
enabled?: boolean;
|
||||
/** DevTools server port (default: 3001) */
|
||||
port?: number;
|
||||
/** DevTools server host (default: localhost) */
|
||||
host?: string;
|
||||
/** Project name for DevTools UI */
|
||||
projectName?: string;
|
||||
/** Custom metadata to include */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DevToolsState {
|
||||
enabled: boolean;
|
||||
initialized: boolean;
|
||||
port?: number;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global State
|
||||
// ============================================================================
|
||||
|
||||
let devToolsState: DevToolsState = {
|
||||
enabled: false,
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
let devToolsInstance: unknown = null;
|
||||
|
||||
// ============================================================================
|
||||
// DevTools Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Enable AI SDK DevTools for debugging.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Enable in development
|
||||
* if (process.env.NODE_ENV === 'development') {
|
||||
* await enableDevTools();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example With configuration
|
||||
* ```typescript
|
||||
* await enableDevTools({
|
||||
* port: 3001,
|
||||
* projectName: 'My AI App'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function enableDevTools(config?: DevToolsConfig): Promise<void> {
|
||||
if (devToolsState.initialized && devToolsState.enabled) {
|
||||
return; // Already enabled
|
||||
}
|
||||
|
||||
// Check if we should enable
|
||||
const shouldEnable = config?.enabled ??
|
||||
(process.env.NODE_ENV === 'development' || process.env.PRAISONAI_DEVTOOLS === 'true');
|
||||
|
||||
if (!shouldEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to import @ai-sdk/devtools (optional dependency)
|
||||
// @ts-ignore - Optional dependency
|
||||
const devtools = await import('@ai-sdk/devtools');
|
||||
|
||||
if ('enableDevTools' in devtools) {
|
||||
await (devtools as any).enableDevTools({
|
||||
port: config?.port,
|
||||
host: config?.host,
|
||||
projectName: config?.projectName,
|
||||
metadata: config?.metadata,
|
||||
});
|
||||
|
||||
devToolsInstance = devtools;
|
||||
devToolsState = {
|
||||
enabled: true,
|
||||
initialized: true,
|
||||
port: config?.port || 3001,
|
||||
host: config?.host || 'localhost',
|
||||
};
|
||||
|
||||
console.log(`🔧 AI SDK DevTools enabled at http://${devToolsState.host}:${devToolsState.port}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') {
|
||||
console.warn(
|
||||
'⚠️ @ai-sdk/devtools not installed. Install with:\n' +
|
||||
' npm install @ai-sdk/devtools\n' +
|
||||
' or: pnpm add @ai-sdk/devtools'
|
||||
);
|
||||
} else {
|
||||
console.warn('⚠️ Failed to enable DevTools:', error.message);
|
||||
}
|
||||
|
||||
devToolsState = {
|
||||
enabled: false,
|
||||
initialized: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable DevTools.
|
||||
*/
|
||||
export async function disableDevTools(): Promise<void> {
|
||||
if (!devToolsState.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (devToolsInstance && typeof (devToolsInstance as any).disableDevTools === 'function') {
|
||||
await (devToolsInstance as any).disableDevTools();
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during disable
|
||||
}
|
||||
|
||||
devToolsState = {
|
||||
enabled: false,
|
||||
initialized: true,
|
||||
};
|
||||
devToolsInstance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DevTools is enabled.
|
||||
*/
|
||||
export function isDevToolsEnabled(): boolean {
|
||||
return devToolsState.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DevTools state.
|
||||
*/
|
||||
export function getDevToolsState(): DevToolsState {
|
||||
return { ...devToolsState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DevTools URL.
|
||||
*/
|
||||
export function getDevToolsUrl(): string | null {
|
||||
if (!devToolsState.enabled) {
|
||||
return null;
|
||||
}
|
||||
return `http://${devToolsState.host || 'localhost'}:${devToolsState.port || 3001}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Middleware for DevTools
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create middleware that logs to DevTools.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const model = wrapModel(openai('gpt-4o'), createDevToolsMiddleware());
|
||||
* ```
|
||||
*/
|
||||
export function createDevToolsMiddleware(): any {
|
||||
return {
|
||||
transformParams: async ({ params }: { params: any }) => {
|
||||
if (!devToolsState.enabled) {
|
||||
return params;
|
||||
}
|
||||
|
||||
// Add DevTools metadata
|
||||
return {
|
||||
...params,
|
||||
experimental_telemetry: {
|
||||
...params.experimental_telemetry,
|
||||
isEnabled: true,
|
||||
metadata: {
|
||||
...params.experimental_telemetry?.metadata,
|
||||
devtools: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Auto-enable in Development
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Auto-enable DevTools if in development mode.
|
||||
* Call this at app startup.
|
||||
*/
|
||||
export async function autoEnableDevTools(): Promise<void> {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const isExplicitlyEnabled = process.env.PRAISONAI_DEVTOOLS === 'true';
|
||||
const isExplicitlyDisabled = process.env.PRAISONAI_DEVTOOLS === 'false';
|
||||
|
||||
if (isExplicitlyDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDev || isExplicitlyEnabled) {
|
||||
await enableDevTools();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Embed - AI SDK Wrapper
|
||||
*
|
||||
* Provides embed and embedMany functions for text embeddings.
|
||||
*/
|
||||
|
||||
// Lazy load AI SDK
|
||||
let aiSdk: any = null;
|
||||
let aiSdkLoaded = false;
|
||||
|
||||
async function loadAISDK() {
|
||||
if (aiSdkLoaded) return aiSdk;
|
||||
try {
|
||||
aiSdk = await import('ai');
|
||||
aiSdkLoaded = true;
|
||||
return aiSdk;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EmbedOptions {
|
||||
/** Model to use (e.g., 'text-embedding-3-small', 'openai/text-embedding-3-large') */
|
||||
model: string;
|
||||
/** Text to embed */
|
||||
value: string;
|
||||
/** Maximum retries */
|
||||
maxRetries?: number;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface EmbedResult {
|
||||
/** Embedding vector */
|
||||
embedding: number[];
|
||||
/** Token usage */
|
||||
usage: {
|
||||
tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EmbedManyOptions {
|
||||
/** Model to use */
|
||||
model: string;
|
||||
/** Texts to embed */
|
||||
values: string[];
|
||||
/** Maximum retries */
|
||||
maxRetries?: number;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface EmbedManyResult {
|
||||
/** Embedding vectors */
|
||||
embeddings: number[][];
|
||||
/** Token usage */
|
||||
usage: {
|
||||
tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve model string to AI SDK embedding model instance
|
||||
*/
|
||||
async function resolveEmbeddingModel(modelString: string) {
|
||||
let provider = 'openai';
|
||||
let modelId = modelString;
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const parts = modelString.split('/');
|
||||
provider = parts[0].toLowerCase();
|
||||
modelId = parts.slice(1).join('/');
|
||||
} else {
|
||||
if (modelString.startsWith('text-embedding')) provider = 'openai';
|
||||
else if (modelString.startsWith('voyage')) provider = 'anthropic';
|
||||
else if (modelString.startsWith('embed-')) provider = 'cohere';
|
||||
}
|
||||
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai.embedding(modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
return providerModule.google.embedding(modelId);
|
||||
default:
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai.embedding(modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to load embedding provider '${provider}': ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed a single text using an embedding model.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await embed({
|
||||
* model: 'text-embedding-3-small',
|
||||
* value: 'Hello, world!'
|
||||
* });
|
||||
* console.log(result.embedding); // [0.1, 0.2, ...]
|
||||
* ```
|
||||
*/
|
||||
export async function embed(options: EmbedOptions): Promise<EmbedResult> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
const model = await resolveEmbeddingModel(options.model);
|
||||
|
||||
const result = await sdk.embed({
|
||||
model,
|
||||
value: options.value,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
});
|
||||
|
||||
return {
|
||||
embedding: result.embedding,
|
||||
usage: result.usage || { tokens: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed multiple texts using an embedding model.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await embedMany({
|
||||
* model: 'text-embedding-3-small',
|
||||
* values: ['Hello', 'World']
|
||||
* });
|
||||
* console.log(result.embeddings); // [[0.1, ...], [0.2, ...]]
|
||||
* ```
|
||||
*/
|
||||
export async function embedMany(options: EmbedManyOptions): Promise<EmbedManyResult> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
const model = await resolveEmbeddingModel(options.model);
|
||||
|
||||
const result = await sdk.embedMany({
|
||||
model,
|
||||
values: options.values,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
});
|
||||
|
||||
return {
|
||||
embeddings: result.embeddings,
|
||||
usage: result.usage || { tokens: 0 },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Generate Image - AI SDK Wrapper
|
||||
*
|
||||
* Provides generateImage function for image generation.
|
||||
*/
|
||||
|
||||
// Lazy load AI SDK
|
||||
let aiSdk: any = null;
|
||||
let aiSdkLoaded = false;
|
||||
|
||||
async function loadAISDK() {
|
||||
if (aiSdkLoaded) return aiSdk;
|
||||
try {
|
||||
aiSdk = await import('ai');
|
||||
aiSdkLoaded = true;
|
||||
return aiSdk;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenerateImageOptions {
|
||||
/** Model to use (e.g., 'dall-e-3', 'google/imagen-3') */
|
||||
model: string;
|
||||
/** Text prompt for image generation */
|
||||
prompt: string;
|
||||
/** Number of images to generate */
|
||||
n?: number;
|
||||
/** Image size (e.g., '1024x1024') */
|
||||
size?: `${number}x${number}`;
|
||||
/** Aspect ratio (e.g., '16:9') */
|
||||
aspectRatio?: `${number}:${number}`;
|
||||
/** Seed for reproducibility */
|
||||
seed?: number;
|
||||
/** Maximum retries */
|
||||
maxRetries?: number;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Provider-specific options */
|
||||
providerOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GeneratedImage {
|
||||
/** Base64-encoded image data */
|
||||
base64: string;
|
||||
/** Image as Uint8Array */
|
||||
uint8Array: Uint8Array;
|
||||
/** MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface GenerateImageResult {
|
||||
/** Generated images */
|
||||
images: GeneratedImage[];
|
||||
/** Warnings */
|
||||
warnings?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve model string to AI SDK image model instance
|
||||
*/
|
||||
async function resolveImageModel(modelString: string) {
|
||||
let provider = 'openai';
|
||||
let modelId = modelString;
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const parts = modelString.split('/');
|
||||
provider = parts[0].toLowerCase();
|
||||
modelId = parts.slice(1).join('/');
|
||||
} else {
|
||||
if (modelString.startsWith('imagen')) provider = 'google';
|
||||
else if (modelString.startsWith('dall-e')) provider = 'openai';
|
||||
}
|
||||
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai.image(modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
return providerModule.google.image(modelId);
|
||||
default:
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai.image(modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to load image provider '${provider}': ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate images using an image model.
|
||||
*
|
||||
* @example Generate with DALL-E
|
||||
* ```typescript
|
||||
* const result = await generateImage({
|
||||
* model: 'dall-e-3',
|
||||
* prompt: 'A futuristic city at sunset',
|
||||
* size: '1024x1024'
|
||||
* });
|
||||
*
|
||||
* // Save the image
|
||||
* fs.writeFileSync('image.png', result.images[0].uint8Array);
|
||||
* ```
|
||||
*
|
||||
* @example Generate with Google Imagen
|
||||
* ```typescript
|
||||
* const result = await generateImage({
|
||||
* model: 'google/imagen-3',
|
||||
* prompt: 'A beautiful landscape',
|
||||
* aspectRatio: '16:9'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function generateImage(options: GenerateImageOptions): Promise<GenerateImageResult> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
const model = await resolveImageModel(options.model);
|
||||
|
||||
const result = await sdk.generateImage({
|
||||
model,
|
||||
prompt: options.prompt,
|
||||
n: options.n,
|
||||
size: options.size,
|
||||
aspectRatio: options.aspectRatio,
|
||||
seed: options.seed,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
providerOptions: options.providerOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
images: result.images || [],
|
||||
warnings: result.warnings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Generate Object - AI SDK Wrapper
|
||||
*
|
||||
* Provides generateObject and streamObject functions for structured output generation.
|
||||
*/
|
||||
|
||||
import type { Message } from './types';
|
||||
|
||||
// Lazy load AI SDK
|
||||
let aiSdk: any = null;
|
||||
let aiSdkLoaded = false;
|
||||
|
||||
async function loadAISDK() {
|
||||
if (aiSdkLoaded) return aiSdk;
|
||||
try {
|
||||
aiSdk = await import('ai');
|
||||
aiSdkLoaded = true;
|
||||
return aiSdk;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenerateObjectOptions<T = unknown> {
|
||||
/** Model to use */
|
||||
model: string;
|
||||
/** Schema for the output (Zod schema or JSON schema) */
|
||||
schema: any;
|
||||
/** Schema name for the output */
|
||||
schemaName?: string;
|
||||
/** Schema description */
|
||||
schemaDescription?: string;
|
||||
/** Output mode: 'object' | 'array' | 'enum' | 'no-schema' */
|
||||
mode?: 'object' | 'array' | 'enum' | 'no-schema';
|
||||
/** Simple text prompt */
|
||||
prompt?: string;
|
||||
/** Chat messages */
|
||||
messages?: Message[];
|
||||
/** System message */
|
||||
system?: string;
|
||||
/** Maximum tokens */
|
||||
maxTokens?: number;
|
||||
/** Temperature */
|
||||
temperature?: number;
|
||||
/** Top P */
|
||||
topP?: number;
|
||||
/** Maximum retries */
|
||||
maxRetries?: number;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Callback on finish */
|
||||
onFinish?: (result: GenerateObjectResult<T>) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface GenerateObjectResult<T = unknown> {
|
||||
/** Generated object */
|
||||
object: T;
|
||||
/** Token usage */
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
/** Finish reason */
|
||||
finishReason: string;
|
||||
/** Warnings */
|
||||
warnings?: any[];
|
||||
}
|
||||
|
||||
export interface StreamObjectOptions<T = unknown> extends GenerateObjectOptions<T> {
|
||||
/** Callback for partial object updates */
|
||||
onPartialObject?: (partial: Partial<T>) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface StreamObjectResult<T = unknown> {
|
||||
/** Async iterator for partial objects */
|
||||
partialObjectStream: AsyncIterable<Partial<T>>;
|
||||
/** Promise that resolves to the final object */
|
||||
object: Promise<T>;
|
||||
/** Promise that resolves to usage */
|
||||
usage: Promise<{
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}>;
|
||||
/** Promise that resolves to finish reason */
|
||||
finishReason: Promise<string>;
|
||||
/** Convert to Response for streaming */
|
||||
toTextStreamResponse(options?: { headers?: Record<string, string> }): Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve model string to AI SDK model instance
|
||||
*/
|
||||
async function resolveModel(modelString: string) {
|
||||
let provider = 'openai';
|
||||
let modelId = modelString;
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const parts = modelString.split('/');
|
||||
provider = parts[0].toLowerCase();
|
||||
modelId = parts.slice(1).join('/');
|
||||
} else {
|
||||
if (modelString.startsWith('claude')) provider = 'anthropic';
|
||||
else if (modelString.startsWith('gemini')) provider = 'google';
|
||||
}
|
||||
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(modelId);
|
||||
case 'anthropic':
|
||||
providerModule = await import('@ai-sdk/anthropic');
|
||||
return providerModule.anthropic(modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
return providerModule.google(modelId);
|
||||
default:
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to load provider '${provider}': ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a structured object using a language model.
|
||||
*
|
||||
* @example With Zod schema
|
||||
* ```typescript
|
||||
* import { z } from 'zod';
|
||||
*
|
||||
* const result = await generateObject({
|
||||
* model: 'gpt-4o',
|
||||
* schema: z.object({
|
||||
* name: z.string(),
|
||||
* age: z.number()
|
||||
* }),
|
||||
* prompt: 'Generate a person'
|
||||
* });
|
||||
* console.log(result.object); // { name: 'John', age: 30 }
|
||||
* ```
|
||||
*
|
||||
* @example With JSON schema
|
||||
* ```typescript
|
||||
* const result = await generateObject({
|
||||
* model: 'gpt-4o',
|
||||
* schema: {
|
||||
* type: 'object',
|
||||
* properties: {
|
||||
* name: { type: 'string' },
|
||||
* age: { type: 'number' }
|
||||
* },
|
||||
* required: ['name', 'age']
|
||||
* },
|
||||
* prompt: 'Generate a person'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function generateObject<T = unknown>(
|
||||
options: GenerateObjectOptions<T>
|
||||
): Promise<GenerateObjectResult<T>> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
const model = await resolveModel(options.model);
|
||||
|
||||
const result = await sdk.generateObject({
|
||||
model,
|
||||
schema: options.schema,
|
||||
schemaName: options.schemaName,
|
||||
schemaDescription: options.schemaDescription,
|
||||
mode: options.mode,
|
||||
prompt: options.prompt,
|
||||
messages: options.messages,
|
||||
system: options.system,
|
||||
maxTokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
onFinish: options.onFinish,
|
||||
});
|
||||
|
||||
return {
|
||||
object: result.object as T,
|
||||
usage: result.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
||||
finishReason: result.finishReason || 'stop',
|
||||
warnings: result.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a structured object using a language model.
|
||||
*
|
||||
* @example Streaming partial objects
|
||||
* ```typescript
|
||||
* import { z } from 'zod';
|
||||
*
|
||||
* const result = await streamObject({
|
||||
* model: 'gpt-4o',
|
||||
* schema: z.object({
|
||||
* story: z.string(),
|
||||
* characters: z.array(z.string())
|
||||
* }),
|
||||
* prompt: 'Write a short story',
|
||||
* onPartialObject: (partial) => {
|
||||
* console.log('Partial:', partial);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* const finalObject = await result.object;
|
||||
* ```
|
||||
*/
|
||||
export async function streamObject<T = unknown>(
|
||||
options: StreamObjectOptions<T>
|
||||
): Promise<StreamObjectResult<T>> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
const model = await resolveModel(options.model);
|
||||
|
||||
const result = sdk.streamObject({
|
||||
model,
|
||||
schema: options.schema,
|
||||
schemaName: options.schemaName,
|
||||
schemaDescription: options.schemaDescription,
|
||||
mode: options.mode,
|
||||
prompt: options.prompt,
|
||||
messages: options.messages,
|
||||
system: options.system,
|
||||
maxTokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
onFinish: options.onFinish,
|
||||
});
|
||||
|
||||
return result as StreamObjectResult<T>;
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Generate Text - AI SDK Wrapper
|
||||
*
|
||||
* Provides generateText and streamText functions that wrap the AI SDK.
|
||||
* These are the primary text generation primitives.
|
||||
*/
|
||||
|
||||
import type { Message } from './types';
|
||||
|
||||
// Lazy load AI SDK to avoid import errors when not installed
|
||||
let aiSdk: any = null;
|
||||
let aiSdkLoaded = false;
|
||||
|
||||
async function loadAISDK() {
|
||||
if (aiSdkLoaded) return aiSdk;
|
||||
try {
|
||||
aiSdk = await import('ai');
|
||||
aiSdkLoaded = true;
|
||||
return aiSdk;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenerateTextOptions {
|
||||
/** Model to use (e.g., 'gpt-4o', 'claude-3-sonnet', 'openai/gpt-4o') */
|
||||
model: string;
|
||||
/** Simple text prompt */
|
||||
prompt?: string;
|
||||
/** Chat messages (alternative to prompt) */
|
||||
messages?: Message[];
|
||||
/** System message */
|
||||
system?: string;
|
||||
/** Tools available to the model */
|
||||
tools?: Record<string, any>;
|
||||
/** Tool choice strategy */
|
||||
toolChoice?: 'auto' | 'none' | 'required' | { type: 'tool'; toolName: string };
|
||||
/** Maximum tokens to generate */
|
||||
maxTokens?: number;
|
||||
/** Temperature (0-2) */
|
||||
temperature?: number;
|
||||
/** Top P (0-1) */
|
||||
topP?: number;
|
||||
/** Top K */
|
||||
topK?: number;
|
||||
/** Presence penalty */
|
||||
presencePenalty?: number;
|
||||
/** Frequency penalty */
|
||||
frequencyPenalty?: number;
|
||||
/** Stop sequences */
|
||||
stopSequences?: string[];
|
||||
/** Seed for reproducibility */
|
||||
seed?: number;
|
||||
/** Maximum retries */
|
||||
maxRetries?: number;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Maximum steps for tool calling */
|
||||
maxSteps?: number;
|
||||
/** Callback on step finish */
|
||||
onStepFinish?: (step: StepResult) => void | Promise<void>;
|
||||
/** Callback on finish */
|
||||
onFinish?: (result: GenerateTextResult) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface StepResult {
|
||||
text: string;
|
||||
toolCalls: ToolCall[];
|
||||
toolResults: ToolResult[];
|
||||
usage: TokenUsage;
|
||||
finishReason: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: any;
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: any;
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
export interface GenerateTextResult {
|
||||
/** Generated text */
|
||||
text: string;
|
||||
/** Tool calls made */
|
||||
toolCalls: ToolCall[];
|
||||
/** Tool results */
|
||||
toolResults: ToolResult[];
|
||||
/** Token usage */
|
||||
usage: TokenUsage;
|
||||
/** Finish reason */
|
||||
finishReason: string;
|
||||
/** All steps (for multi-step) */
|
||||
steps: StepResult[];
|
||||
/** Response messages */
|
||||
responseMessages: Message[];
|
||||
/** Warnings */
|
||||
warnings?: any[];
|
||||
}
|
||||
|
||||
export interface StreamTextOptions extends GenerateTextOptions {
|
||||
/** Callback for each text chunk */
|
||||
onChunk?: (chunk: TextStreamPart) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface TextStreamPart {
|
||||
type: 'text-delta' | 'tool-call' | 'tool-result' | 'finish' | 'error';
|
||||
textDelta?: string;
|
||||
toolCall?: ToolCall;
|
||||
toolResult?: ToolResult;
|
||||
finishReason?: string;
|
||||
usage?: TokenUsage;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface StreamTextResult {
|
||||
/** Async iterator for text chunks */
|
||||
textStream: AsyncIterable<string>;
|
||||
/** Full text stream with all events */
|
||||
fullStream: AsyncIterable<TextStreamPart>;
|
||||
/** Promise that resolves to the final text */
|
||||
text: Promise<string>;
|
||||
/** Promise that resolves to tool calls */
|
||||
toolCalls: Promise<ToolCall[]>;
|
||||
/** Promise that resolves to tool results */
|
||||
toolResults: Promise<ToolResult[]>;
|
||||
/** Promise that resolves to usage */
|
||||
usage: Promise<TokenUsage>;
|
||||
/** Promise that resolves to finish reason */
|
||||
finishReason: Promise<string>;
|
||||
/** Promise that resolves to all steps */
|
||||
steps: Promise<StepResult[]>;
|
||||
/** Promise that resolves to response messages */
|
||||
responseMessages: Promise<Message[]>;
|
||||
/** Convert to Response object for streaming */
|
||||
toDataStreamResponse(options?: { headers?: Record<string, string> }): Response;
|
||||
/** Pipe to a writable stream */
|
||||
pipeDataStreamToResponse(response: any, options?: { headers?: Record<string, string> }): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve model string to AI SDK model instance
|
||||
*/
|
||||
async function resolveModel(modelString: string) {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
// Parse provider/model format
|
||||
let provider = 'openai';
|
||||
let modelId = modelString;
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const parts = modelString.split('/');
|
||||
provider = parts[0].toLowerCase();
|
||||
modelId = parts.slice(1).join('/');
|
||||
} else {
|
||||
// Infer provider from model name
|
||||
if (modelString.startsWith('claude')) {
|
||||
provider = 'anthropic';
|
||||
} else if (modelString.startsWith('gemini')) {
|
||||
provider = 'google';
|
||||
} else if (modelString.startsWith('gpt') || modelString.startsWith('o1') || modelString.startsWith('o3')) {
|
||||
provider = 'openai';
|
||||
}
|
||||
}
|
||||
|
||||
// Load provider
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(modelId);
|
||||
case 'anthropic':
|
||||
providerModule = await import('@ai-sdk/anthropic');
|
||||
return providerModule.anthropic(modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
return providerModule.google(modelId);
|
||||
default:
|
||||
// Try OpenAI-compatible
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to load provider '${provider}': ${error.message}. Install with: npm install @ai-sdk/${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a language model.
|
||||
*
|
||||
* @example Simple prompt
|
||||
* ```typescript
|
||||
* const result = await generateText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'What is the capital of France?'
|
||||
* });
|
||||
* console.log(result.text);
|
||||
* ```
|
||||
*
|
||||
* @example Chat messages
|
||||
* ```typescript
|
||||
* const result = await generateText({
|
||||
* model: 'claude-3-sonnet',
|
||||
* messages: [
|
||||
* { role: 'user', content: 'Hello!' }
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example With tools
|
||||
* ```typescript
|
||||
* const result = await generateText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'What is the weather in Paris?',
|
||||
* tools: {
|
||||
* getWeather: {
|
||||
* description: 'Get weather for a city',
|
||||
* parameters: z.object({ city: z.string() }),
|
||||
* execute: async ({ city }) => `Weather in ${city}: 20°C`
|
||||
* }
|
||||
* },
|
||||
* maxSteps: 5
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
// Handle both string model names and pre-resolved model objects
|
||||
const model = typeof options.model === 'string'
|
||||
? await resolveModel(options.model)
|
||||
: options.model;
|
||||
|
||||
const result = await sdk.generateText({
|
||||
model,
|
||||
prompt: options.prompt,
|
||||
messages: options.messages,
|
||||
system: options.system,
|
||||
tools: options.tools,
|
||||
toolChoice: options.toolChoice,
|
||||
maxTokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
topK: options.topK,
|
||||
presencePenalty: options.presencePenalty,
|
||||
frequencyPenalty: options.frequencyPenalty,
|
||||
stopSequences: options.stopSequences,
|
||||
seed: options.seed,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
maxSteps: options.maxSteps,
|
||||
onStepFinish: options.onStepFinish,
|
||||
onFinish: options.onFinish,
|
||||
});
|
||||
|
||||
return {
|
||||
text: result.text,
|
||||
toolCalls: result.toolCalls || [],
|
||||
toolResults: result.toolResults || [],
|
||||
usage: result.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
||||
finishReason: result.finishReason || 'stop',
|
||||
steps: result.steps || [],
|
||||
responseMessages: result.responseMessages || [],
|
||||
warnings: result.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text using a language model.
|
||||
*
|
||||
* @example Simple streaming
|
||||
* ```typescript
|
||||
* const result = await streamText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'Write a poem about AI'
|
||||
* });
|
||||
*
|
||||
* for await (const chunk of result.textStream) {
|
||||
* process.stdout.write(chunk);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example With tools and multi-step
|
||||
* ```typescript
|
||||
* const result = await streamText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'Search for AI news and summarize',
|
||||
* tools: { webSearch },
|
||||
* maxSteps: 5,
|
||||
* onChunk: (chunk) => console.log(chunk)
|
||||
* });
|
||||
*
|
||||
* const text = await result.text;
|
||||
* ```
|
||||
*/
|
||||
export async function streamText(options: StreamTextOptions): Promise<StreamTextResult> {
|
||||
const sdk = await loadAISDK();
|
||||
if (!sdk) {
|
||||
throw new Error('AI SDK not available. Install with: npm install ai @ai-sdk/openai');
|
||||
}
|
||||
|
||||
// Handle both string model names and pre-resolved model objects
|
||||
const model = typeof options.model === 'string'
|
||||
? await resolveModel(options.model)
|
||||
: options.model;
|
||||
|
||||
const result = sdk.streamText({
|
||||
model,
|
||||
prompt: options.prompt,
|
||||
messages: options.messages,
|
||||
system: options.system,
|
||||
tools: options.tools,
|
||||
toolChoice: options.toolChoice,
|
||||
maxTokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
topK: options.topK,
|
||||
presencePenalty: options.presencePenalty,
|
||||
frequencyPenalty: options.frequencyPenalty,
|
||||
stopSequences: options.stopSequences,
|
||||
seed: options.seed,
|
||||
maxRetries: options.maxRetries,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
maxSteps: options.maxSteps,
|
||||
onStepFinish: options.onStepFinish,
|
||||
onFinish: options.onFinish,
|
||||
onChunk: options.onChunk ? async (chunk: any) => {
|
||||
await options.onChunk!(chunk as TextStreamPart);
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
return result as StreamTextResult;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* AI SDK Wrapper Module
|
||||
*
|
||||
* This module provides a stable wrapper over the AI SDK (aisdk) primitives.
|
||||
* It isolates aisdk imports and provides typed wrappers to ensure future
|
||||
* aisdk upgrades don't break praisonai-ts users.
|
||||
*
|
||||
* @module ai
|
||||
*/
|
||||
|
||||
// Re-export core execution functions
|
||||
export {
|
||||
generateText,
|
||||
streamText,
|
||||
type GenerateTextOptions,
|
||||
type GenerateTextResult,
|
||||
type StreamTextOptions,
|
||||
type StreamTextResult,
|
||||
type TextStreamPart,
|
||||
} from './generate-text';
|
||||
|
||||
export {
|
||||
generateObject,
|
||||
streamObject,
|
||||
type GenerateObjectOptions,
|
||||
type GenerateObjectResult,
|
||||
type StreamObjectOptions,
|
||||
type StreamObjectResult,
|
||||
} from './generate-object';
|
||||
|
||||
export {
|
||||
generateImage,
|
||||
type GenerateImageOptions,
|
||||
type GenerateImageResult,
|
||||
} from './generate-image';
|
||||
|
||||
export {
|
||||
embed,
|
||||
embedMany,
|
||||
type EmbedOptions,
|
||||
type EmbedResult,
|
||||
type EmbedManyResult,
|
||||
} from './embed';
|
||||
|
||||
// Re-export tool utilities
|
||||
export {
|
||||
defineTool,
|
||||
createToolSet,
|
||||
functionToTool,
|
||||
type ToolDefinition,
|
||||
type ToolExecuteFunction,
|
||||
type ToolInput,
|
||||
type ToolOutput,
|
||||
} from './tools';
|
||||
|
||||
// Re-export model utilities
|
||||
export {
|
||||
createModel,
|
||||
getModel,
|
||||
parseModel,
|
||||
MODEL_ALIASES,
|
||||
listModelAliases,
|
||||
hasModelAlias,
|
||||
resolveModelAlias,
|
||||
type ModelConfig,
|
||||
type ModelId,
|
||||
} from './models';
|
||||
|
||||
// Re-export middleware
|
||||
export {
|
||||
createCachingMiddleware,
|
||||
createLoggingMiddleware,
|
||||
wrapModel,
|
||||
applyMiddleware,
|
||||
clearCache,
|
||||
getCacheStats,
|
||||
type Middleware,
|
||||
type MiddlewareConfig,
|
||||
type MiddlewareRequest,
|
||||
type MiddlewareResponse,
|
||||
} from './middleware';
|
||||
|
||||
// Re-export multimodal utilities
|
||||
export {
|
||||
createImagePart,
|
||||
createFilePart,
|
||||
createPdfPart,
|
||||
createTextPart,
|
||||
createMultimodalMessage,
|
||||
toMessageContent,
|
||||
base64ToUint8Array,
|
||||
uint8ArrayToBase64,
|
||||
isUrl,
|
||||
isDataUrl,
|
||||
type InputPart,
|
||||
type ImagePart,
|
||||
type FilePart,
|
||||
type PdfPart,
|
||||
type TextPart,
|
||||
} from './multimodal';
|
||||
|
||||
// Re-export MCP client
|
||||
export {
|
||||
createMCP,
|
||||
getMCPClient,
|
||||
closeMCPClient,
|
||||
closeAllMCPClients,
|
||||
mcpToolsToAITools,
|
||||
type MCPConfig,
|
||||
type MCPClient,
|
||||
type MCPTool,
|
||||
type MCPResource,
|
||||
type MCPPrompt,
|
||||
} from './mcp';
|
||||
|
||||
// Re-export server adapters
|
||||
export {
|
||||
createHttpHandler,
|
||||
createExpressHandler,
|
||||
createHonoHandler,
|
||||
createFastifyHandler,
|
||||
createNestHandler,
|
||||
type ServerHandler,
|
||||
type ServerHandlerConfig,
|
||||
} from './server';
|
||||
|
||||
// Re-export React/Next.js utilities (lazy loaded)
|
||||
export {
|
||||
createRouteHandler,
|
||||
createPagesHandler,
|
||||
type RouteHandlerConfig,
|
||||
type UseChatConfig,
|
||||
} from './nextjs';
|
||||
|
||||
// Re-export agent loop utilities
|
||||
export {
|
||||
createAgentLoop,
|
||||
AgentLoop,
|
||||
stopAfterSteps,
|
||||
stopWhenNoToolCalls,
|
||||
stopWhen,
|
||||
type AgentLoopConfig,
|
||||
type AgentStep,
|
||||
type AgentLoopResult,
|
||||
type StopCondition,
|
||||
} from './agent-loop';
|
||||
|
||||
// Re-export UI message utilities
|
||||
export {
|
||||
convertToModelMessages,
|
||||
convertToUIMessages,
|
||||
validateUIMessages,
|
||||
safeValidateUIMessages,
|
||||
createTextMessage,
|
||||
createSystemMessage,
|
||||
hasPendingApprovals,
|
||||
getToolsNeedingApproval,
|
||||
createApprovalResponse,
|
||||
toUIMessageStreamResponse,
|
||||
pipeUIMessageStreamToResponse,
|
||||
type UIMessage,
|
||||
type UIMessagePart,
|
||||
type TextUIPart,
|
||||
type ReasoningUIPart,
|
||||
type ToolUIPart,
|
||||
type FileUIPart,
|
||||
type DataUIPart,
|
||||
type ModelMessage,
|
||||
type UIMessageStreamOptions,
|
||||
} from './ui-message';
|
||||
|
||||
// Re-export tool approval utilities
|
||||
export {
|
||||
ApprovalManager,
|
||||
getApprovalManager,
|
||||
setApprovalManager,
|
||||
withApproval,
|
||||
createCLIApprovalPrompt,
|
||||
ToolApprovalDeniedError,
|
||||
ToolApprovalTimeoutError,
|
||||
DANGEROUS_PATTERNS,
|
||||
isDangerous,
|
||||
createDangerousPatternChecker,
|
||||
type ToolApprovalConfig,
|
||||
type ToolApprovalRequest,
|
||||
type ToolApprovalResponse,
|
||||
type ApprovalState,
|
||||
type ApprovalHandler,
|
||||
} from './tool-approval';
|
||||
|
||||
// Re-export speech and transcription
|
||||
export {
|
||||
generateSpeech,
|
||||
transcribe,
|
||||
SPEECH_MODELS,
|
||||
TRANSCRIPTION_MODELS,
|
||||
type GenerateSpeechOptions,
|
||||
type GenerateSpeechResult,
|
||||
type TranscribeOptions,
|
||||
type TranscribeResult,
|
||||
type TranscriptionSegment,
|
||||
} from './speech';
|
||||
|
||||
// Re-export DevTools
|
||||
export {
|
||||
enableDevTools,
|
||||
disableDevTools,
|
||||
isDevToolsEnabled,
|
||||
getDevToolsState,
|
||||
getDevToolsUrl,
|
||||
createDevToolsMiddleware,
|
||||
autoEnableDevTools,
|
||||
type DevToolsConfig,
|
||||
type DevToolsState,
|
||||
} from './devtools';
|
||||
|
||||
// Re-export OAuth provider for MCP
|
||||
export type { OAuthClientProvider } from './mcp';
|
||||
|
||||
// Re-export telemetry utilities
|
||||
export {
|
||||
configureTelemetry,
|
||||
getTelemetrySettings,
|
||||
enableTelemetry as enableAITelemetry,
|
||||
disableTelemetry as disableAITelemetry,
|
||||
isTelemetryEnabled,
|
||||
initOpenTelemetry,
|
||||
getTracer,
|
||||
createAISpan,
|
||||
withSpan,
|
||||
createTelemetryMiddleware,
|
||||
recordEvent,
|
||||
getEvents,
|
||||
clearEvents,
|
||||
createTelemetrySettings,
|
||||
type TelemetrySettings,
|
||||
type Tracer,
|
||||
type Span,
|
||||
type SpanOptions,
|
||||
type SpanKind,
|
||||
type SpanStatus,
|
||||
type TelemetryEvent,
|
||||
} from './telemetry';
|
||||
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* MCP (Model Context Protocol) - AI SDK Wrapper
|
||||
*
|
||||
* Provides MCP client utilities for connecting to MCP servers and using their tools.
|
||||
*/
|
||||
|
||||
export interface MCPConfig {
|
||||
/** Transport configuration */
|
||||
transport: MCPTransportConfig;
|
||||
/** Client name */
|
||||
name?: string;
|
||||
/** Client version */
|
||||
version?: string;
|
||||
/** Callback for uncaught errors */
|
||||
onUncaughtError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export type MCPTransportConfig =
|
||||
| { type: 'stdio'; command: string; args?: string[]; env?: Record<string, string> }
|
||||
| { type: 'sse'; url: string; headers?: Record<string, string>; authProvider?: OAuthClientProvider }
|
||||
| { type: 'http'; url: string; headers?: Record<string, string>; authProvider?: OAuthClientProvider }
|
||||
| { type: 'websocket'; url: string; headers?: Record<string, string> };
|
||||
|
||||
/**
|
||||
* OAuth client provider for MCP authentication.
|
||||
*/
|
||||
export interface OAuthClientProvider {
|
||||
/** Get the current access token */
|
||||
getAccessToken(): Promise<string | null>;
|
||||
/** Refresh the access token */
|
||||
refreshToken?(): Promise<string | null>;
|
||||
/** Handle OAuth redirect */
|
||||
handleRedirect?(url: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MCPClient {
|
||||
/** Get tools from the MCP server */
|
||||
tools(): Promise<Record<string, MCPTool>>;
|
||||
/** List available resources */
|
||||
listResources(): Promise<MCPResource[]>;
|
||||
/** Read a resource */
|
||||
readResource(uri: string): Promise<MCPResourceContent>;
|
||||
/** List available prompts */
|
||||
listPrompts(): Promise<MCPPrompt[]>;
|
||||
/** Get a prompt */
|
||||
getPrompt(name: string, args?: Record<string, string>): Promise<MCPPromptResult>;
|
||||
/** Close the connection */
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MCPTool {
|
||||
/** Tool name */
|
||||
name: string;
|
||||
/** Tool description */
|
||||
description?: string;
|
||||
/** Input schema */
|
||||
inputSchema: any;
|
||||
/** Execute the tool */
|
||||
execute: (args: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface MCPResource {
|
||||
/** Resource URI */
|
||||
uri: string;
|
||||
/** Resource name */
|
||||
name: string;
|
||||
/** Resource description */
|
||||
description?: string;
|
||||
/** MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface MCPResourceContent {
|
||||
/** Content URI */
|
||||
uri: string;
|
||||
/** Content MIME type */
|
||||
mimeType?: string;
|
||||
/** Text content */
|
||||
text?: string;
|
||||
/** Binary content (base64) */
|
||||
blob?: string;
|
||||
}
|
||||
|
||||
export interface MCPPrompt {
|
||||
/** Prompt name */
|
||||
name: string;
|
||||
/** Prompt description */
|
||||
description?: string;
|
||||
/** Arguments */
|
||||
arguments?: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface MCPPromptResult {
|
||||
/** Prompt description */
|
||||
description?: string;
|
||||
/** Messages */
|
||||
messages: Array<{
|
||||
role: 'user' | 'assistant';
|
||||
content: { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string };
|
||||
}>;
|
||||
}
|
||||
|
||||
// Connection pool for MCP clients
|
||||
const clientPool = new Map<string, MCPClient>();
|
||||
|
||||
/**
|
||||
* Create an MCP client.
|
||||
*
|
||||
* @example Stdio transport (local server)
|
||||
* ```typescript
|
||||
* const client = await createMCP({
|
||||
* transport: {
|
||||
* type: 'stdio',
|
||||
* command: 'npx',
|
||||
* args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/dir']
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* const tools = await client.tools();
|
||||
* ```
|
||||
*
|
||||
* @example SSE transport (remote server)
|
||||
* ```typescript
|
||||
* const client = await createMCP({
|
||||
* transport: {
|
||||
* type: 'sse',
|
||||
* url: 'https://mcp-server.example.com/sse'
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createMCP(config: MCPConfig): Promise<MCPClient> {
|
||||
// Try to use AI SDK MCP client (optional dependency)
|
||||
try {
|
||||
// @ts-ignore - Optional dependency
|
||||
const mcpModule = await import('@ai-sdk/mcp');
|
||||
|
||||
const transport = createTransport(config.transport);
|
||||
const client = await mcpModule.createMCPClient({
|
||||
transport,
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
onUncaughtError: config.onUncaughtError,
|
||||
});
|
||||
|
||||
return {
|
||||
tools: async () => {
|
||||
const toolSet = await client.tools();
|
||||
return toolSet as Record<string, MCPTool>;
|
||||
},
|
||||
listResources: async () => {
|
||||
const result = await client.listResources();
|
||||
return result.resources || [];
|
||||
},
|
||||
readResource: async (uri: string) => {
|
||||
const result = await client.readResource({ uri });
|
||||
return result.contents?.[0] || { uri };
|
||||
},
|
||||
listPrompts: async () => {
|
||||
const result = await client.listPrompts();
|
||||
return result.prompts || [];
|
||||
},
|
||||
getPrompt: async (name: string, args?: Record<string, string>) => {
|
||||
const result = await client.getPrompt({ name, arguments: args });
|
||||
return result as MCPPromptResult;
|
||||
},
|
||||
close: async () => {
|
||||
await client.close();
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
// Fall back to native implementation
|
||||
return createNativeMCPClient(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create transport configuration for AI SDK MCP.
|
||||
*/
|
||||
function createTransport(config: MCPTransportConfig): any {
|
||||
switch (config.type) {
|
||||
case 'stdio':
|
||||
return {
|
||||
type: 'stdio',
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: config.env,
|
||||
};
|
||||
case 'sse':
|
||||
return {
|
||||
type: 'sse',
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
authProvider: config.authProvider,
|
||||
};
|
||||
case 'http':
|
||||
return {
|
||||
type: 'http',
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
authProvider: config.authProvider,
|
||||
};
|
||||
case 'websocket':
|
||||
return {
|
||||
type: 'websocket',
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown transport type: ${(config as any).type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Native MCP client implementation (fallback).
|
||||
*/
|
||||
async function createNativeMCPClient(config: MCPConfig): Promise<MCPClient> {
|
||||
// This is a simplified native implementation
|
||||
// In production, this would use the @modelcontextprotocol/sdk directly
|
||||
|
||||
const tools: Record<string, MCPTool> = {};
|
||||
const resources: MCPResource[] = [];
|
||||
const prompts: MCPPrompt[] = [];
|
||||
|
||||
return {
|
||||
tools: async () => tools,
|
||||
listResources: async () => resources,
|
||||
readResource: async (uri: string) => ({ uri }),
|
||||
listPrompts: async () => prompts,
|
||||
getPrompt: async (name: string) => ({ messages: [] }),
|
||||
close: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a pooled MCP client.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = await getMCPClient('filesystem', {
|
||||
* transport: { type: 'stdio', command: 'npx', args: ['-y', '@mcp/server-fs'] }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function getMCPClient(key: string, config: MCPConfig): Promise<MCPClient> {
|
||||
if (clientPool.has(key)) {
|
||||
return clientPool.get(key)!;
|
||||
}
|
||||
|
||||
const client = await createMCP(config);
|
||||
clientPool.set(key, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and remove a pooled MCP client.
|
||||
*/
|
||||
export async function closeMCPClient(key: string): Promise<void> {
|
||||
const client = clientPool.get(key);
|
||||
if (client) {
|
||||
await client.close();
|
||||
clientPool.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all pooled MCP clients.
|
||||
*/
|
||||
export async function closeAllMCPClients(): Promise<void> {
|
||||
for (const [key, client] of clientPool) {
|
||||
await client.close();
|
||||
clientPool.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MCP tools to AI SDK tool format.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = await createMCP({ ... });
|
||||
* const mcpTools = await client.tools();
|
||||
* const aiTools = mcpToolsToAITools(mcpTools);
|
||||
*
|
||||
* const result = await generateText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'List files in the current directory',
|
||||
* tools: aiTools
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function mcpToolsToAITools(mcpTools: Record<string, MCPTool>): Record<string, any> {
|
||||
const aiTools: Record<string, any> = {};
|
||||
|
||||
for (const [name, tool] of Object.entries(mcpTools)) {
|
||||
aiTools[name] = {
|
||||
description: tool.description || `MCP tool: ${name}`,
|
||||
parameters: tool.inputSchema,
|
||||
execute: tool.execute,
|
||||
};
|
||||
}
|
||||
|
||||
return aiTools;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Middleware - AI SDK Wrapper
|
||||
*
|
||||
* Provides middleware utilities for caching, logging, and model wrapping.
|
||||
*/
|
||||
|
||||
export interface Middleware {
|
||||
/** Middleware name */
|
||||
name: string;
|
||||
/** Transform request before sending */
|
||||
transformRequest?: (request: MiddlewareRequest) => Promise<MiddlewareRequest> | MiddlewareRequest;
|
||||
/** Transform response after receiving */
|
||||
transformResponse?: (response: MiddlewareResponse) => Promise<MiddlewareResponse> | MiddlewareResponse;
|
||||
/** Handle errors */
|
||||
onError?: (error: Error) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface MiddlewareRequest {
|
||||
model: string;
|
||||
prompt?: string;
|
||||
messages?: any[];
|
||||
system?: string;
|
||||
tools?: Record<string, any>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MiddlewareResponse {
|
||||
text?: string;
|
||||
object?: any;
|
||||
toolCalls?: any[];
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MiddlewareConfig {
|
||||
/** Enable caching */
|
||||
cache?: boolean;
|
||||
/** Cache TTL in seconds */
|
||||
cacheTTL?: number;
|
||||
/** Enable logging */
|
||||
logging?: boolean;
|
||||
/** Log level */
|
||||
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
||||
/** Custom middlewares */
|
||||
middlewares?: Middleware[];
|
||||
}
|
||||
|
||||
// In-memory cache for middleware
|
||||
const responseCache = new Map<string, { response: any; timestamp: number }>();
|
||||
|
||||
/**
|
||||
* Create a caching middleware.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cachingMiddleware = createCachingMiddleware({
|
||||
* ttl: 3600, // 1 hour
|
||||
* keyGenerator: (request) => `${request.model}:${request.prompt}`
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createCachingMiddleware(options?: {
|
||||
ttl?: number;
|
||||
keyGenerator?: (request: MiddlewareRequest) => string;
|
||||
storage?: {
|
||||
get: (key: string) => Promise<any>;
|
||||
set: (key: string, value: any, ttl?: number) => Promise<void>;
|
||||
};
|
||||
}): Middleware {
|
||||
const ttl = options?.ttl ?? 3600;
|
||||
const keyGenerator = options?.keyGenerator ?? defaultKeyGenerator;
|
||||
const storage = options?.storage;
|
||||
|
||||
return {
|
||||
name: 'caching',
|
||||
transformRequest: async (request) => {
|
||||
const key = keyGenerator(request);
|
||||
|
||||
if (storage) {
|
||||
const cached = await storage.get(key);
|
||||
if (cached) {
|
||||
(request as any).__cached = cached;
|
||||
(request as any).__cacheHit = true;
|
||||
}
|
||||
} else {
|
||||
const cached = responseCache.get(key);
|
||||
if (cached && Date.now() - cached.timestamp < ttl * 1000) {
|
||||
(request as any).__cached = cached.response;
|
||||
(request as any).__cacheHit = true;
|
||||
}
|
||||
}
|
||||
|
||||
(request as any).__cacheKey = key;
|
||||
return request;
|
||||
},
|
||||
transformResponse: async (response) => {
|
||||
const key = (response as any).__cacheKey;
|
||||
if (key && !(response as any).__cacheHit) {
|
||||
if (storage) {
|
||||
await storage.set(key, response, ttl);
|
||||
} else {
|
||||
responseCache.set(key, { response, timestamp: Date.now() });
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logging middleware.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const loggingMiddleware = createLoggingMiddleware({
|
||||
* level: 'debug',
|
||||
* onRequest: (request) => console.log('Request:', request),
|
||||
* onResponse: (response) => console.log('Response:', response)
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createLoggingMiddleware(options?: {
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
onRequest?: (request: MiddlewareRequest) => void;
|
||||
onResponse?: (response: MiddlewareResponse) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}): Middleware {
|
||||
const level = options?.level ?? 'info';
|
||||
const log = (msg: string, data?: any) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
console[level](`[${timestamp}] ${msg}`, data ?? '');
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'logging',
|
||||
transformRequest: async (request) => {
|
||||
log('AI Request', { model: request.model, hasPrompt: !!request.prompt, hasMessages: !!request.messages });
|
||||
options?.onRequest?.(request);
|
||||
return request;
|
||||
},
|
||||
transformResponse: async (response) => {
|
||||
log('AI Response', { hasText: !!response.text, hasObject: !!response.object, usage: response.usage });
|
||||
options?.onResponse?.(response);
|
||||
return response;
|
||||
},
|
||||
onError: async (error) => {
|
||||
log('AI Error', { message: error.message });
|
||||
options?.onError?.(error);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a model with middleware.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const wrappedModel = wrapModel(model, [
|
||||
* createCachingMiddleware(),
|
||||
* createLoggingMiddleware()
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
export function wrapModel(model: any, middlewares: Middleware[]): any {
|
||||
// This is a placeholder - actual implementation would wrap the model's
|
||||
// doGenerate and doStream methods with middleware transformations
|
||||
return {
|
||||
...model,
|
||||
__middlewares: middlewares,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply middleware to a request/response cycle.
|
||||
*/
|
||||
export async function applyMiddleware(
|
||||
middlewares: Middleware[],
|
||||
request: MiddlewareRequest,
|
||||
execute: (req: MiddlewareRequest) => Promise<MiddlewareResponse>
|
||||
): Promise<MiddlewareResponse> {
|
||||
// Transform request through all middlewares
|
||||
let transformedRequest = request;
|
||||
for (const middleware of middlewares) {
|
||||
if (middleware.transformRequest) {
|
||||
transformedRequest = await middleware.transformRequest(transformedRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cache hit
|
||||
if ((transformedRequest as any).__cacheHit) {
|
||||
return (transformedRequest as any).__cached;
|
||||
}
|
||||
|
||||
// Execute the actual request
|
||||
let response: MiddlewareResponse;
|
||||
try {
|
||||
response = await execute(transformedRequest);
|
||||
(response as any).__cacheKey = (transformedRequest as any).__cacheKey;
|
||||
} catch (error: any) {
|
||||
// Handle errors through middlewares
|
||||
for (const middleware of middlewares) {
|
||||
if (middleware.onError) {
|
||||
await middleware.onError(error);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Transform response through all middlewares (in reverse order)
|
||||
let transformedResponse = response;
|
||||
for (const middleware of [...middlewares].reverse()) {
|
||||
if (middleware.transformResponse) {
|
||||
transformedResponse = await middleware.transformResponse(transformedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return transformedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default cache key generator.
|
||||
*/
|
||||
function defaultKeyGenerator(request: MiddlewareRequest): string {
|
||||
const parts = [
|
||||
request.model,
|
||||
request.prompt || '',
|
||||
JSON.stringify(request.messages || []),
|
||||
request.system || '',
|
||||
];
|
||||
return parts.join(':').slice(0, 256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the response cache.
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
responseCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics.
|
||||
*/
|
||||
export function getCacheStats(): { size: number; keys: string[] } {
|
||||
return {
|
||||
size: responseCache.size,
|
||||
keys: Array.from(responseCache.keys()),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Models - AI SDK Wrapper
|
||||
*
|
||||
* Provides model aliases and utilities for model resolution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Model aliases for common models across providers.
|
||||
* Maps friendly names to provider/model format.
|
||||
*/
|
||||
export const MODEL_ALIASES: Record<string, string> = {
|
||||
// OpenAI
|
||||
'gpt-4o': 'openai/gpt-4o',
|
||||
'gpt-4o-mini': 'openai/gpt-4o-mini',
|
||||
'gpt-4-turbo': 'openai/gpt-4-turbo',
|
||||
'gpt-4': 'openai/gpt-4',
|
||||
'gpt-3.5-turbo': 'openai/gpt-3.5-turbo',
|
||||
'gpt-5': 'openai/gpt-5',
|
||||
'o1': 'openai/o1',
|
||||
'o1-mini': 'openai/o1-mini',
|
||||
'o1-preview': 'openai/o1-preview',
|
||||
'o3': 'openai/o3',
|
||||
'o3-mini': 'openai/o3-mini',
|
||||
|
||||
// Anthropic
|
||||
'claude-4': 'anthropic/claude-4-sonnet-20250514',
|
||||
'claude-4-opus': 'anthropic/claude-4-opus-20250514',
|
||||
'claude-4-sonnet': 'anthropic/claude-4-sonnet-20250514',
|
||||
'claude-3.7-sonnet': 'anthropic/claude-3-7-sonnet-20250219',
|
||||
'claude-3.5-sonnet': 'anthropic/claude-3-5-sonnet-20241022',
|
||||
'claude-3-sonnet': 'anthropic/claude-3-sonnet-20240229',
|
||||
'claude-3-opus': 'anthropic/claude-3-opus-20240229',
|
||||
'claude-3-haiku': 'anthropic/claude-3-haiku-20240307',
|
||||
|
||||
// Google
|
||||
'gemini-3': 'google/gemini-3.0-pro',
|
||||
'gemini-2.5-pro': 'google/gemini-2.5-pro-preview-06-05',
|
||||
'gemini-2.5-flash': 'google/gemini-2.5-flash-preview-05-20',
|
||||
'gemini-2.0-flash': 'google/gemini-2.0-flash',
|
||||
'gemini-1.5-pro': 'google/gemini-1.5-pro',
|
||||
'gemini-1.5-flash': 'google/gemini-1.5-flash',
|
||||
|
||||
// DeepSeek
|
||||
'deepseek-r1': 'deepseek/deepseek-reasoner',
|
||||
'deepseek-v3': 'deepseek/deepseek-chat',
|
||||
'deepseek-v3.2': 'deepseek/deepseek-chat-v3.2',
|
||||
|
||||
// Meta Llama
|
||||
'llama-3.1': 'groq/llama-3.1-70b-versatile',
|
||||
'llama-3.1-8b': 'groq/llama-3.1-8b-instant',
|
||||
'llama-3.1-70b': 'groq/llama-3.1-70b-versatile',
|
||||
'llama-3.1-405b': 'together/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
|
||||
|
||||
// Embedding models
|
||||
'text-embedding-3-small': 'openai/text-embedding-3-small',
|
||||
'text-embedding-3-large': 'openai/text-embedding-3-large',
|
||||
'text-embedding-004': 'google/text-embedding-004',
|
||||
|
||||
// Image models
|
||||
'dall-e-3': 'openai/dall-e-3',
|
||||
'dall-e-2': 'openai/dall-e-2',
|
||||
'imagen-3': 'google/imagen-3.0-generate-002',
|
||||
};
|
||||
|
||||
export type ModelId = keyof typeof MODEL_ALIASES | string;
|
||||
|
||||
export interface ModelConfig {
|
||||
/** Provider name */
|
||||
provider: string;
|
||||
/** Model ID */
|
||||
modelId: string;
|
||||
/** Original model string */
|
||||
original: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a model string into provider and model ID.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* parseModel('gpt-4o') // { provider: 'openai', modelId: 'gpt-4o', original: 'gpt-4o' }
|
||||
* parseModel('anthropic/claude-3-sonnet') // { provider: 'anthropic', modelId: 'claude-3-sonnet', original: 'anthropic/claude-3-sonnet' }
|
||||
* ```
|
||||
*/
|
||||
export function parseModel(model: ModelId): ModelConfig {
|
||||
// Check aliases first
|
||||
const aliased = MODEL_ALIASES[model];
|
||||
const modelString = aliased || model;
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const parts = modelString.split('/');
|
||||
return {
|
||||
provider: parts[0].toLowerCase(),
|
||||
modelId: parts.slice(1).join('/'),
|
||||
original: model,
|
||||
};
|
||||
}
|
||||
|
||||
// Infer provider from model name
|
||||
let provider = 'openai';
|
||||
if (modelString.startsWith('claude')) provider = 'anthropic';
|
||||
else if (modelString.startsWith('gemini')) provider = 'google';
|
||||
else if (modelString.startsWith('deepseek')) provider = 'deepseek';
|
||||
else if (modelString.startsWith('llama')) provider = 'groq';
|
||||
|
||||
return {
|
||||
provider,
|
||||
modelId: modelString,
|
||||
original: model,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a model instance from a model string.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const model = await getModel('gpt-4o');
|
||||
* const result = await generateText({ model, prompt: 'Hello' });
|
||||
* ```
|
||||
*/
|
||||
export async function getModel(model: ModelId): Promise<any> {
|
||||
const config = parseModel(model);
|
||||
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (config.provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(config.modelId);
|
||||
case 'anthropic':
|
||||
providerModule = await import('@ai-sdk/anthropic');
|
||||
return providerModule.anthropic(config.modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
return providerModule.google(config.modelId);
|
||||
case 'groq':
|
||||
// @ts-ignore - Optional dependency
|
||||
providerModule = await import('@ai-sdk/groq');
|
||||
return providerModule.groq(config.modelId);
|
||||
case 'deepseek':
|
||||
// @ts-ignore - Optional dependency
|
||||
providerModule = await import('@ai-sdk/deepseek');
|
||||
return providerModule.deepseek(config.modelId);
|
||||
default:
|
||||
// Try OpenAI-compatible
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
return providerModule.openai(config.modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to load provider '${config.provider}': ${error.message}. ` +
|
||||
`Install with: npm install @ai-sdk/${config.provider}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model with custom configuration.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const model = await createModel('gpt-4o', {
|
||||
* baseURL: 'https://custom-endpoint.com/v1',
|
||||
* apiKey: 'custom-key'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createModel(
|
||||
model: ModelId,
|
||||
options?: {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<any> {
|
||||
const config = parseModel(model);
|
||||
|
||||
let providerModule: any;
|
||||
try {
|
||||
switch (config.provider) {
|
||||
case 'openai':
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
const openaiProvider = providerModule.createOpenAI({
|
||||
baseURL: options?.baseURL,
|
||||
apiKey: options?.apiKey,
|
||||
headers: options?.headers,
|
||||
});
|
||||
return openaiProvider(config.modelId);
|
||||
case 'anthropic':
|
||||
providerModule = await import('@ai-sdk/anthropic');
|
||||
const anthropicProvider = providerModule.createAnthropic({
|
||||
baseURL: options?.baseURL,
|
||||
apiKey: options?.apiKey,
|
||||
headers: options?.headers,
|
||||
});
|
||||
return anthropicProvider(config.modelId);
|
||||
case 'google':
|
||||
providerModule = await import('@ai-sdk/google');
|
||||
const googleProvider = providerModule.createGoogleGenerativeAI({
|
||||
baseURL: options?.baseURL,
|
||||
apiKey: options?.apiKey,
|
||||
headers: options?.headers,
|
||||
});
|
||||
return googleProvider(config.modelId);
|
||||
default:
|
||||
providerModule = await import('@ai-sdk/openai');
|
||||
const defaultProvider = providerModule.createOpenAI({
|
||||
baseURL: options?.baseURL,
|
||||
apiKey: options?.apiKey,
|
||||
headers: options?.headers,
|
||||
});
|
||||
return defaultProvider(config.modelId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to create model '${model}': ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available model aliases.
|
||||
*/
|
||||
export function listModelAliases(): string[] {
|
||||
return Object.keys(MODEL_ALIASES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model alias exists.
|
||||
*/
|
||||
export function hasModelAlias(alias: string): boolean {
|
||||
return alias in MODEL_ALIASES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model alias to its full provider/model format.
|
||||
*/
|
||||
export function resolveModelAlias(alias: string): string {
|
||||
return MODEL_ALIASES[alias] || alias;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Multimodal - AI SDK Wrapper
|
||||
*
|
||||
* Provides utilities for multimodal inputs (images, files, PDFs).
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export type InputPart = TextPart | ImagePart | FilePart | PdfPart;
|
||||
|
||||
export interface TextPart {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ImagePart {
|
||||
type: 'image';
|
||||
/** Image data as URL, base64 string, or Uint8Array */
|
||||
image: string | Uint8Array | URL;
|
||||
/** MIME type (auto-detected if not provided) */
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface FilePart {
|
||||
type: 'file';
|
||||
/** File data as URL, base64 string, or Uint8Array */
|
||||
data: string | Uint8Array | URL;
|
||||
/** MIME type (required) */
|
||||
mimeType: string;
|
||||
/** File name (optional) */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface PdfPart {
|
||||
type: 'pdf';
|
||||
/** PDF data as URL, base64 string, or Uint8Array */
|
||||
data: string | Uint8Array | URL;
|
||||
/** Page range to extract (optional) */
|
||||
pages?: { start?: number; end?: number };
|
||||
/** Extract text only (default: false) */
|
||||
textOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a text part.
|
||||
*/
|
||||
export function createTextPart(text: string): TextPart {
|
||||
return { type: 'text', text };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an image part from various sources.
|
||||
*
|
||||
* @example From URL
|
||||
* ```typescript
|
||||
* const imagePart = createImagePart('https://example.com/image.png');
|
||||
* ```
|
||||
*
|
||||
* @example From base64
|
||||
* ```typescript
|
||||
* const imagePart = createImagePart('data:image/png;base64,...');
|
||||
* ```
|
||||
*
|
||||
* @example From file path
|
||||
* ```typescript
|
||||
* const imagePart = await createImagePart('./image.png');
|
||||
* ```
|
||||
*/
|
||||
export function createImagePart(
|
||||
source: string | Uint8Array | URL,
|
||||
mimeType?: string
|
||||
): ImagePart {
|
||||
// If it's a file path, read it
|
||||
if (typeof source === 'string' && !source.startsWith('http') && !source.startsWith('data:')) {
|
||||
if (fs.existsSync(source)) {
|
||||
const buffer = fs.readFileSync(source);
|
||||
const detectedMime = mimeType || detectImageMimeType(source);
|
||||
return {
|
||||
type: 'image',
|
||||
image: buffer,
|
||||
mimeType: detectedMime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'image',
|
||||
image: source,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file part from various sources.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const filePart = createFilePart('./document.txt', 'text/plain');
|
||||
* ```
|
||||
*/
|
||||
export function createFilePart(
|
||||
source: string | Uint8Array | URL,
|
||||
mimeType: string,
|
||||
name?: string
|
||||
): FilePart {
|
||||
// If it's a file path, read it
|
||||
if (typeof source === 'string' && !source.startsWith('http') && !source.startsWith('data:')) {
|
||||
if (fs.existsSync(source)) {
|
||||
const buffer = fs.readFileSync(source);
|
||||
return {
|
||||
type: 'file',
|
||||
data: buffer,
|
||||
mimeType,
|
||||
name: name || path.basename(source),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'file',
|
||||
data: source,
|
||||
mimeType,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PDF part from various sources.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const pdfPart = createPdfPart('./document.pdf');
|
||||
* ```
|
||||
*
|
||||
* @example With page range
|
||||
* ```typescript
|
||||
* const pdfPart = createPdfPart('./document.pdf', { pages: { start: 1, end: 5 } });
|
||||
* ```
|
||||
*/
|
||||
export function createPdfPart(
|
||||
source: string | Uint8Array | URL,
|
||||
options?: { pages?: { start?: number; end?: number }; textOnly?: boolean }
|
||||
): PdfPart {
|
||||
// If it's a file path, read it
|
||||
if (typeof source === 'string' && !source.startsWith('http') && !source.startsWith('data:')) {
|
||||
if (fs.existsSync(source)) {
|
||||
const buffer = fs.readFileSync(source);
|
||||
return {
|
||||
type: 'pdf',
|
||||
data: buffer,
|
||||
pages: options?.pages,
|
||||
textOnly: options?.textOnly,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'pdf',
|
||||
data: source,
|
||||
pages: options?.pages,
|
||||
textOnly: options?.textOnly,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input parts to AI SDK message content format.
|
||||
*/
|
||||
export function toMessageContent(parts: InputPart[]): any[] {
|
||||
return parts.map(part => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
return { type: 'text', text: part.text };
|
||||
case 'image':
|
||||
return {
|
||||
type: 'image',
|
||||
image: part.image,
|
||||
mimeType: part.mimeType,
|
||||
};
|
||||
case 'file':
|
||||
return {
|
||||
type: 'file',
|
||||
data: part.data,
|
||||
mimeType: part.mimeType,
|
||||
};
|
||||
case 'pdf':
|
||||
// PDFs are treated as files with application/pdf mime type
|
||||
return {
|
||||
type: 'file',
|
||||
data: part.data,
|
||||
mimeType: 'application/pdf',
|
||||
};
|
||||
default:
|
||||
return part;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a multimodal message with text and images.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const message = createMultimodalMessage(
|
||||
* 'What is in this image?',
|
||||
* ['https://example.com/image.png']
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createMultimodalMessage(
|
||||
text: string,
|
||||
images: (string | Uint8Array | URL)[],
|
||||
role: 'user' | 'assistant' = 'user'
|
||||
): { role: string; content: any[] } {
|
||||
const content: any[] = [];
|
||||
|
||||
// Add images first
|
||||
for (const image of images) {
|
||||
content.push(createImagePart(image));
|
||||
}
|
||||
|
||||
// Add text
|
||||
content.push(createTextPart(text));
|
||||
|
||||
return {
|
||||
role,
|
||||
content: toMessageContent(content as InputPart[]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect image MIME type from file extension.
|
||||
*/
|
||||
function detectImageMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.bmp': 'image/bmp',
|
||||
'.ico': 'image/x-icon',
|
||||
};
|
||||
return mimeTypes[ext] || 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a base64 string to Uint8Array.
|
||||
*/
|
||||
export function base64ToUint8Array(base64: string): Uint8Array {
|
||||
// Handle data URLs
|
||||
const base64Data = base64.includes(',') ? base64.split(',')[1] : base64;
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Uint8Array to base64 string.
|
||||
*/
|
||||
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid URL.
|
||||
*/
|
||||
export function isUrl(str: string): boolean {
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a data URL.
|
||||
*/
|
||||
export function isDataUrl(str: string): boolean {
|
||||
return str.startsWith('data:');
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Next.js Integration - AI SDK Wrapper
|
||||
*
|
||||
* Provides route handlers and React hooks for Next.js applications.
|
||||
*/
|
||||
|
||||
export interface RouteHandlerConfig {
|
||||
/** Agent or handler function */
|
||||
handler: (input: any) => Promise<any>;
|
||||
/** Enable streaming (default: true) */
|
||||
streaming?: boolean;
|
||||
/** Maximum duration in seconds */
|
||||
maxDuration?: number;
|
||||
/** CORS configuration */
|
||||
cors?: boolean | {
|
||||
origin?: string | string[];
|
||||
methods?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Next.js App Router route handler.
|
||||
*
|
||||
* @example app/api/chat/route.ts
|
||||
* ```typescript
|
||||
* import { createRouteHandler } from 'praisonai/ai';
|
||||
* import { streamText } from 'praisonai/ai';
|
||||
*
|
||||
* export const POST = createRouteHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({
|
||||
* model: 'gpt-4o',
|
||||
* messages: input.messages
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example With Agent
|
||||
* ```typescript
|
||||
* import { createRouteHandler } from 'praisonai/ai';
|
||||
* import { Agent } from 'praisonai';
|
||||
*
|
||||
* const agent = new Agent({ instructions: 'You are helpful' });
|
||||
*
|
||||
* export const POST = createRouteHandler({
|
||||
* handler: async (input) => {
|
||||
* return await agent.chat(input.message);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createRouteHandler(config: RouteHandlerConfig) {
|
||||
return async (request: Request): Promise<Response> => {
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
|
||||
// Execute handler
|
||||
const result = await config.handler(body);
|
||||
|
||||
// Handle streaming response
|
||||
if (config.streaming !== false && result.textStream) {
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
try {
|
||||
for await (const chunk of result.textStream) {
|
||||
const data = JSON.stringify({ text: chunk });
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
||||
}
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
...getCorsHeaders(config.cors),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle toDataStreamResponse if available (AI SDK)
|
||||
if (result.toDataStreamResponse) {
|
||||
return result.toDataStreamResponse();
|
||||
}
|
||||
|
||||
// Handle regular response
|
||||
const responseData = typeof result === 'string'
|
||||
? { text: result }
|
||||
: result.text
|
||||
? { text: result.text, usage: result.usage }
|
||||
: result;
|
||||
|
||||
return new Response(JSON.stringify(responseData), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getCorsHeaders(config.cors),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Route handler error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Internal server error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getCorsHeaders(config.cors),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Next.js Pages Router API handler.
|
||||
*
|
||||
* @example pages/api/chat.ts
|
||||
* ```typescript
|
||||
* import { createPagesHandler } from 'praisonai/ai';
|
||||
* import { streamText } from 'praisonai/ai';
|
||||
*
|
||||
* export default createPagesHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({
|
||||
* model: 'gpt-4o',
|
||||
* messages: input.messages
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createPagesHandler(config: RouteHandlerConfig) {
|
||||
return async (req: any, res: any): Promise<void> => {
|
||||
try {
|
||||
// Set CORS headers
|
||||
if (config.cors) {
|
||||
const headers = getCorsHeaders(config.cors);
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute handler
|
||||
const result = await config.handler(req.body);
|
||||
|
||||
// Handle streaming response
|
||||
if (config.streaming !== false && result.textStream) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
|
||||
}
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle regular response
|
||||
const responseData = typeof result === 'string'
|
||||
? { text: result }
|
||||
: result.text
|
||||
? { text: result.text, usage: result.usage }
|
||||
: result;
|
||||
|
||||
res.status(200).json(responseData);
|
||||
} catch (error: any) {
|
||||
console.error('Pages handler error:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CORS headers based on configuration.
|
||||
*/
|
||||
function getCorsHeaders(cors?: boolean | { origin?: string | string[]; methods?: string[] }): Record<string, string> {
|
||||
if (!cors) return {};
|
||||
|
||||
const config = typeof cors === 'boolean'
|
||||
? { origin: '*', methods: ['GET', 'POST', 'OPTIONS'] }
|
||||
: cors;
|
||||
|
||||
const origin = Array.isArray(config.origin)
|
||||
? config.origin.join(', ')
|
||||
: (config.origin || '*');
|
||||
|
||||
return {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Methods': (config.methods || ['GET', 'POST', 'OPTIONS']).join(', '),
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for useChat hook wrapper.
|
||||
*/
|
||||
export interface UseChatConfig {
|
||||
/** API endpoint */
|
||||
api?: string;
|
||||
/** Initial messages */
|
||||
initialMessages?: Array<{ role: string; content: string }>;
|
||||
/** On error callback */
|
||||
onError?: (error: Error) => void;
|
||||
/** On finish callback */
|
||||
onFinish?: (message: any) => void;
|
||||
/** Custom headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Custom body */
|
||||
body?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: For React hooks (useChat, useCompletion, useObject),
|
||||
* use the AI SDK React package directly:
|
||||
*
|
||||
* ```typescript
|
||||
* import { useChat } from 'ai/react';
|
||||
*
|
||||
* function ChatComponent() {
|
||||
* const { messages, input, handleInputChange, handleSubmit } = useChat({
|
||||
* api: '/api/chat'
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <form onSubmit={handleSubmit}>
|
||||
* {messages.map(m => <div key={m.id}>{m.content}</div>)}
|
||||
* <input value={input} onChange={handleInputChange} />
|
||||
* </form>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The praisonai package focuses on server-side functionality.
|
||||
* For client-side React hooks, install and use 'ai/react' directly.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Export runtime configuration for Next.js edge runtime.
|
||||
*/
|
||||
export const runtime = 'edge';
|
||||
|
||||
/**
|
||||
* Export max duration configuration.
|
||||
*/
|
||||
export const maxDuration = 30;
|
||||
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* Server Adapters - AI SDK Wrapper
|
||||
*
|
||||
* Provides server adapters for HTTP, Express, Hono, Fastify, and Nest.js.
|
||||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
export interface ServerHandler {
|
||||
/** Handle a request */
|
||||
handle(req: any, res: any): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ServerHandlerConfig {
|
||||
/** Agent or generateText/streamText function */
|
||||
handler: (input: any) => Promise<any>;
|
||||
/** Enable streaming (default: true) */
|
||||
streaming?: boolean;
|
||||
/** CORS headers */
|
||||
cors?: boolean | CorsConfig;
|
||||
/** Request body parser */
|
||||
bodyParser?: (req: any) => Promise<any>;
|
||||
/** Response formatter */
|
||||
responseFormatter?: (result: any) => any;
|
||||
/** Error handler */
|
||||
onError?: (error: Error, req: any, res: any) => void;
|
||||
}
|
||||
|
||||
export interface CorsConfig {
|
||||
origin?: string | string[];
|
||||
methods?: string[];
|
||||
headers?: string[];
|
||||
credentials?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP server handler.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createServer } from 'http';
|
||||
* import { createHttpHandler } from 'praisonai/ai';
|
||||
*
|
||||
* const handler = createHttpHandler({
|
||||
* handler: async (input) => {
|
||||
* return await generateText({ model: 'gpt-4o', prompt: input.prompt });
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* createServer(handler.handle).listen(3000);
|
||||
* ```
|
||||
*/
|
||||
export function createHttpHandler(config: ServerHandlerConfig): ServerHandler {
|
||||
return {
|
||||
handle: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
try {
|
||||
// Set CORS headers
|
||||
if (config.cors) {
|
||||
setCorsHeaders(res, config.cors);
|
||||
}
|
||||
|
||||
// Handle preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse body
|
||||
const body = config.bodyParser
|
||||
? await config.bodyParser(req)
|
||||
: await parseJsonBody(req);
|
||||
|
||||
// Execute handler
|
||||
const result = await config.handler(body);
|
||||
|
||||
// Format response
|
||||
const response = config.responseFormatter
|
||||
? config.responseFormatter(result)
|
||||
: result;
|
||||
|
||||
// Send response
|
||||
if (config.streaming && result.textStream) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
|
||||
}
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(response));
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (config.onError) {
|
||||
config.onError(error, req, res);
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Express handler.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import express from 'express';
|
||||
* import { createExpressHandler } from 'praisonai/ai';
|
||||
*
|
||||
* const app = express();
|
||||
* app.use(express.json());
|
||||
*
|
||||
* app.post('/api/chat', createExpressHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({ model: 'gpt-4o', messages: input.messages });
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*/
|
||||
export function createExpressHandler(config: ServerHandlerConfig): (req: any, res: any) => Promise<void> {
|
||||
return async (req: any, res: any) => {
|
||||
try {
|
||||
// Set CORS headers
|
||||
if (config.cors) {
|
||||
setCorsHeaders(res, config.cors);
|
||||
}
|
||||
|
||||
// Execute handler
|
||||
const result = await config.handler(req.body);
|
||||
|
||||
// Format response
|
||||
const response = config.responseFormatter
|
||||
? config.responseFormatter(result)
|
||||
: result;
|
||||
|
||||
// Send response
|
||||
if (config.streaming && result.textStream) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
|
||||
}
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
} else {
|
||||
res.json(response);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (config.onError) {
|
||||
config.onError(error, req, res);
|
||||
} else {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Hono handler.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Hono } from 'hono';
|
||||
* import { createHonoHandler } from 'praisonai/ai';
|
||||
*
|
||||
* const app = new Hono();
|
||||
*
|
||||
* app.post('/api/chat', createHonoHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({ model: 'gpt-4o', messages: input.messages });
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*/
|
||||
export function createHonoHandler(config: ServerHandlerConfig): (c: any) => Promise<Response> {
|
||||
return async (c: any) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const result = await config.handler(body);
|
||||
|
||||
const response = config.responseFormatter
|
||||
? config.responseFormatter(result)
|
||||
: result;
|
||||
|
||||
if (config.streaming && result.textStream) {
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of result.textStream) {
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text: chunk })}\n\n`));
|
||||
}
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(response);
|
||||
} catch (error: any) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Fastify handler.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import Fastify from 'fastify';
|
||||
* import { createFastifyHandler } from 'praisonai/ai';
|
||||
*
|
||||
* const fastify = Fastify();
|
||||
*
|
||||
* fastify.post('/api/chat', createFastifyHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({ model: 'gpt-4o', messages: input.messages });
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*/
|
||||
export function createFastifyHandler(config: ServerHandlerConfig): (request: any, reply: any) => Promise<void> {
|
||||
return async (request: any, reply: any) => {
|
||||
try {
|
||||
const result = await config.handler(request.body);
|
||||
|
||||
const response = config.responseFormatter
|
||||
? config.responseFormatter(result)
|
||||
: result;
|
||||
|
||||
if (config.streaming && result.textStream) {
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
reply.raw.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
|
||||
}
|
||||
reply.raw.write('data: [DONE]\n\n');
|
||||
reply.raw.end();
|
||||
} else {
|
||||
reply.send(response);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (config.onError) {
|
||||
config.onError(error, request, reply);
|
||||
} else {
|
||||
reply.status(500).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Nest.js controller method decorator config.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Controller, Post, Body, Res } from '@nestjs/common';
|
||||
* import { createNestHandler } from 'praisonai/ai';
|
||||
*
|
||||
* @Controller('api')
|
||||
* export class ChatController {
|
||||
* private handler = createNestHandler({
|
||||
* handler: async (input) => {
|
||||
* return await streamText({ model: 'gpt-4o', messages: input.messages });
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @Post('chat')
|
||||
* async chat(@Body() body: any, @Res() res: any) {
|
||||
* return this.handler(body, res);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createNestHandler(config: ServerHandlerConfig): (body: any, res: any) => Promise<void> {
|
||||
return async (body: any, res: any) => {
|
||||
try {
|
||||
const result = await config.handler(body);
|
||||
|
||||
const response = config.responseFormatter
|
||||
? config.responseFormatter(result)
|
||||
: result;
|
||||
|
||||
if (config.streaming && result.textStream) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
|
||||
}
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
} else {
|
||||
res.json(response);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (config.onError) {
|
||||
config.onError(error, body, res);
|
||||
} else {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON body from HTTP request.
|
||||
*/
|
||||
async function parseJsonBody(req: IncomingMessage): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch (e) {
|
||||
reject(new Error('Invalid JSON body'));
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CORS headers on response.
|
||||
*/
|
||||
function setCorsHeaders(res: any, cors: boolean | CorsConfig): void {
|
||||
const config: CorsConfig = typeof cors === 'boolean'
|
||||
? { origin: '*', methods: ['GET', 'POST', 'OPTIONS'], headers: ['Content-Type'] }
|
||||
: cors;
|
||||
|
||||
const origin = Array.isArray(config.origin) ? config.origin.join(', ') : (config.origin || '*');
|
||||
|
||||
if (res.setHeader) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', (config.methods || ['GET', 'POST', 'OPTIONS']).join(', '));
|
||||
res.setHeader('Access-Control-Allow-Headers', (config.headers || ['Content-Type']).join(', '));
|
||||
if (config.credentials) {
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
} else if (res.set) {
|
||||
res.set('Access-Control-Allow-Origin', origin);
|
||||
res.set('Access-Control-Allow-Methods', (config.methods || ['GET', 'POST', 'OPTIONS']).join(', '));
|
||||
res.set('Access-Control-Allow-Headers', (config.headers || ['Content-Type']).join(', '));
|
||||
if (config.credentials) {
|
||||
res.set('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* Speech & Transcription - AI SDK v6 Compatible
|
||||
*
|
||||
* Provides speech generation and transcription utilities.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface GenerateSpeechOptions {
|
||||
/** Model to use (e.g., 'openai/tts-1', 'elevenlabs/eleven_multilingual_v2') */
|
||||
model: string;
|
||||
/** Text to convert to speech */
|
||||
text: string;
|
||||
/** Voice to use */
|
||||
voice?: string;
|
||||
/** Speed multiplier (0.25 to 4.0) */
|
||||
speed?: number;
|
||||
/** Output format */
|
||||
format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm';
|
||||
/** Additional provider options */
|
||||
providerOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GenerateSpeechResult {
|
||||
/** Audio data as Uint8Array */
|
||||
audio: Uint8Array;
|
||||
/** Audio format */
|
||||
format: string;
|
||||
/** Duration in seconds (if available) */
|
||||
durationSeconds?: number;
|
||||
/** Provider metadata */
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TranscribeOptions {
|
||||
/** Model to use (e.g., 'openai/whisper-1') */
|
||||
model: string;
|
||||
/** Audio data - can be Uint8Array, ArrayBuffer, Buffer, base64 string, or URL */
|
||||
audio: Uint8Array | ArrayBuffer | Buffer | string;
|
||||
/** Language hint (ISO 639-1 code) */
|
||||
language?: string;
|
||||
/** Prompt to guide transcription */
|
||||
prompt?: string;
|
||||
/** Temperature for sampling */
|
||||
temperature?: number;
|
||||
/** Output format */
|
||||
format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt';
|
||||
/** Additional provider options */
|
||||
providerOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TranscribeResult {
|
||||
/** Transcribed text */
|
||||
text: string;
|
||||
/** Detected language */
|
||||
language?: string;
|
||||
/** Duration in seconds */
|
||||
durationSeconds?: number;
|
||||
/** Segments with timestamps */
|
||||
segments?: TranscriptionSegment[];
|
||||
/** Provider metadata */
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TranscriptionSegment {
|
||||
/** Segment ID */
|
||||
id: number;
|
||||
/** Start time in seconds */
|
||||
start: number;
|
||||
/** End time in seconds */
|
||||
end: number;
|
||||
/** Segment text */
|
||||
text: string;
|
||||
/** Confidence score */
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Speech Generation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Generate speech from text.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await generateSpeech({
|
||||
* model: 'openai/tts-1',
|
||||
* text: 'Hello, world!',
|
||||
* voice: 'alloy'
|
||||
* });
|
||||
*
|
||||
* // Save to file
|
||||
* await fs.writeFile('speech.mp3', result.audio);
|
||||
* ```
|
||||
*/
|
||||
export async function generateSpeech(options: GenerateSpeechOptions): Promise<GenerateSpeechResult> {
|
||||
const [provider, model] = parseModel(options.model);
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return generateSpeechOpenAI(model, options);
|
||||
case 'elevenlabs':
|
||||
return generateSpeechElevenLabs(model, options);
|
||||
default:
|
||||
// Try AI SDK
|
||||
return generateSpeechAISDK(options);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSpeechOpenAI(model: string, options: GenerateSpeechOptions): Promise<GenerateSpeechResult> {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY environment variable is required for OpenAI speech generation');
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.openai.com/v1/audio/speech', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model || 'tts-1',
|
||||
input: options.text,
|
||||
voice: options.voice || 'alloy',
|
||||
speed: options.speed,
|
||||
response_format: options.format || 'mp3',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenAI speech generation failed: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return {
|
||||
audio: new Uint8Array(arrayBuffer),
|
||||
format: options.format || 'mp3',
|
||||
};
|
||||
}
|
||||
|
||||
async function generateSpeechElevenLabs(model: string, options: GenerateSpeechOptions): Promise<GenerateSpeechResult> {
|
||||
const apiKey = process.env.ELEVENLABS_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('ELEVENLABS_API_KEY environment variable is required for ElevenLabs speech generation');
|
||||
}
|
||||
|
||||
const voiceId = options.voice || '21m00Tcm4TlvDq8ikWAM'; // Default: Rachel
|
||||
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: options.text,
|
||||
model_id: model || 'eleven_multilingual_v2',
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.75,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`ElevenLabs speech generation failed: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return {
|
||||
audio: new Uint8Array(arrayBuffer),
|
||||
format: 'mp3',
|
||||
};
|
||||
}
|
||||
|
||||
async function generateSpeechAISDK(options: GenerateSpeechOptions): Promise<GenerateSpeechResult> {
|
||||
try {
|
||||
const ai = await import('ai');
|
||||
if ('generateSpeech' in ai) {
|
||||
const result = await (ai as any).generateSpeech(options);
|
||||
return {
|
||||
audio: result.audio,
|
||||
format: options.format || 'mp3',
|
||||
durationSeconds: result.durationSeconds,
|
||||
providerMetadata: result.providerMetadata,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// AI SDK not available or doesn't support speech
|
||||
}
|
||||
|
||||
throw new Error(`Speech generation not supported for model: ${options.model}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transcription
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Transcribe audio to text.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await transcribe({
|
||||
* model: 'openai/whisper-1',
|
||||
* audio: await fs.readFile('audio.mp3')
|
||||
* });
|
||||
*
|
||||
* console.log(result.text);
|
||||
* ```
|
||||
*/
|
||||
export async function transcribe(options: TranscribeOptions): Promise<TranscribeResult> {
|
||||
const [provider, model] = parseModel(options.model);
|
||||
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return transcribeOpenAI(model, options);
|
||||
default:
|
||||
// Try AI SDK
|
||||
return transcribeAISDK(options);
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeOpenAI(model: string, options: TranscribeOptions): Promise<TranscribeResult> {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY environment variable is required for OpenAI transcription');
|
||||
}
|
||||
|
||||
// Convert audio to appropriate format
|
||||
let audioData: Uint8Array;
|
||||
if (typeof options.audio === 'string') {
|
||||
if (options.audio.startsWith('http')) {
|
||||
const response = await fetch(options.audio);
|
||||
audioData = new Uint8Array(await response.arrayBuffer());
|
||||
} else {
|
||||
// Assume base64
|
||||
audioData = Uint8Array.from(atob(options.audio), c => c.charCodeAt(0));
|
||||
}
|
||||
} else if (options.audio instanceof ArrayBuffer) {
|
||||
audioData = new Uint8Array(options.audio);
|
||||
} else {
|
||||
audioData = options.audio as Uint8Array;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob([audioData.buffer as ArrayBuffer]), 'audio.mp3');
|
||||
formData.append('model', model || 'whisper-1');
|
||||
if (options.language) formData.append('language', options.language);
|
||||
if (options.prompt) formData.append('prompt', options.prompt);
|
||||
if (options.temperature) formData.append('temperature', String(options.temperature));
|
||||
formData.append('response_format', options.format === 'verbose_json' ? 'verbose_json' : 'json');
|
||||
|
||||
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`OpenAI transcription failed: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (options.format === 'verbose_json') {
|
||||
return {
|
||||
text: data.text,
|
||||
language: data.language,
|
||||
durationSeconds: data.duration,
|
||||
segments: data.segments?.map((s: any, i: number) => ({
|
||||
id: i,
|
||||
start: s.start,
|
||||
end: s.end,
|
||||
text: s.text,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: data.text,
|
||||
};
|
||||
}
|
||||
|
||||
async function transcribeAISDK(options: TranscribeOptions): Promise<TranscribeResult> {
|
||||
try {
|
||||
const ai = await import('ai');
|
||||
if ('transcribe' in ai) {
|
||||
const result = await (ai as any).transcribe(options);
|
||||
return {
|
||||
text: result.text,
|
||||
language: result.language,
|
||||
durationSeconds: result.durationSeconds,
|
||||
segments: result.segments,
|
||||
providerMetadata: result.providerMetadata,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// AI SDK not available or doesn't support transcription
|
||||
}
|
||||
|
||||
throw new Error(`Transcription not supported for model: ${options.model}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utilities
|
||||
// ============================================================================
|
||||
|
||||
function parseModel(model: string): [string, string] {
|
||||
if (model.includes('/')) {
|
||||
const [provider, ...rest] = model.split('/');
|
||||
return [provider.toLowerCase(), rest.join('/')];
|
||||
}
|
||||
|
||||
// Infer provider from model name
|
||||
if (model.startsWith('tts-') || model.startsWith('whisper')) {
|
||||
return ['openai', model];
|
||||
}
|
||||
if (model.startsWith('eleven_')) {
|
||||
return ['elevenlabs', model];
|
||||
}
|
||||
|
||||
return ['openai', model];
|
||||
}
|
||||
|
||||
/**
|
||||
* Available speech models.
|
||||
*/
|
||||
export const SPEECH_MODELS = {
|
||||
openai: {
|
||||
'tts-1': { description: 'Standard TTS model', voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] },
|
||||
'tts-1-hd': { description: 'High-definition TTS model', voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] },
|
||||
},
|
||||
elevenlabs: {
|
||||
'eleven_multilingual_v2': { description: 'Multilingual model', voices: [] },
|
||||
'eleven_turbo_v2': { description: 'Fast model', voices: [] },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Available transcription models.
|
||||
*/
|
||||
export const TRANSCRIPTION_MODELS = {
|
||||
openai: {
|
||||
'whisper-1': { description: 'Whisper large-v2 model', languages: 'auto-detect' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Telemetry - AI SDK v6 Compatible Telemetry
|
||||
*
|
||||
* Provides telemetry settings compatible with AI SDK v6's experimental_telemetry.
|
||||
* Optionally bridges to OpenTelemetry for production observability.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Telemetry settings compatible with AI SDK v6.
|
||||
*/
|
||||
export interface TelemetrySettings {
|
||||
/** Enable telemetry (default: false) */
|
||||
isEnabled?: boolean;
|
||||
/** Record input data (default: true when enabled) */
|
||||
recordInputs?: boolean;
|
||||
/** Record output data (default: true when enabled) */
|
||||
recordOutputs?: boolean;
|
||||
/** Function identifier for grouping telemetry */
|
||||
functionId?: string;
|
||||
/** Additional metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
/** Custom tracer (OpenTelemetry compatible) */
|
||||
tracer?: Tracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenTelemetry-compatible tracer interface.
|
||||
*/
|
||||
export interface Tracer {
|
||||
startSpan(name: string, options?: SpanOptions): Span;
|
||||
startActiveSpan<T>(name: string, fn: (span: Span) => T): T;
|
||||
startActiveSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenTelemetry-compatible span interface.
|
||||
*/
|
||||
export interface Span {
|
||||
setAttribute(key: string, value: string | number | boolean): this;
|
||||
setAttributes(attributes: Record<string, string | number | boolean>): this;
|
||||
addEvent(name: string, attributes?: Record<string, unknown>): this;
|
||||
setStatus(status: SpanStatus): this;
|
||||
end(): void;
|
||||
recordException(exception: Error): void;
|
||||
isRecording(): boolean;
|
||||
}
|
||||
|
||||
export interface SpanOptions {
|
||||
attributes?: Record<string, string | number | boolean>;
|
||||
kind?: SpanKind;
|
||||
}
|
||||
|
||||
export type SpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer';
|
||||
|
||||
export interface SpanStatus {
|
||||
code: 'ok' | 'error' | 'unset';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global State
|
||||
// ============================================================================
|
||||
|
||||
let globalTelemetrySettings: TelemetrySettings = {
|
||||
isEnabled: false,
|
||||
recordInputs: true,
|
||||
recordOutputs: true,
|
||||
};
|
||||
|
||||
let otelTracer: Tracer | null = null;
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configure global telemetry settings.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* configureTelemetry({
|
||||
* isEnabled: true,
|
||||
* functionId: 'my-app',
|
||||
* metadata: { version: '1.0.0' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function configureTelemetry(settings: TelemetrySettings): void {
|
||||
globalTelemetrySettings = {
|
||||
...globalTelemetrySettings,
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current telemetry settings.
|
||||
*/
|
||||
export function getTelemetrySettings(): TelemetrySettings {
|
||||
return { ...globalTelemetrySettings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable telemetry globally.
|
||||
*/
|
||||
export function enableTelemetry(options?: Omit<TelemetrySettings, 'isEnabled'>): void {
|
||||
globalTelemetrySettings = {
|
||||
...globalTelemetrySettings,
|
||||
...options,
|
||||
isEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable telemetry globally.
|
||||
*/
|
||||
export function disableTelemetry(): void {
|
||||
globalTelemetrySettings.isEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled.
|
||||
*/
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
return globalTelemetrySettings.isEnabled ?? false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OpenTelemetry Integration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Initialize OpenTelemetry integration.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { trace } from '@opentelemetry/api';
|
||||
*
|
||||
* initOpenTelemetry({
|
||||
* tracer: trace.getTracer('praisonai'),
|
||||
* serviceName: 'my-ai-app'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function initOpenTelemetry(options?: {
|
||||
tracer?: Tracer;
|
||||
serviceName?: string;
|
||||
endpoint?: string;
|
||||
}): Promise<void> {
|
||||
if (options?.tracer) {
|
||||
otelTracer = options.tracer;
|
||||
globalTelemetrySettings.tracer = options.tracer;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to auto-initialize OpenTelemetry
|
||||
try {
|
||||
const otelApi = await import('@opentelemetry/api');
|
||||
otelTracer = otelApi.trace.getTracer(options?.serviceName || 'praisonai') as unknown as Tracer;
|
||||
globalTelemetrySettings.tracer = otelTracer || undefined;
|
||||
} catch {
|
||||
console.warn(
|
||||
'⚠️ @opentelemetry/api not installed. Install with:\n' +
|
||||
' npm install @opentelemetry/api @opentelemetry/sdk-node\n' +
|
||||
' or: pnpm add @opentelemetry/api @opentelemetry/sdk-node'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OpenTelemetry tracer.
|
||||
*/
|
||||
export function getTracer(): Tracer | null {
|
||||
return otelTracer;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Span Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a span for an AI operation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const span = createAISpan('generateText', {
|
||||
* model: 'gpt-4o',
|
||||
* provider: 'openai'
|
||||
* });
|
||||
*
|
||||
* try {
|
||||
* const result = await generateText({ ... });
|
||||
* span.setAttribute('tokens', result.usage.totalTokens);
|
||||
* span.setStatus({ code: 'ok' });
|
||||
* } catch (error) {
|
||||
* span.recordException(error);
|
||||
* span.setStatus({ code: 'error', message: error.message });
|
||||
* } finally {
|
||||
* span.end();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createAISpan(
|
||||
name: string,
|
||||
attributes?: Record<string, string | number | boolean>
|
||||
): Span {
|
||||
if (!globalTelemetrySettings.isEnabled || !otelTracer) {
|
||||
return createNoopSpan();
|
||||
}
|
||||
|
||||
return otelTracer.startSpan(`ai.${name}`, {
|
||||
attributes: {
|
||||
'ai.operation': name,
|
||||
...attributes,
|
||||
},
|
||||
kind: 'client',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function within a span.
|
||||
*/
|
||||
export async function withSpan<T>(
|
||||
name: string,
|
||||
attributes: Record<string, string | number | boolean>,
|
||||
fn: (span: Span) => Promise<T>
|
||||
): Promise<T> {
|
||||
const span = createAISpan(name, attributes);
|
||||
|
||||
try {
|
||||
const result = await fn(span);
|
||||
span.setStatus({ code: 'ok' });
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
span.recordException(error);
|
||||
span.setStatus({ code: 'error', message: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Noop Span (when telemetry is disabled)
|
||||
// ============================================================================
|
||||
|
||||
function createNoopSpan(): Span {
|
||||
return {
|
||||
setAttribute: function() { return this; },
|
||||
setAttributes: function() { return this; },
|
||||
addEvent: function() { return this; },
|
||||
setStatus: function() { return this; },
|
||||
end: function() {},
|
||||
recordException: function() {},
|
||||
isRecording: function() { return false; },
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Telemetry Middleware
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create middleware that adds telemetry to AI SDK calls.
|
||||
*/
|
||||
export function createTelemetryMiddleware(settings?: TelemetrySettings): any {
|
||||
const mergedSettings = {
|
||||
...globalTelemetrySettings,
|
||||
...settings,
|
||||
};
|
||||
|
||||
return {
|
||||
transformParams: async ({ params }: { params: any }) => {
|
||||
if (!mergedSettings.isEnabled) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
experimental_telemetry: {
|
||||
isEnabled: true,
|
||||
recordInputs: mergedSettings.recordInputs,
|
||||
recordOutputs: mergedSettings.recordOutputs,
|
||||
functionId: mergedSettings.functionId,
|
||||
metadata: mergedSettings.metadata,
|
||||
tracer: mergedSettings.tracer,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event Recording
|
||||
// ============================================================================
|
||||
|
||||
export interface TelemetryEvent {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
attributes: Record<string, unknown>;
|
||||
spanId?: string;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
const eventBuffer: TelemetryEvent[] = [];
|
||||
const MAX_BUFFER_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* Record a telemetry event.
|
||||
*/
|
||||
export function recordEvent(
|
||||
name: string,
|
||||
attributes: Record<string, unknown> = {}
|
||||
): void {
|
||||
if (!globalTelemetrySettings.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event: TelemetryEvent = {
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
attributes: {
|
||||
...attributes,
|
||||
functionId: globalTelemetrySettings.functionId,
|
||||
...globalTelemetrySettings.metadata,
|
||||
},
|
||||
};
|
||||
|
||||
eventBuffer.push(event);
|
||||
|
||||
// Prevent memory leak
|
||||
if (eventBuffer.length > MAX_BUFFER_SIZE) {
|
||||
eventBuffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recorded events.
|
||||
*/
|
||||
export function getEvents(): TelemetryEvent[] {
|
||||
return [...eventBuffer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear recorded events.
|
||||
*/
|
||||
export function clearEvents(): void {
|
||||
eventBuffer.length = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI SDK Telemetry Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create telemetry settings for AI SDK calls.
|
||||
*/
|
||||
export function createTelemetrySettings(options?: {
|
||||
functionId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
recordInputs?: boolean;
|
||||
recordOutputs?: boolean;
|
||||
}): TelemetrySettings | undefined {
|
||||
if (!globalTelemetrySettings.isEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
isEnabled: true,
|
||||
functionId: options?.functionId || globalTelemetrySettings.functionId,
|
||||
metadata: {
|
||||
...globalTelemetrySettings.metadata,
|
||||
...options?.metadata,
|
||||
},
|
||||
recordInputs: options?.recordInputs ?? globalTelemetrySettings.recordInputs,
|
||||
recordOutputs: options?.recordOutputs ?? globalTelemetrySettings.recordOutputs,
|
||||
tracer: globalTelemetrySettings.tracer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* Tool Approval - Human-in-the-Loop Tool Execution
|
||||
*
|
||||
* Provides utilities for requiring human approval before tool execution.
|
||||
* Compatible with AI SDK v6's needsApproval pattern.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Tool approval configuration.
|
||||
*/
|
||||
export interface ToolApprovalConfig {
|
||||
/** Tool name */
|
||||
name: string;
|
||||
/** Tool description */
|
||||
description?: string;
|
||||
/** Whether approval is needed - can be boolean or async function */
|
||||
needsApproval?: boolean | ((input: unknown) => boolean | Promise<boolean>);
|
||||
/** Timeout for approval in ms (default: 5 minutes) */
|
||||
approvalTimeout?: number;
|
||||
/** Auto-approve patterns (regex or function) */
|
||||
autoApprove?: RegExp | ((input: unknown) => boolean);
|
||||
/** Auto-deny patterns (regex or function) */
|
||||
autoDeny?: RegExp | ((input: unknown) => boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool approval request.
|
||||
*/
|
||||
export interface ToolApprovalRequest {
|
||||
/** Unique ID for this approval request */
|
||||
requestId: string;
|
||||
/** Tool invocation ID */
|
||||
toolInvocationId: string;
|
||||
/** Tool name */
|
||||
toolName: string;
|
||||
/** Tool input arguments */
|
||||
input: unknown;
|
||||
/** Timestamp when request was created */
|
||||
timestamp: number;
|
||||
/** Optional context about why approval is needed */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool approval response.
|
||||
*/
|
||||
export interface ToolApprovalResponse {
|
||||
/** Request ID being responded to */
|
||||
requestId: string;
|
||||
/** Whether the tool call is approved */
|
||||
approved: boolean;
|
||||
/** Optional message from approver */
|
||||
message?: string;
|
||||
/** Who approved/denied (for audit) */
|
||||
approver?: string;
|
||||
/** Timestamp of response */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approval state for tracking pending approvals.
|
||||
*/
|
||||
export type ApprovalState = 'pending' | 'approved' | 'denied' | 'timeout' | 'auto-approved' | 'auto-denied';
|
||||
|
||||
/**
|
||||
* Approval handler function type.
|
||||
*/
|
||||
export type ApprovalHandler = (request: ToolApprovalRequest) => Promise<boolean>;
|
||||
|
||||
// ============================================================================
|
||||
// Approval Manager
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Manages tool approval requests and responses.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```typescript
|
||||
* const manager = new ApprovalManager();
|
||||
*
|
||||
* // Register a handler
|
||||
* manager.onApprovalRequest(async (request) => {
|
||||
* console.log(`Tool ${request.toolName} wants to run with:`, request.input);
|
||||
* return await askUser(`Approve ${request.toolName}?`);
|
||||
* });
|
||||
*
|
||||
* // Request approval
|
||||
* const approved = await manager.requestApproval({
|
||||
* toolName: 'deleteFile',
|
||||
* input: { path: '/important/file.txt' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ApprovalManager extends EventEmitter {
|
||||
private pendingRequests = new Map<string, {
|
||||
request: ToolApprovalRequest;
|
||||
resolve: (approved: boolean) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout?: NodeJS.Timeout;
|
||||
}>();
|
||||
|
||||
private handlers: ApprovalHandler[] = [];
|
||||
private defaultTimeout: number;
|
||||
private autoApprovePatterns: Array<{ toolName: string | RegExp; inputPattern?: RegExp | ((input: unknown) => boolean) }> = [];
|
||||
private autoDenyPatterns: Array<{ toolName: string | RegExp; inputPattern?: RegExp | ((input: unknown) => boolean) }> = [];
|
||||
|
||||
constructor(options?: { defaultTimeout?: number }) {
|
||||
super();
|
||||
this.defaultTimeout = options?.defaultTimeout ?? 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an approval handler.
|
||||
*/
|
||||
onApprovalRequest(handler: ApprovalHandler): void {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add auto-approve pattern.
|
||||
*/
|
||||
addAutoApprove(toolName: string | RegExp, inputPattern?: RegExp | ((input: unknown) => boolean)): void {
|
||||
this.autoApprovePatterns.push({ toolName, inputPattern });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add auto-deny pattern.
|
||||
*/
|
||||
addAutoDeny(toolName: string | RegExp, inputPattern?: RegExp | ((input: unknown) => boolean)): void {
|
||||
this.autoDenyPatterns.push({ toolName, inputPattern });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool call should be auto-approved.
|
||||
*/
|
||||
private checkAutoApprove(toolName: string, input: unknown): boolean {
|
||||
for (const pattern of this.autoApprovePatterns) {
|
||||
const nameMatches = typeof pattern.toolName === 'string'
|
||||
? pattern.toolName === toolName
|
||||
: pattern.toolName.test(toolName);
|
||||
|
||||
if (!nameMatches) continue;
|
||||
|
||||
if (!pattern.inputPattern) return true;
|
||||
|
||||
if (typeof pattern.inputPattern === 'function') {
|
||||
if (pattern.inputPattern(input)) return true;
|
||||
} else {
|
||||
const inputStr = JSON.stringify(input);
|
||||
if (pattern.inputPattern.test(inputStr)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool call should be auto-denied.
|
||||
*/
|
||||
private checkAutoDeny(toolName: string, input: unknown): boolean {
|
||||
for (const pattern of this.autoDenyPatterns) {
|
||||
const nameMatches = typeof pattern.toolName === 'string'
|
||||
? pattern.toolName === toolName
|
||||
: pattern.toolName.test(toolName);
|
||||
|
||||
if (!nameMatches) continue;
|
||||
|
||||
if (!pattern.inputPattern) return true;
|
||||
|
||||
if (typeof pattern.inputPattern === 'function') {
|
||||
if (pattern.inputPattern(input)) return true;
|
||||
} else {
|
||||
const inputStr = JSON.stringify(input);
|
||||
if (pattern.inputPattern.test(inputStr)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request approval for a tool call.
|
||||
*/
|
||||
async requestApproval(options: {
|
||||
toolInvocationId?: string;
|
||||
toolName: string;
|
||||
input: unknown;
|
||||
reason?: string;
|
||||
timeout?: number;
|
||||
}): Promise<boolean> {
|
||||
const requestId = crypto.randomUUID();
|
||||
const toolInvocationId = options.toolInvocationId || crypto.randomUUID();
|
||||
|
||||
// Check auto-deny first (safety)
|
||||
if (this.checkAutoDeny(options.toolName, options.input)) {
|
||||
this.emit('auto-denied', { toolName: options.toolName, input: options.input });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check auto-approve
|
||||
if (this.checkAutoApprove(options.toolName, options.input)) {
|
||||
this.emit('auto-approved', { toolName: options.toolName, input: options.input });
|
||||
return true;
|
||||
}
|
||||
|
||||
const request: ToolApprovalRequest = {
|
||||
requestId,
|
||||
toolInvocationId,
|
||||
toolName: options.toolName,
|
||||
input: options.input,
|
||||
timestamp: Date.now(),
|
||||
reason: options.reason,
|
||||
};
|
||||
|
||||
// If we have handlers, use them
|
||||
if (this.handlers.length > 0) {
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
const approved = await handler(request);
|
||||
if (approved) return true;
|
||||
} catch (error) {
|
||||
// Handler failed, continue to next
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, emit event and wait for response
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId);
|
||||
this.emit('timeout', request);
|
||||
resolve(false); // Default to deny on timeout
|
||||
}, options.timeout ?? this.defaultTimeout);
|
||||
|
||||
this.pendingRequests.set(requestId, {
|
||||
request,
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
|
||||
this.emit('approval-request', request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to an approval request.
|
||||
*/
|
||||
respond(response: ToolApprovalResponse): void {
|
||||
const pending = this.pendingRequests.get(response.requestId);
|
||||
if (!pending) {
|
||||
throw new Error(`No pending request with ID: ${response.requestId}`);
|
||||
}
|
||||
|
||||
if (pending.timeout) {
|
||||
clearTimeout(pending.timeout);
|
||||
}
|
||||
|
||||
this.pendingRequests.delete(response.requestId);
|
||||
this.emit('approval-response', response);
|
||||
pending.resolve(response.approved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending approval requests.
|
||||
*/
|
||||
getPendingRequests(): ToolApprovalRequest[] {
|
||||
return Array.from(this.pendingRequests.values()).map(p => p.request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending request.
|
||||
*/
|
||||
cancel(requestId: string): void {
|
||||
const pending = this.pendingRequests.get(requestId);
|
||||
if (pending) {
|
||||
if (pending.timeout) {
|
||||
clearTimeout(pending.timeout);
|
||||
}
|
||||
this.pendingRequests.delete(requestId);
|
||||
pending.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all pending requests.
|
||||
*/
|
||||
cancelAll(): void {
|
||||
for (const [requestId] of this.pendingRequests) {
|
||||
this.cancel(requestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global Instance
|
||||
// ============================================================================
|
||||
|
||||
let globalApprovalManager: ApprovalManager | null = null;
|
||||
|
||||
/**
|
||||
* Get the global approval manager.
|
||||
*/
|
||||
export function getApprovalManager(): ApprovalManager {
|
||||
if (!globalApprovalManager) {
|
||||
globalApprovalManager = new ApprovalManager();
|
||||
}
|
||||
return globalApprovalManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom global approval manager.
|
||||
*/
|
||||
export function setApprovalManager(manager: ApprovalManager): void {
|
||||
globalApprovalManager = manager;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tool Wrapper
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Wrap a tool with approval requirement.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const deleteFile = withApproval({
|
||||
* name: 'deleteFile',
|
||||
* needsApproval: true,
|
||||
* execute: async (args) => {
|
||||
* await fs.unlink(args.path);
|
||||
* return { success: true };
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function withApproval<TInput, TOutput>(options: {
|
||||
name: string;
|
||||
description?: string;
|
||||
needsApproval?: boolean | ((input: TInput) => boolean | Promise<boolean>);
|
||||
execute: (input: TInput) => Promise<TOutput>;
|
||||
onDenied?: (input: TInput) => TOutput | Promise<TOutput>;
|
||||
approvalManager?: ApprovalManager;
|
||||
}): (input: TInput) => Promise<TOutput> {
|
||||
const manager = options.approvalManager || getApprovalManager();
|
||||
|
||||
return async (input: TInput): Promise<TOutput> => {
|
||||
// Check if approval is needed
|
||||
let needsApproval = false;
|
||||
if (typeof options.needsApproval === 'function') {
|
||||
needsApproval = await options.needsApproval(input);
|
||||
} else {
|
||||
needsApproval = options.needsApproval ?? false;
|
||||
}
|
||||
|
||||
if (needsApproval) {
|
||||
const approved = await manager.requestApproval({
|
||||
toolName: options.name,
|
||||
input,
|
||||
reason: options.description,
|
||||
});
|
||||
|
||||
if (!approved) {
|
||||
if (options.onDenied) {
|
||||
return options.onDenied(input);
|
||||
}
|
||||
throw new ToolApprovalDeniedError(options.name, input);
|
||||
}
|
||||
}
|
||||
|
||||
return options.execute(input);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Errors
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Error thrown when tool approval is denied.
|
||||
*/
|
||||
export class ToolApprovalDeniedError extends Error {
|
||||
readonly toolName: string;
|
||||
readonly input: unknown;
|
||||
|
||||
constructor(toolName: string, input: unknown) {
|
||||
super(`Tool approval denied for: ${toolName}`);
|
||||
this.name = 'ToolApprovalDeniedError';
|
||||
this.toolName = toolName;
|
||||
this.input = input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when tool approval times out.
|
||||
*/
|
||||
export class ToolApprovalTimeoutError extends Error {
|
||||
readonly toolName: string;
|
||||
readonly input: unknown;
|
||||
|
||||
constructor(toolName: string, input: unknown) {
|
||||
super(`Tool approval timed out for: ${toolName}`);
|
||||
this.name = 'ToolApprovalTimeoutError';
|
||||
this.toolName = toolName;
|
||||
this.input = input;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLI Approval Prompt
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a CLI-based approval prompt.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const manager = getApprovalManager();
|
||||
* manager.onApprovalRequest(createCLIApprovalPrompt());
|
||||
* ```
|
||||
*/
|
||||
export function createCLIApprovalPrompt(options?: {
|
||||
/** Custom prompt message */
|
||||
promptMessage?: (request: ToolApprovalRequest) => string;
|
||||
/** Input stream (default: process.stdin) */
|
||||
input?: NodeJS.ReadableStream;
|
||||
/** Output stream (default: process.stdout) */
|
||||
output?: NodeJS.WritableStream;
|
||||
}): ApprovalHandler {
|
||||
return async (request: ToolApprovalRequest): Promise<boolean> => {
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: options?.input || process.stdin,
|
||||
output: options?.output || process.stdout,
|
||||
});
|
||||
|
||||
const message = options?.promptMessage
|
||||
? options.promptMessage(request)
|
||||
: `\n🔐 Tool "${request.toolName}" requires approval.\n` +
|
||||
` Input: ${JSON.stringify(request.input, null, 2)}\n` +
|
||||
(request.reason ? ` Reason: ${request.reason}\n` : '') +
|
||||
`\nApprove? (y/n): `;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(message, (answer) => {
|
||||
rl.close();
|
||||
const approved = answer.toLowerCase().startsWith('y');
|
||||
console.log(approved ? '✅ Approved' : '❌ Denied');
|
||||
resolve(approved);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dangerous Tool Patterns
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Common dangerous patterns that should require approval.
|
||||
*/
|
||||
export const DANGEROUS_PATTERNS = {
|
||||
/** File deletion patterns */
|
||||
fileDelete: /\b(rm|delete|remove|unlink)\b.*\b(file|dir|folder|path)\b/i,
|
||||
/** Database destructive patterns */
|
||||
dbDestructive: /\b(DROP|DELETE|TRUNCATE|ALTER)\b/i,
|
||||
/** Shell command patterns */
|
||||
shellDangerous: /\b(rm\s+-rf|sudo|chmod|chown|mkfs|dd\s+if=)\b/i,
|
||||
/** Network patterns */
|
||||
networkSensitive: /\b(curl|wget|fetch)\b.*\b(password|token|secret|key)\b/i,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if input matches any dangerous pattern.
|
||||
*/
|
||||
export function isDangerous(input: unknown): boolean {
|
||||
const str = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
return Object.values(DANGEROUS_PATTERNS).some(pattern => pattern.test(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a needsApproval function that checks for dangerous patterns.
|
||||
*/
|
||||
export function createDangerousPatternChecker(
|
||||
additionalPatterns?: RegExp[]
|
||||
): (input: unknown) => boolean {
|
||||
const patterns = [
|
||||
...Object.values(DANGEROUS_PATTERNS),
|
||||
...(additionalPatterns || []),
|
||||
];
|
||||
|
||||
return (input: unknown): boolean => {
|
||||
const str = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
return patterns.some(pattern => pattern.test(str));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Tools - AI SDK Wrapper
|
||||
*
|
||||
* Provides tool definition utilities compatible with AI SDK.
|
||||
*/
|
||||
|
||||
export interface ToolDefinition<TInput = unknown, TOutput = unknown> {
|
||||
/** Tool description */
|
||||
description: string;
|
||||
/** Input parameters schema (Zod or JSON schema) */
|
||||
parameters: any;
|
||||
/** Execute function */
|
||||
execute: ToolExecuteFunction<TInput, TOutput>;
|
||||
}
|
||||
|
||||
export type ToolExecuteFunction<TInput = unknown, TOutput = unknown> = (
|
||||
args: TInput,
|
||||
options?: ToolExecutionOptions
|
||||
) => Promise<TOutput> | TOutput;
|
||||
|
||||
export interface ToolExecutionOptions {
|
||||
/** Tool call ID */
|
||||
toolCallId?: string;
|
||||
/** Abort signal */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Messages context */
|
||||
messages?: any[];
|
||||
}
|
||||
|
||||
export type ToolInput<T extends ToolDefinition> = T extends ToolDefinition<infer I, any> ? I : never;
|
||||
export type ToolOutput<T extends ToolDefinition> = T extends ToolDefinition<any, infer O> ? O : never;
|
||||
|
||||
/**
|
||||
* Define a tool for use with AI SDK.
|
||||
*
|
||||
* @example With Zod schema
|
||||
* ```typescript
|
||||
* import { z } from 'zod';
|
||||
*
|
||||
* const weatherTool = defineTool({
|
||||
* description: 'Get weather for a city',
|
||||
* parameters: z.object({
|
||||
* city: z.string().describe('City name'),
|
||||
* unit: z.enum(['celsius', 'fahrenheit']).optional()
|
||||
* }),
|
||||
* execute: async ({ city, unit }) => {
|
||||
* return { temperature: 20, unit: unit || 'celsius', city };
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example With JSON schema
|
||||
* ```typescript
|
||||
* const searchTool = defineTool({
|
||||
* description: 'Search the web',
|
||||
* parameters: {
|
||||
* type: 'object',
|
||||
* properties: {
|
||||
* query: { type: 'string', description: 'Search query' }
|
||||
* },
|
||||
* required: ['query']
|
||||
* },
|
||||
* execute: async ({ query }) => {
|
||||
* return { results: [`Result for: ${query}`] };
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function defineTool<TInput = unknown, TOutput = unknown>(
|
||||
definition: ToolDefinition<TInput, TOutput>
|
||||
): ToolDefinition<TInput, TOutput> {
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool set from multiple tool definitions.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tools = createToolSet({
|
||||
* weather: weatherTool,
|
||||
* search: searchTool
|
||||
* });
|
||||
*
|
||||
* const result = await generateText({
|
||||
* model: 'gpt-4o',
|
||||
* prompt: 'What is the weather in Paris?',
|
||||
* tools
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createToolSet<T extends Record<string, ToolDefinition>>(
|
||||
tools: T
|
||||
): T {
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a simple function to a tool definition.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const addTool = functionToTool(
|
||||
* 'add',
|
||||
* 'Add two numbers',
|
||||
* z.object({ a: z.number(), b: z.number() }),
|
||||
* ({ a, b }) => a + b
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function functionToTool<TInput = unknown, TOutput = unknown>(
|
||||
name: string,
|
||||
description: string,
|
||||
parameters: any,
|
||||
execute: ToolExecuteFunction<TInput, TOutput>
|
||||
): ToolDefinition<TInput, TOutput> {
|
||||
return {
|
||||
description,
|
||||
parameters,
|
||||
execute,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Shared Types for AI Module
|
||||
*/
|
||||
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string | ContentPart[];
|
||||
name?: string;
|
||||
toolCallId?: string;
|
||||
toolCalls?: ToolCallPart[];
|
||||
}
|
||||
|
||||
export type ContentPart = TextPart | ImagePart | FilePart | ToolCallPart | ToolResultPart;
|
||||
|
||||
export interface TextPart {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ImagePart {
|
||||
type: 'image';
|
||||
image: string | Uint8Array | URL;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface FilePart {
|
||||
type: 'file';
|
||||
data: string | Uint8Array | URL;
|
||||
mimeType: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ToolCallPart {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ToolResultPart {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
export interface ProviderMetadata {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* UIMessage - AI SDK v6 Compatible UI Message Types
|
||||
*
|
||||
* Provides types and utilities for working with UI messages in chat applications.
|
||||
* Compatible with AI SDK v6's UIMessage format for seamless integration.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Core Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* UI Message - The main message type for chat UIs.
|
||||
* Compatible with AI SDK v6's UIMessage format.
|
||||
*/
|
||||
export interface UIMessage<
|
||||
METADATA = unknown,
|
||||
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
||||
TOOLS extends UITools = UITools,
|
||||
> {
|
||||
/** Unique identifier for the message */
|
||||
id: string;
|
||||
/** Role of the message sender */
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
/** Optional metadata */
|
||||
metadata?: METADATA;
|
||||
/** Message parts for rendering */
|
||||
parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>;
|
||||
}
|
||||
|
||||
/** Data types that can be used in UI message data parts */
|
||||
export type UIDataTypes = Record<string, unknown>;
|
||||
|
||||
/** UI Tool definition */
|
||||
export type UITool = {
|
||||
input: unknown;
|
||||
output: unknown | undefined;
|
||||
};
|
||||
|
||||
/** UI Tools map */
|
||||
export type UITools = Record<string, UITool>;
|
||||
|
||||
/** All possible UI message part types */
|
||||
export type UIMessagePart<
|
||||
DATA_TYPES extends UIDataTypes = UIDataTypes,
|
||||
TOOLS extends UITools = UITools,
|
||||
> =
|
||||
| TextUIPart
|
||||
| ReasoningUIPart
|
||||
| ToolUIPart<TOOLS>
|
||||
| SourceUrlUIPart
|
||||
| SourceDocumentUIPart
|
||||
| FileUIPart
|
||||
| DataUIPart<DATA_TYPES>
|
||||
| StepStartUIPart;
|
||||
|
||||
// ============================================================================
|
||||
// Part Types
|
||||
// ============================================================================
|
||||
|
||||
/** Text part of a message */
|
||||
export interface TextUIPart {
|
||||
type: 'text';
|
||||
text: string;
|
||||
state?: 'streaming' | 'done';
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Reasoning part of a message */
|
||||
export interface ReasoningUIPart {
|
||||
type: 'reasoning';
|
||||
text: string;
|
||||
state?: 'streaming' | 'done';
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Tool invocation part */
|
||||
export interface ToolUIPart<TOOLS extends UITools = UITools> {
|
||||
type: 'tool';
|
||||
toolInvocationId: string;
|
||||
toolName: keyof TOOLS & string;
|
||||
state: 'input-streaming' | 'input-available' | 'output-streaming' | 'output-available' | 'error';
|
||||
input?: TOOLS[keyof TOOLS]['input'];
|
||||
output?: TOOLS[keyof TOOLS]['output'];
|
||||
error?: string;
|
||||
/** Whether this tool needs approval before execution */
|
||||
needsApproval?: boolean;
|
||||
/** Approval status */
|
||||
approvalStatus?: 'pending' | 'approved' | 'denied';
|
||||
}
|
||||
|
||||
/** Source URL part */
|
||||
export interface SourceUrlUIPart {
|
||||
type: 'source-url';
|
||||
sourceId: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Source document part */
|
||||
export interface SourceDocumentUIPart {
|
||||
type: 'source-document';
|
||||
sourceId: string;
|
||||
mediaType: string;
|
||||
title?: string;
|
||||
providerMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** File part */
|
||||
export interface FileUIPart {
|
||||
type: 'file';
|
||||
mediaType: string;
|
||||
url?: string;
|
||||
data?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
/** Data part for custom data */
|
||||
export interface DataUIPart<DATA_TYPES extends UIDataTypes = UIDataTypes> {
|
||||
type: 'data';
|
||||
dataType: keyof DATA_TYPES & string;
|
||||
data: DATA_TYPES[keyof DATA_TYPES];
|
||||
}
|
||||
|
||||
/** Step start marker */
|
||||
export interface StepStartUIPart {
|
||||
type: 'step-start';
|
||||
stepNumber: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Model Message Types (for conversion)
|
||||
// ============================================================================
|
||||
|
||||
export interface ModelMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string | ModelMessageContent[];
|
||||
name?: string;
|
||||
toolCallId?: string;
|
||||
toolCalls?: ToolCall[];
|
||||
}
|
||||
|
||||
export type ModelMessageContent =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; image: string | Uint8Array; mimeType?: string }
|
||||
| { type: 'file'; data: string | Uint8Array; mimeType: string }
|
||||
| { type: 'tool-call'; toolCallId: string; toolName: string; args: unknown }
|
||||
| { type: 'tool-result'; toolCallId: string; toolName: string; result: unknown; isError?: boolean };
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Conversion Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert UI messages to model messages for AI SDK functions.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const modelMessages = await convertToModelMessages(uiMessages);
|
||||
* const result = await generateText({ model, messages: modelMessages });
|
||||
* ```
|
||||
*/
|
||||
export async function convertToModelMessages(
|
||||
messages: Array<Omit<UIMessage, 'id'>>,
|
||||
options?: {
|
||||
tools?: Record<string, unknown>;
|
||||
ignoreIncompleteToolCalls?: boolean;
|
||||
}
|
||||
): Promise<ModelMessage[]> {
|
||||
const modelMessages: ModelMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
switch (message.role) {
|
||||
case 'system': {
|
||||
const textParts = message.parts.filter(
|
||||
(part): part is TextUIPart => part.type === 'text'
|
||||
);
|
||||
if (textParts.length > 0) {
|
||||
modelMessages.push({
|
||||
role: 'system',
|
||||
content: textParts.map(p => p.text).join(''),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'user': {
|
||||
const content: ModelMessageContent[] = [];
|
||||
for (const part of message.parts) {
|
||||
if (part.type === 'text') {
|
||||
content.push({ type: 'text', text: part.text });
|
||||
} else if (part.type === 'file') {
|
||||
content.push({
|
||||
type: 'file',
|
||||
data: part.data || '',
|
||||
mimeType: part.mediaType,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (content.length > 0) {
|
||||
modelMessages.push({
|
||||
role: 'user',
|
||||
content: content.length === 1 && content[0].type === 'text'
|
||||
? content[0].text
|
||||
: content,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'assistant': {
|
||||
const content: ModelMessageContent[] = [];
|
||||
const toolCalls: ToolCall[] = [];
|
||||
|
||||
for (const part of message.parts) {
|
||||
if (part.type === 'text') {
|
||||
content.push({ type: 'text', text: part.text });
|
||||
} else if (part.type === 'tool') {
|
||||
// Skip incomplete tool calls if requested
|
||||
if (options?.ignoreIncompleteToolCalls &&
|
||||
(part.state === 'input-streaming' || part.state === 'input-available')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: part.toolInvocationId,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: part.toolName,
|
||||
arguments: JSON.stringify(part.input || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (content.length > 0 || toolCalls.length > 0) {
|
||||
modelMessages.push({
|
||||
role: 'assistant',
|
||||
content: content.length === 1 && content[0].type === 'text'
|
||||
? content[0].text
|
||||
: content.length > 0 ? content : '',
|
||||
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Add tool results as separate messages
|
||||
for (const part of message.parts) {
|
||||
if (part.type === 'tool' && part.state === 'output-available' && part.output !== undefined) {
|
||||
modelMessages.push({
|
||||
role: 'tool',
|
||||
content: typeof part.output === 'string' ? part.output : JSON.stringify(part.output),
|
||||
toolCallId: part.toolInvocationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modelMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert model messages to UI messages.
|
||||
*/
|
||||
export function convertToUIMessages(
|
||||
messages: ModelMessage[],
|
||||
generateId: () => string = () => crypto.randomUUID()
|
||||
): UIMessage[] {
|
||||
const uiMessages: UIMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
const parts: UIMessagePart[] = [];
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
parts.push({ type: 'text', text: message.content, state: 'done' });
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const content of message.content) {
|
||||
if (content.type === 'text') {
|
||||
parts.push({ type: 'text', text: content.text, state: 'done' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.toolCalls) {
|
||||
for (const toolCall of message.toolCalls) {
|
||||
parts.push({
|
||||
type: 'tool',
|
||||
toolInvocationId: toolCall.id,
|
||||
toolName: toolCall.function.name,
|
||||
state: 'input-available',
|
||||
input: JSON.parse(toolCall.function.arguments || '{}'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (message.role !== 'tool') {
|
||||
uiMessages.push({
|
||||
id: generateId(),
|
||||
role: message.role as 'system' | 'user' | 'assistant',
|
||||
parts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return uiMessages;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validate UI messages array.
|
||||
*/
|
||||
export function validateUIMessages(messages: unknown[]): messages is UIMessage[] {
|
||||
if (!Array.isArray(messages)) return false;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== 'object') return false;
|
||||
const m = msg as Record<string, unknown>;
|
||||
if (typeof m.id !== 'string') return false;
|
||||
if (!['system', 'user', 'assistant'].includes(m.role as string)) return false;
|
||||
if (!Array.isArray(m.parts)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe validation that returns result object.
|
||||
*/
|
||||
export function safeValidateUIMessages(messages: unknown[]): {
|
||||
success: boolean;
|
||||
value?: UIMessage[];
|
||||
error?: string;
|
||||
} {
|
||||
try {
|
||||
if (validateUIMessages(messages)) {
|
||||
return { success: true, value: messages };
|
||||
}
|
||||
return { success: false, error: 'Invalid UI messages format' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a text UI message.
|
||||
*/
|
||||
export function createTextMessage(
|
||||
role: 'user' | 'assistant',
|
||||
text: string,
|
||||
id?: string
|
||||
): UIMessage {
|
||||
return {
|
||||
id: id || crypto.randomUUID(),
|
||||
role,
|
||||
parts: [{ type: 'text', text, state: 'done' }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a system message.
|
||||
*/
|
||||
export function createSystemMessage(text: string, id?: string): UIMessage {
|
||||
return {
|
||||
id: id || crypto.randomUUID(),
|
||||
role: 'system',
|
||||
parts: [{ type: 'text', text, state: 'done' }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message has pending tool approvals.
|
||||
*/
|
||||
export function hasPendingApprovals(message: UIMessage): boolean {
|
||||
return message.parts.some(
|
||||
part => part.type === 'tool' && part.needsApproval && part.approvalStatus === 'pending'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool parts that need approval.
|
||||
*/
|
||||
export function getToolsNeedingApproval(message: UIMessage): ToolUIPart[] {
|
||||
return message.parts.filter(
|
||||
(part): part is ToolUIPart =>
|
||||
part.type === 'tool' && part.needsApproval === true && part.approvalStatus === 'pending'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an approval response for a tool.
|
||||
*/
|
||||
export function createApprovalResponse(
|
||||
toolInvocationId: string,
|
||||
approved: boolean
|
||||
): { toolInvocationId: string; approved: boolean } {
|
||||
return { toolInvocationId, approved };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stream Response Helpers
|
||||
// ============================================================================
|
||||
|
||||
export interface UIMessageStreamOptions {
|
||||
/** Called when a message is created */
|
||||
onMessage?: (message: UIMessage) => void;
|
||||
/** Called when a part is added */
|
||||
onPart?: (part: UIMessagePart) => void;
|
||||
/** Called on error */
|
||||
onError?: (error: Error) => void;
|
||||
/** Called when stream completes */
|
||||
onFinish?: (messages: UIMessage[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a UI message stream response for Next.js route handlers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In app/api/chat/route.ts
|
||||
* export async function POST(req: Request) {
|
||||
* const { messages } = await req.json();
|
||||
* const result = await streamText({ model, messages });
|
||||
* return toUIMessageStreamResponse(result);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function toUIMessageStreamResponse(
|
||||
stream: AsyncIterable<any>,
|
||||
options?: {
|
||||
headers?: Record<string, string>;
|
||||
status?: number;
|
||||
}
|
||||
): Response {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const readable = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const data = JSON.stringify(chunk);
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
||||
}
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(readable, {
|
||||
status: options?.status || 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipe UI message stream to a Node.js response.
|
||||
*/
|
||||
export function pipeUIMessageStreamToResponse(
|
||||
stream: AsyncIterable<any>,
|
||||
response: { write: (chunk: string) => void; end: () => void }
|
||||
): void {
|
||||
(async () => {
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const data = JSON.stringify(chunk);
|
||||
response.write(`data: ${data}\n\n`);
|
||||
}
|
||||
response.write('data: [DONE]\n\n');
|
||||
response.end();
|
||||
} catch (error) {
|
||||
response.write(`data: ${JSON.stringify({ error: String(error) })}\n\n`);
|
||||
response.end();
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
* AutoAgents - Automatic agent generation from task descriptions
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider, type Message } from '../llm/providers';
|
||||
import { type LLMProvider, type Message } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export interface AgentConfig {
|
||||
name: string;
|
||||
@@ -36,25 +37,48 @@ export interface AutoAgentsConfig {
|
||||
* AutoAgents - Generate agent configurations from task descriptions
|
||||
*/
|
||||
export class AutoAgents {
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
private pattern: string;
|
||||
private singleAgent: boolean;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(config: AutoAgentsConfig = {}) {
|
||||
this.provider = createProvider(config.llm || 'openai/gpt-4o-mini');
|
||||
this.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.pattern = config.pattern || 'sequential';
|
||||
this.singleAgent = config.singleAgent ?? false;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LLM provider (lazy initialization with AI SDK backend)
|
||||
*/
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, {
|
||||
attribution: { agentId: 'AutoAgents' },
|
||||
});
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agent configuration from task description
|
||||
*/
|
||||
async generate(taskDescription: string): Promise<TeamStructure> {
|
||||
const prompt = this.buildPrompt(taskDescription);
|
||||
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [
|
||||
{ role: 'system', content: this.getSystemPrompt() },
|
||||
{ role: 'user', content: prompt }
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Approval command - Tool approval management for AI SDK v6 parity
|
||||
*/
|
||||
|
||||
import {
|
||||
getApprovalManager,
|
||||
ApprovalManager,
|
||||
createCLIApprovalPrompt
|
||||
} from '../../ai/tool-approval';
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
|
||||
export interface ApprovalOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export async function execute(args: string[], options: ApprovalOptions): Promise<void> {
|
||||
const action = args[0] || 'status';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'status':
|
||||
await showStatus(outputFormat);
|
||||
break;
|
||||
case 'pending':
|
||||
await showPending(outputFormat);
|
||||
break;
|
||||
case 'approve':
|
||||
await approveRequest(args[1], outputFormat);
|
||||
break;
|
||||
case 'deny':
|
||||
await denyRequest(args[1], outputFormat);
|
||||
break;
|
||||
case 'cancel':
|
||||
await cancelRequest(args[1], outputFormat);
|
||||
break;
|
||||
case 'cancel-all':
|
||||
await cancelAll(outputFormat);
|
||||
break;
|
||||
case 'auto-approve':
|
||||
await addAutoApprove(args.slice(1), outputFormat);
|
||||
break;
|
||||
case 'auto-deny':
|
||||
await addAutoDeny(args.slice(1), outputFormat);
|
||||
break;
|
||||
case 'interactive':
|
||||
await startInteractive(options);
|
||||
break;
|
||||
case 'help':
|
||||
default:
|
||||
await showHelp(outputFormat);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, error instanceof Error ? error.message : String(error)));
|
||||
} else {
|
||||
await pretty.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async function showStatus(outputFormat: string): Promise<void> {
|
||||
const manager = getApprovalManager();
|
||||
const pending = manager.getPendingRequests();
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
pendingCount: pending.length,
|
||||
pending: pending.map(r => ({
|
||||
requestId: r.requestId,
|
||||
toolName: r.toolName,
|
||||
timestamp: r.timestamp,
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Approval Status');
|
||||
await pretty.plain(`Pending requests: ${pending.length}`);
|
||||
if (pending.length > 0) {
|
||||
await pretty.plain('\nPending:');
|
||||
for (const req of pending) {
|
||||
await pretty.plain(` - ${req.toolName} (${req.requestId})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function showPending(outputFormat: string): Promise<void> {
|
||||
const manager = getApprovalManager();
|
||||
const pending = manager.getPendingRequests();
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ pending }));
|
||||
} else {
|
||||
if (pending.length === 0) {
|
||||
await pretty.plain('No pending approval requests');
|
||||
return;
|
||||
}
|
||||
|
||||
await pretty.heading('Pending Approval Requests');
|
||||
for (const req of pending) {
|
||||
await pretty.plain(`\n🔐 ${req.toolName}`);
|
||||
await pretty.plain(` ID: ${req.requestId}`);
|
||||
await pretty.plain(` Input: ${JSON.stringify(req.input)}`);
|
||||
await pretty.plain(` Time: ${new Date(req.timestamp).toISOString()}`);
|
||||
if (req.reason) {
|
||||
await pretty.plain(` Reason: ${req.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function approveRequest(requestId: string, outputFormat: string): Promise<void> {
|
||||
if (!requestId) {
|
||||
throw new Error('Request ID required. Usage: praisonai approval approve <requestId>');
|
||||
}
|
||||
|
||||
const manager = getApprovalManager();
|
||||
manager.respond({
|
||||
requestId,
|
||||
approved: true,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ requestId, approved: true }));
|
||||
} else {
|
||||
await pretty.success(`Approved request: ${requestId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function denyRequest(requestId: string, outputFormat: string): Promise<void> {
|
||||
if (!requestId) {
|
||||
throw new Error('Request ID required. Usage: praisonai approval deny <requestId>');
|
||||
}
|
||||
|
||||
const manager = getApprovalManager();
|
||||
manager.respond({
|
||||
requestId,
|
||||
approved: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ requestId, approved: false }));
|
||||
} else {
|
||||
await pretty.success(`Denied request: ${requestId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelRequest(requestId: string, outputFormat: string): Promise<void> {
|
||||
if (!requestId) {
|
||||
throw new Error('Request ID required. Usage: praisonai approval cancel <requestId>');
|
||||
}
|
||||
|
||||
const manager = getApprovalManager();
|
||||
manager.cancel(requestId);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ requestId, cancelled: true }));
|
||||
} else {
|
||||
await pretty.success(`Cancelled request: ${requestId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelAll(outputFormat: string): Promise<void> {
|
||||
const manager = getApprovalManager();
|
||||
const pending = manager.getPendingRequests();
|
||||
const count = pending.length;
|
||||
|
||||
manager.cancelAll();
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ cancelled: count }));
|
||||
} else {
|
||||
await pretty.success(`Cancelled ${count} pending requests`);
|
||||
}
|
||||
}
|
||||
|
||||
async function addAutoApprove(args: string[], outputFormat: string): Promise<void> {
|
||||
const toolName = args[0];
|
||||
if (!toolName) {
|
||||
throw new Error('Tool name required. Usage: praisonai approval auto-approve <toolName>');
|
||||
}
|
||||
|
||||
const manager = getApprovalManager();
|
||||
manager.addAutoApprove(toolName);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ toolName, autoApprove: true }));
|
||||
} else {
|
||||
await pretty.success(`Added auto-approve for: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function addAutoDeny(args: string[], outputFormat: string): Promise<void> {
|
||||
const toolName = args[0];
|
||||
if (!toolName) {
|
||||
throw new Error('Tool name required. Usage: praisonai approval auto-deny <toolName>');
|
||||
}
|
||||
|
||||
const manager = getApprovalManager();
|
||||
manager.addAutoDeny(toolName);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ toolName, autoDeny: true }));
|
||||
} else {
|
||||
await pretty.success(`Added auto-deny for: ${toolName}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startInteractive(options: ApprovalOptions): Promise<void> {
|
||||
await pretty.heading('Interactive Approval Mode');
|
||||
await pretty.plain('Waiting for approval requests... (Ctrl+C to exit)\n');
|
||||
|
||||
const manager = getApprovalManager();
|
||||
const handler = createCLIApprovalPrompt();
|
||||
|
||||
manager.onApprovalRequest(handler);
|
||||
|
||||
// Keep process alive
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
async function showHelp(outputFormat: string): Promise<void> {
|
||||
const help = {
|
||||
command: 'approval',
|
||||
description: 'Manage tool approval requests (AI SDK v6 parity)',
|
||||
subcommands: [
|
||||
{ name: 'status', description: 'Show approval status' },
|
||||
{ name: 'pending', description: 'List pending approval requests' },
|
||||
{ name: 'approve <id>', description: 'Approve a pending request' },
|
||||
{ name: 'deny <id>', description: 'Deny a pending request' },
|
||||
{ name: 'cancel <id>', description: 'Cancel a pending request' },
|
||||
{ name: 'cancel-all', description: 'Cancel all pending requests' },
|
||||
{ name: 'auto-approve <tool>', description: 'Auto-approve a tool' },
|
||||
{ name: 'auto-deny <tool>', description: 'Auto-deny a tool' },
|
||||
{ name: 'interactive', description: 'Start interactive approval mode' },
|
||||
],
|
||||
examples: [
|
||||
'praisonai approval status',
|
||||
'praisonai approval pending',
|
||||
'praisonai approval approve abc123',
|
||||
'praisonai approval auto-approve readFile',
|
||||
'praisonai approval interactive',
|
||||
],
|
||||
};
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess(help));
|
||||
} else {
|
||||
await pretty.heading('Approval Command');
|
||||
await pretty.plain('Manage tool approval requests for safe AI agent execution.\n');
|
||||
|
||||
await pretty.plain('Subcommands:');
|
||||
for (const cmd of help.subcommands) {
|
||||
await pretty.plain(` ${cmd.name.padEnd(20)} ${cmd.description}`);
|
||||
}
|
||||
|
||||
await pretty.plain('\nExamples:');
|
||||
for (const ex of help.examples) {
|
||||
await pretty.plain(` ${ex}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
/**
|
||||
* Benchmark Command - Performance benchmarks for AI SDK vs Native backends
|
||||
*
|
||||
* Measures:
|
||||
* - Import time (cold start)
|
||||
* - Memory usage
|
||||
* - First-call latency
|
||||
* - Streaming throughput
|
||||
* - Embedding throughput
|
||||
*/
|
||||
|
||||
import { outputJson, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
|
||||
export interface BenchmarkOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
iterations?: number;
|
||||
backend?: 'ai-sdk' | 'native' | 'both';
|
||||
real?: boolean;
|
||||
}
|
||||
|
||||
interface BenchmarkResult {
|
||||
name: string;
|
||||
iterations: number;
|
||||
mean: number;
|
||||
min: number;
|
||||
max: number;
|
||||
p95: number;
|
||||
stdDev: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface BenchmarkReport {
|
||||
timestamp: string;
|
||||
results: BenchmarkResult[];
|
||||
comparison?: {
|
||||
aiSdk: BenchmarkResult;
|
||||
native: BenchmarkResult;
|
||||
difference: number;
|
||||
winner: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const EXIT_CODES = {
|
||||
SUCCESS: 0,
|
||||
GENERAL_ERROR: 1,
|
||||
INVALID_ARGS: 2,
|
||||
} as const;
|
||||
|
||||
function formatBenchmarkSuccess(data: any, meta?: any): { success: true; data: any; meta?: any } {
|
||||
return {
|
||||
success: true as const,
|
||||
data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateStats(values: number[]): { mean: number; min: number; max: number; p95: number; stdDev: number } {
|
||||
if (values.length === 0) return { mean: 0, min: 0, max: 0, p95: 0, stdDev: 0 };
|
||||
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
const min = sorted[0];
|
||||
const max = sorted[sorted.length - 1];
|
||||
const p95Index = Math.floor(sorted.length * 0.95);
|
||||
const p95 = sorted[p95Index] || max;
|
||||
|
||||
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
|
||||
const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
|
||||
const stdDev = Math.sqrt(avgSquaredDiff);
|
||||
|
||||
return { mean, min, max, p95, stdDev };
|
||||
}
|
||||
|
||||
export async function execute(args: string[], options: BenchmarkOptions): Promise<void> {
|
||||
const subcommand = args[0] || 'run';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
switch (subcommand) {
|
||||
case 'run':
|
||||
await runAllBenchmarks(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'import':
|
||||
await importBenchmark(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'memory':
|
||||
await memoryBenchmark(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'latency':
|
||||
await latencyBenchmark(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'streaming':
|
||||
await streamingBenchmark(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'embedding':
|
||||
await embeddingBenchmark(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
default:
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown subcommand: ${subcommand}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown subcommand: ${subcommand}`);
|
||||
await pretty.info('Available subcommands: run, import, memory, latency, streaming, embedding');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all benchmarks
|
||||
*/
|
||||
async function runAllBenchmarks(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 5;
|
||||
const results: BenchmarkResult[] = [];
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('Running All Benchmarks');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Import benchmark
|
||||
const importResult = await measureImportTime(iterations);
|
||||
results.push(importResult);
|
||||
|
||||
// Memory benchmark
|
||||
const memoryResult = await measureMemory(iterations);
|
||||
results.push(memoryResult);
|
||||
|
||||
// Latency benchmark (mock mode unless --real)
|
||||
if (options.real) {
|
||||
const latencyResult = await measureLatency(iterations, true);
|
||||
results.push(latencyResult);
|
||||
} else {
|
||||
const latencyResult = await measureLatency(iterations, false);
|
||||
results.push(latencyResult);
|
||||
}
|
||||
|
||||
const report: BenchmarkReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
results,
|
||||
};
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess(report));
|
||||
} else {
|
||||
printBenchmarkTable(results);
|
||||
console.log('');
|
||||
await pretty.info('Use --real flag to run benchmarks with real API calls');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark import time
|
||||
*/
|
||||
async function importBenchmark(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 10;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('Import Time Benchmark');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const results: BenchmarkResult[] = [];
|
||||
|
||||
// Measure core import (without AI SDK)
|
||||
const coreResult = await measureCoreImportTime(iterations);
|
||||
results.push(coreResult);
|
||||
|
||||
// Measure AI SDK import
|
||||
const aiSdkResult = await measureAISDKImportTime(iterations);
|
||||
results.push(aiSdkResult);
|
||||
|
||||
// Measure full import (with AI SDK loaded)
|
||||
const fullResult = await measureImportTime(iterations);
|
||||
results.push(fullResult);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({ results }));
|
||||
} else {
|
||||
printBenchmarkTable(results);
|
||||
console.log('');
|
||||
|
||||
// Show comparison
|
||||
const overhead = aiSdkResult.mean;
|
||||
console.log(`AI SDK import overhead: ${overhead.toFixed(2)}ms`);
|
||||
console.log(`Core import (no AI SDK): ${coreResult.mean.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark memory usage
|
||||
*/
|
||||
async function memoryBenchmark(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 5;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('Memory Usage Benchmark');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const result = await measureMemory(iterations);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({ result }));
|
||||
} else {
|
||||
printBenchmarkTable([result]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark first-call latency
|
||||
*/
|
||||
async function latencyBenchmark(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 3;
|
||||
const useReal = options.real || false;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('First-Call Latency Benchmark');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log(`Mode: ${useReal ? 'Real API' : 'Mock'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const result = await measureLatency(iterations, useReal);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({ result }));
|
||||
} else {
|
||||
printBenchmarkTable([result]);
|
||||
if (!useReal) {
|
||||
console.log('');
|
||||
await pretty.info('Use --real flag to measure with real API calls');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark streaming throughput
|
||||
*/
|
||||
async function streamingBenchmark(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 3;
|
||||
const useReal = options.real || false;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('Streaming Throughput Benchmark');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log(`Mode: ${useReal ? 'Real API' : 'Mock'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (!useReal) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({
|
||||
message: 'Streaming benchmark requires --real flag',
|
||||
result: null
|
||||
}));
|
||||
} else {
|
||||
await pretty.info('Streaming benchmark requires --real flag for meaningful results');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await measureStreaming(iterations);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({ result }));
|
||||
} else {
|
||||
printBenchmarkTable([result]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark embedding throughput
|
||||
*/
|
||||
async function embeddingBenchmark(args: string[], options: BenchmarkOptions, outputFormat: string): Promise<void> {
|
||||
const iterations = options.iterations || 3;
|
||||
const useReal = options.real || false;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.success('Embedding Throughput Benchmark');
|
||||
console.log(`Iterations: ${iterations}`);
|
||||
console.log(`Mode: ${useReal ? 'Real API' : 'Mock'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (!useReal) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({
|
||||
message: 'Embedding benchmark requires --real flag',
|
||||
result: null
|
||||
}));
|
||||
} else {
|
||||
await pretty.info('Embedding benchmark requires --real flag for meaningful results');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await measureEmbedding(iterations);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatBenchmarkSuccess({ result }));
|
||||
} else {
|
||||
printBenchmarkTable([result]);
|
||||
}
|
||||
}
|
||||
|
||||
// Measurement functions
|
||||
|
||||
async function measureCoreImportTime(iterations: number): Promise<BenchmarkResult> {
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// Clear require cache for accurate measurement
|
||||
const cacheKeys = Object.keys(require.cache).filter(k =>
|
||||
k.includes('praisonai-ts') && !k.includes('node_modules')
|
||||
);
|
||||
cacheKeys.forEach(k => delete require.cache[k]);
|
||||
|
||||
const start = performance.now();
|
||||
// Import just the Agent class
|
||||
await import('../../agent/simple');
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
|
||||
const stats = calculateStats(times);
|
||||
return {
|
||||
name: 'Core Import (Agent)',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'ms',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureAISDKImportTime(iterations: number): Promise<BenchmarkResult> {
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// Clear AI SDK from cache
|
||||
const cacheKeys = Object.keys(require.cache).filter(k => k.includes('ai'));
|
||||
cacheKeys.forEach(k => delete require.cache[k]);
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
await import('ai');
|
||||
} catch {
|
||||
// AI SDK not installed
|
||||
}
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
|
||||
const stats = calculateStats(times);
|
||||
return {
|
||||
name: 'AI SDK Import',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'ms',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureImportTime(iterations: number): Promise<BenchmarkResult> {
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
// Import the main module
|
||||
await import('../../index');
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
|
||||
const stats = calculateStats(times);
|
||||
return {
|
||||
name: 'Full Import',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'ms',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureMemory(iterations: number): Promise<BenchmarkResult> {
|
||||
const measurements: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// Force GC if available
|
||||
if (global.gc) global.gc();
|
||||
|
||||
const before = process.memoryUsage().heapUsed;
|
||||
|
||||
// Import and create an agent
|
||||
const { Agent } = await import('../../agent/simple');
|
||||
const agent = new Agent({ instructions: 'Test agent' });
|
||||
|
||||
const after = process.memoryUsage().heapUsed;
|
||||
measurements.push((after - before) / 1024 / 1024); // Convert to MB
|
||||
|
||||
// Keep reference to prevent GC
|
||||
void agent;
|
||||
}
|
||||
|
||||
const stats = calculateStats(measurements);
|
||||
return {
|
||||
name: 'Memory (Agent creation)',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'MB',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureLatency(iterations: number, useReal: boolean): Promise<BenchmarkResult> {
|
||||
const times: number[] = [];
|
||||
|
||||
if (useReal) {
|
||||
const { Agent } = await import('../../agent/simple');
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const agent = new Agent({
|
||||
instructions: 'Say "OK" and nothing else.',
|
||||
stream: false,
|
||||
});
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
await agent.chat('Hi');
|
||||
} catch {
|
||||
// API error, skip
|
||||
}
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
} else {
|
||||
// Mock latency measurement
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
// Simulate backend resolution
|
||||
const { resolveBackend } = await import('../../llm/backend-resolver');
|
||||
await resolveBackend('openai/gpt-4o-mini');
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
}
|
||||
|
||||
const stats = calculateStats(times);
|
||||
return {
|
||||
name: useReal ? 'First-Call Latency (Real)' : 'Backend Resolution',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'ms',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureStreaming(iterations: number): Promise<BenchmarkResult> {
|
||||
const throughputs: number[] = [];
|
||||
|
||||
const { Agent } = await import('../../agent/simple');
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const agent = new Agent({
|
||||
instructions: 'Count from 1 to 20.',
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let tokenCount = 0;
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
// Note: This is a simplified measurement
|
||||
const response = await agent.chat('Count');
|
||||
tokenCount = response.split(/\s+/).length;
|
||||
} catch {
|
||||
// API error
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
const duration = (end - start) / 1000; // seconds
|
||||
throughputs.push(tokenCount / duration);
|
||||
}
|
||||
|
||||
const stats = calculateStats(throughputs);
|
||||
return {
|
||||
name: 'Streaming Throughput',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'tokens/sec',
|
||||
};
|
||||
}
|
||||
|
||||
async function measureEmbedding(iterations: number): Promise<BenchmarkResult> {
|
||||
const throughputs: number[] = [];
|
||||
|
||||
const { embedMany } = await import('../../llm/embeddings');
|
||||
|
||||
// Test texts
|
||||
const texts = Array(10).fill(0).map((_, i) => `This is test text number ${i + 1} for embedding benchmark.`);
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
await embedMany(texts);
|
||||
} catch {
|
||||
// API error
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
const duration = (end - start) / 1000; // seconds
|
||||
throughputs.push(texts.length / duration);
|
||||
}
|
||||
|
||||
const stats = calculateStats(throughputs);
|
||||
return {
|
||||
name: 'Embedding Throughput',
|
||||
iterations,
|
||||
...stats,
|
||||
unit: 'vectors/sec',
|
||||
};
|
||||
}
|
||||
|
||||
function printBenchmarkTable(results: BenchmarkResult[]): void {
|
||||
console.log('Benchmark Results');
|
||||
console.log('─'.repeat(80));
|
||||
console.log(
|
||||
'Name'.padEnd(30) +
|
||||
'Mean'.padStart(10) +
|
||||
'Min'.padStart(10) +
|
||||
'Max'.padStart(10) +
|
||||
'P95'.padStart(10) +
|
||||
'Unit'.padStart(10)
|
||||
);
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
for (const r of results) {
|
||||
console.log(
|
||||
r.name.padEnd(30) +
|
||||
r.mean.toFixed(2).padStart(10) +
|
||||
r.min.toFixed(2).padStart(10) +
|
||||
r.max.toFixed(2).padStart(10) +
|
||||
r.p95.toFixed(2).padStart(10) +
|
||||
r.unit.padStart(10)
|
||||
);
|
||||
}
|
||||
|
||||
console.log('─'.repeat(80));
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* Chat command - Chat with an AI agent
|
||||
*/
|
||||
|
||||
import { createProvider } from '../../llm/providers';
|
||||
import { resolveBackend } from '../../llm/backend-resolver';
|
||||
import { Session } from '../../session';
|
||||
import { resolveConfig } from '../config/resolve';
|
||||
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
|
||||
@@ -46,7 +46,7 @@ export async function execute(args: string[], options: ChatOptions): Promise<voi
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
try {
|
||||
const provider = createProvider(config.model);
|
||||
const { provider } = await resolveBackend(config.model);
|
||||
const session = new Session({ id: options.session });
|
||||
session.addMessage({ role: 'user', content: prompt });
|
||||
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Embed Command - Generate embeddings using AI SDK (preferred) or native provider
|
||||
*
|
||||
* Subcommands:
|
||||
* - text: Embed a single text or multiple texts
|
||||
* - file: Embed contents of a file
|
||||
* - query: Find similar texts using embeddings
|
||||
*/
|
||||
|
||||
import { outputJson, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
|
||||
// Simple success formatter for embed command
|
||||
function formatEmbedSuccess(data: any, meta?: any): { success: true; data: any; meta?: any } {
|
||||
return {
|
||||
success: true as const,
|
||||
data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface EmbedOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
backend?: 'ai-sdk' | 'native' | 'auto';
|
||||
dimensions?: number;
|
||||
file?: string;
|
||||
save?: string;
|
||||
}
|
||||
|
||||
const EXIT_CODES = {
|
||||
SUCCESS: 0,
|
||||
GENERAL_ERROR: 1,
|
||||
INVALID_ARGS: 2,
|
||||
} as const;
|
||||
|
||||
export async function execute(args: string[], options: EmbedOptions): Promise<void> {
|
||||
const subcommand = args[0] || 'text';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
switch (subcommand) {
|
||||
case 'text':
|
||||
await textCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'file':
|
||||
await fileCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'query':
|
||||
await queryCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'models':
|
||||
await modelsCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
default:
|
||||
// If no subcommand, treat args[0] as text to embed
|
||||
if (args[0] && !['text', 'file', 'query', 'models'].includes(args[0])) {
|
||||
await textCommand(args, options, outputFormat);
|
||||
} else {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown subcommand: ${subcommand}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown subcommand: ${subcommand}`);
|
||||
await pretty.info('Available subcommands: text, file, query, models');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed text(s)
|
||||
*/
|
||||
async function textCommand(args: string[], options: EmbedOptions, outputFormat: string): Promise<void> {
|
||||
const texts = args.filter(a => !a.startsWith('-'));
|
||||
|
||||
if (texts.length === 0) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide text to embed'));
|
||||
} else {
|
||||
await pretty.error('Please provide text to embed');
|
||||
await pretty.info('Usage: praisonai-ts embed text "Hello world"');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
|
||||
try {
|
||||
const { embed, embedMany, getDefaultEmbeddingModel, isAISDKAvailable } = await import('../../llm');
|
||||
|
||||
const startTime = Date.now();
|
||||
const model = options.model || getDefaultEmbeddingModel(options.provider);
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
|
||||
let result: { embeddings: number[][]; usage?: { tokens: number } };
|
||||
|
||||
if (texts.length === 1) {
|
||||
const single = await embed(texts[0], {
|
||||
model,
|
||||
backend: options.backend
|
||||
});
|
||||
result = {
|
||||
embeddings: [single.embedding],
|
||||
usage: single.usage
|
||||
};
|
||||
} else {
|
||||
result = await embedMany(texts, {
|
||||
model,
|
||||
backend: options.backend
|
||||
});
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Save to file if requested
|
||||
if (options.save) {
|
||||
const saveData = {
|
||||
texts,
|
||||
embeddings: result.embeddings,
|
||||
model,
|
||||
backend: aiSdkAvailable ? 'ai-sdk' : 'native',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
fs.writeFileSync(options.save, JSON.stringify(saveData, null, 2));
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatEmbedSuccess({
|
||||
texts,
|
||||
embeddings: result.embeddings,
|
||||
dimensions: result.embeddings[0]?.length || 0,
|
||||
count: result.embeddings.length,
|
||||
}, {
|
||||
model,
|
||||
backend: aiSdkAvailable ? 'ai-sdk' : 'native',
|
||||
duration_ms: duration,
|
||||
tokens: result.usage?.tokens,
|
||||
saved_to: options.save,
|
||||
}));
|
||||
} else {
|
||||
await pretty.success(`Embedded ${texts.length} text(s)`);
|
||||
await pretty.info(`Model: ${model}`);
|
||||
await pretty.info(`Backend: ${aiSdkAvailable ? 'AI SDK' : 'Native'}`);
|
||||
await pretty.info(`Dimensions: ${result.embeddings[0]?.length || 0}`);
|
||||
await pretty.info(`Duration: ${duration}ms`);
|
||||
if (result.usage?.tokens) {
|
||||
await pretty.info(`Tokens: ${result.usage.tokens}`);
|
||||
}
|
||||
if (options.save) {
|
||||
await pretty.info(`Saved to: ${options.save}`);
|
||||
}
|
||||
if (options.verbose) {
|
||||
console.log('\nEmbeddings (first 5 values each):');
|
||||
result.embeddings.forEach((emb, i) => {
|
||||
console.log(` [${i}]: [${emb.slice(0, 5).join(', ')}...]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, error.message));
|
||||
} else {
|
||||
await pretty.error(`Embedding failed: ${error.message}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed file contents
|
||||
*/
|
||||
async function fileCommand(args: string[], options: EmbedOptions, outputFormat: string): Promise<void> {
|
||||
const filePath = args[0];
|
||||
|
||||
if (!filePath) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a file path'));
|
||||
} else {
|
||||
await pretty.error('Please provide a file path');
|
||||
await pretty.info('Usage: praisonai-ts embed file ./document.txt');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
|
||||
try {
|
||||
const absolutePath = path.resolve(filePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`File not found: ${absolutePath}`);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(absolutePath, 'utf-8');
|
||||
const { embed, getDefaultEmbeddingModel, isAISDKAvailable } = await import('../../llm');
|
||||
|
||||
const startTime = Date.now();
|
||||
const model = options.model || getDefaultEmbeddingModel(options.provider);
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
|
||||
const result = await embed(content, {
|
||||
model,
|
||||
backend: options.backend
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Save to file if requested
|
||||
if (options.save) {
|
||||
const saveData = {
|
||||
file: absolutePath,
|
||||
embedding: result.embedding,
|
||||
model,
|
||||
backend: aiSdkAvailable ? 'ai-sdk' : 'native',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
fs.writeFileSync(options.save, JSON.stringify(saveData, null, 2));
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatEmbedSuccess({
|
||||
file: absolutePath,
|
||||
embedding: result.embedding,
|
||||
dimensions: result.embedding.length,
|
||||
content_length: content.length,
|
||||
}, {
|
||||
model,
|
||||
backend: aiSdkAvailable ? 'ai-sdk' : 'native',
|
||||
duration_ms: duration,
|
||||
tokens: result.usage?.tokens,
|
||||
saved_to: options.save,
|
||||
}));
|
||||
} else {
|
||||
await pretty.success(`Embedded file: ${filePath}`);
|
||||
await pretty.info(`Model: ${model}`);
|
||||
await pretty.info(`Backend: ${aiSdkAvailable ? 'AI SDK' : 'Native'}`);
|
||||
await pretty.info(`Dimensions: ${result.embedding.length}`);
|
||||
await pretty.info(`Content length: ${content.length} chars`);
|
||||
await pretty.info(`Duration: ${duration}ms`);
|
||||
if (options.save) {
|
||||
await pretty.info(`Saved to: ${options.save}`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, error.message));
|
||||
} else {
|
||||
await pretty.error(`Embedding failed: ${error.message}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query similar texts
|
||||
*/
|
||||
async function queryCommand(args: string[], options: EmbedOptions, outputFormat: string): Promise<void> {
|
||||
const query = args[0];
|
||||
const dataFile = options.file;
|
||||
|
||||
if (!query) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a query'));
|
||||
} else {
|
||||
await pretty.error('Please provide a query');
|
||||
await pretty.info('Usage: praisonai-ts embed query "search text" --file embeddings.json');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
|
||||
if (!dataFile) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide --file with embeddings data'));
|
||||
} else {
|
||||
await pretty.error('Please provide --file with embeddings data');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
|
||||
try {
|
||||
const { embed, cosineSimilarity, getDefaultEmbeddingModel } = await import('../../llm');
|
||||
|
||||
// Load embeddings data
|
||||
const data = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
|
||||
const texts: string[] = data.texts || [];
|
||||
const embeddings: number[][] = data.embeddings || [];
|
||||
|
||||
if (embeddings.length === 0) {
|
||||
throw new Error('No embeddings found in data file');
|
||||
}
|
||||
|
||||
// Embed the query
|
||||
const model = options.model || data.model || getDefaultEmbeddingModel(options.provider);
|
||||
const queryResult = await embed(query, { model, backend: options.backend });
|
||||
|
||||
// Calculate similarities
|
||||
const similarities = embeddings.map((emb, i) => ({
|
||||
index: i,
|
||||
text: texts[i] || `[${i}]`,
|
||||
score: cosineSimilarity(queryResult.embedding, emb),
|
||||
}));
|
||||
|
||||
// Sort by similarity (descending)
|
||||
similarities.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Take top 5
|
||||
const topResults = similarities.slice(0, 5);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatEmbedSuccess({
|
||||
query,
|
||||
results: topResults,
|
||||
}, {
|
||||
model,
|
||||
total_embeddings: embeddings.length,
|
||||
}));
|
||||
} else {
|
||||
await pretty.success(`Query: "${query}"`);
|
||||
console.log('\nTop results:');
|
||||
topResults.forEach((r, i) => {
|
||||
console.log(` ${i + 1}. [${(r.score * 100).toFixed(1)}%] ${r.text.substring(0, 80)}${r.text.length > 80 ? '...' : ''}`);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, error.message));
|
||||
} else {
|
||||
await pretty.error(`Query failed: ${error.message}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available embedding models
|
||||
*/
|
||||
async function modelsCommand(args: string[], options: EmbedOptions, outputFormat: string): Promise<void> {
|
||||
const models = [
|
||||
{ provider: 'openai', model: 'text-embedding-3-small', dimensions: 1536, description: 'Fast, cost-effective' },
|
||||
{ provider: 'openai', model: 'text-embedding-3-large', dimensions: 3072, description: 'Higher quality' },
|
||||
{ provider: 'openai', model: 'text-embedding-ada-002', dimensions: 1536, description: 'Legacy model' },
|
||||
{ provider: 'google', model: 'text-embedding-004', dimensions: 768, description: 'Google embedding' },
|
||||
{ provider: 'cohere', model: 'embed-english-v3.0', dimensions: 1024, description: 'Cohere English' },
|
||||
{ provider: 'cohere', model: 'embed-multilingual-v3.0', dimensions: 1024, description: 'Cohere Multilingual' },
|
||||
];
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatEmbedSuccess({ models }));
|
||||
} else {
|
||||
await pretty.success('Available Embedding Models');
|
||||
console.log('');
|
||||
console.log('Provider Model Dimensions Description');
|
||||
console.log('─'.repeat(75));
|
||||
models.forEach(m => {
|
||||
console.log(`${m.provider.padEnd(12)}${m.model.padEnd(27)}${String(m.dimensions).padEnd(12)}${m.description}`);
|
||||
});
|
||||
console.log('');
|
||||
await pretty.info('Usage: praisonai-ts embed text "Hello" --model openai/text-embedding-3-small');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
/**
|
||||
* LLM Command - AI SDK provider management and testing
|
||||
*
|
||||
* Subcommands:
|
||||
* - providers: List available AI SDK providers
|
||||
* - test: Test connectivity to a provider
|
||||
* - validate: Validate provider configuration
|
||||
* - run: Run a prompt with a specific model
|
||||
*/
|
||||
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
|
||||
export interface LLMOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
model?: string;
|
||||
stream?: boolean;
|
||||
timeout?: number;
|
||||
provider?: string;
|
||||
schema?: string;
|
||||
tool?: string;
|
||||
}
|
||||
|
||||
// Exit codes specific to LLM commands
|
||||
const LLM_EXIT_CODES = {
|
||||
SUCCESS: 0,
|
||||
GENERAL_ERROR: 1,
|
||||
INVALID_ARGS: 2,
|
||||
AUTH_ERROR: 3,
|
||||
PROVIDER_NOT_FOUND: 4,
|
||||
NETWORK_ERROR: 5,
|
||||
TIMEOUT: 6,
|
||||
} as const;
|
||||
|
||||
export async function execute(args: string[], options: LLMOptions): Promise<void> {
|
||||
const subcommand = args[0] || 'providers';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
switch (subcommand) {
|
||||
case 'providers':
|
||||
await providersCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'test':
|
||||
await testCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'validate':
|
||||
await validateCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'run':
|
||||
await runCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'models':
|
||||
await modelsCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'config':
|
||||
await configCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'trace':
|
||||
await traceCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'tools':
|
||||
await toolsCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'json':
|
||||
await jsonCommand(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
default:
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown subcommand: ${subcommand}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown subcommand: ${subcommand}`);
|
||||
await pretty.info('Available subcommands: providers, test, validate, run, models, config, trace, tools, json');
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.INVALID_ARGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available AI SDK providers
|
||||
*/
|
||||
async function providersCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
listSupportedProviders,
|
||||
AISDK_PROVIDERS,
|
||||
validateProviderApiKey,
|
||||
isAISDKAvailable,
|
||||
getAISDKVersion
|
||||
} = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
const providers = listSupportedProviders();
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
const aiSdkVersion = await getAISDKVersion();
|
||||
|
||||
const providerDetails = providers.map((id: string) => {
|
||||
const info = AISDK_PROVIDERS[id as keyof typeof AISDK_PROVIDERS];
|
||||
const hasApiKey = validateProviderApiKey(id);
|
||||
return {
|
||||
id,
|
||||
package: info?.package || 'custom',
|
||||
envKey: info?.envKey || 'N/A',
|
||||
hasApiKey,
|
||||
status: hasApiKey ? 'ready' : 'missing_key'
|
||||
};
|
||||
});
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
ai_sdk_available: aiSdkAvailable,
|
||||
ai_sdk_version: aiSdkVersion,
|
||||
providers: providerDetails,
|
||||
total: providers.length
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('AI SDK Providers');
|
||||
|
||||
if (aiSdkAvailable) {
|
||||
await pretty.success(`AI SDK: ${aiSdkVersion || 'installed'}`);
|
||||
} else {
|
||||
await pretty.warn('AI SDK not installed. Run: npm install ai');
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Available Providers:');
|
||||
|
||||
for (const p of providerDetails) {
|
||||
const status = p.hasApiKey ? '✅' : '⚠️';
|
||||
const keyInfo = p.hasApiKey ? '🔑' : `Missing ${p.envKey}`;
|
||||
await pretty.plain(` ${status} ${p.id.padEnd(20)} ${keyInfo}`);
|
||||
|
||||
if (options.verbose) {
|
||||
await pretty.dim(` Package: ${p.package}`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Set API keys via environment variables');
|
||||
await pretty.dim(' Example: export OPENAI_API_KEY=sk-...');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to list providers');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to a provider
|
||||
*/
|
||||
async function testCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
const providerId = args[0] || options.provider;
|
||||
|
||||
if (!providerId) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Provider ID required'));
|
||||
} else {
|
||||
await pretty.error('Provider ID required. Usage: llm test <provider>');
|
||||
await pretty.dim(' Example: praisonai-ts llm test openai');
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.INVALID_ARGS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
createAISDKBackend,
|
||||
validateProviderApiKey,
|
||||
getMissingApiKeyMessage,
|
||||
isProviderSupported
|
||||
} = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
// Check if provider is supported
|
||||
if (!isProviderSupported(providerId)) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError('PROVIDER_NOT_FOUND', `Provider '${providerId}' is not supported`));
|
||||
} else {
|
||||
await pretty.error(`Provider '${providerId}' is not supported`);
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.PROVIDER_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check API key
|
||||
if (!validateProviderApiKey(providerId)) {
|
||||
const msg = getMissingApiKeyMessage(providerId);
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError('AUTH_ERROR', msg));
|
||||
} else {
|
||||
await pretty.error(msg);
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.AUTH_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine test model based on provider
|
||||
const testModels: Record<string, string> = {
|
||||
openai: 'gpt-4o-mini',
|
||||
anthropic: 'claude-3-haiku-20240307',
|
||||
google: 'gemini-1.5-flash',
|
||||
groq: 'llama-3.1-8b-instant',
|
||||
mistral: 'mistral-small-latest',
|
||||
cohere: 'command-light',
|
||||
deepseek: 'deepseek-chat',
|
||||
};
|
||||
|
||||
const modelId = options.model || testModels[providerId] || 'default';
|
||||
const modelString = `${providerId}/${modelId}`;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.info(`Testing ${modelString}...`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
timeout: options.timeout || 30000,
|
||||
maxRetries: 0, // No retries for test
|
||||
});
|
||||
|
||||
const result = await backend.generateText({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Say "OK" and nothing else.' }
|
||||
],
|
||||
maxTokens: 10,
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
provider: providerId,
|
||||
model: modelId,
|
||||
status: 'success',
|
||||
duration_ms: duration,
|
||||
response: result.text,
|
||||
usage: result.usage
|
||||
}));
|
||||
} else {
|
||||
await pretty.success(`Connection successful! (${duration}ms)`);
|
||||
await pretty.plain(` Provider: ${providerId}`);
|
||||
await pretty.plain(` Model: ${modelId}`);
|
||||
await pretty.plain(` Response: ${result.text.trim()}`);
|
||||
if (result.usage) {
|
||||
await pretty.dim(` Tokens: ${result.usage.totalTokens}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, `Failed to test provider '${providerId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate provider configuration
|
||||
*/
|
||||
async function validateCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
const providerId = args[0] || options.provider;
|
||||
|
||||
if (!providerId) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Provider ID required'));
|
||||
} else {
|
||||
await pretty.error('Provider ID required. Usage: llm validate --provider <id>');
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.INVALID_ARGS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
isProviderSupported,
|
||||
validateProviderApiKey,
|
||||
getProviderEnvKey,
|
||||
getProviderPackage,
|
||||
isAISDKAvailable
|
||||
} = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
const checks: Array<{ name: string; passed: boolean; message: string }> = [];
|
||||
|
||||
// Check AI SDK availability
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
checks.push({
|
||||
name: 'AI SDK Installed',
|
||||
passed: aiSdkAvailable,
|
||||
message: aiSdkAvailable ? 'AI SDK is installed' : 'AI SDK not found. Run: npm install ai'
|
||||
});
|
||||
|
||||
// Check provider support
|
||||
const isSupported = isProviderSupported(providerId);
|
||||
checks.push({
|
||||
name: 'Provider Supported',
|
||||
passed: isSupported,
|
||||
message: isSupported ? `Provider '${providerId}' is supported` : `Provider '${providerId}' is not supported`
|
||||
});
|
||||
|
||||
if (isSupported) {
|
||||
// Check provider package
|
||||
const pkg = getProviderPackage(providerId);
|
||||
if (pkg) {
|
||||
let pkgInstalled = false;
|
||||
try {
|
||||
require.resolve(pkg);
|
||||
pkgInstalled = true;
|
||||
} catch {
|
||||
pkgInstalled = false;
|
||||
}
|
||||
checks.push({
|
||||
name: 'Provider Package',
|
||||
passed: pkgInstalled,
|
||||
message: pkgInstalled ? `${pkg} is installed` : `${pkg} not found. Run: npm install ${pkg}`
|
||||
});
|
||||
}
|
||||
|
||||
// Check API key
|
||||
const hasApiKey = validateProviderApiKey(providerId);
|
||||
const envKey = getProviderEnvKey(providerId);
|
||||
checks.push({
|
||||
name: 'API Key',
|
||||
passed: hasApiKey,
|
||||
message: hasApiKey
|
||||
? `${envKey} is set`
|
||||
: `${envKey} is not set`
|
||||
});
|
||||
}
|
||||
|
||||
const allPassed = checks.every(c => c.passed);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
provider: providerId,
|
||||
valid: allPassed,
|
||||
checks
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading(`Validation: ${providerId}`);
|
||||
|
||||
for (const check of checks) {
|
||||
const icon = check.passed ? '✅' : '❌';
|
||||
await pretty.plain(` ${icon} ${check.name}: ${check.message}`);
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
if (allPassed) {
|
||||
await pretty.success('All checks passed!');
|
||||
} else {
|
||||
await pretty.warn('Some checks failed. Fix the issues above.');
|
||||
process.exit(LLM_EXIT_CODES.GENERAL_ERROR);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, `Failed to validate provider '${providerId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a prompt with a specific model
|
||||
*/
|
||||
async function runCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
const prompt = args.join(' ');
|
||||
|
||||
if (!prompt) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Prompt required'));
|
||||
} else {
|
||||
await pretty.error('Prompt required. Usage: llm run "<prompt>" --model <provider/model>');
|
||||
await pretty.dim(' Example: praisonai-ts llm run "Hello!" --model openai/gpt-4o-mini');
|
||||
}
|
||||
process.exit(LLM_EXIT_CODES.INVALID_ARGS);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelString = options.model || 'openai/gpt-4o-mini';
|
||||
|
||||
try {
|
||||
const { createAISDKBackend } = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
timeout: options.timeout || 60000,
|
||||
});
|
||||
|
||||
if (options.stream) {
|
||||
// Streaming mode
|
||||
if (outputFormat === 'json') {
|
||||
// JSON streaming - output each chunk as JSON line
|
||||
const stream = await backend.streamText({
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
process.stdout.write('{"type":"stream_start"}\n');
|
||||
|
||||
let fullText = '';
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.text) {
|
||||
fullText += chunk.text;
|
||||
process.stdout.write(JSON.stringify({ type: 'text', text: chunk.text }) + '\n');
|
||||
}
|
||||
if (chunk.finishReason) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
type: 'finish',
|
||||
finishReason: chunk.finishReason,
|
||||
usage: chunk.usage
|
||||
}) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: 'stream_end', fullText }) + '\n');
|
||||
} else {
|
||||
// Pretty streaming - output text directly
|
||||
const stream = await backend.streamText({
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.text) {
|
||||
process.stdout.write(chunk.text);
|
||||
}
|
||||
}
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
} else {
|
||||
// Non-streaming mode
|
||||
const startTime = Date.now();
|
||||
const result = await backend.generateText({
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
model: modelString,
|
||||
text: result.text,
|
||||
usage: result.usage,
|
||||
finishReason: result.finishReason,
|
||||
duration_ms: duration
|
||||
}));
|
||||
} else {
|
||||
await pretty.plain(result.text);
|
||||
|
||||
if (options.verbose) {
|
||||
await pretty.newline();
|
||||
await pretty.dim(`Model: ${modelString}`);
|
||||
await pretty.dim(`Duration: ${duration}ms`);
|
||||
if (result.usage) {
|
||||
await pretty.dim(`Tokens: ${result.usage.totalTokens} (prompt: ${result.usage.promptTokens}, completion: ${result.usage.completionTokens})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, `Failed to run prompt with model '${modelString}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors with proper exit codes and formatting
|
||||
*/
|
||||
/**
|
||||
* List common models for each provider
|
||||
*/
|
||||
async function modelsCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const { listSupportedProviders, AISDK_PROVIDERS } = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
// Common models per provider (not exhaustive)
|
||||
const commonModels: Record<string, string[]> = {
|
||||
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo', 'o1', 'o1-mini'],
|
||||
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', 'claude-3-opus-latest'],
|
||||
google: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
|
||||
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
||||
mistral: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'],
|
||||
cohere: ['command-r-plus', 'command-r', 'command-light'],
|
||||
deepseek: ['deepseek-chat', 'deepseek-coder'],
|
||||
xai: ['grok-2', 'grok-2-mini'],
|
||||
};
|
||||
|
||||
const providers = listSupportedProviders();
|
||||
const modelsData = providers.map((id: string) => ({
|
||||
provider: id,
|
||||
models: commonModels[id] || ['(check provider docs)'],
|
||||
package: AISDK_PROVIDERS[id as keyof typeof AISDK_PROVIDERS]?.package || 'custom'
|
||||
}));
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
note: 'This is not an exhaustive list. Check provider documentation for all available models.',
|
||||
providers: modelsData
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Common Models by Provider');
|
||||
await pretty.dim('Note: This is not exhaustive. Check provider docs for all models.\n');
|
||||
|
||||
for (const p of modelsData) {
|
||||
await pretty.plain(` ${p.provider}:`);
|
||||
for (const model of p.models) {
|
||||
await pretty.dim(` - ${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Usage: praisonai-ts llm run "prompt" --model provider/model');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to list models');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show resolved configuration (redacted)
|
||||
*/
|
||||
async function configCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const { SAFE_DEFAULTS, AISDK_PROVIDERS, PROVIDER_ALIASES } = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
// Gather environment config (redacted)
|
||||
const envConfig: Record<string, string> = {};
|
||||
for (const [id, info] of Object.entries(AISDK_PROVIDERS)) {
|
||||
const envKey = (info as any).envKey;
|
||||
if (envKey && process.env[envKey]) {
|
||||
envConfig[envKey] = '****' + process.env[envKey]!.slice(-4);
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
defaults: SAFE_DEFAULTS,
|
||||
aliases: PROVIDER_ALIASES,
|
||||
environment: envConfig,
|
||||
model: options.model || 'openai/gpt-4o-mini',
|
||||
timeout: options.timeout || SAFE_DEFAULTS.timeout,
|
||||
};
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess(config));
|
||||
} else {
|
||||
await pretty.heading('AI SDK Configuration');
|
||||
|
||||
await pretty.plain(' Defaults:');
|
||||
await pretty.dim(` timeout: ${SAFE_DEFAULTS.timeout}ms`);
|
||||
await pretty.dim(` maxRetries: ${SAFE_DEFAULTS.maxRetries}`);
|
||||
await pretty.dim(` maxOutputTokens: ${SAFE_DEFAULTS.maxOutputTokens}`);
|
||||
await pretty.dim(` redactLogs: ${SAFE_DEFAULTS.redactLogs}`);
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Provider Aliases:');
|
||||
for (const [alias, target] of Object.entries(PROVIDER_ALIASES)) {
|
||||
await pretty.dim(` ${alias} → ${target}`);
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Environment (redacted):');
|
||||
if (Object.keys(envConfig).length === 0) {
|
||||
await pretty.dim(' No API keys detected');
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
await pretty.dim(` ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to show config');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo trace/attribution headers
|
||||
*/
|
||||
async function traceCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const { createAISDKBackend, createAttributionMiddleware } = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
const modelString = options.model || 'openai/gpt-4o-mini';
|
||||
|
||||
// Generate demo attribution context
|
||||
const attribution = {
|
||||
agentId: `agent-${Date.now().toString(36)}`,
|
||||
runId: `run-${Math.random().toString(36).slice(2, 10)}`,
|
||||
traceId: `trace-${Math.random().toString(36).slice(2, 10)}`,
|
||||
sessionId: options.verbose ? `session-demo` : undefined,
|
||||
};
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.heading('Attribution Trace Demo');
|
||||
await pretty.info(`Running with model: ${modelString}`);
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Attribution Context:');
|
||||
await pretty.dim(` agentId: ${attribution.agentId}`);
|
||||
await pretty.dim(` runId: ${attribution.runId}`);
|
||||
await pretty.dim(` traceId: ${attribution.traceId}`);
|
||||
if (attribution.sessionId) {
|
||||
await pretty.dim(` sessionId: ${attribution.sessionId}`);
|
||||
}
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Headers injected:');
|
||||
await pretty.dim(` X-Agent-Id: ${attribution.agentId}`);
|
||||
await pretty.dim(` X-Run-Id: ${attribution.runId}`);
|
||||
await pretty.dim(` X-Trace-Id: ${attribution.traceId}`);
|
||||
await pretty.newline();
|
||||
}
|
||||
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
timeout: options.timeout || 30000,
|
||||
attribution,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await backend.generateText({
|
||||
messages: [{ role: 'user', content: 'Say "Trace OK" and nothing else.' }],
|
||||
maxTokens: 10,
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
attribution,
|
||||
headers: {
|
||||
'X-Agent-Id': attribution.agentId,
|
||||
'X-Run-Id': attribution.runId,
|
||||
'X-Trace-Id': attribution.traceId,
|
||||
},
|
||||
response: result.text,
|
||||
duration_ms: duration,
|
||||
}));
|
||||
} else {
|
||||
await pretty.success(`Response: ${result.text.trim()} (${duration}ms)`);
|
||||
await pretty.newline();
|
||||
await pretty.info('Attribution headers are automatically injected into LLM requests.');
|
||||
await pretty.dim('Use --verbose for session ID.');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to run trace demo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo tool calling - shows how tools work with AI SDK
|
||||
* Note: AI SDK requires Zod schemas for tools, which requires runtime setup
|
||||
*/
|
||||
async function toolsCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const modelString = options.model || 'openai/gpt-4o-mini';
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.heading('Tool Calling Demo');
|
||||
await pretty.info(`Model: ${modelString}`);
|
||||
await pretty.newline();
|
||||
await pretty.plain(' AI SDK Tool Calling requires Zod schemas.');
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Example code:');
|
||||
await pretty.dim(` import { generateText, tool } from 'ai';`);
|
||||
await pretty.dim(` import { openai } from '@ai-sdk/openai';`);
|
||||
await pretty.dim(` import { z } from 'zod';`);
|
||||
await pretty.newline();
|
||||
await pretty.dim(` const tools = {`);
|
||||
await pretty.dim(` get_weather: tool({`);
|
||||
await pretty.dim(` description: 'Get weather for a city',`);
|
||||
await pretty.dim(` inputSchema: z.object({`);
|
||||
await pretty.dim(` city: z.string().describe('City name'),`);
|
||||
await pretty.dim(` }),`);
|
||||
await pretty.dim(` }),`);
|
||||
await pretty.dim(` };`);
|
||||
await pretty.newline();
|
||||
await pretty.dim(` const result = await generateText({`);
|
||||
await pretty.dim(` model: openai('gpt-4o-mini'),`);
|
||||
await pretty.dim(` messages: [{ role: 'user', content: 'Weather in Paris?' }],`);
|
||||
await pretty.dim(` tools,`);
|
||||
await pretty.dim(` });`);
|
||||
await pretty.newline();
|
||||
await pretty.info('See examples/ai-sdk/tool-calling.ts for a complete example.');
|
||||
} else {
|
||||
outputJson(formatSuccess({
|
||||
model: modelString,
|
||||
note: 'AI SDK tool calling requires Zod schemas at runtime',
|
||||
example: {
|
||||
import: "import { generateText, tool } from 'ai'; import { z } from 'zod';",
|
||||
tool: "tool({ description: '...', inputSchema: z.object({ city: z.string() }) })",
|
||||
},
|
||||
docs: 'See examples/ai-sdk/tool-calling.ts',
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to run tools demo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo structured output (JSON schema) - shows how to use generateObject
|
||||
* Note: AI SDK requires Zod schemas for structured output
|
||||
*/
|
||||
async function jsonCommand(args: string[], options: LLMOptions, outputFormat: string): Promise<void> {
|
||||
try {
|
||||
const modelString = options.model || 'openai/gpt-4o-mini';
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.heading('Structured Output (JSON) Demo');
|
||||
await pretty.info(`Model: ${modelString}`);
|
||||
await pretty.newline();
|
||||
await pretty.plain(' AI SDK Structured Output requires Zod schemas.');
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Example code:');
|
||||
await pretty.dim(` import { generateObject } from 'ai';`);
|
||||
await pretty.dim(` import { openai } from '@ai-sdk/openai';`);
|
||||
await pretty.dim(` import { z } from 'zod';`);
|
||||
await pretty.newline();
|
||||
await pretty.dim(` const schema = z.object({`);
|
||||
await pretty.dim(` name: z.string(),`);
|
||||
await pretty.dim(` age: z.number(),`);
|
||||
await pretty.dim(` city: z.string(),`);
|
||||
await pretty.dim(` });`);
|
||||
await pretty.newline();
|
||||
await pretty.dim(` const result = await generateObject({`);
|
||||
await pretty.dim(` model: openai('gpt-4o-mini'),`);
|
||||
await pretty.dim(` schema,`);
|
||||
await pretty.dim(` prompt: 'Generate a fictional person profile.',`);
|
||||
await pretty.dim(` });`);
|
||||
await pretty.newline();
|
||||
await pretty.dim(` console.log(result.object); // { name: '...', age: 25, city: '...' }`);
|
||||
await pretty.newline();
|
||||
await pretty.info('See examples/ai-sdk/structured-output.ts for a complete example.');
|
||||
} else {
|
||||
outputJson(formatSuccess({
|
||||
model: modelString,
|
||||
note: 'AI SDK structured output requires Zod schemas at runtime',
|
||||
example: {
|
||||
import: "import { generateObject } from 'ai'; import { z } from 'zod';",
|
||||
schema: "z.object({ name: z.string(), age: z.number(), city: z.string() })",
|
||||
},
|
||||
docs: 'See examples/ai-sdk/structured-output.ts',
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, outputFormat, 'Failed to run JSON demo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors with proper exit codes and formatting
|
||||
*/
|
||||
async function handleError(error: unknown, outputFormat: string, context: string): Promise<never> {
|
||||
const { AISDKError } = await import('../../llm/providers/ai-sdk');
|
||||
|
||||
let exitCode: number = LLM_EXIT_CODES.GENERAL_ERROR;
|
||||
let errorCode = 'UNKNOWN_ERROR';
|
||||
let message = context;
|
||||
|
||||
if (error instanceof AISDKError) {
|
||||
message = `${context}: ${error.message}`;
|
||||
errorCode = error.code;
|
||||
|
||||
switch (error.code) {
|
||||
case 'AUTHENTICATION':
|
||||
exitCode = 3; // AUTH_ERROR
|
||||
break;
|
||||
case 'PROVIDER_NOT_FOUND':
|
||||
case 'MODEL_NOT_FOUND':
|
||||
exitCode = 4; // PROVIDER_NOT_FOUND
|
||||
break;
|
||||
case 'NETWORK':
|
||||
exitCode = 5; // NETWORK_ERROR
|
||||
break;
|
||||
case 'TIMEOUT':
|
||||
exitCode = 6; // TIMEOUT
|
||||
break;
|
||||
case 'INVALID_REQUEST':
|
||||
exitCode = 2; // INVALID_ARGS
|
||||
break;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
message = `${context}: ${error.message}`;
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(errorCode, message));
|
||||
} else {
|
||||
await pretty.error(message);
|
||||
}
|
||||
|
||||
process.exit(exitCode);
|
||||
}
|
||||
@@ -1,16 +1,29 @@
|
||||
/**
|
||||
* Observability command - Monitoring and tracing
|
||||
*
|
||||
* Supports 14+ observability integrations:
|
||||
* - list: List all available observability tools
|
||||
* - doctor: Check environment variables for selected tool
|
||||
* - test: Run a test trace to verify tool works
|
||||
* - info: Show observability feature information
|
||||
*/
|
||||
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
import { ERROR_CODES } from '../output/errors';
|
||||
import {
|
||||
OBSERVABILITY_TOOLS,
|
||||
listObservabilityTools,
|
||||
hasObservabilityToolEnvVar,
|
||||
type ObservabilityToolName
|
||||
} from '../../observability/types';
|
||||
|
||||
export interface ObservabilityOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
tool?: string;
|
||||
}
|
||||
|
||||
export async function execute(args: string[], options: ObservabilityOptions): Promise<void> {
|
||||
@@ -19,8 +32,15 @@ export async function execute(args: string[], options: ObservabilityOptions): Pr
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'list':
|
||||
case 'providers':
|
||||
await listProviders(outputFormat);
|
||||
await listProvidersCommand(options, outputFormat);
|
||||
break;
|
||||
case 'doctor':
|
||||
await doctorCommand(args[1] || options.tool, options, outputFormat);
|
||||
break;
|
||||
case 'test':
|
||||
await testCommand(args[1] || options.tool, options, outputFormat);
|
||||
break;
|
||||
case 'info':
|
||||
await showInfo(outputFormat);
|
||||
@@ -76,20 +96,211 @@ async function showInfo(outputFormat: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function listProviders(outputFormat: string): Promise<void> {
|
||||
const providers = [
|
||||
{ name: 'console', description: 'Console logging (default)', available: true },
|
||||
{ name: 'memory', description: 'In-memory storage', available: true },
|
||||
{ name: 'langfuse', description: 'Langfuse observability platform', available: true }
|
||||
];
|
||||
/**
|
||||
* List all observability tools
|
||||
*/
|
||||
async function listProvidersCommand(options: ObservabilityOptions, outputFormat: string): Promise<void> {
|
||||
const tools = listObservabilityTools();
|
||||
|
||||
const providers = tools.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
package: tool.package,
|
||||
envKey: tool.envKey,
|
||||
hasEnvKey: hasObservabilityToolEnvVar(tool.name),
|
||||
features: tool.features
|
||||
}));
|
||||
|
||||
const ready = providers.filter(p => p.hasEnvKey);
|
||||
const builtIn = providers.filter(p => ['console', 'memory', 'noop'].includes(p.name));
|
||||
const external = providers.filter(p => !['console', 'memory', 'noop'].includes(p.name));
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ providers }));
|
||||
outputJson(formatSuccess({
|
||||
providers,
|
||||
total: providers.length,
|
||||
ready: ready.length,
|
||||
builtin: builtIn.length,
|
||||
external: external.length
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Observability Providers');
|
||||
for (const p of providers) {
|
||||
const status = p.available ? '✓' : '✗';
|
||||
await pretty.plain(` ${status} ${p.name.padEnd(15)} ${p.description}`);
|
||||
await pretty.heading('Observability Tools');
|
||||
await pretty.plain(`\n Total: ${providers.length} tools (${ready.length} ready)`);
|
||||
|
||||
await pretty.plain('\n Built-in (no setup required):');
|
||||
for (const p of builtIn) {
|
||||
await pretty.plain(` ✅ ${p.name.padEnd(12)} ${p.description}`);
|
||||
}
|
||||
|
||||
await pretty.plain('\n External Integrations:');
|
||||
for (const p of external) {
|
||||
const status = p.hasEnvKey ? '✅' : '⚠️';
|
||||
const keyInfo = p.envKey ? ` (${p.envKey})` : '';
|
||||
await pretty.plain(` ${status} ${p.name.padEnd(12)} ${p.description}${options.verbose ? keyInfo : ''}`);
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Run "observability doctor <tool>" for setup instructions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Doctor command - Check environment for a tool
|
||||
*/
|
||||
async function doctorCommand(toolName: string | undefined, options: ObservabilityOptions, outputFormat: string): Promise<void> {
|
||||
if (!toolName) {
|
||||
// Check all tools
|
||||
const tools = listObservabilityTools();
|
||||
const results = tools.map(tool => ({
|
||||
name: tool.name,
|
||||
envKey: tool.envKey,
|
||||
hasKey: hasObservabilityToolEnvVar(tool.name),
|
||||
package: tool.package
|
||||
}));
|
||||
|
||||
const ready = results.filter(r => r.hasKey);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
total: results.length,
|
||||
ready: ready.length,
|
||||
tools: results
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Observability Environment Check');
|
||||
await pretty.plain(`\n Total Tools: ${results.length}`);
|
||||
await pretty.plain(` Ready: ${ready.length} ✅`);
|
||||
|
||||
if (ready.length > 0) {
|
||||
await pretty.plain('\n Ready Tools:');
|
||||
for (const t of ready) {
|
||||
await pretty.plain(` ✅ ${t.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Run "observability doctor <tool>" for specific tool setup');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check specific tool
|
||||
const tool = OBSERVABILITY_TOOLS[toolName.toLowerCase() as ObservabilityToolName];
|
||||
|
||||
if (!tool) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown tool: ${toolName}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown tool: ${toolName}`);
|
||||
await pretty.info(`Available: ${Object.keys(OBSERVABILITY_TOOLS).join(', ')}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const hasKey = hasObservabilityToolEnvVar(tool.name);
|
||||
const keyValue = tool.envKey ? process.env[tool.envKey] : null;
|
||||
const maskedKey = keyValue ? `${keyValue.slice(0, 4)}...${keyValue.slice(-4)}` : 'not set';
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
tool: tool.name,
|
||||
env_key: tool.envKey,
|
||||
has_key: hasKey,
|
||||
key_preview: hasKey ? maskedKey : null,
|
||||
package: tool.package,
|
||||
description: tool.description,
|
||||
features: tool.features,
|
||||
status: hasKey || !tool.envKey ? 'ready' : 'missing_key'
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading(`Observability Doctor: ${tool.name}`);
|
||||
await pretty.plain(` Description: ${tool.description}`);
|
||||
if (tool.package) {
|
||||
await pretty.plain(` Package: ${tool.package}`);
|
||||
}
|
||||
if (tool.envKey) {
|
||||
await pretty.plain(` Environment Variable: ${tool.envKey}`);
|
||||
await pretty.plain(` Status: ${hasKey ? '✅ Ready' : '❌ Missing API Key'}`);
|
||||
if (hasKey) {
|
||||
await pretty.dim(` Key Preview: ${maskedKey}`);
|
||||
} else {
|
||||
await pretty.newline();
|
||||
await pretty.info(`Set the API key with:`);
|
||||
await pretty.dim(` export ${tool.envKey}=your-api-key`);
|
||||
}
|
||||
} else {
|
||||
await pretty.plain(` Status: ✅ Ready (no API key required)`);
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Features:');
|
||||
const f = tool.features;
|
||||
await pretty.plain(` Traces: ${f.traces ? '✅' : '❌'} Spans: ${f.spans ? '✅' : '❌'} Events: ${f.events ? '✅' : '❌'}`);
|
||||
await pretty.plain(` Errors: ${f.errors ? '✅' : '❌'} Metrics: ${f.metrics ? '✅' : '❌'} Export: ${f.export ? '✅' : '❌'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test command - Run a test trace
|
||||
*/
|
||||
async function testCommand(toolName: string | undefined, options: ObservabilityOptions, outputFormat: string): Promise<void> {
|
||||
const tool = toolName?.toLowerCase() || 'memory';
|
||||
|
||||
if (!OBSERVABILITY_TOOLS[tool as ObservabilityToolName]) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown tool: ${toolName}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.info(`Testing ${tool} observability...`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { createObservabilityAdapter } = await import('../../observability/adapters');
|
||||
const adapter = await createObservabilityAdapter(tool as ObservabilityToolName);
|
||||
|
||||
// Run a test trace
|
||||
const trace = adapter.startTrace('test-trace', { test: true });
|
||||
const span = adapter.startSpan(trace.traceId, 'test-span', 'custom');
|
||||
adapter.addEvent(span.spanId, 'test-event', { message: 'hello' });
|
||||
adapter.endSpan(span.spanId, 'completed');
|
||||
adapter.endTrace(trace.traceId, 'completed');
|
||||
await adapter.flush();
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
tool,
|
||||
status: 'success',
|
||||
latency_ms: latency,
|
||||
trace_id: trace.traceId
|
||||
}));
|
||||
} else {
|
||||
await pretty.plain(`\n ✅ Test Passed`);
|
||||
await pretty.plain(` Tool: ${tool}`);
|
||||
await pretty.plain(` Latency: ${latency}ms`);
|
||||
await pretty.dim(` Trace ID: ${trace.traceId}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const latency = Date.now() - startTime;
|
||||
const errorMessage = error.message || String(error);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, errorMessage, {
|
||||
tool,
|
||||
latency_ms: latency
|
||||
}));
|
||||
} else {
|
||||
await pretty.error(`Test Failed: ${errorMessage}`);
|
||||
await pretty.plain(` Tool: ${tool}`);
|
||||
await pretty.plain(` Latency: ${latency}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* Providers command - List and manage LLM providers
|
||||
*
|
||||
* Supports:
|
||||
* - list: List all registered providers
|
||||
* - register: Register a custom provider (programmatic only)
|
||||
* - list: List all registered providers (60+ AI SDK v6 providers)
|
||||
* - doctor: Check environment variables for selected provider
|
||||
* - test: Run a small test request to verify provider works
|
||||
* - info: Show provider details
|
||||
*/
|
||||
|
||||
@@ -14,6 +15,12 @@ import {
|
||||
getDefaultRegistry,
|
||||
isProviderAvailable
|
||||
} from '../../llm/providers';
|
||||
import {
|
||||
AISDK_PROVIDERS,
|
||||
PROVIDER_ALIASES,
|
||||
COMMUNITY_PROVIDERS,
|
||||
type ProviderInfo as AISDKProviderInfo
|
||||
} from '../../llm/providers/ai-sdk/types';
|
||||
import { hasApiKey } from '../runtime/env';
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
@@ -23,6 +30,8 @@ export interface ProvidersOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
interface ProviderInfo {
|
||||
@@ -32,14 +41,31 @@ interface ProviderInfo {
|
||||
hasApiKey: boolean;
|
||||
isBuiltin: boolean;
|
||||
models?: string[];
|
||||
modalities?: {
|
||||
text: boolean;
|
||||
chat: boolean;
|
||||
embeddings: boolean;
|
||||
image: boolean;
|
||||
audio: boolean;
|
||||
speech: boolean;
|
||||
tools: boolean;
|
||||
};
|
||||
description?: string;
|
||||
envKey?: string;
|
||||
package?: string;
|
||||
}
|
||||
|
||||
const BUILTIN_PROVIDERS = ['openai', 'anthropic', 'google', 'gemini', 'oai', 'claude'];
|
||||
|
||||
const PROVIDER_MODELS: Record<string, string[]> = {
|
||||
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
|
||||
google: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash']
|
||||
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo', 'gpt-5', 'gpt-5-mini'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307', 'claude-opus-4-5'],
|
||||
google: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
|
||||
xai: ['grok-4', 'grok-3', 'grok-3-fast', 'grok-3-mini'],
|
||||
groq: ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768', 'gemma2-9b-it'],
|
||||
mistral: ['mistral-large-latest', 'mistral-medium-latest', 'mistral-small-latest'],
|
||||
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
|
||||
cerebras: ['llama3.1-8b', 'llama3.3-70b'],
|
||||
};
|
||||
|
||||
export async function execute(args: string[], options: ProvidersOptions): Promise<void> {
|
||||
@@ -50,6 +76,12 @@ export async function execute(args: string[], options: ProvidersOptions): Promis
|
||||
case 'list':
|
||||
await listProvidersCommand(options, outputFormat);
|
||||
break;
|
||||
case 'doctor':
|
||||
await doctorCommand(args[1] || options.provider, options, outputFormat);
|
||||
break;
|
||||
case 'test':
|
||||
await testCommand(args[1] || options.provider, args[2] || options.model, options, outputFormat);
|
||||
break;
|
||||
case 'info':
|
||||
await providerInfoCommand(args[1], options, outputFormat);
|
||||
break;
|
||||
@@ -128,6 +160,202 @@ async function listProvidersCommand(options: ProvidersOptions, outputFormat: str
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Doctor command - Check environment variables for a provider
|
||||
*/
|
||||
async function doctorCommand(providerName: string | undefined, options: ProvidersOptions, outputFormat: string): Promise<void> {
|
||||
// Get all AI SDK providers
|
||||
const allProviders = Object.entries(AISDK_PROVIDERS);
|
||||
|
||||
if (providerName) {
|
||||
// Check specific provider
|
||||
const resolved = PROVIDER_ALIASES[providerName.toLowerCase()] || providerName.toLowerCase();
|
||||
const providerInfo = AISDK_PROVIDERS[resolved];
|
||||
|
||||
if (!providerInfo) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown provider: ${providerName}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown provider: ${providerName}`);
|
||||
await pretty.info(`Available providers: ${Object.keys(AISDK_PROVIDERS).slice(0, 10).join(', ')}...`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const hasKey = !!process.env[providerInfo.envKey];
|
||||
const keyValue = process.env[providerInfo.envKey];
|
||||
const maskedKey = keyValue ? `${keyValue.slice(0, 4)}...${keyValue.slice(-4)}` : 'not set';
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
provider: resolved,
|
||||
env_key: providerInfo.envKey,
|
||||
has_key: hasKey,
|
||||
key_preview: hasKey ? maskedKey : null,
|
||||
package: providerInfo.package,
|
||||
description: providerInfo.description,
|
||||
modalities: providerInfo.modalities,
|
||||
status: hasKey ? 'ready' : 'missing_key'
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading(`Provider Doctor: ${resolved}`);
|
||||
await pretty.plain(` Package: ${providerInfo.package}`);
|
||||
await pretty.plain(` Description: ${providerInfo.description || 'N/A'}`);
|
||||
await pretty.plain(` Environment Variable: ${providerInfo.envKey}`);
|
||||
await pretty.plain(` Status: ${hasKey ? '✅ Ready' : '❌ Missing API Key'}`);
|
||||
if (hasKey) {
|
||||
await pretty.dim(` Key Preview: ${maskedKey}`);
|
||||
} else {
|
||||
await pretty.newline();
|
||||
await pretty.info(`Set the API key with:`);
|
||||
await pretty.dim(` export ${providerInfo.envKey}=your-api-key`);
|
||||
}
|
||||
await pretty.newline();
|
||||
await pretty.plain(' Modalities:');
|
||||
const m = providerInfo.modalities;
|
||||
await pretty.plain(` Text/Chat: ${m.chat ? '✅' : '❌'} Embeddings: ${m.embeddings ? '✅' : '❌'} Image: ${m.image ? '✅' : '❌'}`);
|
||||
await pretty.plain(` Audio: ${m.audio ? '✅' : '❌'} Speech: ${m.speech ? '✅' : '❌'} Tools: ${m.tools ? '✅' : '❌'}`);
|
||||
}
|
||||
} else {
|
||||
// Check all providers
|
||||
const results = allProviders.map(([name, info]) => ({
|
||||
name,
|
||||
envKey: info.envKey,
|
||||
hasKey: !!process.env[info.envKey],
|
||||
description: info.description
|
||||
}));
|
||||
|
||||
const ready = results.filter(r => r.hasKey);
|
||||
const missing = results.filter(r => !r.hasKey);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
total: results.length,
|
||||
ready: ready.length,
|
||||
missing: missing.length,
|
||||
providers: results
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Provider Environment Check');
|
||||
await pretty.plain(`\n Total Providers: ${results.length}`);
|
||||
await pretty.plain(` Ready: ${ready.length} ✅`);
|
||||
await pretty.plain(` Missing Keys: ${missing.length} ⚠️`);
|
||||
|
||||
if (ready.length > 0) {
|
||||
await pretty.plain('\n Ready Providers:');
|
||||
for (const p of ready.slice(0, 10)) {
|
||||
await pretty.plain(` ✅ ${p.name}`);
|
||||
}
|
||||
if (ready.length > 10) {
|
||||
await pretty.dim(` ... and ${ready.length - 10} more`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info('Run "providers doctor <name>" for detailed info on a specific provider');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test command - Run a small test request to verify provider works
|
||||
*/
|
||||
async function testCommand(providerName: string | undefined, modelName: string | undefined, options: ProvidersOptions, outputFormat: string): Promise<void> {
|
||||
if (!providerName) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Provider name required. Usage: providers test <provider> [model]'));
|
||||
} else {
|
||||
await pretty.error('Provider name required. Usage: providers test <provider> [model]');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = PROVIDER_ALIASES[providerName.toLowerCase()] || providerName.toLowerCase();
|
||||
const providerInfo = AISDK_PROVIDERS[resolved];
|
||||
|
||||
if (!providerInfo) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown provider: ${providerName}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown provider: ${providerName}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check API key
|
||||
if (!process.env[providerInfo.envKey]) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, `Missing API key: ${providerInfo.envKey}`));
|
||||
} else {
|
||||
await pretty.error(`Missing API key: ${providerInfo.envKey}`);
|
||||
await pretty.info(`Set it with: export ${providerInfo.envKey}=your-api-key`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get default model
|
||||
const model = modelName || PROVIDER_MODELS[resolved]?.[0] || 'default';
|
||||
const modelString = `${resolved}/${model}`;
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.info(`Testing ${modelString}...`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try to use the backend resolver
|
||||
const { resolveBackend } = await import('../../llm/backend-resolver');
|
||||
const { provider } = await resolveBackend(modelString);
|
||||
|
||||
// Make a minimal test request
|
||||
const result = await provider.generateText({
|
||||
messages: [{ role: 'user', content: 'Say "test ok" and nothing else.' }],
|
||||
temperature: 0,
|
||||
maxTokens: 10
|
||||
});
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
const responseText = result.text?.slice(0, 50) || '[no response]';
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
provider: resolved,
|
||||
model,
|
||||
status: 'success',
|
||||
latency_ms: latency,
|
||||
response_preview: responseText,
|
||||
usage: result.usage
|
||||
}));
|
||||
} else {
|
||||
await pretty.plain(`\n ✅ Test Passed`);
|
||||
await pretty.plain(` Provider: ${resolved}`);
|
||||
await pretty.plain(` Model: ${model}`);
|
||||
await pretty.plain(` Latency: ${latency}ms`);
|
||||
await pretty.dim(` Response: "${responseText}"`);
|
||||
if (result.usage) {
|
||||
await pretty.dim(` Tokens: ${result.usage.totalTokens || 'N/A'}`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
const latency = Date.now() - startTime;
|
||||
const errorMessage = error.message || String(error);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.UNKNOWN, errorMessage, {
|
||||
provider: resolved,
|
||||
model,
|
||||
latency_ms: latency
|
||||
}));
|
||||
} else {
|
||||
await pretty.error(`Test Failed: ${errorMessage}`);
|
||||
await pretty.plain(` Provider: ${resolved}`);
|
||||
await pretty.plain(` Model: ${model}`);
|
||||
await pretty.plain(` Latency: ${latency}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function providerInfoCommand(name: string | undefined, options: ProvidersOptions, outputFormat: string): Promise<void> {
|
||||
if (!name) {
|
||||
if (outputFormat === 'json') {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
/**
|
||||
* Tools command - List or manage tools
|
||||
* Tools command - List, manage, and test AI SDK tools
|
||||
*
|
||||
* Subcommands:
|
||||
* list - List all available tools
|
||||
* info - Get detailed info about a tool
|
||||
* doctor - Check tool dependencies and env vars
|
||||
* example - Show usage example for a tool
|
||||
* add - Register a custom tool from npm/local
|
||||
* test - Test a tool (dry-run or live)
|
||||
*/
|
||||
|
||||
import { getRegistry, FunctionTool } from '../../tools/decorator';
|
||||
import { getToolsRegistry } from '../../tools/registry/registry';
|
||||
import { registerBuiltinTools } from '../../tools/tools';
|
||||
import { getAllBuiltinMetadata } from '../../tools/builtins';
|
||||
import type { ToolMetadata } from '../../tools/registry/types';
|
||||
import { outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
@@ -12,24 +24,19 @@ export interface ToolsOptions {
|
||||
verbose?: boolean;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
live?: boolean;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
interface ToolInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: unknown;
|
||||
// Ensure built-in tools are registered
|
||||
let builtinsRegistered = false;
|
||||
function ensureBuiltinsRegistered() {
|
||||
if (!builtinsRegistered) {
|
||||
registerBuiltinTools();
|
||||
builtinsRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Built-in tools that are always available
|
||||
const BUILTIN_TOOLS: ToolInfo[] = [
|
||||
{ name: 'arxiv_search', description: 'Search arXiv for academic papers' },
|
||||
{ name: 'web_search', description: 'Search the web for information' },
|
||||
{ name: 'calculator', description: 'Perform mathematical calculations' },
|
||||
{ name: 'code_executor', description: 'Execute code snippets' },
|
||||
{ name: 'file_reader', description: 'Read file contents' },
|
||||
{ name: 'file_writer', description: 'Write content to files' }
|
||||
];
|
||||
|
||||
export async function execute(args: string[], options: ToolsOptions): Promise<void> {
|
||||
const subcommand = args[0] || 'list';
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
@@ -41,54 +48,76 @@ export async function execute(args: string[], options: ToolsOptions): Promise<vo
|
||||
case 'info':
|
||||
await toolInfo(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'doctor':
|
||||
await toolDoctor(options, outputFormat);
|
||||
break;
|
||||
case 'example':
|
||||
await toolExample(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'add':
|
||||
await toolAdd(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
case 'test':
|
||||
await toolTest(args.slice(1), options, outputFormat);
|
||||
break;
|
||||
default:
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.INVALID_ARGS, `Unknown subcommand: ${subcommand}`));
|
||||
} else {
|
||||
await pretty.error(`Unknown subcommand: ${subcommand}`);
|
||||
await pretty.plain('Available subcommands: list, info, doctor, example, add, test');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
}
|
||||
|
||||
async function listTools(options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
// Get registered tools from registry
|
||||
const registry = getRegistry();
|
||||
const registeredTools = registry.list();
|
||||
ensureBuiltinsRegistered();
|
||||
|
||||
// Combine built-in and registered tools
|
||||
const allTools: ToolInfo[] = [
|
||||
...BUILTIN_TOOLS,
|
||||
...registeredTools.map((t: FunctionTool) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters
|
||||
}))
|
||||
];
|
||||
// Get all built-in tool metadata
|
||||
const builtinMetadata = await getAllBuiltinMetadata();
|
||||
|
||||
// Get registered tools from decorator registry (legacy)
|
||||
const decoratorRegistry = getRegistry();
|
||||
const registeredTools = decoratorRegistry.list();
|
||||
|
||||
// Filter by tag if specified
|
||||
let filteredMetadata = builtinMetadata;
|
||||
if (options.tag) {
|
||||
filteredMetadata = builtinMetadata.filter(m => m.tags.includes(options.tag!));
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
tools: allTools.map(t => ({
|
||||
builtinTools: filteredMetadata.map(m => ({
|
||||
id: m.id,
|
||||
name: m.displayName,
|
||||
description: m.description,
|
||||
tags: m.tags,
|
||||
packageName: m.packageName,
|
||||
requiredEnv: m.requiredEnv,
|
||||
capabilities: m.capabilities,
|
||||
})),
|
||||
registeredTools: registeredTools.map((t: FunctionTool) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: options.verbose ? t.parameters : undefined
|
||||
})),
|
||||
count: allTools.length
|
||||
count: filteredMetadata.length + registeredTools.length
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Available Tools');
|
||||
await pretty.heading('AI SDK Tools Registry');
|
||||
|
||||
await pretty.plain('Built-in Tools:');
|
||||
for (const tool of BUILTIN_TOOLS) {
|
||||
await pretty.plain(` • ${tool.name}`);
|
||||
if (tool.description) {
|
||||
await pretty.dim(` ${tool.description}`);
|
||||
}
|
||||
await pretty.plain('\nBuilt-in Tools:');
|
||||
for (const meta of filteredMetadata) {
|
||||
const tags = meta.tags.slice(0, 3).join(', ');
|
||||
await pretty.plain(` • ${meta.id} (${meta.displayName})`);
|
||||
await pretty.dim(` ${meta.description}`);
|
||||
await pretty.dim(` Tags: ${tags} | Package: ${meta.packageName}`);
|
||||
}
|
||||
|
||||
if (registeredTools.length > 0) {
|
||||
await pretty.newline();
|
||||
await pretty.plain('Registered Tools:');
|
||||
await pretty.plain('Custom Registered Tools:');
|
||||
for (const tool of registeredTools) {
|
||||
await pretty.plain(` • ${tool.name}`);
|
||||
if (tool.description) {
|
||||
@@ -98,63 +127,438 @@ async function listTools(options: ToolsOptions, outputFormat: string): Promise<v
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.info(`Total: ${allTools.length} tools`);
|
||||
await pretty.info(`Total: ${filteredMetadata.length + registeredTools.length} tools`);
|
||||
await pretty.dim('\nUse "praisonai-ts tools info <id>" for details');
|
||||
await pretty.dim('Use "praisonai-ts tools doctor" to check dependencies');
|
||||
}
|
||||
}
|
||||
|
||||
async function toolInfo(args: string[], options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
const toolName = args[0];
|
||||
ensureBuiltinsRegistered();
|
||||
const toolId = args[0];
|
||||
|
||||
if (!toolName) {
|
||||
if (!toolId) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a tool name'));
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a tool ID'));
|
||||
} else {
|
||||
await pretty.error('Please provide a tool name');
|
||||
await pretty.error('Please provide a tool ID');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
// Look for tool in built-in and registered tools
|
||||
const builtinTool = BUILTIN_TOOLS.find(t => t.name === toolName);
|
||||
const registry = getRegistry();
|
||||
const registeredTool = registry.get(toolName);
|
||||
// Look for tool in built-in metadata
|
||||
const builtinMetadata = await getAllBuiltinMetadata();
|
||||
const metadata = builtinMetadata.find(m => m.id === toolId);
|
||||
|
||||
const tool = builtinTool || (registeredTool ? {
|
||||
name: registeredTool.name,
|
||||
description: registeredTool.description,
|
||||
parameters: registeredTool.parameters
|
||||
} : null);
|
||||
// Also check decorator registry
|
||||
const decoratorRegistry = getRegistry();
|
||||
const registeredTool = decoratorRegistry.get(toolId);
|
||||
|
||||
if (!tool) {
|
||||
if (!metadata && !registeredTool) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolName}`));
|
||||
outputJson(formatError(ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolId}`));
|
||||
} else {
|
||||
await pretty.error(`Tool not found: ${toolName}`);
|
||||
await pretty.error(`Tool not found: ${toolId}`);
|
||||
await pretty.dim('Use "praisonai-ts tools list" to see available tools');
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
tool: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
type: builtinTool ? 'builtin' : 'registered'
|
||||
tool: metadata ? {
|
||||
id: metadata.id,
|
||||
name: metadata.displayName,
|
||||
description: metadata.description,
|
||||
tags: metadata.tags,
|
||||
packageName: metadata.packageName,
|
||||
requiredEnv: metadata.requiredEnv,
|
||||
optionalEnv: metadata.optionalEnv,
|
||||
install: metadata.install,
|
||||
capabilities: metadata.capabilities,
|
||||
docsSlug: metadata.docsSlug,
|
||||
} : {
|
||||
name: registeredTool!.name,
|
||||
description: registeredTool!.description,
|
||||
type: 'registered'
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading(`Tool: ${tool.name}`);
|
||||
await pretty.keyValue({
|
||||
'Name': tool.name,
|
||||
'Type': builtinTool ? 'Built-in' : 'Registered',
|
||||
'Description': tool.description || 'No description'
|
||||
});
|
||||
|
||||
if (tool.parameters && options.verbose) {
|
||||
if (metadata) {
|
||||
await pretty.heading(`Tool: ${metadata.displayName}`);
|
||||
await pretty.keyValue({
|
||||
'ID': metadata.id,
|
||||
'Description': metadata.description,
|
||||
'Package': metadata.packageName,
|
||||
'Tags': metadata.tags.join(', '),
|
||||
'Docs': `https://docs.praison.ai/js/${metadata.docsSlug}`,
|
||||
});
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain('Parameters:');
|
||||
await pretty.plain(JSON.stringify(tool.parameters, null, 2));
|
||||
await pretty.plain('Required Environment Variables:');
|
||||
if (metadata.requiredEnv.length > 0) {
|
||||
for (const env of metadata.requiredEnv) {
|
||||
const isSet = !!process.env[env];
|
||||
await pretty.plain(` ${isSet ? '✓' : '✗'} ${env}`);
|
||||
}
|
||||
} else {
|
||||
await pretty.dim(' None');
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain('Installation:');
|
||||
await pretty.plain(` ${metadata.install.npm}`);
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain('Capabilities:');
|
||||
const caps = Object.entries(metadata.capabilities)
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k]) => k);
|
||||
await pretty.plain(` ${caps.join(', ') || 'None specified'}`);
|
||||
} else {
|
||||
await pretty.heading(`Tool: ${registeredTool!.name}`);
|
||||
await pretty.keyValue({
|
||||
'Name': registeredTool!.name,
|
||||
'Type': 'Custom Registered',
|
||||
'Description': registeredTool!.description || 'No description'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toolDoctor(options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
ensureBuiltinsRegistered();
|
||||
const builtinMetadata = await getAllBuiltinMetadata();
|
||||
|
||||
const results: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
installed: boolean;
|
||||
missingEnv: string[];
|
||||
installCmd?: string;
|
||||
}> = [];
|
||||
|
||||
for (const meta of builtinMetadata) {
|
||||
// Check env vars
|
||||
const missingEnv = meta.requiredEnv.filter(env => !process.env[env]);
|
||||
|
||||
// Check if package is installed (check node_modules)
|
||||
let installed = false;
|
||||
try {
|
||||
// First try require.resolve for CJS packages
|
||||
require.resolve(meta.packageName);
|
||||
installed = true;
|
||||
} catch {
|
||||
// For ESM packages, check if the package directory exists
|
||||
try {
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
const packagePath = path.join(process.cwd(), 'node_modules', ...meta.packageName.split('/'));
|
||||
installed = fs.existsSync(packagePath);
|
||||
} catch {
|
||||
installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
id: meta.id,
|
||||
name: meta.displayName,
|
||||
installed,
|
||||
missingEnv,
|
||||
installCmd: installed ? undefined : meta.install.npm,
|
||||
});
|
||||
}
|
||||
|
||||
const allGood = results.every(r => r.installed && r.missingEnv.length === 0);
|
||||
const installedCount = results.filter(r => r.installed).length;
|
||||
const envIssues = results.filter(r => r.missingEnv.length > 0).length;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
status: allGood ? 'healthy' : 'issues_found',
|
||||
summary: {
|
||||
total: results.length,
|
||||
installed: installedCount,
|
||||
notInstalled: results.length - installedCount,
|
||||
missingEnvVars: envIssues,
|
||||
},
|
||||
tools: results,
|
||||
}));
|
||||
} else {
|
||||
await pretty.heading('Tools Health Check');
|
||||
|
||||
await pretty.newline();
|
||||
await pretty.plain(`Summary: ${installedCount}/${results.length} packages installed, ${envIssues} with missing env vars`);
|
||||
await pretty.newline();
|
||||
|
||||
for (const result of results) {
|
||||
const status = result.installed && result.missingEnv.length === 0 ? '✓' : '✗';
|
||||
await pretty.plain(`${status} ${result.id} (${result.name})`);
|
||||
|
||||
if (!result.installed) {
|
||||
await pretty.dim(` Package not installed: ${result.installCmd}`);
|
||||
}
|
||||
|
||||
if (result.missingEnv.length > 0) {
|
||||
await pretty.dim(` Missing env vars: ${result.missingEnv.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
await pretty.newline();
|
||||
if (allGood) {
|
||||
await pretty.info('All tools are ready to use!');
|
||||
} else {
|
||||
await pretty.warn('Some tools need attention. Install packages and set env vars as needed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toolExample(args: string[], options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
const toolId = args[0];
|
||||
|
||||
if (!toolId) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a tool ID'));
|
||||
} else {
|
||||
await pretty.error('Please provide a tool ID');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
// Tool examples
|
||||
const examples: Record<string, string> = {
|
||||
'code-execution': `
|
||||
import { Agent, tools } from 'praisonai';
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'CodeRunner',
|
||||
instructions: 'You can execute Python code to solve problems.',
|
||||
tools: [tools.codeExecution()],
|
||||
});
|
||||
|
||||
const result = await agent.run('Calculate the factorial of 10');
|
||||
console.log(result.text);
|
||||
`,
|
||||
'tavily': `
|
||||
import { Agent, tools } from 'praisonai';
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'Researcher',
|
||||
instructions: 'Search the web for information.',
|
||||
tools: [tools.tavily()],
|
||||
});
|
||||
|
||||
const result = await agent.run('What are the latest AI developments?');
|
||||
console.log(result.text);
|
||||
`,
|
||||
'exa': `
|
||||
import { Agent, tools } from 'praisonai';
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'WebSearcher',
|
||||
instructions: 'Search the web using semantic search.',
|
||||
tools: [tools.exa()],
|
||||
});
|
||||
|
||||
const result = await agent.run('Find AI companies in Europe');
|
||||
console.log(result.text);
|
||||
`,
|
||||
'superagent': `
|
||||
import { Agent, tools } from 'praisonai';
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'SecureAgent',
|
||||
instructions: 'Process text securely.',
|
||||
tools: [
|
||||
tools.guard(), // Check for prompt injection
|
||||
tools.redact(), // Remove PII
|
||||
tools.verify(), // Verify claims
|
||||
],
|
||||
});
|
||||
|
||||
const result = await agent.run('Check this text for security issues');
|
||||
console.log(result.text);
|
||||
`,
|
||||
'valyu': `
|
||||
import { Agent, tools } from 'praisonai';
|
||||
|
||||
const agent = new Agent({
|
||||
name: 'FinanceResearcher',
|
||||
instructions: 'Research financial data.',
|
||||
tools: [
|
||||
tools.valyuFinanceSearch(),
|
||||
tools.valyuSecSearch(),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await agent.run('What is Apple stock price trend?');
|
||||
console.log(result.text);
|
||||
`,
|
||||
};
|
||||
|
||||
const example = examples[toolId];
|
||||
|
||||
if (!example) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.TOOL_NOT_FOUND, `No example for tool: ${toolId}`));
|
||||
} else {
|
||||
await pretty.error(`No example available for: ${toolId}`);
|
||||
await pretty.dim(`Available examples: ${Object.keys(examples).join(', ')}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({ toolId, example: example.trim() }));
|
||||
} else {
|
||||
await pretty.heading(`Example: ${toolId}`);
|
||||
await pretty.plain(example);
|
||||
}
|
||||
}
|
||||
|
||||
async function toolAdd(args: string[], options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
const packageOrPath = args[0];
|
||||
|
||||
if (!packageOrPath) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a package name or path'));
|
||||
} else {
|
||||
await pretty.error('Please provide a package name or path');
|
||||
await pretty.dim('Usage: praisonai-ts tools add <package-name>');
|
||||
await pretty.dim(' praisonai-ts tools add ./path/to/tool.js');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
try {
|
||||
const { registerNpmTool, registerLocalTool } = await import('../../tools/builtins/custom');
|
||||
|
||||
const isLocalPath = packageOrPath.startsWith('./') || packageOrPath.startsWith('/');
|
||||
const tool = isLocalPath
|
||||
? await registerLocalTool(packageOrPath)
|
||||
: await registerNpmTool(packageOrPath);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
registered: true,
|
||||
tool: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
source: isLocalPath ? 'local' : 'npm',
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
await pretty.info(`Successfully registered tool: ${tool.name}`);
|
||||
await pretty.dim(`Description: ${tool.description}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.RUNTIME_ERROR, `Failed to register tool: ${error}`));
|
||||
} else {
|
||||
await pretty.error(`Failed to register tool: ${error}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async function toolTest(args: string[], options: ToolsOptions, outputFormat: string): Promise<void> {
|
||||
const toolId = args[0];
|
||||
|
||||
if (!toolId) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, 'Please provide a tool ID'));
|
||||
} else {
|
||||
await pretty.error('Please provide a tool ID');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
ensureBuiltinsRegistered();
|
||||
const registry = getToolsRegistry();
|
||||
const metadata = registry.getMetadata(toolId);
|
||||
|
||||
if (!metadata) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolId}`));
|
||||
} else {
|
||||
await pretty.error(`Tool not found: ${toolId}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
const missingEnv = metadata.requiredEnv.filter(env => !process.env[env]);
|
||||
|
||||
if (missingEnv.length > 0 && !options.live) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
toolId,
|
||||
status: 'dry_run',
|
||||
message: 'Dry run completed. Missing env vars for live test.',
|
||||
missingEnv,
|
||||
}));
|
||||
} else {
|
||||
await pretty.info(`Dry run for ${toolId}:`);
|
||||
await pretty.plain(` Tool ID: ${metadata.id}`);
|
||||
await pretty.plain(` Package: ${metadata.packageName}`);
|
||||
await pretty.warn(` Missing env vars: ${missingEnv.join(', ')}`);
|
||||
await pretty.dim('\nUse --live flag to run actual test (requires env vars)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.live) {
|
||||
if (missingEnv.length > 0) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.MISSING_ARG, `Missing required env vars: ${missingEnv.join(', ')}`));
|
||||
} else {
|
||||
await pretty.error(`Cannot run live test. Missing env vars: ${missingEnv.join(', ')}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
const tool = registry.create(toolId);
|
||||
await pretty.info(`Live test for ${toolId}...`);
|
||||
|
||||
// Simple test execution
|
||||
const testInput = { query: 'test' };
|
||||
const result = await tool.execute(testInput);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
toolId,
|
||||
status: 'success',
|
||||
result,
|
||||
}));
|
||||
} else {
|
||||
await pretty.info('Test passed!');
|
||||
await pretty.plain(`Result: ${JSON.stringify(result, null, 2)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(ERROR_CODES.RUNTIME_ERROR, `Test failed: ${error}`));
|
||||
} else {
|
||||
await pretty.error(`Test failed: ${error}`);
|
||||
}
|
||||
process.exit(EXIT_CODES.RUNTIME_ERROR);
|
||||
}
|
||||
} else {
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess({
|
||||
toolId,
|
||||
status: 'dry_run',
|
||||
message: 'Dry run completed successfully.',
|
||||
metadata: {
|
||||
id: metadata.id,
|
||||
package: metadata.packageName,
|
||||
envVars: metadata.requiredEnv,
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
await pretty.info(`Dry run for ${toolId}: OK`);
|
||||
await pretty.plain(` Tool is registered and ready`);
|
||||
await pretty.dim('\nUse --live flag to run actual API test');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,30 @@ export const COMMANDS: Record<string, Command> = {
|
||||
description: 'List available LLM providers',
|
||||
flags: []
|
||||
},
|
||||
llm: {
|
||||
description: 'AI SDK provider management and testing',
|
||||
subcommands: {
|
||||
providers: { description: 'List available AI SDK providers' },
|
||||
test: {
|
||||
description: 'Test connectivity to a provider',
|
||||
args: [{ name: 'provider', type: 'string', required: false, position: 0 }]
|
||||
},
|
||||
validate: {
|
||||
description: 'Validate provider configuration',
|
||||
args: [{ name: 'provider', type: 'string', required: false, position: 0 }]
|
||||
},
|
||||
run: {
|
||||
description: 'Run a prompt with a specific model',
|
||||
args: [{ name: 'prompt', type: 'string', required: true, position: 0 }]
|
||||
}
|
||||
},
|
||||
flags: [
|
||||
{ name: 'model', short: 'm', type: 'string', description: 'Model to use (provider/model format)' },
|
||||
{ name: 'stream', short: 's', type: 'boolean', default: false, description: 'Enable streaming output' },
|
||||
{ name: 'timeout', type: 'integer', description: 'Request timeout in milliseconds' },
|
||||
{ name: 'provider', short: 'p', type: 'string', description: 'Provider ID' }
|
||||
]
|
||||
},
|
||||
tools: {
|
||||
description: 'List or manage tools',
|
||||
subcommands: {
|
||||
@@ -204,6 +228,23 @@ export const COMMANDS: Record<string, Command> = {
|
||||
export: { description: 'Export telemetry data' }
|
||||
}
|
||||
},
|
||||
approval: {
|
||||
description: 'Tool approval management (AI SDK v6 parity)',
|
||||
subcommands: {
|
||||
status: { description: 'Show approval status' },
|
||||
pending: { description: 'List pending approval requests' },
|
||||
approve: { description: 'Approve a pending request', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
|
||||
deny: { description: 'Deny a pending request', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
|
||||
cancel: { description: 'Cancel a pending request', args: [{ name: 'id', type: 'string', required: true, position: 0 }] },
|
||||
'cancel-all': { description: 'Cancel all pending requests' },
|
||||
'auto-approve': { description: 'Auto-approve a tool', args: [{ name: 'tool', type: 'string', required: true, position: 0 }] },
|
||||
'auto-deny': { description: 'Auto-deny a tool', args: [{ name: 'tool', type: 'string', required: true, position: 0 }] },
|
||||
interactive: { description: 'Start interactive approval mode' }
|
||||
},
|
||||
flags: [
|
||||
{ name: 'timeout', type: 'integer', description: 'Approval timeout in ms' }
|
||||
]
|
||||
},
|
||||
planning: {
|
||||
description: 'Task planning and todo management',
|
||||
subcommands: {
|
||||
@@ -465,6 +506,38 @@ export const COMMANDS: Record<string, Command> = {
|
||||
args: [
|
||||
{ name: 'command', type: 'string', required: false, position: 0 }
|
||||
]
|
||||
},
|
||||
embed: {
|
||||
description: 'Generate embeddings using AI SDK',
|
||||
subcommands: {
|
||||
text: { description: 'Embed text(s)', args: [{ name: 'text', type: 'string', required: true, position: 0 }] },
|
||||
file: { description: 'Embed file contents', args: [{ name: 'path', type: 'string', required: true, position: 0 }] },
|
||||
query: { description: 'Query similar texts', args: [{ name: 'query', type: 'string', required: true, position: 0 }] },
|
||||
models: { description: 'List available embedding models' }
|
||||
},
|
||||
flags: [
|
||||
{ name: 'model', short: 'm', type: 'string', description: 'Embedding model to use' },
|
||||
{ name: 'provider', type: 'string', description: 'Provider (openai, google, cohere)' },
|
||||
{ name: 'backend', type: 'string', description: 'Backend (ai-sdk, native, auto)' },
|
||||
{ name: 'save', type: 'string', description: 'Save embeddings to file' },
|
||||
{ name: 'file', type: 'string', description: 'Embeddings file for query' }
|
||||
]
|
||||
},
|
||||
benchmark: {
|
||||
description: 'Run performance benchmarks',
|
||||
subcommands: {
|
||||
run: { description: 'Run all benchmarks' },
|
||||
import: { description: 'Benchmark import time' },
|
||||
memory: { description: 'Benchmark memory usage' },
|
||||
latency: { description: 'Benchmark first-call latency' },
|
||||
streaming: { description: 'Benchmark streaming throughput' },
|
||||
embedding: { description: 'Benchmark embedding throughput' }
|
||||
},
|
||||
flags: [
|
||||
{ name: 'iterations', type: 'integer', description: 'Number of iterations' },
|
||||
{ name: 'backend', type: 'string', description: 'Backend to test (ai-sdk, native, both)' },
|
||||
{ name: 'real', type: 'boolean', description: 'Use real API calls (requires keys)' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* LLMGuardrail - LLM-based content validation
|
||||
*/
|
||||
|
||||
import { createProvider, type LLMProvider } from '../llm/providers';
|
||||
import { type LLMProvider } from '../llm/providers';
|
||||
import { resolveBackend } from '../llm/backend-resolver';
|
||||
|
||||
export interface LLMGuardrailConfig {
|
||||
name: string;
|
||||
@@ -25,18 +26,41 @@ export interface LLMGuardrailResult {
|
||||
export class LLMGuardrail {
|
||||
readonly name: string;
|
||||
readonly criteria: string;
|
||||
private provider: LLMProvider;
|
||||
private provider: LLMProvider | null = null;
|
||||
private providerPromise: Promise<LLMProvider> | null = null;
|
||||
private llmModel: string;
|
||||
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.llmModel = config.llm || 'openai/gpt-4o-mini';
|
||||
this.threshold = config.threshold ?? 0.7;
|
||||
this.verbose = config.verbose ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LLM provider (lazy initialization with AI SDK backend)
|
||||
*/
|
||||
private async getProvider(): Promise<LLMProvider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
if (!this.providerPromise) {
|
||||
this.providerPromise = (async () => {
|
||||
const result = await resolveBackend(this.llmModel, {
|
||||
attribution: { agentId: `LLMGuardrail:${this.name}` },
|
||||
});
|
||||
this.provider = result.provider;
|
||||
return result.provider;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.providerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check content against criteria
|
||||
*/
|
||||
@@ -55,7 +79,8 @@ Respond with a JSON object containing:
|
||||
JSON response:`;
|
||||
|
||||
try {
|
||||
const result = await this.provider.generateText({
|
||||
const provider = await this.getProvider();
|
||||
const result = await provider.generateText({
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
|
||||
@@ -64,6 +64,35 @@ export {
|
||||
export * from './tools/arxivTools';
|
||||
export * from './tools/mcpSse';
|
||||
|
||||
// AI SDK Tools Registry
|
||||
export { tools, registerBuiltinTools } from './tools/tools';
|
||||
export { getToolsRegistry, createToolsRegistry, resetToolsRegistry, ToolsRegistry } from './tools/registry';
|
||||
export type {
|
||||
ToolExecutionContext, ToolLimits, RedactionHooks, ToolLogger,
|
||||
ToolCapabilities, InstallHints, ToolMetadata, ToolExecutionResult,
|
||||
PraisonTool, ToolParameterSchema, ToolParameterProperty,
|
||||
ToolMiddleware, ToolHooks, ToolFactory, RegisteredTool, ToolInstallStatus
|
||||
} from './tools/registry';
|
||||
export { MissingDependencyError, MissingEnvVarError } from './tools/registry';
|
||||
export {
|
||||
createLoggingMiddleware, createTimeoutMiddleware, createRedactionMiddleware,
|
||||
createRateLimitMiddleware, createRetryMiddleware, createTracingMiddleware,
|
||||
createValidationMiddleware, composeMiddleware
|
||||
} from './tools/registry';
|
||||
|
||||
// Built-in tool factories
|
||||
export {
|
||||
codeExecution, tavilySearch, tavilyExtract, tavilyCrawl,
|
||||
exaSearch, perplexitySearch, parallelSearch,
|
||||
firecrawlScrape, firecrawlCrawl,
|
||||
superagentGuard, superagentRedact, superagentVerify,
|
||||
valyuWebSearch, valyuFinanceSearch, valyuPaperSearch, valyuBioSearch,
|
||||
valyuPatentSearch, valyuSecSearch, valyuEconomicsSearch, valyuCompanyResearch,
|
||||
bedrockCodeInterpreter, bedrockBrowserNavigate, bedrockBrowserClick, bedrockBrowserFill,
|
||||
airweaveSearch, codeMode,
|
||||
registerCustomTool, createCustomTool, registerNpmTool, registerLocalTool
|
||||
} from './tools/builtins';
|
||||
|
||||
// ============================================================================
|
||||
// SESSION & MEMORY
|
||||
// ============================================================================
|
||||
@@ -100,8 +129,7 @@ export { ContextAgent, createContextAgent, type ContextAgentConfig, type Context
|
||||
// Export evaluation framework
|
||||
export { accuracyEval, performanceEval, reliabilityEval, EvalSuite, type EvalResult, type PerformanceResult, type AccuracyEvalConfig, type PerformanceEvalConfig, type ReliabilityEvalConfig } from './eval';
|
||||
|
||||
// Export observability
|
||||
export { MemoryObservabilityAdapter, ConsoleObservabilityAdapter, setObservabilityAdapter, getObservabilityAdapter, type ObservabilityAdapter, type TraceContext, type SpanContext, type SpanData, type TraceData } from './observability';
|
||||
// Note: Observability exports are at the bottom of this file with the full 14+ integrations
|
||||
|
||||
// Export skills
|
||||
export { SkillManager, createSkillManager, parseSkillFile, type Skill, type SkillMetadata, type SkillDiscoveryOptions } from './skills';
|
||||
@@ -267,6 +295,55 @@ export {
|
||||
type ToolDefinition as ProviderToolDefinition,
|
||||
} from './llm/providers';
|
||||
|
||||
// Export Observability (14+ integrations)
|
||||
export {
|
||||
// Types
|
||||
type SpanKind,
|
||||
type SpanStatus,
|
||||
type SpanData,
|
||||
type SpanEvent,
|
||||
type TraceData,
|
||||
type TraceContext,
|
||||
type SpanContext,
|
||||
type ObservabilityAdapter,
|
||||
type AttributionContext,
|
||||
type ProviderMetadata,
|
||||
type ObservabilityToolConfig,
|
||||
type ObservabilityToolName,
|
||||
type ObservabilityToolInfo,
|
||||
// Constants
|
||||
OBSERVABILITY_TOOLS,
|
||||
getObservabilityToolInfo,
|
||||
listObservabilityTools,
|
||||
hasObservabilityToolEnvVar,
|
||||
// Adapters
|
||||
NoopObservabilityAdapter,
|
||||
noopAdapter,
|
||||
MemoryObservabilityAdapter,
|
||||
createMemoryAdapter,
|
||||
ConsoleObservabilityAdapter,
|
||||
createConsoleAdapter,
|
||||
createObservabilityAdapter,
|
||||
clearAdapterCache,
|
||||
// Global adapter management
|
||||
setObservabilityAdapter,
|
||||
getObservabilityAdapter,
|
||||
resetObservabilityAdapter,
|
||||
trace,
|
||||
} from './observability';
|
||||
|
||||
// Export AI SDK Provider Types (60+ providers)
|
||||
export {
|
||||
AISDK_PROVIDERS,
|
||||
PROVIDER_ALIASES,
|
||||
COMMUNITY_PROVIDERS,
|
||||
ADAPTERS,
|
||||
type ProviderInfo,
|
||||
type ProviderModalities,
|
||||
type CommunityProvider,
|
||||
type AdapterInfo,
|
||||
} from './llm/providers/ai-sdk/types';
|
||||
|
||||
// Export CLI Features
|
||||
export {
|
||||
// Slash Commands
|
||||
@@ -318,4 +395,233 @@ export {
|
||||
// Fast Context
|
||||
FastContext, createFastContext, getQuickContext,
|
||||
type FastContextConfig, type ContextSource, type FastContextResult
|
||||
} from './cli/features';
|
||||
} from './cli/features';
|
||||
|
||||
// ============================================================================
|
||||
// AI SDK WRAPPER MODULE - Core execution primitives
|
||||
// ============================================================================
|
||||
export {
|
||||
// Text generation (renamed to avoid conflicts with provider exports)
|
||||
generateText as aiGenerateText,
|
||||
streamText as aiStreamText,
|
||||
type GenerateTextOptions as AIGenerateTextOptions,
|
||||
type GenerateTextResult as AIGenerateTextResult,
|
||||
type StreamTextOptions as AIStreamTextOptions,
|
||||
type StreamTextResult as AIStreamTextResult,
|
||||
type TextStreamPart,
|
||||
// Object generation
|
||||
generateObject as aiGenerateObject,
|
||||
streamObject as aiStreamObject,
|
||||
type GenerateObjectOptions as AIGenerateObjectOptions,
|
||||
type GenerateObjectResult as AIGenerateObjectResult,
|
||||
type StreamObjectOptions as AIStreamObjectOptions,
|
||||
type StreamObjectResult as AIStreamObjectResult,
|
||||
// Image generation
|
||||
generateImage as aiGenerateImage,
|
||||
type GenerateImageOptions as AIGenerateImageOptions,
|
||||
type GenerateImageResult as AIGenerateImageResult,
|
||||
// Embeddings (renamed to avoid conflicts)
|
||||
embed as aiEmbed,
|
||||
embedMany as aiEmbedMany,
|
||||
type EmbedOptions as AIEmbedOptions,
|
||||
type EmbedResult as AIEmbedResult,
|
||||
type EmbedManyResult as AIEmbedManyResult,
|
||||
// Tools
|
||||
defineTool,
|
||||
createToolSet,
|
||||
functionToTool,
|
||||
type ToolDefinition as AIToolDefinition,
|
||||
type ToolExecuteFunction,
|
||||
type ToolInput,
|
||||
type ToolOutput,
|
||||
// Models
|
||||
createModel,
|
||||
getModel,
|
||||
parseModel,
|
||||
MODEL_ALIASES,
|
||||
listModelAliases,
|
||||
hasModelAlias,
|
||||
resolveModelAlias,
|
||||
type ModelConfig,
|
||||
type ModelId,
|
||||
// Middleware (renamed to avoid conflicts)
|
||||
createCachingMiddleware,
|
||||
createLoggingMiddleware as createAILoggingMiddleware,
|
||||
wrapModel,
|
||||
applyMiddleware,
|
||||
clearCache as clearAICache,
|
||||
getCacheStats as getAICacheStats,
|
||||
type Middleware as AIMiddleware,
|
||||
type MiddlewareConfig as AIMiddlewareConfig,
|
||||
type MiddlewareRequest,
|
||||
type MiddlewareResponse,
|
||||
// Multimodal
|
||||
createImagePart,
|
||||
createFilePart,
|
||||
createPdfPart,
|
||||
createTextPart,
|
||||
createMultimodalMessage,
|
||||
toMessageContent,
|
||||
base64ToUint8Array,
|
||||
uint8ArrayToBase64,
|
||||
isUrl,
|
||||
isDataUrl,
|
||||
type InputPart,
|
||||
type ImagePart as AIImagePart,
|
||||
type FilePart as AIFilePart,
|
||||
type PdfPart,
|
||||
type TextPart as AITextPart,
|
||||
// MCP
|
||||
createMCP,
|
||||
getMCPClient,
|
||||
closeMCPClient,
|
||||
closeAllMCPClients,
|
||||
mcpToolsToAITools,
|
||||
type MCPConfig,
|
||||
type MCPClient,
|
||||
type MCPTool,
|
||||
type MCPResource,
|
||||
type MCPPrompt,
|
||||
// Server adapters
|
||||
createHttpHandler,
|
||||
createExpressHandler,
|
||||
createHonoHandler,
|
||||
createFastifyHandler,
|
||||
createNestHandler,
|
||||
type ServerHandler,
|
||||
type ServerHandlerConfig,
|
||||
// Next.js
|
||||
createRouteHandler,
|
||||
createPagesHandler,
|
||||
type RouteHandlerConfig,
|
||||
type UseChatConfig,
|
||||
// Agent loop
|
||||
createAgentLoop,
|
||||
AgentLoop,
|
||||
stopAfterSteps,
|
||||
stopWhenNoToolCalls,
|
||||
stopWhen,
|
||||
type AgentLoopConfig,
|
||||
type AgentStep as AIAgentStep,
|
||||
type AgentLoopResult,
|
||||
type StopCondition,
|
||||
// UI Message (AI SDK v6 parity)
|
||||
convertToModelMessages,
|
||||
convertToUIMessages,
|
||||
validateUIMessages,
|
||||
safeValidateUIMessages,
|
||||
createTextMessage,
|
||||
createSystemMessage,
|
||||
hasPendingApprovals,
|
||||
getToolsNeedingApproval,
|
||||
createApprovalResponse,
|
||||
toUIMessageStreamResponse,
|
||||
pipeUIMessageStreamToResponse,
|
||||
type UIMessage,
|
||||
type UIMessagePart,
|
||||
type TextUIPart,
|
||||
type ReasoningUIPart,
|
||||
type ToolUIPart,
|
||||
type FileUIPart,
|
||||
type DataUIPart,
|
||||
type ModelMessage as AIModelMessage,
|
||||
type UIMessageStreamOptions,
|
||||
// Tool Approval (AI SDK v6 parity)
|
||||
ApprovalManager,
|
||||
getApprovalManager,
|
||||
setApprovalManager,
|
||||
withApproval,
|
||||
ToolApprovalDeniedError,
|
||||
ToolApprovalTimeoutError,
|
||||
DANGEROUS_PATTERNS,
|
||||
isDangerous,
|
||||
createDangerousPatternChecker,
|
||||
type ToolApprovalConfig,
|
||||
type ToolApprovalRequest,
|
||||
type ToolApprovalResponse,
|
||||
type ApprovalState,
|
||||
type ApprovalHandler,
|
||||
// Speech & Transcription
|
||||
generateSpeech,
|
||||
transcribe,
|
||||
SPEECH_MODELS,
|
||||
TRANSCRIPTION_MODELS,
|
||||
type GenerateSpeechOptions,
|
||||
type GenerateSpeechResult,
|
||||
type TranscribeOptions,
|
||||
type TranscribeResult,
|
||||
type TranscriptionSegment,
|
||||
// DevTools
|
||||
enableDevTools,
|
||||
disableDevTools,
|
||||
isDevToolsEnabled,
|
||||
getDevToolsState,
|
||||
getDevToolsUrl,
|
||||
createDevToolsMiddleware,
|
||||
autoEnableDevTools,
|
||||
type DevToolsConfig,
|
||||
type DevToolsState,
|
||||
// Telemetry (AI SDK v6 parity)
|
||||
configureTelemetry,
|
||||
getTelemetrySettings,
|
||||
enableAITelemetry,
|
||||
disableAITelemetry,
|
||||
isTelemetryEnabled,
|
||||
initOpenTelemetry,
|
||||
getTracer,
|
||||
createAISpan,
|
||||
withSpan,
|
||||
createTelemetryMiddleware,
|
||||
recordEvent,
|
||||
getEvents,
|
||||
clearEvents,
|
||||
createTelemetrySettings,
|
||||
type TelemetrySettings as AITelemetrySettings,
|
||||
type Tracer as AITracer,
|
||||
type Span as AISpan,
|
||||
type SpanOptions as AISpanOptions,
|
||||
type SpanKind as AISpanKind,
|
||||
type SpanStatus as AISpanStatus,
|
||||
type TelemetryEvent as AITelemetryEvent,
|
||||
// OAuth for MCP
|
||||
type OAuthClientProvider,
|
||||
} from './ai';
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRATIONS - Slack, Postgres, Computer Use
|
||||
// ============================================================================
|
||||
export {
|
||||
// Slack
|
||||
createSlackBot,
|
||||
SlackBot,
|
||||
verifySlackSignature,
|
||||
parseSlackMessage,
|
||||
type SlackConfig,
|
||||
type SlackMessage,
|
||||
type SlackResponse,
|
||||
type SlackEventHandler,
|
||||
} from './integrations/slack';
|
||||
|
||||
export {
|
||||
// Natural Language Postgres
|
||||
createNLPostgres,
|
||||
NLPostgresClient,
|
||||
createPostgresTool,
|
||||
type PostgresConfig as NLPostgresConfig,
|
||||
type TableSchema,
|
||||
type ColumnSchema,
|
||||
type QueryResult,
|
||||
type NLQueryResult,
|
||||
} from './integrations/postgres';
|
||||
|
||||
export {
|
||||
// Computer Use
|
||||
createComputerUse,
|
||||
ComputerUseClient,
|
||||
createComputerUseAgent,
|
||||
createCLIApprovalPrompt,
|
||||
type ComputerUseConfig,
|
||||
type ComputerUseTools,
|
||||
type ComputerAction,
|
||||
type ScreenshotResult,
|
||||
} from './integrations/computer-use';
|
||||
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Computer Use Integration
|
||||
*
|
||||
* Provides interfaces for computer use capabilities (browser/desktop control).
|
||||
* By default, tools are safe no-ops that require explicit implementation.
|
||||
*/
|
||||
|
||||
export interface ComputerUseConfig {
|
||||
/** Enable browser control */
|
||||
browser?: boolean;
|
||||
/** Enable desktop control */
|
||||
desktop?: boolean;
|
||||
/** Require human approval for actions (default: true) */
|
||||
requireApproval?: boolean;
|
||||
/** Screenshot directory */
|
||||
screenshotDir?: string;
|
||||
/** Action timeout in ms (default: 30000) */
|
||||
timeout?: number;
|
||||
/** Custom tool implementations */
|
||||
tools?: Partial<ComputerUseTools>;
|
||||
}
|
||||
|
||||
export interface ComputerUseTools {
|
||||
/** Take a screenshot */
|
||||
screenshot: () => Promise<ScreenshotResult>;
|
||||
/** Click at coordinates */
|
||||
click: (x: number, y: number, button?: 'left' | 'right' | 'middle') => Promise<void>;
|
||||
/** Double click at coordinates */
|
||||
doubleClick: (x: number, y: number) => Promise<void>;
|
||||
/** Type text */
|
||||
type: (text: string) => Promise<void>;
|
||||
/** Press a key or key combination */
|
||||
key: (key: string) => Promise<void>;
|
||||
/** Move mouse to coordinates */
|
||||
moveMouse: (x: number, y: number) => Promise<void>;
|
||||
/** Scroll */
|
||||
scroll: (direction: 'up' | 'down' | 'left' | 'right', amount?: number) => Promise<void>;
|
||||
/** Get screen size */
|
||||
getScreenSize: () => Promise<{ width: number; height: number }>;
|
||||
/** Wait for a duration */
|
||||
wait: (ms: number) => Promise<void>;
|
||||
/** Execute a shell command (with approval) */
|
||||
execute: (command: string) => Promise<string>;
|
||||
}
|
||||
|
||||
export interface ScreenshotResult {
|
||||
/** Base64-encoded image */
|
||||
base64: string;
|
||||
/** Image width */
|
||||
width: number;
|
||||
/** Image height */
|
||||
height: number;
|
||||
/** File path if saved */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface ComputerAction {
|
||||
type: 'screenshot' | 'click' | 'doubleClick' | 'type' | 'key' | 'moveMouse' | 'scroll' | 'wait' | 'execute';
|
||||
params?: any;
|
||||
requiresApproval?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Computer Use agent with safe defaults.
|
||||
*
|
||||
* @example Basic usage (safe mode - no-op tools)
|
||||
* ```typescript
|
||||
* import { createComputerUse } from 'praisonai/integrations/computer-use';
|
||||
*
|
||||
* const computer = createComputerUse({
|
||||
* requireApproval: true
|
||||
* });
|
||||
*
|
||||
* // Get tools for use with an agent
|
||||
* const tools = computer.getTools();
|
||||
* ```
|
||||
*
|
||||
* @example With Playwright browser control
|
||||
* ```typescript
|
||||
* import { chromium } from 'playwright';
|
||||
* import { createComputerUse } from 'praisonai/integrations/computer-use';
|
||||
*
|
||||
* const browser = await chromium.launch();
|
||||
* const page = await browser.newPage();
|
||||
*
|
||||
* const computer = createComputerUse({
|
||||
* browser: true,
|
||||
* tools: {
|
||||
* screenshot: async () => {
|
||||
* const buffer = await page.screenshot();
|
||||
* return {
|
||||
* base64: buffer.toString('base64'),
|
||||
* width: 1920,
|
||||
* height: 1080
|
||||
* };
|
||||
* },
|
||||
* click: async (x, y) => {
|
||||
* await page.mouse.click(x, y);
|
||||
* },
|
||||
* type: async (text) => {
|
||||
* await page.keyboard.type(text);
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createComputerUse(config: ComputerUseConfig = {}): ComputerUseClient {
|
||||
return new ComputerUseClient(config);
|
||||
}
|
||||
|
||||
export class ComputerUseClient {
|
||||
private config: ComputerUseConfig;
|
||||
private tools: ComputerUseTools;
|
||||
private approvalCallback?: (action: ComputerAction) => Promise<boolean>;
|
||||
|
||||
constructor(config: ComputerUseConfig = {}) {
|
||||
this.config = {
|
||||
requireApproval: true,
|
||||
timeout: 30000,
|
||||
...config,
|
||||
};
|
||||
|
||||
// Initialize with safe no-op tools
|
||||
this.tools = {
|
||||
screenshot: config.tools?.screenshot || this.noOpScreenshot,
|
||||
click: config.tools?.click || this.noOpAction,
|
||||
doubleClick: config.tools?.doubleClick || this.noOpAction,
|
||||
type: config.tools?.type || this.noOpAction,
|
||||
key: config.tools?.key || this.noOpAction,
|
||||
moveMouse: config.tools?.moveMouse || this.noOpAction,
|
||||
scroll: config.tools?.scroll || this.noOpAction,
|
||||
getScreenSize: config.tools?.getScreenSize || (async () => ({ width: 1920, height: 1080 })),
|
||||
wait: config.tools?.wait || (async (ms: number) => new Promise(r => setTimeout(r, ms))),
|
||||
execute: config.tools?.execute || this.noOpExecute,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the approval callback for actions.
|
||||
*/
|
||||
onApproval(callback: (action: ComputerAction) => Promise<boolean>): this {
|
||||
this.approvalCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op screenshot implementation.
|
||||
*/
|
||||
private async noOpScreenshot(): Promise<ScreenshotResult> {
|
||||
console.warn('Computer Use: screenshot() called but no implementation provided');
|
||||
return {
|
||||
base64: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op action implementation.
|
||||
*/
|
||||
private async noOpAction(...args: any[]): Promise<void> {
|
||||
console.warn('Computer Use: action called but no implementation provided', args);
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op execute implementation.
|
||||
*/
|
||||
private async noOpExecute(command: string): Promise<string> {
|
||||
console.warn('Computer Use: execute() called but no implementation provided');
|
||||
return `[No-op] Would execute: ${command}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request approval for an action.
|
||||
*/
|
||||
private async requestApproval(action: ComputerAction): Promise<boolean> {
|
||||
if (!this.config.requireApproval) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.approvalCallback) {
|
||||
return await this.approvalCallback(action);
|
||||
}
|
||||
|
||||
// Default: deny if no callback
|
||||
console.warn('Computer Use: Action requires approval but no callback set', action);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot.
|
||||
*/
|
||||
async screenshot(): Promise<ScreenshotResult> {
|
||||
const action: ComputerAction = { type: 'screenshot' };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Screenshot action denied');
|
||||
}
|
||||
return await this.tools.screenshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click at coordinates.
|
||||
*/
|
||||
async click(x: number, y: number, button: 'left' | 'right' | 'middle' = 'left'): Promise<void> {
|
||||
const action: ComputerAction = { type: 'click', params: { x, y, button }, requiresApproval: true };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Click action denied');
|
||||
}
|
||||
await this.tools.click(x, y, button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type text.
|
||||
*/
|
||||
async type(text: string): Promise<void> {
|
||||
const action: ComputerAction = { type: 'type', params: { text }, requiresApproval: true };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Type action denied');
|
||||
}
|
||||
await this.tools.type(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Press a key.
|
||||
*/
|
||||
async key(key: string): Promise<void> {
|
||||
const action: ComputerAction = { type: 'key', params: { key }, requiresApproval: true };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Key action denied');
|
||||
}
|
||||
await this.tools.key(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command.
|
||||
*/
|
||||
async execute(command: string): Promise<string> {
|
||||
const action: ComputerAction = { type: 'execute', params: { command }, requiresApproval: true };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Execute action denied');
|
||||
}
|
||||
return await this.tools.execute(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools for use with an agent.
|
||||
*/
|
||||
getTools(): Record<string, any> {
|
||||
return {
|
||||
computer_screenshot: {
|
||||
description: 'Take a screenshot of the current screen',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
execute: async () => {
|
||||
const result = await this.screenshot();
|
||||
return JSON.stringify({ width: result.width, height: result.height, hasImage: !!result.base64 });
|
||||
},
|
||||
},
|
||||
computer_click: {
|
||||
description: 'Click at specific coordinates on the screen',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: { type: 'number', description: 'X coordinate' },
|
||||
y: { type: 'number', description: 'Y coordinate' },
|
||||
button: { type: 'string', enum: ['left', 'right', 'middle'], description: 'Mouse button' },
|
||||
},
|
||||
required: ['x', 'y'],
|
||||
},
|
||||
execute: async ({ x, y, button }: { x: number; y: number; button?: 'left' | 'right' | 'middle' }) => {
|
||||
await this.click(x, y, button);
|
||||
return 'Clicked successfully';
|
||||
},
|
||||
},
|
||||
computer_type: {
|
||||
description: 'Type text using the keyboard',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Text to type' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
execute: async ({ text }: { text: string }) => {
|
||||
await this.type(text);
|
||||
return 'Typed successfully';
|
||||
},
|
||||
},
|
||||
computer_key: {
|
||||
description: 'Press a key or key combination (e.g., "Enter", "Control+C")',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Key or key combination to press' },
|
||||
},
|
||||
required: ['key'],
|
||||
},
|
||||
execute: async ({ key }: { key: string }) => {
|
||||
await this.key(key);
|
||||
return 'Key pressed successfully';
|
||||
},
|
||||
},
|
||||
computer_scroll: {
|
||||
description: 'Scroll the screen',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
|
||||
amount: { type: 'number', description: 'Scroll amount in pixels' },
|
||||
},
|
||||
required: ['direction'],
|
||||
},
|
||||
execute: async ({ direction, amount }: { direction: 'up' | 'down' | 'left' | 'right'; amount?: number }) => {
|
||||
const action: ComputerAction = { type: 'scroll', params: { direction, amount }, requiresApproval: true };
|
||||
if (!(await this.requestApproval(action))) {
|
||||
throw new Error('Scroll action denied');
|
||||
}
|
||||
await this.tools.scroll(direction, amount);
|
||||
return 'Scrolled successfully';
|
||||
},
|
||||
},
|
||||
computer_execute: {
|
||||
description: 'Execute a shell command (requires approval)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Shell command to execute' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
execute: async ({ command }: { command: string }) => {
|
||||
const result = await this.execute(command);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple approval prompt for CLI usage.
|
||||
*/
|
||||
export function createCLIApprovalPrompt(): (action: ComputerAction) => Promise<boolean> {
|
||||
return async (action: ComputerAction): Promise<boolean> => {
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const actionDesc = `${action.type}${action.params ? `: ${JSON.stringify(action.params)}` : ''}`;
|
||||
rl.question(`\n⚠️ Approve action: ${actionDesc}? (y/n) `, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Computer Use agent with human-in-the-loop approval.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from 'praisonai';
|
||||
* import { createComputerUseAgent } from 'praisonai/integrations/computer-use';
|
||||
*
|
||||
* const agent = await createComputerUseAgent({
|
||||
* instructions: 'You can control the computer to help the user',
|
||||
* approvalMode: 'cli' // or 'auto' for no approval
|
||||
* });
|
||||
*
|
||||
* await agent.chat('Open a browser and search for AI news');
|
||||
* ```
|
||||
*/
|
||||
export async function createComputerUseAgent(options: {
|
||||
instructions?: string;
|
||||
model?: string;
|
||||
approvalMode?: 'cli' | 'auto' | 'custom';
|
||||
onApproval?: (action: ComputerAction) => Promise<boolean>;
|
||||
tools?: Partial<ComputerUseTools>;
|
||||
}): Promise<any> {
|
||||
const { Agent } = await import('../agent/simple');
|
||||
|
||||
const computer = createComputerUse({
|
||||
requireApproval: options.approvalMode !== 'auto',
|
||||
tools: options.tools,
|
||||
});
|
||||
|
||||
if (options.approvalMode === 'cli') {
|
||||
computer.onApproval(createCLIApprovalPrompt());
|
||||
} else if (options.approvalMode === 'custom' && options.onApproval) {
|
||||
computer.onApproval(options.onApproval);
|
||||
}
|
||||
|
||||
const computerTools = computer.getTools();
|
||||
|
||||
return new Agent({
|
||||
instructions: options.instructions || 'You are a computer use assistant. You can take screenshots, click, type, and execute commands to help the user.',
|
||||
llm: options.model || 'gpt-4o',
|
||||
tools: Object.values(computerTools),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* Natural Language Postgres
|
||||
*
|
||||
* Provides NL->SQL capabilities for querying PostgreSQL databases.
|
||||
*/
|
||||
|
||||
export interface PostgresConfig {
|
||||
/** Database connection URL */
|
||||
connectionUrl: string;
|
||||
/** Schema to use (default: public) */
|
||||
schema?: string;
|
||||
/** Read-only mode (default: true for safety) */
|
||||
readOnly?: boolean;
|
||||
/** Allowed tables (whitelist) */
|
||||
allowedTables?: string[];
|
||||
/** Blocked tables (blacklist) */
|
||||
blockedTables?: string[];
|
||||
/** Maximum rows to return (default: 100) */
|
||||
maxRows?: number;
|
||||
/** Query timeout in ms (default: 30000) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface TableSchema {
|
||||
name: string;
|
||||
columns: ColumnSchema[];
|
||||
primaryKey?: string[];
|
||||
foreignKeys?: ForeignKey[];
|
||||
}
|
||||
|
||||
export interface ColumnSchema {
|
||||
name: string;
|
||||
type: string;
|
||||
nullable: boolean;
|
||||
defaultValue?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ForeignKey {
|
||||
columns: string[];
|
||||
referencedTable: string;
|
||||
referencedColumns: string[];
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
rows: any[];
|
||||
rowCount: number;
|
||||
fields: { name: string; type: string }[];
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
export interface NLQueryResult {
|
||||
/** Natural language query */
|
||||
query: string;
|
||||
/** Generated SQL */
|
||||
sql: string;
|
||||
/** Query result */
|
||||
result: QueryResult;
|
||||
/** Explanation of the query */
|
||||
explanation?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Natural Language Postgres client.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```typescript
|
||||
* import { createNLPostgres } from 'praisonai/integrations/postgres';
|
||||
*
|
||||
* const db = await createNLPostgres({
|
||||
* connectionUrl: process.env.DATABASE_URL!,
|
||||
* readOnly: true
|
||||
* });
|
||||
*
|
||||
* // Query with natural language
|
||||
* const result = await db.query('Show me all users who signed up last month');
|
||||
* console.log(result.rows);
|
||||
* ```
|
||||
*
|
||||
* @example With schema introspection
|
||||
* ```typescript
|
||||
* const db = await createNLPostgres({ connectionUrl: '...' });
|
||||
*
|
||||
* // Get schema information
|
||||
* const schema = await db.getSchema();
|
||||
* console.log('Tables:', schema.map(t => t.name));
|
||||
*
|
||||
* // Query with context
|
||||
* const result = await db.query('How many orders were placed today?');
|
||||
* ```
|
||||
*/
|
||||
export async function createNLPostgres(config: PostgresConfig): Promise<NLPostgresClient> {
|
||||
const client = new NLPostgresClient(config);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
export class NLPostgresClient {
|
||||
private config: PostgresConfig;
|
||||
private pool: any = null;
|
||||
private schema: TableSchema[] = [];
|
||||
private connected = false;
|
||||
|
||||
constructor(config: PostgresConfig) {
|
||||
this.config = {
|
||||
schema: 'public',
|
||||
readOnly: true,
|
||||
maxRows: 100,
|
||||
timeout: 30000,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.connected) return;
|
||||
|
||||
try {
|
||||
// @ts-ignore - Optional dependency
|
||||
const { Pool } = await import('pg');
|
||||
this.pool = new Pool({
|
||||
connectionString: this.config.connectionUrl,
|
||||
statement_timeout: this.config.timeout,
|
||||
});
|
||||
|
||||
// Test connection
|
||||
await this.pool.query('SELECT 1');
|
||||
this.connected = true;
|
||||
|
||||
// Load schema
|
||||
await this.loadSchema();
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to connect to PostgreSQL: ${error.message}. ` +
|
||||
'Install with: npm install pg'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the database.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.pool) {
|
||||
await this.pool.end();
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load database schema.
|
||||
*/
|
||||
private async loadSchema(): Promise<void> {
|
||||
const schemaQuery = `
|
||||
SELECT
|
||||
t.table_name,
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
pgd.description
|
||||
FROM information_schema.tables t
|
||||
JOIN information_schema.columns c
|
||||
ON t.table_name = c.table_name
|
||||
AND t.table_schema = c.table_schema
|
||||
LEFT JOIN pg_catalog.pg_statio_all_tables st
|
||||
ON st.relname = t.table_name
|
||||
LEFT JOIN pg_catalog.pg_description pgd
|
||||
ON pgd.objoid = st.relid
|
||||
AND pgd.objsubid = c.ordinal_position
|
||||
WHERE t.table_schema = $1
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
ORDER BY t.table_name, c.ordinal_position
|
||||
`;
|
||||
|
||||
const result = await this.pool.query(schemaQuery, [this.config.schema]);
|
||||
|
||||
const tableMap = new Map<string, TableSchema>();
|
||||
|
||||
for (const row of result.rows) {
|
||||
// Check allowed/blocked tables
|
||||
if (this.config.allowedTables && !this.config.allowedTables.includes(row.table_name)) {
|
||||
continue;
|
||||
}
|
||||
if (this.config.blockedTables && this.config.blockedTables.includes(row.table_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tableMap.has(row.table_name)) {
|
||||
tableMap.set(row.table_name, {
|
||||
name: row.table_name,
|
||||
columns: [],
|
||||
});
|
||||
}
|
||||
|
||||
const table = tableMap.get(row.table_name)!;
|
||||
table.columns.push({
|
||||
name: row.column_name,
|
||||
type: row.data_type,
|
||||
nullable: row.is_nullable === 'YES',
|
||||
defaultValue: row.column_default,
|
||||
description: row.description,
|
||||
});
|
||||
}
|
||||
|
||||
this.schema = Array.from(tableMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database schema.
|
||||
*/
|
||||
getSchema(): TableSchema[] {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema as a string for LLM context.
|
||||
*/
|
||||
getSchemaContext(): string {
|
||||
let context = 'Database Schema:\n\n';
|
||||
|
||||
for (const table of this.schema) {
|
||||
context += `Table: ${table.name}\n`;
|
||||
context += 'Columns:\n';
|
||||
for (const col of table.columns) {
|
||||
context += ` - ${col.name} (${col.type}${col.nullable ? ', nullable' : ''})`;
|
||||
if (col.description) {
|
||||
context += ` - ${col.description}`;
|
||||
}
|
||||
context += '\n';
|
||||
}
|
||||
context += '\n';
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a raw SQL query.
|
||||
*/
|
||||
async executeSQL(sql: string): Promise<QueryResult> {
|
||||
if (!this.connected) {
|
||||
throw new Error('Not connected to database');
|
||||
}
|
||||
|
||||
// Safety check for read-only mode
|
||||
if (this.config.readOnly) {
|
||||
const normalizedSQL = sql.trim().toLowerCase();
|
||||
const writeOperations = ['insert', 'update', 'delete', 'drop', 'alter', 'create', 'truncate'];
|
||||
|
||||
for (const op of writeOperations) {
|
||||
if (normalizedSQL.startsWith(op)) {
|
||||
throw new Error(`Write operation '${op}' not allowed in read-only mode`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add LIMIT if not present
|
||||
const hasLimit = /\blimit\s+\d+/i.test(sql);
|
||||
const finalSQL = hasLimit ? sql : `${sql} LIMIT ${this.config.maxRows}`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await this.pool.query(finalSQL);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
rows: result.rows,
|
||||
rowCount: result.rowCount,
|
||||
fields: result.fields?.map((f: any) => ({ name: f.name, type: f.dataTypeID?.toString() })) || [],
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database using natural language.
|
||||
*/
|
||||
async query(naturalLanguageQuery: string, options?: { model?: string }): Promise<NLQueryResult> {
|
||||
const model = options?.model || 'gpt-4o-mini';
|
||||
|
||||
// Generate SQL from natural language
|
||||
const { generateText } = await import('../ai/generate-text');
|
||||
|
||||
const schemaContext = this.getSchemaContext();
|
||||
const prompt = `You are a SQL expert. Convert the following natural language query to PostgreSQL SQL.
|
||||
|
||||
${schemaContext}
|
||||
|
||||
Rules:
|
||||
1. Only generate SELECT queries (read-only)
|
||||
2. Use proper PostgreSQL syntax
|
||||
3. Include appropriate JOINs when needed
|
||||
4. Add reasonable LIMIT if not specified
|
||||
5. Return ONLY the SQL query, no explanations
|
||||
|
||||
Natural language query: ${naturalLanguageQuery}
|
||||
|
||||
SQL:`;
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt,
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
// Extract SQL from response
|
||||
let sql = result.text.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (sql.startsWith('```')) {
|
||||
sql = sql.replace(/```sql?\n?/g, '').replace(/```/g, '').trim();
|
||||
}
|
||||
|
||||
// Execute the SQL
|
||||
const queryResult = await this.executeSQL(sql);
|
||||
|
||||
return {
|
||||
query: naturalLanguageQuery,
|
||||
sql,
|
||||
result: queryResult,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat with the database (conversational interface).
|
||||
*/
|
||||
async chat(message: string, options?: { model?: string; history?: Array<{ role: string; content: string }> }): Promise<string> {
|
||||
const model = options?.model || 'gpt-4o-mini';
|
||||
const history = options?.history || [];
|
||||
|
||||
const { generateText } = await import('../ai/generate-text');
|
||||
|
||||
const schemaContext = this.getSchemaContext();
|
||||
const systemPrompt = `You are a helpful database assistant. You can query a PostgreSQL database to answer questions.
|
||||
|
||||
${schemaContext}
|
||||
|
||||
When the user asks a question that requires database data:
|
||||
1. Generate a SQL query to get the data
|
||||
2. Execute it using the query tool
|
||||
3. Summarize the results in natural language
|
||||
|
||||
Always be helpful and explain your findings clearly.`;
|
||||
|
||||
const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...history.map(h => ({ role: h.role as 'user' | 'assistant', content: h.content })),
|
||||
{ role: 'user', content: message },
|
||||
];
|
||||
|
||||
// Define the query tool
|
||||
const tools = {
|
||||
query_database: {
|
||||
description: 'Execute a SQL query on the PostgreSQL database',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sql: {
|
||||
type: 'string',
|
||||
description: 'The SQL query to execute (SELECT only)',
|
||||
},
|
||||
},
|
||||
required: ['sql'],
|
||||
},
|
||||
execute: async ({ sql }: { sql: string }) => {
|
||||
try {
|
||||
const result = await this.executeSQL(sql);
|
||||
return JSON.stringify({
|
||||
rowCount: result.rowCount,
|
||||
rows: result.rows.slice(0, 10), // Limit for context
|
||||
hasMore: result.rowCount > 10,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({ error: error.message });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
maxSteps: 3,
|
||||
});
|
||||
|
||||
return result.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the database structure.
|
||||
*/
|
||||
async inspect(): Promise<{
|
||||
tables: number;
|
||||
schema: TableSchema[];
|
||||
sampleData: Record<string, any[]>;
|
||||
}> {
|
||||
const sampleData: Record<string, any[]> = {};
|
||||
|
||||
for (const table of this.schema) {
|
||||
try {
|
||||
const result = await this.executeSQL(`SELECT * FROM ${table.name} LIMIT 3`);
|
||||
sampleData[table.name] = result.rows;
|
||||
} catch {
|
||||
sampleData[table.name] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tables: this.schema.length,
|
||||
schema: this.schema,
|
||||
sampleData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool for querying Postgres with natural language.
|
||||
*
|
||||
* @example Use with an agent
|
||||
* ```typescript
|
||||
* import { Agent } from 'praisonai';
|
||||
* import { createPostgresTool } from 'praisonai/integrations/postgres';
|
||||
*
|
||||
* const dbTool = await createPostgresTool({
|
||||
* connectionUrl: process.env.DATABASE_URL!
|
||||
* });
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* instructions: 'You can query the database',
|
||||
* tools: [dbTool]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createPostgresTool(config: PostgresConfig): Promise<any> {
|
||||
const client = await createNLPostgres(config);
|
||||
|
||||
return {
|
||||
name: 'query_database',
|
||||
description: `Query the PostgreSQL database using natural language. Available tables: ${client.getSchema().map(t => t.name).join(', ')}`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Natural language query describing what data you want',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
const result = await client.query(query);
|
||||
return JSON.stringify({
|
||||
sql: result.sql,
|
||||
rowCount: result.result.rowCount,
|
||||
rows: result.result.rows,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* Slack Integration
|
||||
*
|
||||
* Provides Slack bot adapters for building AI-powered Slack bots.
|
||||
*/
|
||||
|
||||
export interface SlackConfig {
|
||||
/** Slack Bot Token (xoxb-...) */
|
||||
botToken: string;
|
||||
/** Slack App Token for Socket Mode (xapp-...) */
|
||||
appToken?: string;
|
||||
/** Slack Signing Secret for webhook verification */
|
||||
signingSecret?: string;
|
||||
/** Enable Socket Mode (default: false, uses webhooks) */
|
||||
socketMode?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackMessage {
|
||||
/** Channel ID */
|
||||
channel: string;
|
||||
/** User ID */
|
||||
user: string;
|
||||
/** Message text */
|
||||
text: string;
|
||||
/** Thread timestamp (for replies) */
|
||||
threadTs?: string;
|
||||
/** Message timestamp */
|
||||
ts: string;
|
||||
/** Bot ID (if from a bot) */
|
||||
botId?: string;
|
||||
}
|
||||
|
||||
export interface SlackResponse {
|
||||
/** Response text */
|
||||
text: string;
|
||||
/** Blocks for rich formatting */
|
||||
blocks?: any[];
|
||||
/** Thread timestamp (to reply in thread) */
|
||||
threadTs?: string;
|
||||
/** Unfurl links */
|
||||
unfurlLinks?: boolean;
|
||||
/** Unfurl media */
|
||||
unfurlMedia?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackEventHandler {
|
||||
/** Handle a message event */
|
||||
onMessage: (message: SlackMessage) => Promise<SlackResponse | null>;
|
||||
/** Handle an app mention event */
|
||||
onAppMention?: (message: SlackMessage) => Promise<SlackResponse | null>;
|
||||
/** Handle a reaction added event */
|
||||
onReactionAdded?: (event: any) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Slack bot adapter.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```typescript
|
||||
* import { createSlackBot } from 'praisonai/integrations/slack';
|
||||
* import { Agent } from 'praisonai';
|
||||
*
|
||||
* const agent = new Agent({ instructions: 'You are a helpful Slack bot' });
|
||||
*
|
||||
* const bot = createSlackBot({
|
||||
* botToken: process.env.SLACK_BOT_TOKEN!,
|
||||
* signingSecret: process.env.SLACK_SIGNING_SECRET!
|
||||
* });
|
||||
*
|
||||
* bot.onMessage(async (message) => {
|
||||
* const response = await agent.chat(message.text);
|
||||
* return { text: response, threadTs: message.ts };
|
||||
* });
|
||||
*
|
||||
* // Start webhook server
|
||||
* bot.listen(3000);
|
||||
* ```
|
||||
*
|
||||
* @example With Socket Mode
|
||||
* ```typescript
|
||||
* const bot = createSlackBot({
|
||||
* botToken: process.env.SLACK_BOT_TOKEN!,
|
||||
* appToken: process.env.SLACK_APP_TOKEN!,
|
||||
* socketMode: true
|
||||
* });
|
||||
*
|
||||
* bot.onMessage(async (message) => {
|
||||
* return { text: 'Hello!' };
|
||||
* });
|
||||
*
|
||||
* await bot.start();
|
||||
* ```
|
||||
*/
|
||||
export function createSlackBot(config: SlackConfig): SlackBot {
|
||||
return new SlackBot(config);
|
||||
}
|
||||
|
||||
export class SlackBot {
|
||||
private config: SlackConfig;
|
||||
private messageHandler?: (message: SlackMessage) => Promise<SlackResponse | null>;
|
||||
private mentionHandler?: (message: SlackMessage) => Promise<SlackResponse | null>;
|
||||
private reactionHandler?: (event: any) => Promise<void>;
|
||||
private boltApp: any = null;
|
||||
|
||||
constructor(config: SlackConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message handler.
|
||||
*/
|
||||
onMessage(handler: (message: SlackMessage) => Promise<SlackResponse | null>): this {
|
||||
this.messageHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the app mention handler.
|
||||
*/
|
||||
onAppMention(handler: (message: SlackMessage) => Promise<SlackResponse | null>): this {
|
||||
this.mentionHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reaction handler.
|
||||
*/
|
||||
onReactionAdded(handler: (event: any) => Promise<void>): this {
|
||||
this.reactionHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Bolt app (lazy load).
|
||||
*/
|
||||
private async initBolt(): Promise<any> {
|
||||
if (this.boltApp) return this.boltApp;
|
||||
|
||||
try {
|
||||
// @ts-ignore - Optional dependency
|
||||
const { App } = await import('@slack/bolt');
|
||||
|
||||
this.boltApp = new App({
|
||||
token: this.config.botToken,
|
||||
signingSecret: this.config.signingSecret,
|
||||
appToken: this.config.appToken,
|
||||
socketMode: this.config.socketMode,
|
||||
});
|
||||
|
||||
// Register message handler
|
||||
if (this.messageHandler) {
|
||||
this.boltApp.message(async ({ message, say }: any) => {
|
||||
// Skip bot messages
|
||||
if (message.bot_id) return;
|
||||
|
||||
const slackMessage: SlackMessage = {
|
||||
channel: message.channel,
|
||||
user: message.user,
|
||||
text: message.text || '',
|
||||
threadTs: message.thread_ts,
|
||||
ts: message.ts,
|
||||
};
|
||||
|
||||
const response = await this.messageHandler!(slackMessage);
|
||||
if (response) {
|
||||
await say({
|
||||
text: response.text,
|
||||
blocks: response.blocks,
|
||||
thread_ts: response.threadTs || message.ts,
|
||||
unfurl_links: response.unfurlLinks,
|
||||
unfurl_media: response.unfurlMedia,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register app mention handler
|
||||
if (this.mentionHandler) {
|
||||
this.boltApp.event('app_mention', async ({ event, say }: any) => {
|
||||
const slackMessage: SlackMessage = {
|
||||
channel: event.channel,
|
||||
user: event.user,
|
||||
text: event.text || '',
|
||||
threadTs: event.thread_ts,
|
||||
ts: event.ts,
|
||||
};
|
||||
|
||||
const response = await this.mentionHandler!(slackMessage);
|
||||
if (response) {
|
||||
await say({
|
||||
text: response.text,
|
||||
blocks: response.blocks,
|
||||
thread_ts: response.threadTs || event.ts,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register reaction handler
|
||||
if (this.reactionHandler) {
|
||||
this.boltApp.event('reaction_added', async ({ event }: any) => {
|
||||
await this.reactionHandler!(event);
|
||||
});
|
||||
}
|
||||
|
||||
return this.boltApp;
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to initialize Slack Bolt: ${error.message}. ` +
|
||||
'Install with: npm install @slack/bolt'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the bot (Socket Mode).
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
const app = await this.initBolt();
|
||||
await app.start();
|
||||
console.log('⚡️ Slack bot is running in Socket Mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start webhook server.
|
||||
*/
|
||||
async listen(port: number = 3000): Promise<void> {
|
||||
const app = await this.initBolt();
|
||||
await app.start(port);
|
||||
console.log(`⚡️ Slack bot is running on port ${port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bot.
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this.boltApp) {
|
||||
await this.boltApp.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a channel.
|
||||
*/
|
||||
async sendMessage(channel: string, text: string, options?: Partial<SlackResponse>): Promise<void> {
|
||||
const app = await this.initBolt();
|
||||
await app.client.chat.postMessage({
|
||||
token: this.config.botToken,
|
||||
channel,
|
||||
text,
|
||||
blocks: options?.blocks,
|
||||
thread_ts: options?.threadTs,
|
||||
unfurl_links: options?.unfurlLinks,
|
||||
unfurl_media: options?.unfurlMedia,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Express middleware for webhook handling.
|
||||
*/
|
||||
getExpressMiddleware(): any {
|
||||
return async (req: any, res: any, next: any) => {
|
||||
const app = await this.initBolt();
|
||||
return app.receiver.app(req, res, next);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Slack request signature.
|
||||
*/
|
||||
export function verifySlackSignature(
|
||||
signingSecret: string,
|
||||
signature: string,
|
||||
timestamp: string,
|
||||
body: string
|
||||
): boolean {
|
||||
const crypto = require('crypto');
|
||||
const baseString = `v0:${timestamp}:${body}`;
|
||||
const hmac = crypto.createHmac('sha256', signingSecret);
|
||||
hmac.update(baseString);
|
||||
const computedSignature = `v0=${hmac.digest('hex')}`;
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(computedSignature)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Slack message text to extract mentions and links.
|
||||
*/
|
||||
export function parseSlackMessage(text: string): {
|
||||
mentions: string[];
|
||||
links: string[];
|
||||
cleanText: string;
|
||||
} {
|
||||
const mentions: string[] = [];
|
||||
const links: string[] = [];
|
||||
|
||||
// Extract user mentions <@U123456>
|
||||
const mentionRegex = /<@([A-Z0-9]+)>/g;
|
||||
let match;
|
||||
while ((match = mentionRegex.exec(text)) !== null) {
|
||||
mentions.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract links <http://...|label>
|
||||
const linkRegex = /<(https?:\/\/[^|>]+)(?:\|[^>]+)?>/g;
|
||||
while ((match = linkRegex.exec(text)) !== null) {
|
||||
links.push(match[1]);
|
||||
}
|
||||
|
||||
// Clean text (remove mentions and format links)
|
||||
const cleanText = text
|
||||
.replace(/<@[A-Z0-9]+>/g, '')
|
||||
.replace(/<(https?:\/\/[^|>]+)(?:\|([^>]+))?>/g, '$2 ($1)')
|
||||
.trim();
|
||||
|
||||
return { mentions, links, cleanText };
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Backend Resolver - Unified LLM backend resolution with AI SDK preference
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. AI SDK backend (if installed and enabled)
|
||||
* 2. Native provider registry (OpenAI, Anthropic, Google)
|
||||
* 3. Error with actionable guidance
|
||||
*
|
||||
* Environment variables:
|
||||
* - PRAISONAI_BACKEND: 'ai-sdk' | 'native' | 'auto' (default: 'auto')
|
||||
*/
|
||||
|
||||
import type { LLMProvider, ProviderConfig } from './providers/types';
|
||||
|
||||
export type BackendSource = 'ai-sdk' | 'native' | 'custom' | 'legacy';
|
||||
|
||||
export interface BackendResolutionResult {
|
||||
provider: LLMProvider;
|
||||
source: BackendSource;
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface ResolveBackendOptions {
|
||||
/** Force a specific backend: 'ai-sdk' | 'native' | 'auto' (default: 'auto') */
|
||||
backend?: 'ai-sdk' | 'native' | 'auto';
|
||||
/** Provider configuration (API keys, timeouts, etc.) */
|
||||
config?: ProviderConfig;
|
||||
/** Attribution context for multi-agent tracing */
|
||||
attribution?: {
|
||||
agentId?: string;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Cached availability check
|
||||
let _aiSdkAvailable: boolean | null = null;
|
||||
let _aiSdkCheckPromise: Promise<boolean> | null = null;
|
||||
|
||||
/**
|
||||
* Check if AI SDK is available (installed)
|
||||
* Result is cached after first check
|
||||
*/
|
||||
export async function isAISDKAvailable(): Promise<boolean> {
|
||||
if (_aiSdkAvailable !== null) {
|
||||
return _aiSdkAvailable;
|
||||
}
|
||||
|
||||
if (_aiSdkCheckPromise) {
|
||||
return _aiSdkCheckPromise;
|
||||
}
|
||||
|
||||
_aiSdkCheckPromise = (async () => {
|
||||
try {
|
||||
// Dynamic import to avoid loading if not needed
|
||||
const moduleName = 'ai';
|
||||
await import(moduleName);
|
||||
_aiSdkAvailable = true;
|
||||
return true;
|
||||
} catch {
|
||||
_aiSdkAvailable = false;
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
return _aiSdkCheckPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the AI SDK availability cache (for testing)
|
||||
*/
|
||||
export function resetAISDKAvailabilityCache(): void {
|
||||
_aiSdkAvailable = null;
|
||||
_aiSdkCheckPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred backend from environment
|
||||
*/
|
||||
export function getPreferredBackend(): 'ai-sdk' | 'native' | 'auto' {
|
||||
const env = process.env.PRAISONAI_BACKEND?.toLowerCase();
|
||||
if (env === 'ai-sdk' || env === 'aisdk') return 'ai-sdk';
|
||||
if (env === 'native' || env === 'legacy') return 'native';
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse model string into provider and model ID
|
||||
*/
|
||||
export function parseModelString(model: string): { providerId: string; modelId: string } {
|
||||
if (model.includes('/')) {
|
||||
const [providerId, ...rest] = model.split('/');
|
||||
return { providerId: providerId.toLowerCase(), modelId: rest.join('/') };
|
||||
}
|
||||
|
||||
// Default to OpenAI for common model prefixes
|
||||
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3')) {
|
||||
return { providerId: 'openai', modelId: model };
|
||||
}
|
||||
if (model.startsWith('claude-')) {
|
||||
return { providerId: 'anthropic', modelId: model };
|
||||
}
|
||||
if (model.startsWith('gemini-')) {
|
||||
return { providerId: 'google', modelId: model };
|
||||
}
|
||||
|
||||
// Default to OpenAI
|
||||
return { providerId: 'openai', modelId: model };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best available backend for a model
|
||||
*
|
||||
* @param modelString - Model string in format "provider/model" or just "model"
|
||||
* @param options - Resolution options
|
||||
* @returns Backend resolution result with provider instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { provider, source } = await resolveBackend('openai/gpt-4o-mini');
|
||||
* const result = await provider.generateText({ messages: [...] });
|
||||
* ```
|
||||
*/
|
||||
export async function resolveBackend(
|
||||
modelString: string,
|
||||
options: ResolveBackendOptions = {}
|
||||
): Promise<BackendResolutionResult> {
|
||||
const { providerId, modelId } = parseModelString(modelString);
|
||||
const preferredBackend = options.backend || getPreferredBackend();
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Try AI SDK first if preferred or auto
|
||||
if (preferredBackend === 'ai-sdk' || preferredBackend === 'auto') {
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
|
||||
if (aiSdkAvailable) {
|
||||
try {
|
||||
// Lazy import AI SDK backend
|
||||
const { createAISDKBackend } = await import('./providers/ai-sdk');
|
||||
|
||||
const backend = createAISDKBackend(modelString, {
|
||||
...options.config,
|
||||
attribution: options.attribution ? {
|
||||
agentId: options.attribution.agentId,
|
||||
runId: options.attribution.runId,
|
||||
sessionId: options.attribution.sessionId,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
provider: backend,
|
||||
source: 'ai-sdk',
|
||||
providerId,
|
||||
modelId,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (preferredBackend === 'ai-sdk') {
|
||||
// User explicitly requested AI SDK, throw error
|
||||
throw new Error(
|
||||
`AI SDK backend failed for '${modelString}': ${error.message}. ` +
|
||||
`Set PRAISONAI_BACKEND=native to use native providers.`
|
||||
);
|
||||
}
|
||||
// Auto mode: fall through to native
|
||||
warnings.push(`AI SDK failed: ${error.message}, falling back to native provider`);
|
||||
}
|
||||
} else if (preferredBackend === 'ai-sdk') {
|
||||
throw new Error(
|
||||
`AI SDK is not installed but PRAISONAI_BACKEND=ai-sdk is set. ` +
|
||||
`Install with: npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try native provider registry
|
||||
try {
|
||||
const { createProvider } = await import('./providers');
|
||||
const provider = createProvider(modelString, options.config);
|
||||
|
||||
return {
|
||||
provider,
|
||||
source: 'native',
|
||||
providerId,
|
||||
modelId,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Cannot resolve backend for '${modelString}'. ` +
|
||||
`Neither AI SDK nor native provider available. ` +
|
||||
`Install AI SDK: npm install ai @ai-sdk/${providerId} ` +
|
||||
`or check your provider configuration. ` +
|
||||
`Original error: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve backend synchronously using cached availability
|
||||
* Only works if isAISDKAvailable() has been called before
|
||||
* Falls back to native if cache is not populated
|
||||
*/
|
||||
export function resolveBackendSync(
|
||||
modelString: string,
|
||||
options: ResolveBackendOptions = {}
|
||||
): BackendResolutionResult {
|
||||
const { providerId, modelId } = parseModelString(modelString);
|
||||
const preferredBackend = options.backend || getPreferredBackend();
|
||||
|
||||
// If AI SDK availability is cached and available, try it first
|
||||
if (_aiSdkAvailable === true && (preferredBackend === 'ai-sdk' || preferredBackend === 'auto')) {
|
||||
try {
|
||||
// This will throw if AI SDK module is not already loaded
|
||||
const aiSdk = require('./providers/ai-sdk');
|
||||
const backend = aiSdk.createAISDKBackend(modelString, {
|
||||
...options.config,
|
||||
attribution: options.attribution,
|
||||
});
|
||||
|
||||
return {
|
||||
provider: backend,
|
||||
source: 'ai-sdk',
|
||||
providerId,
|
||||
modelId,
|
||||
};
|
||||
} catch {
|
||||
// Fall through to native
|
||||
}
|
||||
}
|
||||
|
||||
// Use native provider
|
||||
const { createProvider } = require('./providers');
|
||||
const provider = createProvider(modelString, options.config);
|
||||
|
||||
return {
|
||||
provider,
|
||||
source: 'native',
|
||||
providerId,
|
||||
modelId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default model string
|
||||
*/
|
||||
export function getDefaultModel(): string {
|
||||
return process.env.OPENAI_MODEL_NAME ||
|
||||
process.env.PRAISONAI_MODEL ||
|
||||
'openai/gpt-4o-mini';
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { LLMProvider, ProviderConfig };
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Embeddings - AI SDK-backed embedding utilities
|
||||
*
|
||||
* Provides embedding functionality with AI SDK preference and native fallback.
|
||||
* Integrates with existing EmbeddingProvider interface in knowledge/rag.ts and memory/memory.ts
|
||||
*/
|
||||
|
||||
import type { EmbeddingProvider } from '../knowledge/rag';
|
||||
import { isAISDKAvailable, getPreferredBackend } from './backend-resolver';
|
||||
|
||||
export interface EmbeddingOptions {
|
||||
/** Model to use for embeddings (default: text-embedding-3-small) */
|
||||
model?: string;
|
||||
/** Provider to use (default: openai) */
|
||||
provider?: string;
|
||||
/** Maximum retries (default: 2) */
|
||||
maxRetries?: number;
|
||||
/** Abort signal for cancellation */
|
||||
abortSignal?: AbortSignal;
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Force specific backend: 'ai-sdk' | 'native' | 'auto' */
|
||||
backend?: 'ai-sdk' | 'native' | 'auto';
|
||||
}
|
||||
|
||||
export interface EmbeddingResult {
|
||||
embedding: number[];
|
||||
usage?: {
|
||||
tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EmbeddingBatchResult {
|
||||
embeddings: number[][];
|
||||
usage?: {
|
||||
tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Default embedding models per provider
|
||||
const DEFAULT_EMBEDDING_MODELS: Record<string, string> = {
|
||||
openai: 'text-embedding-3-small',
|
||||
anthropic: 'voyage-3', // Anthropic uses Voyage for embeddings
|
||||
google: 'text-embedding-004',
|
||||
cohere: 'embed-english-v3.0',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default embedding model for a provider
|
||||
*/
|
||||
export function getDefaultEmbeddingModel(provider: string = 'openai'): string {
|
||||
return DEFAULT_EMBEDDING_MODELS[provider.toLowerCase()] || DEFAULT_EMBEDDING_MODELS.openai;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse embedding model string into provider and model
|
||||
*/
|
||||
export function parseEmbeddingModel(model: string): { provider: string; model: string } {
|
||||
if (model.includes('/')) {
|
||||
const [provider, ...rest] = model.split('/');
|
||||
return { provider: provider.toLowerCase(), model: rest.join('/') };
|
||||
}
|
||||
|
||||
// Infer provider from model name
|
||||
if (model.startsWith('text-embedding-') || model.startsWith('ada')) {
|
||||
return { provider: 'openai', model };
|
||||
}
|
||||
if (model.startsWith('voyage-')) {
|
||||
return { provider: 'anthropic', model };
|
||||
}
|
||||
if (model.startsWith('embed-')) {
|
||||
return { provider: 'cohere', model };
|
||||
}
|
||||
|
||||
// Default to OpenAI
|
||||
return { provider: 'openai', model };
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed a single text using AI SDK (preferred) or native provider
|
||||
*/
|
||||
export async function embed(
|
||||
text: string,
|
||||
options: EmbeddingOptions = {}
|
||||
): Promise<EmbeddingResult> {
|
||||
const modelString = options.model || getDefaultEmbeddingModel(options.provider);
|
||||
const { provider } = parseEmbeddingModel(modelString);
|
||||
const preferredBackend = options.backend || getPreferredBackend();
|
||||
|
||||
// Try AI SDK first
|
||||
if (preferredBackend === 'ai-sdk' || preferredBackend === 'auto') {
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
|
||||
if (aiSdkAvailable) {
|
||||
try {
|
||||
return await embedWithAISDK(text, modelString, options);
|
||||
} catch (error: any) {
|
||||
if (preferredBackend === 'ai-sdk') {
|
||||
throw error;
|
||||
}
|
||||
// Fall through to native
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to native OpenAI
|
||||
return await embedWithNative(text, modelString, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed multiple texts using AI SDK (preferred) or native provider
|
||||
*/
|
||||
export async function embedMany(
|
||||
texts: string[],
|
||||
options: EmbeddingOptions = {}
|
||||
): Promise<EmbeddingBatchResult> {
|
||||
if (texts.length === 0) {
|
||||
return { embeddings: [] };
|
||||
}
|
||||
|
||||
const modelString = options.model || getDefaultEmbeddingModel(options.provider);
|
||||
const preferredBackend = options.backend || getPreferredBackend();
|
||||
|
||||
// Try AI SDK first
|
||||
if (preferredBackend === 'ai-sdk' || preferredBackend === 'auto') {
|
||||
const aiSdkAvailable = await isAISDKAvailable();
|
||||
|
||||
if (aiSdkAvailable) {
|
||||
try {
|
||||
return await embedManyWithAISDK(texts, modelString, options);
|
||||
} catch (error: any) {
|
||||
if (preferredBackend === 'ai-sdk') {
|
||||
throw error;
|
||||
}
|
||||
// Fall through to native
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to native OpenAI
|
||||
return await embedManyWithNative(texts, modelString, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed using AI SDK
|
||||
*/
|
||||
async function embedWithAISDK(
|
||||
text: string,
|
||||
model: string,
|
||||
options: EmbeddingOptions
|
||||
): Promise<EmbeddingResult> {
|
||||
const { provider, model: modelId } = parseEmbeddingModel(model);
|
||||
|
||||
// Dynamic import AI SDK
|
||||
const ai = await import('ai');
|
||||
|
||||
// Get the embedding model from the appropriate provider
|
||||
const embeddingModel = await getAISDKEmbeddingModel(provider, modelId);
|
||||
|
||||
const result = await ai.embed({
|
||||
model: embeddingModel,
|
||||
value: text,
|
||||
maxRetries: options.maxRetries ?? 2,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
});
|
||||
|
||||
return {
|
||||
embedding: result.embedding,
|
||||
usage: result.usage ? { tokens: result.usage.tokens } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed many using AI SDK
|
||||
*/
|
||||
async function embedManyWithAISDK(
|
||||
texts: string[],
|
||||
model: string,
|
||||
options: EmbeddingOptions
|
||||
): Promise<EmbeddingBatchResult> {
|
||||
const { provider, model: modelId } = parseEmbeddingModel(model);
|
||||
|
||||
// Dynamic import AI SDK
|
||||
const ai = await import('ai');
|
||||
|
||||
// Get the embedding model from the appropriate provider
|
||||
const embeddingModel = await getAISDKEmbeddingModel(provider, modelId);
|
||||
|
||||
const result = await ai.embedMany({
|
||||
model: embeddingModel,
|
||||
values: texts,
|
||||
maxRetries: options.maxRetries ?? 2,
|
||||
abortSignal: options.abortSignal,
|
||||
headers: options.headers,
|
||||
});
|
||||
|
||||
return {
|
||||
embeddings: result.embeddings,
|
||||
usage: result.usage ? { tokens: result.usage.tokens } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI SDK embedding model for a provider
|
||||
*/
|
||||
async function getAISDKEmbeddingModel(provider: string, modelId: string): Promise<any> {
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'openai':
|
||||
case 'oai': {
|
||||
const { createOpenAI } = await import('@ai-sdk/openai');
|
||||
const openai = createOpenAI({});
|
||||
return openai.embedding(modelId);
|
||||
}
|
||||
case 'google':
|
||||
case 'gemini': {
|
||||
const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
|
||||
const google = createGoogleGenerativeAI({});
|
||||
return google.textEmbeddingModel(modelId);
|
||||
}
|
||||
case 'cohere': {
|
||||
try {
|
||||
// Dynamic import - cohere is optional
|
||||
const cohereModule = await import('@ai-sdk/cohere' as string);
|
||||
const cohere = cohereModule.createCohere({});
|
||||
return cohere.embedding(modelId);
|
||||
} catch {
|
||||
throw new Error(`Cohere provider not installed. Install with: npm install @ai-sdk/cohere`);
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Embedding provider '${provider}' not supported via AI SDK. ` +
|
||||
`Supported: openai, google, cohere`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed using native OpenAI client
|
||||
*/
|
||||
async function embedWithNative(
|
||||
text: string,
|
||||
model: string,
|
||||
options: EmbeddingOptions
|
||||
): Promise<EmbeddingResult> {
|
||||
const { model: modelId } = parseEmbeddingModel(model);
|
||||
|
||||
const OpenAI = (await import('openai')).default;
|
||||
const client = new OpenAI();
|
||||
|
||||
const response = await client.embeddings.create({
|
||||
model: modelId,
|
||||
input: text,
|
||||
});
|
||||
|
||||
return {
|
||||
embedding: response.data[0].embedding,
|
||||
usage: response.usage ? { tokens: response.usage.total_tokens } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed many using native OpenAI client
|
||||
*/
|
||||
async function embedManyWithNative(
|
||||
texts: string[],
|
||||
model: string,
|
||||
options: EmbeddingOptions
|
||||
): Promise<EmbeddingBatchResult> {
|
||||
const { model: modelId } = parseEmbeddingModel(model);
|
||||
|
||||
const OpenAI = (await import('openai')).default;
|
||||
const client = new OpenAI();
|
||||
|
||||
const response = await client.embeddings.create({
|
||||
model: modelId,
|
||||
input: texts,
|
||||
});
|
||||
|
||||
// Sort by index to ensure correct order
|
||||
const sorted = response.data.sort((a, b) => a.index - b.index);
|
||||
|
||||
return {
|
||||
embeddings: sorted.map(d => d.embedding),
|
||||
usage: response.usage ? { tokens: response.usage.total_tokens } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EmbeddingProvider that uses AI SDK
|
||||
* Compatible with KnowledgeBase and Memory interfaces
|
||||
*/
|
||||
export function createEmbeddingProvider(options: EmbeddingOptions = {}): EmbeddingProvider {
|
||||
return {
|
||||
async embed(text: string): Promise<number[]> {
|
||||
const result = await embed(text, options);
|
||||
return result.embedding;
|
||||
},
|
||||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||||
const result = await embedMany(texts, options);
|
||||
return result.embeddings;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cosine similarity between two vectors
|
||||
*/
|
||||
export function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Vectors must have the same length');
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
if (normA === 0 || normB === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
|
||||
/**
|
||||
* Euclidean distance between two vectors
|
||||
*/
|
||||
export function euclideanDistance(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Vectors must have the same length');
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const diff = a[i] - b[i];
|
||||
sum += diff * diff;
|
||||
}
|
||||
|
||||
return Math.sqrt(sum);
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { EmbeddingProvider };
|
||||
@@ -37,3 +37,31 @@ export class BaseLLM implements LLM {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
// Export backend resolver
|
||||
export {
|
||||
resolveBackend,
|
||||
resolveBackendSync,
|
||||
isAISDKAvailable,
|
||||
resetAISDKAvailabilityCache,
|
||||
getPreferredBackend,
|
||||
getDefaultModel,
|
||||
parseModelString,
|
||||
type BackendResolutionResult,
|
||||
type BackendSource,
|
||||
type ResolveBackendOptions,
|
||||
} from './backend-resolver';
|
||||
|
||||
// Export embeddings
|
||||
export {
|
||||
embed,
|
||||
embedMany,
|
||||
createEmbeddingProvider,
|
||||
getDefaultEmbeddingModel,
|
||||
parseEmbeddingModel,
|
||||
cosineSimilarity,
|
||||
euclideanDistance,
|
||||
type EmbeddingOptions,
|
||||
type EmbeddingResult,
|
||||
type EmbeddingBatchResult,
|
||||
} from './embeddings';
|
||||
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* AI SDK Adapter
|
||||
*
|
||||
* Converts between praisonai-ts message/tool formats and AI SDK formats.
|
||||
* Handles prompt construction, tool definitions, and result parsing.
|
||||
*/
|
||||
|
||||
import type { Message, ToolDefinition, ToolCall, GenerateTextResult, TokenUsage as PraisonTokenUsage } from '../types';
|
||||
import type {
|
||||
AISDKToolDefinition,
|
||||
AISDKToolCall,
|
||||
AISDKToolResult,
|
||||
TokenUsage,
|
||||
FinishReason,
|
||||
PraisonStreamChunk
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* AI SDK prompt message types (simplified for compatibility)
|
||||
*/
|
||||
export interface AISDKSystemMessage {
|
||||
role: 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AISDKUserMessage {
|
||||
role: 'user';
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
}
|
||||
|
||||
export interface AISDKAssistantMessage {
|
||||
role: 'assistant';
|
||||
content: Array<
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'tool-call'; toolCallId: string; toolName: string; args: unknown }
|
||||
>;
|
||||
}
|
||||
|
||||
export interface AISDKToolMessage {
|
||||
role: 'tool';
|
||||
content: Array<{
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type AISDKMessage =
|
||||
| AISDKSystemMessage
|
||||
| AISDKUserMessage
|
||||
| AISDKAssistantMessage
|
||||
| AISDKToolMessage;
|
||||
|
||||
/**
|
||||
* Convert praisonai-ts messages to AI SDK prompt format
|
||||
*/
|
||||
export function toAISDKPrompt(messages: Message[]): AISDKMessage[] {
|
||||
return messages.map(msg => {
|
||||
switch (msg.role) {
|
||||
case 'system':
|
||||
return {
|
||||
role: 'system' as const,
|
||||
content: msg.content || ''
|
||||
};
|
||||
|
||||
case 'user':
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content: [{ type: 'text' as const, text: msg.content || '' }]
|
||||
};
|
||||
|
||||
case 'assistant':
|
||||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
return {
|
||||
role: 'assistant' as const,
|
||||
content: msg.tool_calls.map(tc => ({
|
||||
type: 'tool-call' as const,
|
||||
toolCallId: tc.id,
|
||||
toolName: tc.function.name,
|
||||
args: safeParseJSON(tc.function.arguments)
|
||||
}))
|
||||
};
|
||||
}
|
||||
return {
|
||||
role: 'assistant' as const,
|
||||
content: [{ type: 'text' as const, text: msg.content || '' }]
|
||||
};
|
||||
|
||||
case 'tool':
|
||||
return {
|
||||
role: 'tool' as const,
|
||||
content: [{
|
||||
type: 'tool-result' as const,
|
||||
toolCallId: msg.tool_call_id || '',
|
||||
toolName: msg.name || '',
|
||||
result: safeParseJSON(msg.content || '')
|
||||
}]
|
||||
};
|
||||
|
||||
default:
|
||||
// Fallback to user message for unknown roles
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content: [{ type: 'text' as const, text: msg.content || '' }]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert praisonai-ts tool definitions to AI SDK format
|
||||
* AI SDK expects tools created with the tool() helper from @ai-sdk/provider-utils
|
||||
*/
|
||||
export async function toAISDKTools(tools: ToolDefinition[]): Promise<Record<string, any>> {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
// Import the tool helper from @ai-sdk/provider-utils
|
||||
let toolHelper: any;
|
||||
let jsonSchemaHelper: any;
|
||||
try {
|
||||
const providerUtils = await import('@ai-sdk/provider-utils');
|
||||
toolHelper = providerUtils.tool;
|
||||
jsonSchemaHelper = providerUtils.jsonSchema;
|
||||
} catch {
|
||||
toolHelper = null;
|
||||
jsonSchemaHelper = null;
|
||||
}
|
||||
|
||||
for (const toolDef of tools) {
|
||||
// Build a proper JSON Schema for the tool parameters
|
||||
const params = toolDef.parameters || {};
|
||||
const schema = {
|
||||
type: 'object' as const,
|
||||
properties: params.properties || {},
|
||||
required: params.required || [],
|
||||
...(params.additionalProperties !== undefined
|
||||
? { additionalProperties: params.additionalProperties }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (toolHelper && jsonSchemaHelper) {
|
||||
// Use AI SDK's tool() helper for proper tool creation
|
||||
result[toolDef.name] = toolHelper({
|
||||
description: toolDef.description || '',
|
||||
parameters: jsonSchemaHelper(schema),
|
||||
});
|
||||
} else {
|
||||
// Fallback format - create tool-like object manually
|
||||
result[toolDef.name] = {
|
||||
description: toolDef.description || '',
|
||||
parameters: schema,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AI SDK tool call to praisonai-ts format
|
||||
*/
|
||||
export function fromAISDKToolCall(toolCall: AISDKToolCall): ToolCall {
|
||||
return {
|
||||
id: toolCall.toolCallId,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolCall.toolName,
|
||||
arguments: typeof toolCall.args === 'string'
|
||||
? toolCall.args
|
||||
: JSON.stringify(toolCall.args)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert praisonai-ts tool result to AI SDK format
|
||||
*/
|
||||
export function toAISDKToolResult(
|
||||
toolCallId: string,
|
||||
toolName: string,
|
||||
result: unknown
|
||||
): AISDKToolResult {
|
||||
return {
|
||||
toolCallId,
|
||||
toolName,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from AI SDK response content
|
||||
*/
|
||||
export function extractTextFromContent(content: unknown[]): string {
|
||||
if (!Array.isArray(content)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.filter((part): part is { type: 'text'; text: string } =>
|
||||
typeof part === 'object' &&
|
||||
part !== null &&
|
||||
'type' in part &&
|
||||
part.type === 'text' &&
|
||||
'text' in part
|
||||
)
|
||||
.map(part => part.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool calls from AI SDK response content
|
||||
*/
|
||||
export function extractToolCallsFromContent(content: unknown[]): ToolCall[] {
|
||||
if (!Array.isArray(content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return content
|
||||
.filter((part): part is { type: 'tool-call'; toolCallId: string; toolName: string; args: unknown } =>
|
||||
typeof part === 'object' &&
|
||||
part !== null &&
|
||||
'type' in part &&
|
||||
part.type === 'tool-call'
|
||||
)
|
||||
.map(part => fromAISDKToolCall({
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
args: part.args
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AI SDK generate result to praisonai-ts format
|
||||
*/
|
||||
export function fromAISDKResult(result: {
|
||||
text?: string;
|
||||
content?: unknown[];
|
||||
toolCalls?: AISDKToolCall[];
|
||||
usage?: { promptTokens: number; completionTokens: number };
|
||||
finishReason?: string;
|
||||
}): GenerateTextResult {
|
||||
// Extract text from content or use text directly
|
||||
let text = result.text || '';
|
||||
if (!text && result.content) {
|
||||
text = extractTextFromContent(result.content);
|
||||
}
|
||||
|
||||
// Extract tool calls
|
||||
let toolCalls: ToolCall[] = [];
|
||||
if (result.toolCalls && result.toolCalls.length > 0) {
|
||||
toolCalls = result.toolCalls.map(fromAISDKToolCall);
|
||||
} else if (result.content) {
|
||||
toolCalls = extractToolCallsFromContent(result.content);
|
||||
}
|
||||
|
||||
// Map usage - provide defaults if not available
|
||||
const usage: PraisonTokenUsage = result.usage ? {
|
||||
promptTokens: result.usage.promptTokens,
|
||||
completionTokens: result.usage.completionTokens,
|
||||
totalTokens: result.usage.promptTokens + result.usage.completionTokens
|
||||
} : {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0
|
||||
};
|
||||
|
||||
// Map finish reason to praisonai-ts format
|
||||
const finishReason = mapFinishReasonToProvider(result.finishReason);
|
||||
|
||||
return {
|
||||
text,
|
||||
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
||||
usage,
|
||||
finishReason,
|
||||
raw: result
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map AI SDK finish reason to internal format
|
||||
*/
|
||||
export function mapFinishReason(reason?: string): FinishReason {
|
||||
if (!reason) return 'unknown';
|
||||
|
||||
switch (reason.toLowerCase()) {
|
||||
case 'stop':
|
||||
case 'end_turn':
|
||||
case 'end':
|
||||
return 'stop';
|
||||
case 'length':
|
||||
case 'max_tokens':
|
||||
return 'length';
|
||||
case 'tool_calls':
|
||||
case 'tool-calls':
|
||||
case 'function_call':
|
||||
return 'tool-calls';
|
||||
case 'content_filter':
|
||||
case 'content-filter':
|
||||
return 'content-filter';
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
return 'cancelled';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map AI SDK finish reason to praisonai-ts provider format
|
||||
* Uses underscore format for tool_calls as expected by GenerateTextResult
|
||||
*/
|
||||
export function mapFinishReasonToProvider(reason?: string): 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error' {
|
||||
if (!reason) return 'stop';
|
||||
|
||||
switch (reason.toLowerCase()) {
|
||||
case 'stop':
|
||||
case 'end_turn':
|
||||
case 'end':
|
||||
return 'stop';
|
||||
case 'length':
|
||||
case 'max_tokens':
|
||||
return 'length';
|
||||
case 'tool_calls':
|
||||
case 'tool-calls':
|
||||
case 'function_call':
|
||||
return 'tool_calls';
|
||||
case 'content_filter':
|
||||
case 'content-filter':
|
||||
return 'content_filter';
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
default:
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AI SDK stream chunk to praisonai-ts format
|
||||
*/
|
||||
export function fromAISDKStreamChunk(chunk: {
|
||||
type: string;
|
||||
textDelta?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
argsTextDelta?: string;
|
||||
args?: unknown;
|
||||
finishReason?: string;
|
||||
usage?: { promptTokens: number; completionTokens: number };
|
||||
}): PraisonStreamChunk | null {
|
||||
switch (chunk.type) {
|
||||
case 'text-delta':
|
||||
if (chunk.textDelta) {
|
||||
return { type: 'text', text: chunk.textDelta };
|
||||
}
|
||||
return null;
|
||||
|
||||
case 'tool-call':
|
||||
if (chunk.toolCallId && chunk.toolName) {
|
||||
return {
|
||||
type: 'tool-call-end',
|
||||
toolCallId: chunk.toolCallId,
|
||||
args: chunk.args
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
case 'tool-call-streaming-start':
|
||||
if (chunk.toolCallId && chunk.toolName) {
|
||||
return {
|
||||
type: 'tool-call-start',
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: chunk.toolName
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
case 'tool-call-delta':
|
||||
if (chunk.toolCallId && chunk.argsTextDelta) {
|
||||
return {
|
||||
type: 'tool-call-delta',
|
||||
toolCallId: chunk.toolCallId,
|
||||
argsTextDelta: chunk.argsTextDelta
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
case 'finish':
|
||||
case 'step-finish':
|
||||
return {
|
||||
type: 'finish',
|
||||
finishReason: mapFinishReason(chunk.finishReason),
|
||||
usage: chunk.usage ? {
|
||||
promptTokens: chunk.usage.promptTokens,
|
||||
completionTokens: chunk.usage.completionTokens,
|
||||
totalTokens: chunk.usage.promptTokens + chunk.usage.completionTokens
|
||||
} : undefined
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map praisonai-ts tool choice to AI SDK format
|
||||
*/
|
||||
export function toAISDKToolChoice(
|
||||
toolChoice?: 'auto' | 'none' | 'required' | { type: 'tool'; toolName: string }
|
||||
): 'auto' | 'none' | 'required' | { type: 'tool'; toolName: string } | undefined {
|
||||
if (!toolChoice) return undefined;
|
||||
|
||||
if (typeof toolChoice === 'string') {
|
||||
return toolChoice;
|
||||
}
|
||||
|
||||
return toolChoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON, returning the original string if parsing fails
|
||||
*/
|
||||
function safeParseJSON(str: string): unknown {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple prompt from a string
|
||||
*/
|
||||
export function createSimplePrompt(prompt: string, systemPrompt?: string): AISDKMessage[] {
|
||||
const messages: AISDKMessage[] = [];
|
||||
|
||||
if (systemPrompt) {
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: prompt }]
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user