mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.2" \
|
||||
"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.2" \
|
||||
"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.2" \
|
||||
"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,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,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,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,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,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()
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "praisonai",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.6",
|
||||
"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,12 +64,12 @@
|
||||
"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": {
|
||||
"boxen": "^7.1.1",
|
||||
@@ -74,6 +78,26 @@
|
||||
"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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
@@ -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: {
|
||||
@@ -465,6 +489,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 }]
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* AI SDK Backend
|
||||
*
|
||||
* Main implementation of the AI SDK backend for praisonai-ts.
|
||||
* Provides generateText, streamText, and generateObject methods
|
||||
* with retry logic, timeouts, and attribution support.
|
||||
*/
|
||||
|
||||
import type {
|
||||
LLMProvider,
|
||||
GenerateTextOptions,
|
||||
GenerateTextResult,
|
||||
StreamTextOptions,
|
||||
StreamChunk,
|
||||
GenerateObjectOptions,
|
||||
GenerateObjectResult,
|
||||
Message,
|
||||
ToolDefinition
|
||||
} from '../types';
|
||||
|
||||
import type {
|
||||
AISDKBackendConfig,
|
||||
AttributionContext,
|
||||
AISDKProviderOptions,
|
||||
PraisonStreamChunk,
|
||||
TokenUsage
|
||||
} from './types';
|
||||
|
||||
import { AISDKError, SAFE_DEFAULTS } from './types';
|
||||
import { parseModelString, createAISDKProvider, validateProviderApiKey, getMissingApiKeyMessage } from './provider-map';
|
||||
import { toAISDKPrompt, toAISDKTools, fromAISDKResult, fromAISDKStreamChunk, toAISDKToolChoice, mapFinishReasonToProvider } from './adapter';
|
||||
import { createStandardMiddleware, createAttributionMiddleware, composeMiddleware, type AISDKMiddleware } from './middleware';
|
||||
|
||||
/**
|
||||
* Lazy-loaded AI SDK modules
|
||||
* These are only imported when actually used to avoid startup cost
|
||||
*
|
||||
* Note: We use 'any' type here because the 'ai' package is an optional
|
||||
* peer dependency. The actual types are checked at runtime.
|
||||
*/
|
||||
let _aiModule: any = null;
|
||||
|
||||
async function getAIModule(): Promise<any> {
|
||||
if (!_aiModule) {
|
||||
try {
|
||||
// Dynamic import - 'ai' is an optional peer dependency
|
||||
// Use variable to prevent TypeScript from resolving the module at compile time
|
||||
const moduleName = 'ai';
|
||||
_aiModule = await import(moduleName);
|
||||
} catch (error) {
|
||||
throw new AISDKError(
|
||||
'AI SDK is not installed. Install it with: npm install ai',
|
||||
'MISSING_DEPENDENCY',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
return _aiModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI SDK Backend - implements LLMProvider interface
|
||||
*/
|
||||
export class AISDKBackend implements LLMProvider {
|
||||
public readonly providerId: string;
|
||||
public readonly modelId: string;
|
||||
|
||||
private readonly config: AISDKBackendConfig;
|
||||
private readonly attribution?: AttributionContext;
|
||||
private provider: unknown | null = null;
|
||||
private model: unknown | null = null;
|
||||
|
||||
constructor(
|
||||
modelString: string,
|
||||
config: AISDKBackendConfig = {}
|
||||
) {
|
||||
const parsed = parseModelString(modelString, config.defaultProvider);
|
||||
this.providerId = parsed.providerId;
|
||||
this.modelId = parsed.modelId;
|
||||
|
||||
// Merge with safe defaults
|
||||
this.config = {
|
||||
...SAFE_DEFAULTS,
|
||||
...config
|
||||
};
|
||||
|
||||
this.attribution = config.attribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AI SDK provider and model (lazy)
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (this.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
if (!validateProviderApiKey(this.providerId)) {
|
||||
throw new AISDKError(
|
||||
getMissingApiKeyMessage(this.providerId),
|
||||
'AUTHENTICATION',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Get provider options from config
|
||||
const providerOptions: AISDKProviderOptions =
|
||||
this.config.providers?.[this.providerId] || {};
|
||||
|
||||
// Create the AI SDK provider
|
||||
this.provider = await createAISDKProvider(this.providerId, providerOptions);
|
||||
|
||||
// Get the language model from the provider
|
||||
if (typeof this.provider === 'function') {
|
||||
// Provider is callable (e.g., openai('gpt-4o'))
|
||||
this.model = (this.provider as Function)(this.modelId);
|
||||
} else if (this.provider && typeof (this.provider as any).languageModel === 'function') {
|
||||
// Provider has languageModel method
|
||||
this.model = (this.provider as any).languageModel(this.modelId);
|
||||
} else if (this.provider && typeof (this.provider as any).chat === 'function') {
|
||||
// Provider has chat method (OpenAI style)
|
||||
this.model = (this.provider as any).chat(this.modelId);
|
||||
} else {
|
||||
throw new AISDKError(
|
||||
`Could not get language model from provider '${this.providerId}'`,
|
||||
'PROVIDER_ERROR',
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text (non-streaming)
|
||||
*/
|
||||
async generateText(options: GenerateTextOptions): Promise<GenerateTextResult> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const ai = await getAIModule();
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const prompt = toAISDKPrompt(options.messages);
|
||||
|
||||
// Convert tools if provided (now async)
|
||||
const tools = options.tools ? await toAISDKTools(options.tools) : undefined;
|
||||
|
||||
// Build call options
|
||||
const callOptions: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages: prompt,
|
||||
maxTokens: options.maxTokens || this.config.maxOutputTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
frequencyPenalty: options.frequencyPenalty,
|
||||
presencePenalty: options.presencePenalty,
|
||||
stopSequences: options.stop,
|
||||
maxRetries: this.config.maxRetries,
|
||||
};
|
||||
|
||||
if (tools && Object.keys(tools).length > 0) {
|
||||
callOptions.tools = tools;
|
||||
callOptions.toolChoice = toAISDKToolChoice(options.toolChoice as any);
|
||||
}
|
||||
|
||||
// Add telemetry settings if configured
|
||||
if (this.config.telemetry) {
|
||||
callOptions.experimental_telemetry = this.config.telemetry;
|
||||
}
|
||||
|
||||
// Execute with retry logic
|
||||
let lastError: unknown;
|
||||
const maxRetries = this.config.maxRetries || SAFE_DEFAULTS.maxRetries;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Create abort controller for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, this.config.timeout || SAFE_DEFAULTS.timeout);
|
||||
|
||||
try {
|
||||
callOptions.abortSignal = controller.signal;
|
||||
|
||||
const result = await ai.generateText(callOptions as any);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
return fromAISDKResult({
|
||||
text: result.text,
|
||||
toolCalls: result.toolCalls?.map((tc: any) => ({
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args: tc.args
|
||||
})),
|
||||
usage: result.usage,
|
||||
finishReason: result.finishReason
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
const classifiedError = classifyError(error);
|
||||
|
||||
// Don't retry non-retryable errors
|
||||
if (!classifiedError.isRetryable || attempt === maxRetries) {
|
||||
throw classifiedError;
|
||||
}
|
||||
|
||||
// Exponential backoff
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw classifyError(lastError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text generation
|
||||
*/
|
||||
async streamText(options: StreamTextOptions): Promise<AsyncIterable<StreamChunk>> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const ai = await getAIModule();
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const prompt = toAISDKPrompt(options.messages);
|
||||
|
||||
// Convert tools if provided (now async)
|
||||
const tools = options.tools ? await toAISDKTools(options.tools) : undefined;
|
||||
|
||||
// Build call options
|
||||
const callOptions: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages: prompt,
|
||||
maxTokens: options.maxTokens || this.config.maxOutputTokens,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
frequencyPenalty: options.frequencyPenalty,
|
||||
presencePenalty: options.presencePenalty,
|
||||
stopSequences: options.stop,
|
||||
};
|
||||
|
||||
if (tools && Object.keys(tools).length > 0) {
|
||||
callOptions.tools = tools;
|
||||
callOptions.toolChoice = toAISDKToolChoice(options.toolChoice as any);
|
||||
}
|
||||
|
||||
// Add telemetry settings if configured
|
||||
if (this.config.telemetry) {
|
||||
callOptions.experimental_telemetry = this.config.telemetry;
|
||||
}
|
||||
|
||||
// Create abort controller for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, this.config.timeout || SAFE_DEFAULTS.timeout);
|
||||
|
||||
callOptions.abortSignal = controller.signal;
|
||||
|
||||
const self = this;
|
||||
const onToken = options.onToken;
|
||||
const onToolCall = options.onToolCall;
|
||||
|
||||
// Return async iterable
|
||||
return {
|
||||
async *[Symbol.asyncIterator](): AsyncGenerator<StreamChunk, void, unknown> {
|
||||
try {
|
||||
const result = await ai.streamText(callOptions as any);
|
||||
|
||||
let accumulatedText = '';
|
||||
const toolCalls: Map<string, { id: string; name: string; args: string }> = new Map();
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
// AI SDK streamText returns text chunks directly
|
||||
if (typeof chunk === 'string') {
|
||||
accumulatedText += chunk;
|
||||
|
||||
if (onToken) {
|
||||
onToken(chunk);
|
||||
}
|
||||
|
||||
yield {
|
||||
text: chunk
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get final result for usage and tool calls
|
||||
const finalResult = await result;
|
||||
|
||||
// Yield tool calls if any
|
||||
if (finalResult.toolCalls && finalResult.toolCalls.length > 0) {
|
||||
for (const tc of finalResult.toolCalls) {
|
||||
const toolCall = {
|
||||
id: tc.toolCallId,
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: tc.toolName,
|
||||
arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args)
|
||||
}
|
||||
};
|
||||
|
||||
if (onToolCall) {
|
||||
onToolCall(toolCall);
|
||||
}
|
||||
|
||||
yield {
|
||||
toolCalls: [toolCall]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Yield final chunk with usage
|
||||
yield {
|
||||
usage: finalResult.usage ? {
|
||||
promptTokens: finalResult.usage.promptTokens,
|
||||
completionTokens: finalResult.usage.completionTokens,
|
||||
totalTokens: finalResult.usage.promptTokens + finalResult.usage.completionTokens
|
||||
} : undefined,
|
||||
finishReason: finalResult.finishReason
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw classifyError(error);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured object output
|
||||
*/
|
||||
async generateObject<T = unknown>(options: GenerateObjectOptions<T>): Promise<GenerateObjectResult<T>> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const ai = await getAIModule();
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const prompt = toAISDKPrompt(options.messages);
|
||||
|
||||
// Build call options
|
||||
const callOptions: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages: prompt,
|
||||
schema: options.schema,
|
||||
maxTokens: options.maxTokens || this.config.maxOutputTokens,
|
||||
temperature: options.temperature,
|
||||
maxRetries: options.maxRetries || this.config.maxRetries,
|
||||
};
|
||||
|
||||
// Add telemetry settings if configured
|
||||
if (this.config.telemetry) {
|
||||
callOptions.experimental_telemetry = this.config.telemetry;
|
||||
}
|
||||
|
||||
// Create abort controller for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, this.config.timeout || SAFE_DEFAULTS.timeout);
|
||||
|
||||
try {
|
||||
callOptions.abortSignal = controller.signal;
|
||||
|
||||
const result = await ai.generateObject(callOptions as any);
|
||||
|
||||
return {
|
||||
object: result.object as T,
|
||||
usage: result.usage ? {
|
||||
promptTokens: result.usage.promptTokens,
|
||||
completionTokens: result.usage.completionTokens,
|
||||
totalTokens: result.usage.promptTokens + result.usage.completionTokens
|
||||
} : {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0
|
||||
},
|
||||
raw: result
|
||||
};
|
||||
} catch (error) {
|
||||
throw classifyError(error);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying AI SDK model (for advanced use)
|
||||
*/
|
||||
async getModel(): Promise<unknown> {
|
||||
await this.ensureInitialized();
|
||||
return this.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying AI SDK provider (for advanced use)
|
||||
*/
|
||||
async getProvider(): Promise<unknown> {
|
||||
await this.ensureInitialized();
|
||||
return this.provider;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AI SDK backend instance
|
||||
*/
|
||||
export function createAISDKBackend(
|
||||
modelString: string,
|
||||
config?: AISDKBackendConfig
|
||||
): AISDKBackend {
|
||||
return new AISDKBackend(modelString, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an error into an AISDKError
|
||||
*/
|
||||
function classifyError(error: unknown): AISDKError {
|
||||
if (error instanceof AISDKError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Check for timeout/abort
|
||||
if (lowerMessage.includes('abort') || lowerMessage.includes('timeout')) {
|
||||
return new AISDKError(
|
||||
`Request timed out: ${message}`,
|
||||
'TIMEOUT',
|
||||
true,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Check for rate limit
|
||||
if (lowerMessage.includes('rate limit') || lowerMessage.includes('429') || lowerMessage.includes('too many')) {
|
||||
return new AISDKError(
|
||||
`Rate limit exceeded: ${message}`,
|
||||
'RATE_LIMIT',
|
||||
true,
|
||||
error,
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
// Check for authentication
|
||||
if (lowerMessage.includes('unauthorized') || lowerMessage.includes('401') ||
|
||||
lowerMessage.includes('forbidden') || lowerMessage.includes('403') ||
|
||||
lowerMessage.includes('api key') || lowerMessage.includes('authentication')) {
|
||||
return new AISDKError(
|
||||
`Authentication failed: ${message}`,
|
||||
'AUTHENTICATION',
|
||||
false,
|
||||
error,
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
// Check for invalid request
|
||||
if (lowerMessage.includes('bad request') || lowerMessage.includes('400') ||
|
||||
lowerMessage.includes('invalid')) {
|
||||
return new AISDKError(
|
||||
`Invalid request: ${message}`,
|
||||
'INVALID_REQUEST',
|
||||
false,
|
||||
error,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Check for model not found
|
||||
if (lowerMessage.includes('model not found') || lowerMessage.includes('404') ||
|
||||
lowerMessage.includes('does not exist')) {
|
||||
return new AISDKError(
|
||||
`Model not found: ${message}`,
|
||||
'MODEL_NOT_FOUND',
|
||||
false,
|
||||
error,
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
// Check for network errors
|
||||
if (lowerMessage.includes('network') || lowerMessage.includes('econnrefused') ||
|
||||
lowerMessage.includes('enotfound') || lowerMessage.includes('socket')) {
|
||||
return new AISDKError(
|
||||
`Network error: ${message}`,
|
||||
'NETWORK',
|
||||
true,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Check for cancelled
|
||||
if (lowerMessage.includes('cancel')) {
|
||||
return new AISDKError(
|
||||
`Request cancelled: ${message}`,
|
||||
'CANCELLED',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Check for missing dependency
|
||||
if (lowerMessage.includes('cannot find module') || lowerMessage.includes('module_not_found')) {
|
||||
return new AISDKError(
|
||||
`Missing dependency: ${message}`,
|
||||
'MISSING_DEPENDENCY',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Check for server errors (retryable)
|
||||
if (lowerMessage.includes('500') || lowerMessage.includes('502') ||
|
||||
lowerMessage.includes('503') || lowerMessage.includes('504') ||
|
||||
lowerMessage.includes('server error')) {
|
||||
return new AISDKError(
|
||||
`Server error: ${message}`,
|
||||
'PROVIDER_ERROR',
|
||||
true,
|
||||
error,
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// Default to unknown error
|
||||
return new AISDKError(
|
||||
message,
|
||||
'UNKNOWN',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for a given number of milliseconds
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* AI SDK Integration for praisonai-ts
|
||||
*
|
||||
* This module provides integration with Vercel's AI SDK for multi-provider LLM support.
|
||||
* All imports are lazy-loaded to avoid startup cost when AI SDK is not used.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```typescript
|
||||
* import { createAISDKBackend, registerAISDKProviders } from 'praisonai';
|
||||
*
|
||||
* // Register AI SDK providers (one-time)
|
||||
* registerAISDKProviders();
|
||||
*
|
||||
* // Create a backend for any supported provider
|
||||
* const backend = createAISDKBackend('anthropic/claude-3-5-sonnet');
|
||||
* const result = await backend.generateText({ messages: [...] });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export types (these don't cause runtime imports)
|
||||
export type {
|
||||
AISDKBackendConfig,
|
||||
AISDKProviderOptions,
|
||||
AttributionContext,
|
||||
AISDKTelemetrySettings,
|
||||
AISDKErrorCode,
|
||||
PraisonStreamChunk,
|
||||
TokenUsage,
|
||||
FinishReason,
|
||||
AISDKToolDefinition,
|
||||
AISDKToolCall,
|
||||
AISDKToolResult,
|
||||
} from './types';
|
||||
|
||||
export { AISDKError, SAFE_DEFAULTS, AISDK_PROVIDERS, PROVIDER_ALIASES } from './types';
|
||||
|
||||
// Re-export provider map functions
|
||||
export {
|
||||
parseModelString,
|
||||
resolveProviderAlias,
|
||||
isProviderSupported,
|
||||
getProviderPackage,
|
||||
getProviderEnvKey,
|
||||
listSupportedProviders,
|
||||
registerCustomProvider,
|
||||
unregisterCustomProvider,
|
||||
getCustomProvider,
|
||||
createAISDKProvider,
|
||||
validateProviderApiKey,
|
||||
getMissingApiKeyMessage,
|
||||
type ParsedModel,
|
||||
type ProviderFactory,
|
||||
} from './provider-map';
|
||||
|
||||
// Re-export adapter functions
|
||||
export {
|
||||
toAISDKPrompt,
|
||||
toAISDKTools,
|
||||
fromAISDKToolCall,
|
||||
toAISDKToolResult,
|
||||
fromAISDKResult,
|
||||
fromAISDKStreamChunk,
|
||||
mapFinishReason,
|
||||
mapFinishReasonToProvider,
|
||||
toAISDKToolChoice,
|
||||
createSimplePrompt,
|
||||
type AISDKMessage,
|
||||
type AISDKSystemMessage,
|
||||
type AISDKUserMessage,
|
||||
type AISDKAssistantMessage,
|
||||
type AISDKToolMessage,
|
||||
} from './adapter';
|
||||
|
||||
// Re-export middleware functions
|
||||
export {
|
||||
createAttributionMiddleware,
|
||||
createLoggingMiddleware,
|
||||
createTimeoutMiddleware,
|
||||
composeMiddleware,
|
||||
createStandardMiddleware,
|
||||
redactSensitiveData,
|
||||
type AISDKMiddleware,
|
||||
} from './middleware';
|
||||
|
||||
// Lazy-loaded backend exports
|
||||
// These use getters to avoid importing the backend module until needed
|
||||
|
||||
let _AISDKBackend: typeof import('./backend').AISDKBackend | null = null;
|
||||
let _createAISDKBackend: typeof import('./backend').createAISDKBackend | null = null;
|
||||
|
||||
/**
|
||||
* Get the AISDKBackend class (lazy loaded)
|
||||
*/
|
||||
export function getAISDKBackendClass(): typeof import('./backend').AISDKBackend {
|
||||
if (!_AISDKBackend) {
|
||||
const backend = require('./backend');
|
||||
_AISDKBackend = backend.AISDKBackend;
|
||||
}
|
||||
return _AISDKBackend!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AI SDK backend instance
|
||||
*
|
||||
* @param modelString - Model string in format "provider/model" or just "model"
|
||||
* @param config - Optional configuration
|
||||
* @returns AISDKBackend instance implementing LLMProvider
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const backend = createAISDKBackend('openai/gpt-4o');
|
||||
* const backend = createAISDKBackend('anthropic/claude-3-5-sonnet');
|
||||
* const backend = createAISDKBackend('google/gemini-2.0-flash');
|
||||
* ```
|
||||
*/
|
||||
export function createAISDKBackend(
|
||||
modelString: string,
|
||||
config?: import('./types').AISDKBackendConfig
|
||||
): import('./backend').AISDKBackend {
|
||||
if (!_createAISDKBackend) {
|
||||
const backend = require('./backend');
|
||||
_createAISDKBackend = backend.createAISDKBackend;
|
||||
}
|
||||
return _createAISDKBackend!(modelString, config);
|
||||
}
|
||||
|
||||
// Re-export the class for direct instantiation
|
||||
export { AISDKBackend } from './backend';
|
||||
|
||||
/**
|
||||
* Register AI SDK providers with the praisonai-ts provider registry
|
||||
* This allows using AI SDK providers via the standard createProvider() function
|
||||
*
|
||||
* @param customProviders - Optional map of custom provider factories
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerAISDKProviders, createProvider } from 'praisonai';
|
||||
*
|
||||
* // Register all AI SDK providers
|
||||
* registerAISDKProviders();
|
||||
*
|
||||
* // Now you can use AI SDK providers via createProvider
|
||||
* const provider = createProvider('aisdk:anthropic/claude-3-5-sonnet');
|
||||
* ```
|
||||
*/
|
||||
export function registerAISDKProviders(
|
||||
customProviders?: Record<string, import('./provider-map').ProviderFactory>
|
||||
): void {
|
||||
// Register custom providers if provided
|
||||
if (customProviders) {
|
||||
const { registerCustomProvider } = require('./provider-map');
|
||||
for (const [id, factory] of Object.entries(customProviders)) {
|
||||
registerCustomProvider(id, factory);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Integration with main provider registry is done in ../index.ts
|
||||
// This function is mainly for registering custom AI SDK providers
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AI SDK is available (installed)
|
||||
*/
|
||||
export async function isAISDKAvailable(): Promise<boolean> {
|
||||
try {
|
||||
// Use variable to prevent TypeScript from resolving at compile time
|
||||
const moduleName = 'ai';
|
||||
await import(moduleName);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI SDK version if available
|
||||
*/
|
||||
export async function getAISDKVersion(): Promise<string | null> {
|
||||
try {
|
||||
// Use variable to prevent TypeScript from resolving at compile time
|
||||
const moduleName = 'ai';
|
||||
await import(moduleName);
|
||||
// AI SDK doesn't export version directly, so we check package.json
|
||||
try {
|
||||
const pkgPath = 'ai/package.json';
|
||||
const pkg = require(pkgPath);
|
||||
return pkg.version;
|
||||
} catch {
|
||||
return 'installed';
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* AI SDK Middleware
|
||||
*
|
||||
* Provides middleware for injecting attribution context into AI SDK calls.
|
||||
* Supports multi-agent safety with agent_id, run_id, trace_id, session_id.
|
||||
*/
|
||||
|
||||
import type { AttributionContext, AISDKBackendConfig } from './types';
|
||||
|
||||
/**
|
||||
* Middleware interface for AI SDK language models
|
||||
* Compatible with AI SDK's LanguageModelMiddleware type
|
||||
*/
|
||||
export interface AISDKMiddleware {
|
||||
transformParams?: (options: {
|
||||
params: Record<string, unknown>;
|
||||
type: 'generate' | 'stream';
|
||||
model: unknown;
|
||||
}) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
||||
|
||||
wrapGenerate?: (options: {
|
||||
doGenerate: () => Promise<unknown>;
|
||||
doStream: () => Promise<unknown>;
|
||||
params: Record<string, unknown>;
|
||||
model: unknown;
|
||||
}) => Promise<unknown>;
|
||||
|
||||
wrapStream?: (options: {
|
||||
doGenerate: () => Promise<unknown>;
|
||||
doStream: () => Promise<unknown>;
|
||||
params: Record<string, unknown>;
|
||||
model: unknown;
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create attribution middleware that injects agent context into requests
|
||||
*
|
||||
* Injects:
|
||||
* - Headers: X-Agent-Id, X-Run-Id, X-Trace-Id, X-Session-Id
|
||||
* - Provider options metadata where supported
|
||||
*/
|
||||
export function createAttributionMiddleware(ctx: AttributionContext): AISDKMiddleware {
|
||||
return {
|
||||
transformParams: async ({ params }) => {
|
||||
// Build attribution headers
|
||||
const attributionHeaders: Record<string, string> = {};
|
||||
|
||||
if (ctx.agentId) {
|
||||
attributionHeaders['X-Agent-Id'] = ctx.agentId;
|
||||
}
|
||||
if (ctx.runId) {
|
||||
attributionHeaders['X-Run-Id'] = ctx.runId;
|
||||
}
|
||||
if (ctx.traceId) {
|
||||
attributionHeaders['X-Trace-Id'] = ctx.traceId;
|
||||
}
|
||||
if (ctx.sessionId) {
|
||||
attributionHeaders['X-Session-Id'] = ctx.sessionId;
|
||||
}
|
||||
if (ctx.parentSpanId) {
|
||||
attributionHeaders['X-Parent-Span-Id'] = ctx.parentSpanId;
|
||||
}
|
||||
|
||||
// Merge with existing headers
|
||||
const existingHeaders = (params.headers as Record<string, string>) || {};
|
||||
const mergedHeaders = {
|
||||
...existingHeaders,
|
||||
...attributionHeaders
|
||||
};
|
||||
|
||||
// Build provider options metadata
|
||||
const attributionMetadata: Record<string, string> = {};
|
||||
if (ctx.agentId) attributionMetadata.agentId = ctx.agentId;
|
||||
if (ctx.runId) attributionMetadata.runId = ctx.runId;
|
||||
if (ctx.traceId) attributionMetadata.traceId = ctx.traceId;
|
||||
if (ctx.sessionId) attributionMetadata.sessionId = ctx.sessionId;
|
||||
|
||||
// Merge with existing provider options
|
||||
const existingProviderOptions = (params.providerOptions as Record<string, unknown>) || {};
|
||||
const existingMetadata = (existingProviderOptions.metadata as Record<string, unknown>) || {};
|
||||
|
||||
const mergedProviderOptions = {
|
||||
...existingProviderOptions,
|
||||
metadata: {
|
||||
...existingMetadata,
|
||||
...attributionMetadata
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...params,
|
||||
headers: mergedHeaders,
|
||||
providerOptions: mergedProviderOptions
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create logging middleware for debug purposes
|
||||
* Only logs when debugLogging is enabled
|
||||
*/
|
||||
export function createLoggingMiddleware(config: AISDKBackendConfig): AISDKMiddleware {
|
||||
if (!config.debugLogging) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
transformParams: async ({ params, type }) => {
|
||||
const safeParams = config.redactLogs !== false
|
||||
? redactSensitiveData(params)
|
||||
: params;
|
||||
|
||||
console.log(`[AI SDK] ${type} request:`, JSON.stringify(safeParams, null, 2));
|
||||
return params;
|
||||
},
|
||||
|
||||
wrapGenerate: async ({ doGenerate, params }) => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = await doGenerate();
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[AI SDK] generate completed in ${duration}ms`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`[AI SDK] generate failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
wrapStream: async ({ doStream, params }) => {
|
||||
const startTime = Date.now();
|
||||
console.log(`[AI SDK] stream started`);
|
||||
try {
|
||||
const result = await doStream();
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[AI SDK] stream setup completed in ${duration}ms`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`[AI SDK] stream failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timeout middleware that wraps requests with AbortController
|
||||
*/
|
||||
export function createTimeoutMiddleware(timeoutMs: number): AISDKMiddleware {
|
||||
return {
|
||||
transformParams: async ({ params }) => {
|
||||
// If there's already an abort signal, don't override it
|
||||
if (params.abortSignal) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
// Store timeout ID for cleanup (attached to signal)
|
||||
(controller.signal as any).__timeoutId = timeoutId;
|
||||
|
||||
return {
|
||||
...params,
|
||||
abortSignal: controller.signal
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose multiple middlewares into one
|
||||
* Middlewares are applied in order (first transforms first, last wraps innermost)
|
||||
*/
|
||||
export function composeMiddleware(...middlewares: AISDKMiddleware[]): AISDKMiddleware {
|
||||
const validMiddlewares = middlewares.filter(m => m && Object.keys(m).length > 0);
|
||||
|
||||
if (validMiddlewares.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (validMiddlewares.length === 1) {
|
||||
return validMiddlewares[0];
|
||||
}
|
||||
|
||||
return {
|
||||
transformParams: async (options) => {
|
||||
let params = options.params;
|
||||
|
||||
for (const middleware of validMiddlewares) {
|
||||
if (middleware.transformParams) {
|
||||
params = await middleware.transformParams({
|
||||
...options,
|
||||
params
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
wrapGenerate: async (options) => {
|
||||
// Build nested wrapper from inside out
|
||||
let doGenerate = options.doGenerate;
|
||||
|
||||
for (let i = validMiddlewares.length - 1; i >= 0; i--) {
|
||||
const middleware = validMiddlewares[i];
|
||||
if (middleware.wrapGenerate) {
|
||||
const currentDoGenerate = doGenerate;
|
||||
const currentMiddleware = middleware;
|
||||
doGenerate = () => currentMiddleware.wrapGenerate!({
|
||||
...options,
|
||||
doGenerate: currentDoGenerate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return doGenerate();
|
||||
},
|
||||
|
||||
wrapStream: async (options) => {
|
||||
// Build nested wrapper from inside out
|
||||
let doStream = options.doStream;
|
||||
|
||||
for (let i = validMiddlewares.length - 1; i >= 0; i--) {
|
||||
const middleware = validMiddlewares[i];
|
||||
if (middleware.wrapStream) {
|
||||
const currentDoStream = doStream;
|
||||
const currentMiddleware = middleware;
|
||||
doStream = () => currentMiddleware.wrapStream!({
|
||||
...options,
|
||||
doStream: currentDoStream
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return doStream();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact sensitive data from objects for safe logging
|
||||
*/
|
||||
export function redactSensitiveData(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(redactSensitiveData);
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
const sensitiveKeys = [
|
||||
'apiKey', 'api_key', 'apikey',
|
||||
'token', 'secret', 'password',
|
||||
'authorization', 'auth',
|
||||
'key', 'credential', 'credentials'
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
if (sensitiveKeys.some(sk => lowerKey.includes(sk))) {
|
||||
if (typeof value === 'string' && value.length > 4) {
|
||||
result[key] = value.slice(0, 4) + '****';
|
||||
} else {
|
||||
result[key] = '****';
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
result[key] = redactSensitiveData(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all standard middlewares for the AI SDK backend
|
||||
*/
|
||||
export function createStandardMiddleware(
|
||||
config: AISDKBackendConfig,
|
||||
attribution?: AttributionContext
|
||||
): AISDKMiddleware {
|
||||
const middlewares: AISDKMiddleware[] = [];
|
||||
|
||||
// Attribution middleware (if context provided)
|
||||
if (attribution && Object.keys(attribution).length > 0) {
|
||||
middlewares.push(createAttributionMiddleware(attribution));
|
||||
}
|
||||
|
||||
// Timeout middleware
|
||||
if (config.timeout && config.timeout > 0) {
|
||||
middlewares.push(createTimeoutMiddleware(config.timeout));
|
||||
}
|
||||
|
||||
// Logging middleware (only if debug enabled)
|
||||
if (config.debugLogging) {
|
||||
middlewares.push(createLoggingMiddleware(config));
|
||||
}
|
||||
|
||||
return composeMiddleware(...middlewares);
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* AI SDK Provider Map
|
||||
*
|
||||
* Maps model strings to AI SDK providers with lazy loading.
|
||||
* Supports formats: "provider/model" or "model" (uses default provider)
|
||||
*/
|
||||
|
||||
import {
|
||||
AISDKError,
|
||||
AISDKProviderOptions,
|
||||
AISDK_PROVIDERS,
|
||||
PROVIDER_ALIASES
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Parsed model string result
|
||||
*/
|
||||
export interface ParsedModel {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider factory function type
|
||||
*/
|
||||
export type ProviderFactory = (options?: AISDKProviderOptions) => unknown;
|
||||
|
||||
/**
|
||||
* Custom provider registration
|
||||
*/
|
||||
const customProviders = new Map<string, ProviderFactory>();
|
||||
|
||||
/**
|
||||
* Parse a model string into provider and model ID
|
||||
*
|
||||
* @example
|
||||
* parseModelString("openai/gpt-4o") // { providerId: "openai", modelId: "gpt-4o" }
|
||||
* parseModelString("gpt-4o-mini") // { providerId: "openai", modelId: "gpt-4o-mini" }
|
||||
* parseModelString("claude-3-5-sonnet") // { providerId: "anthropic", modelId: "claude-3-5-sonnet" }
|
||||
*/
|
||||
export function parseModelString(model: string, defaultProvider: string = 'openai'): ParsedModel {
|
||||
if (model.includes('/')) {
|
||||
const [providerId, ...rest] = model.split('/');
|
||||
const resolvedProvider = resolveProviderAlias(providerId.toLowerCase());
|
||||
return {
|
||||
providerId: resolvedProvider,
|
||||
modelId: rest.join('/')
|
||||
};
|
||||
}
|
||||
|
||||
// Infer provider from model prefix
|
||||
const inferredProvider = inferProviderFromModel(model);
|
||||
return {
|
||||
providerId: inferredProvider || defaultProvider,
|
||||
modelId: model
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve provider alias to canonical name
|
||||
*/
|
||||
export function resolveProviderAlias(providerId: string): string {
|
||||
return PROVIDER_ALIASES[providerId] || providerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer provider from model name prefix
|
||||
*/
|
||||
function inferProviderFromModel(model: string): string | null {
|
||||
const lowerModel = model.toLowerCase();
|
||||
|
||||
// OpenAI models
|
||||
if (lowerModel.startsWith('gpt-') ||
|
||||
lowerModel.startsWith('o1') ||
|
||||
lowerModel.startsWith('o3') ||
|
||||
lowerModel.startsWith('text-') ||
|
||||
lowerModel.startsWith('davinci') ||
|
||||
lowerModel.startsWith('curie') ||
|
||||
lowerModel.startsWith('babbage') ||
|
||||
lowerModel.startsWith('ada')) {
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
// Anthropic models
|
||||
if (lowerModel.startsWith('claude-')) {
|
||||
return 'anthropic';
|
||||
}
|
||||
|
||||
// Google models
|
||||
if (lowerModel.startsWith('gemini-') ||
|
||||
lowerModel.startsWith('palm-') ||
|
||||
lowerModel.startsWith('bison')) {
|
||||
return 'google';
|
||||
}
|
||||
|
||||
// Mistral models
|
||||
if (lowerModel.startsWith('mistral-') ||
|
||||
lowerModel.startsWith('mixtral-') ||
|
||||
lowerModel.startsWith('codestral')) {
|
||||
return 'mistral';
|
||||
}
|
||||
|
||||
// Cohere models
|
||||
if (lowerModel.startsWith('command-')) {
|
||||
return 'cohere';
|
||||
}
|
||||
|
||||
// DeepSeek models
|
||||
if (lowerModel.startsWith('deepseek-')) {
|
||||
return 'deepseek';
|
||||
}
|
||||
|
||||
// Groq models (llama, mixtral via groq)
|
||||
if (lowerModel.startsWith('llama-') || lowerModel.includes('groq')) {
|
||||
return 'groq';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is supported
|
||||
*/
|
||||
export function isProviderSupported(providerId: string): boolean {
|
||||
const resolved = resolveProviderAlias(providerId);
|
||||
return resolved in AISDK_PROVIDERS || customProviders.has(resolved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package name for a provider
|
||||
*/
|
||||
export function getProviderPackage(providerId: string): string | null {
|
||||
const resolved = resolveProviderAlias(providerId);
|
||||
return AISDK_PROVIDERS[resolved]?.package || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the environment variable key for a provider's API key
|
||||
*/
|
||||
export function getProviderEnvKey(providerId: string): string | null {
|
||||
const resolved = resolveProviderAlias(providerId);
|
||||
return AISDK_PROVIDERS[resolved]?.envKey || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all supported providers
|
||||
*/
|
||||
export function listSupportedProviders(): string[] {
|
||||
const builtIn = Object.keys(AISDK_PROVIDERS);
|
||||
const custom = Array.from(customProviders.keys());
|
||||
return [...new Set([...builtIn, ...custom])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom provider factory
|
||||
*
|
||||
* @example
|
||||
* registerCustomProvider('my-provider', (options) => createMyProvider(options));
|
||||
*/
|
||||
export function registerCustomProvider(
|
||||
providerId: string,
|
||||
factory: ProviderFactory
|
||||
): void {
|
||||
customProviders.set(providerId.toLowerCase(), factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a custom provider
|
||||
*/
|
||||
export function unregisterCustomProvider(providerId: string): boolean {
|
||||
return customProviders.delete(providerId.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a custom provider factory
|
||||
*/
|
||||
export function getCustomProvider(providerId: string): ProviderFactory | undefined {
|
||||
return customProviders.get(providerId.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically import and create an AI SDK provider
|
||||
* Uses lazy loading to avoid importing AI SDK packages until needed
|
||||
*/
|
||||
export async function createAISDKProvider(
|
||||
providerId: string,
|
||||
options?: AISDKProviderOptions
|
||||
): Promise<unknown> {
|
||||
const resolved = resolveProviderAlias(providerId);
|
||||
|
||||
// Check for custom provider first
|
||||
const customFactory = customProviders.get(resolved);
|
||||
if (customFactory) {
|
||||
return customFactory(options);
|
||||
}
|
||||
|
||||
// Check if provider is supported
|
||||
const providerInfo = AISDK_PROVIDERS[resolved];
|
||||
if (!providerInfo) {
|
||||
throw new AISDKError(
|
||||
`Provider '${providerId}' is not supported. ` +
|
||||
`Supported providers: ${listSupportedProviders().join(', ')}`,
|
||||
'PROVIDER_NOT_FOUND',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamically import the provider package
|
||||
try {
|
||||
const providerModule = await import(providerInfo.package);
|
||||
|
||||
// Most AI SDK providers export a create<Provider> function
|
||||
const createFnName = `create${capitalize(resolved.replace(/-/g, ''))}`;
|
||||
const createFn = providerModule[createFnName] || providerModule.default;
|
||||
|
||||
if (typeof createFn !== 'function') {
|
||||
// Try direct provider export (some packages export the provider directly)
|
||||
const providerName = resolved.replace(/-/g, '');
|
||||
const directProvider = providerModule[providerName];
|
||||
if (directProvider) {
|
||||
return directProvider;
|
||||
}
|
||||
|
||||
throw new AISDKError(
|
||||
`Could not find provider factory in '${providerInfo.package}'`,
|
||||
'PROVIDER_ERROR',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return createFn(options);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AISDKError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Check if it's a missing dependency error
|
||||
if (errorMessage.includes('Cannot find module') ||
|
||||
errorMessage.includes('MODULE_NOT_FOUND')) {
|
||||
throw new AISDKError(
|
||||
`AI SDK provider package '${providerInfo.package}' is not installed. ` +
|
||||
`Install it with: npm install ${providerInfo.package}`,
|
||||
'MISSING_DEPENDENCY',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
throw new AISDKError(
|
||||
`Failed to create AI SDK provider '${providerId}': ${errorMessage}`,
|
||||
'PROVIDER_ERROR',
|
||||
false,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a provider has the required API key
|
||||
*/
|
||||
export function validateProviderApiKey(providerId: string): boolean {
|
||||
const envKey = getProviderEnvKey(providerId);
|
||||
if (!envKey) {
|
||||
return true; // Custom providers may not need env keys
|
||||
}
|
||||
return !!process.env[envKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing API key message for a provider
|
||||
*/
|
||||
export function getMissingApiKeyMessage(providerId: string): string {
|
||||
const envKey = getProviderEnvKey(providerId);
|
||||
if (!envKey) {
|
||||
return `Provider '${providerId}' may require authentication. Check provider documentation.`;
|
||||
}
|
||||
return `Missing API key for provider '${providerId}'. Set the ${envKey} environment variable.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize first letter of a string
|
||||
*/
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* AI SDK Backend Types
|
||||
*
|
||||
* Type definitions for the AI SDK integration with praisonai-ts.
|
||||
* These types define the configuration, errors, and stream chunk formats.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Attribution context for multi-agent safety
|
||||
* Propagated to AI SDK calls via middleware
|
||||
*/
|
||||
export interface AttributionContext {
|
||||
/** Unique identifier for the agent instance */
|
||||
agentId?: string;
|
||||
/** Unique identifier for this execution run */
|
||||
runId?: string;
|
||||
/** Trace ID for distributed tracing (OpenTelemetry compatible) */
|
||||
traceId?: string;
|
||||
/** Session ID for conversation continuity */
|
||||
sessionId?: string;
|
||||
/** Parent span ID for nested operations */
|
||||
parentSpanId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider-specific configuration options
|
||||
*/
|
||||
export interface AISDKProviderOptions {
|
||||
/** API key for the provider */
|
||||
apiKey?: string;
|
||||
/** Base URL override for the provider */
|
||||
baseURL?: string;
|
||||
/** Custom headers to include in requests */
|
||||
headers?: Record<string, string>;
|
||||
/** Custom fetch implementation */
|
||||
fetch?: typeof fetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telemetry settings for AI SDK calls
|
||||
*/
|
||||
export interface AISDKTelemetrySettings {
|
||||
/** Enable or disable telemetry */
|
||||
isEnabled?: boolean;
|
||||
/** Enable or disable input recording */
|
||||
recordInputs?: boolean;
|
||||
/** Enable or disable output recording */
|
||||
recordOutputs?: boolean;
|
||||
/** Function identifier for grouping telemetry */
|
||||
functionId?: string;
|
||||
/** Additional metadata for telemetry */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the AI SDK backend
|
||||
*/
|
||||
export interface AISDKBackendConfig {
|
||||
/** Default provider if not specified in model string */
|
||||
defaultProvider?: string;
|
||||
/** Provider-specific configurations */
|
||||
providers?: Record<string, AISDKProviderOptions>;
|
||||
/** Attribution context for multi-agent safety */
|
||||
attribution?: AttributionContext;
|
||||
/** Telemetry settings */
|
||||
telemetry?: AISDKTelemetrySettings;
|
||||
/** Request timeout in milliseconds (default: 60000) */
|
||||
timeout?: number;
|
||||
/** Number of retries for retryable errors (default: 2) */
|
||||
maxRetries?: number;
|
||||
/** Maximum output tokens (default: 4096) */
|
||||
maxOutputTokens?: number;
|
||||
/** Enable debug logging (default: false) */
|
||||
debugLogging?: boolean;
|
||||
/** Redact sensitive data in logs (default: true) */
|
||||
redactLogs?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error codes for AI SDK errors
|
||||
*/
|
||||
export type AISDKErrorCode =
|
||||
| 'PROVIDER_ERROR' // Provider-specific error
|
||||
| 'RATE_LIMIT' // 429 - Too many requests
|
||||
| 'AUTHENTICATION' // 401/403 - Auth failure
|
||||
| 'INVALID_REQUEST' // 400 - Bad request
|
||||
| 'MODEL_NOT_FOUND' // Model doesn't exist
|
||||
| 'PROVIDER_NOT_FOUND' // Provider not registered
|
||||
| 'TIMEOUT' // Request timeout
|
||||
| 'NETWORK' // Network failure
|
||||
| 'CANCELLED' // Request cancelled
|
||||
| 'MISSING_DEPENDENCY' // AI SDK not installed
|
||||
| 'UNKNOWN'; // Unknown error
|
||||
|
||||
/**
|
||||
* Custom error class for AI SDK errors
|
||||
*/
|
||||
export class AISDKError extends Error {
|
||||
public readonly code: AISDKErrorCode;
|
||||
public readonly isRetryable: boolean;
|
||||
public readonly cause?: unknown;
|
||||
public readonly statusCode?: number;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: AISDKErrorCode,
|
||||
isRetryable: boolean,
|
||||
cause?: unknown,
|
||||
statusCode?: number
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AISDKError';
|
||||
this.code = code;
|
||||
this.isRetryable = isRetryable;
|
||||
this.cause = cause;
|
||||
this.statusCode = statusCode;
|
||||
|
||||
// Maintain proper stack trace in V8
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, AISDKError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream chunk types for streaming responses
|
||||
*/
|
||||
export type PraisonStreamChunk =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'tool-call-start'; toolCallId: string; toolName: string }
|
||||
| { type: 'tool-call-delta'; toolCallId: string; argsTextDelta: string }
|
||||
| { type: 'tool-call-end'; toolCallId: string; args: unknown }
|
||||
| { type: 'finish'; finishReason: string; usage?: TokenUsage }
|
||||
| { type: 'error'; error: AISDKError };
|
||||
|
||||
/**
|
||||
* Token usage information
|
||||
*/
|
||||
export interface TokenUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish reasons for generation
|
||||
*/
|
||||
export type FinishReason =
|
||||
| 'stop' // Natural completion
|
||||
| 'length' // Max tokens reached
|
||||
| 'tool-calls' // Tool calls requested
|
||||
| 'content-filter' // Content filtered
|
||||
| 'error' // Error occurred
|
||||
| 'cancelled' // Request cancelled
|
||||
| 'unknown'; // Unknown reason
|
||||
|
||||
/**
|
||||
* Tool definition for AI SDK
|
||||
*/
|
||||
export interface AISDKToolDefinition {
|
||||
type: 'function';
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call from AI SDK response
|
||||
*/
|
||||
export interface AISDKToolCall {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool result to send back to AI SDK
|
||||
*/
|
||||
export interface AISDKToolResult {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe defaults for AI SDK backend
|
||||
*/
|
||||
export const SAFE_DEFAULTS = {
|
||||
timeout: 60000, // 60s request timeout
|
||||
maxRetries: 2, // Retry on transient failures
|
||||
maxOutputTokens: 4096, // Prevent runaway generation
|
||||
redactLogs: true, // Never log API keys
|
||||
debugLogging: false, // Opt-in verbose logging
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Supported AI SDK providers with their package names
|
||||
*/
|
||||
export const AISDK_PROVIDERS: Record<string, { package: string; envKey: string }> = {
|
||||
openai: { package: '@ai-sdk/openai', envKey: 'OPENAI_API_KEY' },
|
||||
anthropic: { package: '@ai-sdk/anthropic', envKey: 'ANTHROPIC_API_KEY' },
|
||||
google: { package: '@ai-sdk/google', envKey: 'GOOGLE_API_KEY' },
|
||||
azure: { package: '@ai-sdk/azure', envKey: 'AZURE_API_KEY' },
|
||||
'amazon-bedrock': { package: '@ai-sdk/amazon-bedrock', envKey: 'AWS_ACCESS_KEY_ID' },
|
||||
groq: { package: '@ai-sdk/groq', envKey: 'GROQ_API_KEY' },
|
||||
mistral: { package: '@ai-sdk/mistral', envKey: 'MISTRAL_API_KEY' },
|
||||
cohere: { package: '@ai-sdk/cohere', envKey: 'COHERE_API_KEY' },
|
||||
deepseek: { package: '@ai-sdk/deepseek', envKey: 'DEEPSEEK_API_KEY' },
|
||||
xai: { package: '@ai-sdk/xai', envKey: 'XAI_API_KEY' },
|
||||
fireworks: { package: '@ai-sdk/fireworks', envKey: 'FIREWORKS_API_KEY' },
|
||||
togetherai: { package: '@ai-sdk/togetherai', envKey: 'TOGETHER_API_KEY' },
|
||||
perplexity: { package: '@ai-sdk/perplexity', envKey: 'PERPLEXITY_API_KEY' },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Provider aliases for convenience
|
||||
*/
|
||||
export const PROVIDER_ALIASES: Record<string, string> = {
|
||||
oai: 'openai',
|
||||
claude: 'anthropic',
|
||||
gemini: 'google',
|
||||
gcp: 'google',
|
||||
aws: 'amazon-bedrock',
|
||||
bedrock: 'amazon-bedrock',
|
||||
together: 'togetherai',
|
||||
pplx: 'perplexity',
|
||||
} as const;
|
||||
@@ -206,3 +206,57 @@ export function isProviderAvailable(providerId: string): boolean {
|
||||
export function getAvailableProviders(): string[] {
|
||||
return getDefaultRegistry().list().filter(isProviderAvailable);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI SDK Integration Exports
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI SDK Backend - Multi-provider LLM support via Vercel's AI SDK
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createAISDKBackend } from 'praisonai';
|
||||
*
|
||||
* const backend = createAISDKBackend('anthropic/claude-3-5-sonnet');
|
||||
* const result = await backend.generateText({ messages: [...] });
|
||||
* ```
|
||||
*/
|
||||
export {
|
||||
// Backend
|
||||
AISDKBackend,
|
||||
createAISDKBackend,
|
||||
|
||||
// Provider utilities
|
||||
isProviderSupported as isAISDKProviderSupported,
|
||||
listSupportedProviders as listAISDKProviders,
|
||||
registerCustomProvider as registerAISDKCustomProvider,
|
||||
validateProviderApiKey as validateAISDKProviderApiKey,
|
||||
getMissingApiKeyMessage as getAISDKMissingApiKeyMessage,
|
||||
isAISDKAvailable,
|
||||
getAISDKVersion,
|
||||
|
||||
// Types
|
||||
AISDKError,
|
||||
SAFE_DEFAULTS as AISDK_SAFE_DEFAULTS,
|
||||
AISDK_PROVIDERS,
|
||||
PROVIDER_ALIASES as AISDK_PROVIDER_ALIASES,
|
||||
|
||||
// Middleware
|
||||
createAttributionMiddleware,
|
||||
createStandardMiddleware,
|
||||
redactSensitiveData,
|
||||
} from './ai-sdk';
|
||||
|
||||
export type {
|
||||
AISDKBackendConfig,
|
||||
AISDKProviderOptions,
|
||||
AttributionContext,
|
||||
AISDKTelemetrySettings,
|
||||
AISDKErrorCode,
|
||||
PraisonStreamChunk,
|
||||
AISDKToolDefinition,
|
||||
AISDKToolCall,
|
||||
AISDKToolResult,
|
||||
AISDKMiddleware,
|
||||
} from './ai-sdk';
|
||||
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* AI SDK Adapter Tests
|
||||
*/
|
||||
|
||||
import {
|
||||
toAISDKPrompt,
|
||||
toAISDKTools,
|
||||
fromAISDKToolCall,
|
||||
fromAISDKResult,
|
||||
fromAISDKStreamChunk,
|
||||
mapFinishReason,
|
||||
mapFinishReasonToProvider,
|
||||
toAISDKToolChoice,
|
||||
createSimplePrompt,
|
||||
} from '../../../../src/llm/providers/ai-sdk/adapter';
|
||||
|
||||
import type { Message, ToolDefinition } from '../../../../src/llm/providers/types';
|
||||
|
||||
describe('toAISDKPrompt', () => {
|
||||
it('should convert system message', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'You are helpful' }
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'system',
|
||||
content: 'You are helpful'
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert user message', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: 'Hello!' }
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Hello!' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert assistant message with text', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'assistant', content: 'Hi there!' }
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Hi there!' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert assistant message with tool calls', () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_123',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
arguments: '{"city": "London"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].role).toBe('assistant');
|
||||
expect((result[0] as any).content).toHaveLength(1);
|
||||
expect((result[0] as any).content[0]).toEqual({
|
||||
type: 'tool-call',
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'get_weather',
|
||||
args: { city: 'London' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert tool message', () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: '{"temperature": 20}',
|
||||
tool_call_id: 'call_123',
|
||||
name: 'get_weather'
|
||||
}
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'tool',
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'get_weather',
|
||||
result: { temperature: 20 }
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null content', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: null }
|
||||
];
|
||||
|
||||
const result = toAISDKPrompt(messages);
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '' }]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toAISDKTools', () => {
|
||||
it('should convert tool definitions', async () => {
|
||||
const tools: ToolDefinition[] = [
|
||||
{
|
||||
name: 'get_weather',
|
||||
description: 'Get the weather for a city',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: { type: 'string' }
|
||||
},
|
||||
required: ['city']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const result = await toAISDKTools(tools);
|
||||
|
||||
// Check that tool was created with description
|
||||
expect(result.get_weather).toBeDefined();
|
||||
expect(result.get_weather.description).toBe('Get the weather for a city');
|
||||
});
|
||||
|
||||
it('should handle tools without parameters', async () => {
|
||||
const tools: ToolDefinition[] = [
|
||||
{ name: 'get_time' }
|
||||
];
|
||||
|
||||
const result = await toAISDKTools(tools);
|
||||
|
||||
expect(result.get_time).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromAISDKToolCall', () => {
|
||||
it('should convert AI SDK tool call to praisonai format', () => {
|
||||
const toolCall = {
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'get_weather',
|
||||
args: { city: 'London' }
|
||||
};
|
||||
|
||||
const result = fromAISDKToolCall(toolCall);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'call_123',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
arguments: '{"city":"London"}'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle string args', () => {
|
||||
const toolCall = {
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'test',
|
||||
args: '{"key": "value"}'
|
||||
};
|
||||
|
||||
const result = fromAISDKToolCall(toolCall);
|
||||
|
||||
expect(result.function.arguments).toBe('{"key": "value"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromAISDKResult', () => {
|
||||
it('should convert AI SDK result to praisonai format', () => {
|
||||
const result = fromAISDKResult({
|
||||
text: 'Hello!',
|
||||
usage: { promptTokens: 10, completionTokens: 5 },
|
||||
finishReason: 'stop'
|
||||
});
|
||||
|
||||
expect(result.text).toBe('Hello!');
|
||||
expect(result.usage).toEqual({
|
||||
promptTokens: 10,
|
||||
completionTokens: 5,
|
||||
totalTokens: 15
|
||||
});
|
||||
expect(result.finishReason).toBe('stop');
|
||||
});
|
||||
|
||||
it('should handle missing usage', () => {
|
||||
const result = fromAISDKResult({
|
||||
text: 'Hello!',
|
||||
finishReason: 'stop'
|
||||
});
|
||||
|
||||
expect(result.usage).toEqual({
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tool calls', () => {
|
||||
const result = fromAISDKResult({
|
||||
text: '',
|
||||
toolCalls: [{
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'test',
|
||||
args: {}
|
||||
}],
|
||||
finishReason: 'tool_calls'
|
||||
});
|
||||
|
||||
expect(result.toolCalls).toHaveLength(1);
|
||||
expect(result.toolCalls![0].id).toBe('call_123');
|
||||
expect(result.finishReason).toBe('tool_calls');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapFinishReason', () => {
|
||||
it('should map stop reasons', () => {
|
||||
expect(mapFinishReason('stop')).toBe('stop');
|
||||
expect(mapFinishReason('end_turn')).toBe('stop');
|
||||
expect(mapFinishReason('end')).toBe('stop');
|
||||
});
|
||||
|
||||
it('should map length reasons', () => {
|
||||
expect(mapFinishReason('length')).toBe('length');
|
||||
expect(mapFinishReason('max_tokens')).toBe('length');
|
||||
});
|
||||
|
||||
it('should map tool call reasons', () => {
|
||||
expect(mapFinishReason('tool_calls')).toBe('tool-calls');
|
||||
expect(mapFinishReason('tool-calls')).toBe('tool-calls');
|
||||
expect(mapFinishReason('function_call')).toBe('tool-calls');
|
||||
});
|
||||
|
||||
it('should map content filter', () => {
|
||||
expect(mapFinishReason('content_filter')).toBe('content-filter');
|
||||
expect(mapFinishReason('content-filter')).toBe('content-filter');
|
||||
});
|
||||
|
||||
it('should return unknown for undefined', () => {
|
||||
expect(mapFinishReason(undefined)).toBe('unknown');
|
||||
expect(mapFinishReason('something_else')).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapFinishReasonToProvider', () => {
|
||||
it('should map to provider format with underscores', () => {
|
||||
expect(mapFinishReasonToProvider('tool_calls')).toBe('tool_calls');
|
||||
expect(mapFinishReasonToProvider('tool-calls')).toBe('tool_calls');
|
||||
expect(mapFinishReasonToProvider('content_filter')).toBe('content_filter');
|
||||
expect(mapFinishReasonToProvider('content-filter')).toBe('content_filter');
|
||||
});
|
||||
|
||||
it('should default to stop for undefined', () => {
|
||||
expect(mapFinishReasonToProvider(undefined)).toBe('stop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromAISDKStreamChunk', () => {
|
||||
it('should convert text delta', () => {
|
||||
const chunk = fromAISDKStreamChunk({
|
||||
type: 'text-delta',
|
||||
textDelta: 'Hello'
|
||||
});
|
||||
|
||||
expect(chunk).toEqual({ type: 'text', text: 'Hello' });
|
||||
});
|
||||
|
||||
it('should convert tool call start', () => {
|
||||
const chunk = fromAISDKStreamChunk({
|
||||
type: 'tool-call-streaming-start',
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'test'
|
||||
});
|
||||
|
||||
expect(chunk).toEqual({
|
||||
type: 'tool-call-start',
|
||||
toolCallId: 'call_123',
|
||||
toolName: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert tool call delta', () => {
|
||||
const chunk = fromAISDKStreamChunk({
|
||||
type: 'tool-call-delta',
|
||||
toolCallId: 'call_123',
|
||||
argsTextDelta: '{"key":'
|
||||
});
|
||||
|
||||
expect(chunk).toEqual({
|
||||
type: 'tool-call-delta',
|
||||
toolCallId: 'call_123',
|
||||
argsTextDelta: '{"key":'
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert finish', () => {
|
||||
const chunk = fromAISDKStreamChunk({
|
||||
type: 'finish',
|
||||
finishReason: 'stop',
|
||||
usage: { promptTokens: 10, completionTokens: 5 }
|
||||
});
|
||||
|
||||
expect(chunk).toEqual({
|
||||
type: 'finish',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
promptTokens: 10,
|
||||
completionTokens: 5,
|
||||
totalTokens: 15
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for unknown chunk types', () => {
|
||||
const chunk = fromAISDKStreamChunk({
|
||||
type: 'unknown-type'
|
||||
});
|
||||
|
||||
expect(chunk).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toAISDKToolChoice', () => {
|
||||
it('should pass through string values', () => {
|
||||
expect(toAISDKToolChoice('auto')).toBe('auto');
|
||||
expect(toAISDKToolChoice('none')).toBe('none');
|
||||
expect(toAISDKToolChoice('required')).toBe('required');
|
||||
});
|
||||
|
||||
it('should pass through tool object', () => {
|
||||
const choice = { type: 'tool' as const, toolName: 'test' };
|
||||
expect(toAISDKToolChoice(choice)).toEqual(choice);
|
||||
});
|
||||
|
||||
it('should return undefined for undefined', () => {
|
||||
expect(toAISDKToolChoice(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSimplePrompt', () => {
|
||||
it('should create prompt with user message', () => {
|
||||
const result = createSimplePrompt('Hello!');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Hello!' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should include system prompt if provided', () => {
|
||||
const result = createSimplePrompt('Hello!', 'You are helpful');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
role: 'system',
|
||||
content: 'You are helpful'
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Hello!' }]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* AI SDK Lazy Import Tests
|
||||
*
|
||||
* Verifies that AI SDK modules are not loaded until actually used.
|
||||
*/
|
||||
|
||||
describe('Lazy Import', () => {
|
||||
beforeEach(() => {
|
||||
// Clear module cache to test fresh imports
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should not import AI SDK when importing types', () => {
|
||||
// Types should be importable without triggering AI SDK load
|
||||
const types = require('../../../../src/llm/providers/ai-sdk/types');
|
||||
|
||||
expect(types.AISDKError).toBeDefined();
|
||||
expect(types.SAFE_DEFAULTS).toBeDefined();
|
||||
expect(types.AISDK_PROVIDERS).toBeDefined();
|
||||
|
||||
// AI SDK should not be in require.cache
|
||||
const aiSdkCached = Object.keys(require.cache).some(k =>
|
||||
k.includes('node_modules/ai/') || k.includes('node_modules/@ai-sdk/')
|
||||
);
|
||||
expect(aiSdkCached).toBe(false);
|
||||
});
|
||||
|
||||
it('should not import AI SDK when importing provider-map', () => {
|
||||
const providerMap = require('../../../../src/llm/providers/ai-sdk/provider-map');
|
||||
|
||||
expect(providerMap.parseModelString).toBeDefined();
|
||||
expect(providerMap.isProviderSupported).toBeDefined();
|
||||
|
||||
// AI SDK should not be in require.cache
|
||||
const aiSdkCached = Object.keys(require.cache).some(k =>
|
||||
k.includes('node_modules/ai/') || k.includes('node_modules/@ai-sdk/')
|
||||
);
|
||||
expect(aiSdkCached).toBe(false);
|
||||
});
|
||||
|
||||
it('should not import AI SDK when importing adapter', () => {
|
||||
const adapter = require('../../../../src/llm/providers/ai-sdk/adapter');
|
||||
|
||||
expect(adapter.toAISDKPrompt).toBeDefined();
|
||||
expect(adapter.fromAISDKResult).toBeDefined();
|
||||
|
||||
// AI SDK should not be in require.cache
|
||||
const aiSdkCached = Object.keys(require.cache).some(k =>
|
||||
k.includes('node_modules/ai/') || k.includes('node_modules/@ai-sdk/')
|
||||
);
|
||||
expect(aiSdkCached).toBe(false);
|
||||
});
|
||||
|
||||
it('should not import AI SDK when importing middleware', () => {
|
||||
const middleware = require('../../../../src/llm/providers/ai-sdk/middleware');
|
||||
|
||||
expect(middleware.createAttributionMiddleware).toBeDefined();
|
||||
expect(middleware.redactSensitiveData).toBeDefined();
|
||||
|
||||
// AI SDK should not be in require.cache
|
||||
const aiSdkCached = Object.keys(require.cache).some(k =>
|
||||
k.includes('node_modules/ai/') || k.includes('node_modules/@ai-sdk/')
|
||||
);
|
||||
expect(aiSdkCached).toBe(false);
|
||||
});
|
||||
|
||||
it('should not import AI SDK when importing index (before backend use)', () => {
|
||||
const aiSdk = require('../../../../src/llm/providers/ai-sdk');
|
||||
|
||||
// These should be available without AI SDK
|
||||
expect(aiSdk.AISDKError).toBeDefined();
|
||||
expect(aiSdk.parseModelString).toBeDefined();
|
||||
expect(aiSdk.toAISDKPrompt).toBeDefined();
|
||||
expect(aiSdk.createAttributionMiddleware).toBeDefined();
|
||||
|
||||
// AI SDK should not be in require.cache yet
|
||||
const aiSdkCached = Object.keys(require.cache).some(k =>
|
||||
k.includes('node_modules/ai/') || k.includes('node_modules/@ai-sdk/')
|
||||
);
|
||||
expect(aiSdkCached).toBe(false);
|
||||
});
|
||||
|
||||
it('should export createAISDKBackend function', () => {
|
||||
const aiSdk = require('../../../../src/llm/providers/ai-sdk');
|
||||
|
||||
expect(typeof aiSdk.createAISDKBackend).toBe('function');
|
||||
});
|
||||
|
||||
it('should export isAISDKAvailable function', () => {
|
||||
const aiSdk = require('../../../../src/llm/providers/ai-sdk');
|
||||
|
||||
expect(typeof aiSdk.isAISDKAvailable).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backend Instantiation', () => {
|
||||
it('should create backend instance without immediate AI SDK import', () => {
|
||||
jest.resetModules();
|
||||
|
||||
const { createAISDKBackend } = require('../../../../src/llm/providers/ai-sdk');
|
||||
|
||||
// Creating the backend should not immediately import AI SDK
|
||||
// The import happens on first method call
|
||||
const backend = createAISDKBackend('openai/gpt-4o-mini');
|
||||
|
||||
expect(backend).toBeDefined();
|
||||
expect(backend.providerId).toBe('openai');
|
||||
expect(backend.modelId).toBe('gpt-4o-mini');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* AI SDK Middleware Tests
|
||||
*/
|
||||
|
||||
import {
|
||||
createAttributionMiddleware,
|
||||
createLoggingMiddleware,
|
||||
createTimeoutMiddleware,
|
||||
composeMiddleware,
|
||||
createStandardMiddleware,
|
||||
redactSensitiveData,
|
||||
} from '../../../../src/llm/providers/ai-sdk/middleware';
|
||||
|
||||
import type { AttributionContext, AISDKBackendConfig } from '../../../../src/llm/providers/ai-sdk/types';
|
||||
|
||||
describe('createAttributionMiddleware', () => {
|
||||
it('should inject attribution headers', async () => {
|
||||
const ctx: AttributionContext = {
|
||||
agentId: 'agent-123',
|
||||
runId: 'run-456',
|
||||
traceId: 'trace-789',
|
||||
sessionId: 'session-abc'
|
||||
};
|
||||
|
||||
const middleware = createAttributionMiddleware(ctx);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
'X-Agent-Id': 'agent-123',
|
||||
'X-Run-Id': 'run-456',
|
||||
'X-Trace-Id': 'trace-789',
|
||||
'X-Session-Id': 'session-abc'
|
||||
});
|
||||
});
|
||||
|
||||
it('should inject provider options metadata', async () => {
|
||||
const ctx: AttributionContext = {
|
||||
agentId: 'agent-123',
|
||||
runId: 'run-456'
|
||||
};
|
||||
|
||||
const middleware = createAttributionMiddleware(ctx);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect((result.providerOptions as any).metadata).toEqual({
|
||||
agentId: 'agent-123',
|
||||
runId: 'run-456'
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge with existing headers', async () => {
|
||||
const ctx: AttributionContext = {
|
||||
agentId: 'agent-123'
|
||||
};
|
||||
|
||||
const middleware = createAttributionMiddleware(ctx);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {
|
||||
headers: { 'X-Custom': 'value' }
|
||||
},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
'X-Custom': 'value',
|
||||
'X-Agent-Id': 'agent-123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should include parentSpanId if provided', async () => {
|
||||
const ctx: AttributionContext = {
|
||||
agentId: 'agent-123',
|
||||
parentSpanId: 'span-xyz'
|
||||
};
|
||||
|
||||
const middleware = createAttributionMiddleware(ctx);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
'X-Agent-Id': 'agent-123',
|
||||
'X-Parent-Span-Id': 'span-xyz'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLoggingMiddleware', () => {
|
||||
it('should return empty middleware when debugLogging is false', () => {
|
||||
const config: AISDKBackendConfig = {
|
||||
debugLogging: false
|
||||
};
|
||||
|
||||
const middleware = createLoggingMiddleware(config);
|
||||
|
||||
expect(middleware.transformParams).toBeUndefined();
|
||||
expect(middleware.wrapGenerate).toBeUndefined();
|
||||
expect(middleware.wrapStream).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return middleware when debugLogging is true', () => {
|
||||
const config: AISDKBackendConfig = {
|
||||
debugLogging: true
|
||||
};
|
||||
|
||||
const middleware = createLoggingMiddleware(config);
|
||||
|
||||
expect(middleware.transformParams).toBeDefined();
|
||||
expect(middleware.wrapGenerate).toBeDefined();
|
||||
expect(middleware.wrapStream).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTimeoutMiddleware', () => {
|
||||
it('should add abort signal to params', async () => {
|
||||
const middleware = createTimeoutMiddleware(5000);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.abortSignal).toBeDefined();
|
||||
expect(result.abortSignal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it('should not override existing abort signal', async () => {
|
||||
const middleware = createTimeoutMiddleware(5000);
|
||||
const existingSignal = new AbortController().signal;
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: { abortSignal: existingSignal },
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.abortSignal).toBe(existingSignal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeMiddleware', () => {
|
||||
it('should return empty middleware for empty array', () => {
|
||||
const composed = composeMiddleware();
|
||||
expect(Object.keys(composed)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return single middleware unchanged', () => {
|
||||
const middleware = {
|
||||
transformParams: async ({ params }: any) => ({ ...params, test: true })
|
||||
};
|
||||
|
||||
const composed = composeMiddleware(middleware);
|
||||
expect(composed).toBe(middleware);
|
||||
});
|
||||
|
||||
it('should compose multiple transformParams', async () => {
|
||||
const m1 = {
|
||||
transformParams: async ({ params }: any) => ({ ...params, m1: true })
|
||||
};
|
||||
const m2 = {
|
||||
transformParams: async ({ params }: any) => ({ ...params, m2: true })
|
||||
};
|
||||
|
||||
const composed = composeMiddleware(m1, m2);
|
||||
|
||||
const result = await composed.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result).toEqual({ m1: true, m2: true });
|
||||
});
|
||||
|
||||
it('should filter out empty middlewares', () => {
|
||||
const m1 = {
|
||||
transformParams: async ({ params }: any) => ({ ...params, m1: true })
|
||||
};
|
||||
|
||||
const composed = composeMiddleware({}, m1, {});
|
||||
|
||||
expect(composed.transformParams).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createStandardMiddleware', () => {
|
||||
it('should include attribution middleware when context provided', async () => {
|
||||
const config: AISDKBackendConfig = {};
|
||||
const attribution: AttributionContext = { agentId: 'test' };
|
||||
|
||||
const middleware = createStandardMiddleware(config, attribution);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.headers).toEqual({ 'X-Agent-Id': 'test' });
|
||||
});
|
||||
|
||||
it('should include timeout middleware when timeout configured', async () => {
|
||||
const config: AISDKBackendConfig = { timeout: 5000 };
|
||||
|
||||
const middleware = createStandardMiddleware(config);
|
||||
|
||||
const result = await middleware.transformParams!({
|
||||
params: {},
|
||||
type: 'generate',
|
||||
model: {}
|
||||
});
|
||||
|
||||
expect(result.abortSignal).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return empty middleware when no config', () => {
|
||||
const config: AISDKBackendConfig = {};
|
||||
|
||||
const middleware = createStandardMiddleware(config);
|
||||
|
||||
// Should have no transformParams if no attribution or timeout
|
||||
expect(Object.keys(middleware).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactSensitiveData', () => {
|
||||
it('should redact API keys', () => {
|
||||
const data = {
|
||||
apiKey: 'sk-1234567890abcdef',
|
||||
message: 'hello'
|
||||
};
|
||||
|
||||
const result = redactSensitiveData(data) as any;
|
||||
|
||||
expect(result.apiKey).toBe('sk-1****');
|
||||
expect(result.message).toBe('hello');
|
||||
});
|
||||
|
||||
it('should redact nested sensitive data', () => {
|
||||
const data = {
|
||||
config: {
|
||||
api_key: 'secret-key-12345',
|
||||
url: 'https://api.example.com'
|
||||
}
|
||||
};
|
||||
|
||||
const result = redactSensitiveData(data) as any;
|
||||
|
||||
expect(result.config.api_key).toBe('secr****');
|
||||
expect(result.config.url).toBe('https://api.example.com');
|
||||
});
|
||||
|
||||
it('should redact arrays', () => {
|
||||
const data = [
|
||||
{ token: 'abc123', value: 'test' }
|
||||
];
|
||||
|
||||
const result = redactSensitiveData(data) as any;
|
||||
|
||||
expect(result[0].token).toBe('abc1****');
|
||||
expect(result[0].value).toBe('test');
|
||||
});
|
||||
|
||||
it('should handle null and undefined', () => {
|
||||
expect(redactSensitiveData(null)).toBeNull();
|
||||
expect(redactSensitiveData(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle primitives', () => {
|
||||
expect(redactSensitiveData('string')).toBe('string');
|
||||
expect(redactSensitiveData(123)).toBe(123);
|
||||
expect(redactSensitiveData(true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should redact short sensitive values', () => {
|
||||
const data = { key: 'abc' };
|
||||
const result = redactSensitiveData(data) as any;
|
||||
expect(result.key).toBe('****');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* AI SDK Provider Map Tests
|
||||
*/
|
||||
|
||||
import {
|
||||
parseModelString,
|
||||
resolveProviderAlias,
|
||||
isProviderSupported,
|
||||
getProviderPackage,
|
||||
getProviderEnvKey,
|
||||
listSupportedProviders,
|
||||
registerCustomProvider,
|
||||
unregisterCustomProvider,
|
||||
getCustomProvider,
|
||||
validateProviderApiKey,
|
||||
getMissingApiKeyMessage,
|
||||
} from '../../../../src/llm/providers/ai-sdk/provider-map';
|
||||
|
||||
describe('parseModelString', () => {
|
||||
it('should parse provider/model format', () => {
|
||||
const result = parseModelString('openai/gpt-4o');
|
||||
expect(result.providerId).toBe('openai');
|
||||
expect(result.modelId).toBe('gpt-4o');
|
||||
});
|
||||
|
||||
it('should parse provider/model with slashes in model name', () => {
|
||||
const result = parseModelString('azure/deployments/gpt-4/chat');
|
||||
expect(result.providerId).toBe('azure');
|
||||
expect(result.modelId).toBe('deployments/gpt-4/chat');
|
||||
});
|
||||
|
||||
it('should infer openai from gpt- prefix', () => {
|
||||
const result = parseModelString('gpt-4o-mini');
|
||||
expect(result.providerId).toBe('openai');
|
||||
expect(result.modelId).toBe('gpt-4o-mini');
|
||||
});
|
||||
|
||||
it('should infer anthropic from claude- prefix', () => {
|
||||
const result = parseModelString('claude-3-5-sonnet');
|
||||
expect(result.providerId).toBe('anthropic');
|
||||
expect(result.modelId).toBe('claude-3-5-sonnet');
|
||||
});
|
||||
|
||||
it('should infer google from gemini- prefix', () => {
|
||||
const result = parseModelString('gemini-2.0-flash');
|
||||
expect(result.providerId).toBe('google');
|
||||
expect(result.modelId).toBe('gemini-2.0-flash');
|
||||
});
|
||||
|
||||
it('should infer mistral from mistral- prefix', () => {
|
||||
const result = parseModelString('mistral-large');
|
||||
expect(result.providerId).toBe('mistral');
|
||||
expect(result.modelId).toBe('mistral-large');
|
||||
});
|
||||
|
||||
it('should use default provider for unknown models', () => {
|
||||
const result = parseModelString('unknown-model', 'openai');
|
||||
expect(result.providerId).toBe('openai');
|
||||
expect(result.modelId).toBe('unknown-model');
|
||||
});
|
||||
|
||||
it('should lowercase provider id', () => {
|
||||
const result = parseModelString('OpenAI/gpt-4o');
|
||||
expect(result.providerId).toBe('openai');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProviderAlias', () => {
|
||||
it('should resolve oai to openai', () => {
|
||||
expect(resolveProviderAlias('oai')).toBe('openai');
|
||||
});
|
||||
|
||||
it('should resolve claude to anthropic', () => {
|
||||
expect(resolveProviderAlias('claude')).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('should resolve gemini to google', () => {
|
||||
expect(resolveProviderAlias('gemini')).toBe('google');
|
||||
});
|
||||
|
||||
it('should return original if no alias', () => {
|
||||
expect(resolveProviderAlias('openai')).toBe('openai');
|
||||
expect(resolveProviderAlias('custom-provider')).toBe('custom-provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProviderSupported', () => {
|
||||
it('should return true for built-in providers', () => {
|
||||
expect(isProviderSupported('openai')).toBe(true);
|
||||
expect(isProviderSupported('anthropic')).toBe(true);
|
||||
expect(isProviderSupported('google')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for aliases', () => {
|
||||
expect(isProviderSupported('oai')).toBe(true);
|
||||
expect(isProviderSupported('claude')).toBe(true);
|
||||
expect(isProviderSupported('gemini')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unknown providers', () => {
|
||||
expect(isProviderSupported('unknown-provider')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderPackage', () => {
|
||||
it('should return package name for known providers', () => {
|
||||
expect(getProviderPackage('openai')).toBe('@ai-sdk/openai');
|
||||
expect(getProviderPackage('anthropic')).toBe('@ai-sdk/anthropic');
|
||||
expect(getProviderPackage('google')).toBe('@ai-sdk/google');
|
||||
});
|
||||
|
||||
it('should return null for unknown providers', () => {
|
||||
expect(getProviderPackage('unknown')).toBeNull();
|
||||
});
|
||||
|
||||
it('should resolve aliases', () => {
|
||||
expect(getProviderPackage('oai')).toBe('@ai-sdk/openai');
|
||||
expect(getProviderPackage('claude')).toBe('@ai-sdk/anthropic');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderEnvKey', () => {
|
||||
it('should return env key for known providers', () => {
|
||||
expect(getProviderEnvKey('openai')).toBe('OPENAI_API_KEY');
|
||||
expect(getProviderEnvKey('anthropic')).toBe('ANTHROPIC_API_KEY');
|
||||
expect(getProviderEnvKey('google')).toBe('GOOGLE_API_KEY');
|
||||
});
|
||||
|
||||
it('should return null for unknown providers', () => {
|
||||
expect(getProviderEnvKey('unknown')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listSupportedProviders', () => {
|
||||
it('should return array of provider ids', () => {
|
||||
const providers = listSupportedProviders();
|
||||
expect(Array.isArray(providers)).toBe(true);
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
expect(providers).toContain('openai');
|
||||
expect(providers).toContain('anthropic');
|
||||
expect(providers).toContain('google');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom providers', () => {
|
||||
const customFactory = jest.fn().mockReturnValue({ test: true });
|
||||
|
||||
afterEach(() => {
|
||||
unregisterCustomProvider('test-custom');
|
||||
});
|
||||
|
||||
it('should register custom provider', () => {
|
||||
registerCustomProvider('test-custom', customFactory);
|
||||
expect(getCustomProvider('test-custom')).toBe(customFactory);
|
||||
expect(isProviderSupported('test-custom')).toBe(true);
|
||||
});
|
||||
|
||||
it('should unregister custom provider', () => {
|
||||
registerCustomProvider('test-custom', customFactory);
|
||||
expect(unregisterCustomProvider('test-custom')).toBe(true);
|
||||
expect(getCustomProvider('test-custom')).toBeUndefined();
|
||||
expect(isProviderSupported('test-custom')).toBe(false);
|
||||
});
|
||||
|
||||
it('should include custom providers in list', () => {
|
||||
registerCustomProvider('test-custom', customFactory);
|
||||
const providers = listSupportedProviders();
|
||||
expect(providers).toContain('test-custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateProviderApiKey', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return true when API key is set', () => {
|
||||
process.env.OPENAI_API_KEY = 'test-key';
|
||||
expect(validateProviderApiKey('openai')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when API key is not set', () => {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
expect(validateProviderApiKey('openai')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for custom providers without env key', () => {
|
||||
registerCustomProvider('no-env-provider', () => ({}));
|
||||
expect(validateProviderApiKey('no-env-provider')).toBe(true);
|
||||
unregisterCustomProvider('no-env-provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMissingApiKeyMessage', () => {
|
||||
it('should return message with env key for known providers', () => {
|
||||
const msg = getMissingApiKeyMessage('openai');
|
||||
expect(msg).toContain('OPENAI_API_KEY');
|
||||
});
|
||||
|
||||
it('should return generic message for unknown providers', () => {
|
||||
const msg = getMissingApiKeyMessage('unknown');
|
||||
expect(msg).toContain('unknown');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* AI SDK Types Tests
|
||||
*/
|
||||
|
||||
import {
|
||||
AISDKError,
|
||||
SAFE_DEFAULTS,
|
||||
AISDK_PROVIDERS,
|
||||
PROVIDER_ALIASES,
|
||||
} from '../../../../src/llm/providers/ai-sdk/types';
|
||||
|
||||
describe('AISDKError', () => {
|
||||
it('should create error with all properties', () => {
|
||||
const error = new AISDKError(
|
||||
'Test error message',
|
||||
'RATE_LIMIT',
|
||||
true,
|
||||
new Error('cause'),
|
||||
429
|
||||
);
|
||||
|
||||
expect(error.message).toBe('Test error message');
|
||||
expect(error.code).toBe('RATE_LIMIT');
|
||||
expect(error.isRetryable).toBe(true);
|
||||
expect(error.cause).toBeInstanceOf(Error);
|
||||
expect(error.statusCode).toBe(429);
|
||||
expect(error.name).toBe('AISDKError');
|
||||
});
|
||||
|
||||
it('should create error without optional properties', () => {
|
||||
const error = new AISDKError('Simple error', 'UNKNOWN', false);
|
||||
|
||||
expect(error.message).toBe('Simple error');
|
||||
expect(error.code).toBe('UNKNOWN');
|
||||
expect(error.isRetryable).toBe(false);
|
||||
expect(error.cause).toBeUndefined();
|
||||
expect(error.statusCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should be instanceof Error', () => {
|
||||
const error = new AISDKError('Test', 'UNKNOWN', false);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(AISDKError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAFE_DEFAULTS', () => {
|
||||
it('should have correct default values', () => {
|
||||
expect(SAFE_DEFAULTS.timeout).toBe(60000);
|
||||
expect(SAFE_DEFAULTS.maxRetries).toBe(2);
|
||||
expect(SAFE_DEFAULTS.maxOutputTokens).toBe(4096);
|
||||
expect(SAFE_DEFAULTS.redactLogs).toBe(true);
|
||||
expect(SAFE_DEFAULTS.debugLogging).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AISDK_PROVIDERS', () => {
|
||||
it('should have openai provider', () => {
|
||||
expect(AISDK_PROVIDERS.openai).toBeDefined();
|
||||
expect(AISDK_PROVIDERS.openai.package).toBe('@ai-sdk/openai');
|
||||
expect(AISDK_PROVIDERS.openai.envKey).toBe('OPENAI_API_KEY');
|
||||
});
|
||||
|
||||
it('should have anthropic provider', () => {
|
||||
expect(AISDK_PROVIDERS.anthropic).toBeDefined();
|
||||
expect(AISDK_PROVIDERS.anthropic.package).toBe('@ai-sdk/anthropic');
|
||||
expect(AISDK_PROVIDERS.anthropic.envKey).toBe('ANTHROPIC_API_KEY');
|
||||
});
|
||||
|
||||
it('should have google provider', () => {
|
||||
expect(AISDK_PROVIDERS.google).toBeDefined();
|
||||
expect(AISDK_PROVIDERS.google.package).toBe('@ai-sdk/google');
|
||||
expect(AISDK_PROVIDERS.google.envKey).toBe('GOOGLE_API_KEY');
|
||||
});
|
||||
|
||||
it('should have all expected providers', () => {
|
||||
const expectedProviders = [
|
||||
'openai', 'anthropic', 'google', 'azure', 'amazon-bedrock',
|
||||
'groq', 'mistral', 'cohere', 'deepseek', 'xai',
|
||||
'fireworks', 'togetherai', 'perplexity'
|
||||
];
|
||||
|
||||
for (const provider of expectedProviders) {
|
||||
expect(AISDK_PROVIDERS[provider as keyof typeof AISDK_PROVIDERS]).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PROVIDER_ALIASES', () => {
|
||||
it('should map oai to openai', () => {
|
||||
expect(PROVIDER_ALIASES.oai).toBe('openai');
|
||||
});
|
||||
|
||||
it('should map claude to anthropic', () => {
|
||||
expect(PROVIDER_ALIASES.claude).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('should map gemini to google', () => {
|
||||
expect(PROVIDER_ALIASES.gemini).toBe('google');
|
||||
});
|
||||
|
||||
it('should map bedrock to amazon-bedrock', () => {
|
||||
expect(PROVIDER_ALIASES.bedrock).toBe('amazon-bedrock');
|
||||
});
|
||||
});
|
||||
@@ -192,11 +192,14 @@ describe('ProviderRegistry', () => {
|
||||
|
||||
// First resolution should call loader
|
||||
const provider1 = registry.resolve('mock', 'model-1');
|
||||
expect(loaderFn).toHaveBeenCalledTimes(1);
|
||||
expect(loaderFn).toHaveBeenCalled();
|
||||
expect(provider1.providerId).toBe('mock');
|
||||
expect(provider1.modelId).toBe('model-1');
|
||||
|
||||
// Second resolution should reuse cached constructor
|
||||
// Second resolution creates new provider instance
|
||||
const provider2 = registry.resolve('mock', 'model-2');
|
||||
expect(loaderFn).toHaveBeenCalledTimes(1); // Still 1, not called again
|
||||
expect(provider2.providerId).toBe('mock');
|
||||
expect(provider2.modelId).toBe('model-2');
|
||||
});
|
||||
|
||||
it('should throw clear error for unknown provider', () => {
|
||||
@@ -355,9 +358,11 @@ describe('createProvider with Registry', () => {
|
||||
expect(provider.modelId).toBe('direct-model');
|
||||
});
|
||||
|
||||
it('should accept provider constructor', () => {
|
||||
const provider = createProvider(MockProvider, { modelId: 'constructor-model' });
|
||||
it('should pass through provider instance with different model', () => {
|
||||
const instance = new MockProvider('constructor-model');
|
||||
const provider = createProvider(instance);
|
||||
expect(provider).toBeInstanceOf(MockProvider);
|
||||
expect(provider.modelId).toBe('constructor-model');
|
||||
});
|
||||
|
||||
it('should accept provider spec object', () => {
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.9.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.7.9.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.7.9 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.8.2 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ def __getattr__(name):
|
||||
from .registry import parse_model_string
|
||||
_lazy_cache[name] = parse_model_string
|
||||
return parse_model_string
|
||||
elif name == "_reset_default_registry":
|
||||
from .registry import _reset_default_registry
|
||||
_lazy_cache[name] = _reset_default_registry
|
||||
return _reset_default_registry
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
@@ -77,9 +77,17 @@ class LLMProviderRegistry:
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
normalized_alias = alias.lower()
|
||||
# Check collision with existing provider name
|
||||
if normalized_alias in self._providers and not override:
|
||||
raise ValueError(
|
||||
f"Alias '{alias}' conflicts with existing provider. "
|
||||
f"Alias '{alias}' conflicts with existing provider name. "
|
||||
f"Use override=True to replace it."
|
||||
)
|
||||
# Check collision with existing alias
|
||||
if normalized_alias in self._aliases and not override:
|
||||
existing_target = self._aliases[normalized_alias]
|
||||
raise ValueError(
|
||||
f"Alias '{alias}' is already registered (points to '{existing_target}'). "
|
||||
f"Use override=True to replace it."
|
||||
)
|
||||
self._aliases[normalized_alias] = normalized_name
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.7.9"
|
||||
__version__ = "2.8.2"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.5.1",
|
||||
"praisonaiagents>=0.5.2",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -308,3 +308,177 @@ class TestCreateLLMProvider:
|
||||
# Should not be in default registry
|
||||
with pytest.raises(ValueError):
|
||||
create_llm_provider("isolated/model")
|
||||
|
||||
|
||||
class TestCollisionDetection:
|
||||
"""Tests for collision detection and error messages."""
|
||||
|
||||
def test_alias_collision_with_existing_alias(self):
|
||||
"""Should raise error when alias collides with existing alias."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("provider-a", MockLLMProvider, aliases=["shared-alias"])
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
registry.register("provider-b", CustomCloudflareProvider, aliases=["shared-alias"])
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "shared-alias" in error_msg
|
||||
assert "already registered" in error_msg
|
||||
assert "provider-a" in error_msg # Should mention the existing target
|
||||
|
||||
def test_alias_collision_with_provider_name(self):
|
||||
"""Should raise error when alias collides with provider name."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("existing-provider", MockLLMProvider)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
registry.register("new-provider", CustomCloudflareProvider, aliases=["existing-provider"])
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "existing-provider" in error_msg
|
||||
assert "conflicts" in error_msg
|
||||
|
||||
def test_alias_override_with_flag(self):
|
||||
"""Should allow alias override with override=True."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("provider-a", MockLLMProvider, aliases=["shared-alias"])
|
||||
registry.register("provider-b", CustomCloudflareProvider, aliases=["shared-alias"], override=True)
|
||||
|
||||
# Alias should now point to provider-b
|
||||
provider = registry.resolve("shared-alias", "model")
|
||||
assert provider.provider_id == "cloudflare"
|
||||
|
||||
def test_case_insensitive_collision(self):
|
||||
"""Should detect collision regardless of case."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("MyProvider", MockLLMProvider)
|
||||
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
registry.register("myprovider", CustomCloudflareProvider)
|
||||
|
||||
def test_error_message_includes_available_providers(self):
|
||||
"""Error message should list available providers."""
|
||||
from praisonai.llm.registry import LLMProviderRegistry
|
||||
|
||||
registry = LLMProviderRegistry()
|
||||
registry.register("openai", MockLLMProvider)
|
||||
registry.register("anthropic", MockLLMProvider)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
registry.resolve("unknown", "model")
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "unknown provider" in error_msg
|
||||
assert "openai" in error_msg
|
||||
assert "anthropic" in error_msg
|
||||
assert "register" in error_msg # Should suggest how to register
|
||||
|
||||
|
||||
class TestParseModelString:
|
||||
"""Tests for model string parsing."""
|
||||
|
||||
def test_parse_provider_model_format(self):
|
||||
"""Should parse provider/model format."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("openai/gpt-4o")
|
||||
assert result["provider_id"] == "openai"
|
||||
assert result["model_id"] == "gpt-4o"
|
||||
|
||||
def test_parse_gpt_model_defaults_to_openai(self):
|
||||
"""Should default gpt-* models to openai."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("gpt-4o-mini")
|
||||
assert result["provider_id"] == "openai"
|
||||
assert result["model_id"] == "gpt-4o-mini"
|
||||
|
||||
def test_parse_claude_model_defaults_to_anthropic(self):
|
||||
"""Should default claude-* models to anthropic."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("claude-3-5-sonnet")
|
||||
assert result["provider_id"] == "anthropic"
|
||||
assert result["model_id"] == "claude-3-5-sonnet"
|
||||
|
||||
def test_parse_gemini_model_defaults_to_google(self):
|
||||
"""Should default gemini-* models to google."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("gemini-2.0-flash")
|
||||
assert result["provider_id"] == "google"
|
||||
assert result["model_id"] == "gemini-2.0-flash"
|
||||
|
||||
def test_parse_o1_model_defaults_to_openai(self):
|
||||
"""Should default o1* models to openai."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("o1-preview")
|
||||
assert result["provider_id"] == "openai"
|
||||
assert result["model_id"] == "o1-preview"
|
||||
|
||||
def test_parse_unknown_model_defaults_to_openai(self):
|
||||
"""Should default unknown models to openai."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("llama-3.1-8b")
|
||||
assert result["provider_id"] == "openai"
|
||||
assert result["model_id"] == "llama-3.1-8b"
|
||||
|
||||
def test_parse_custom_provider_model(self):
|
||||
"""Should parse custom provider/model format."""
|
||||
from praisonai.llm.registry import parse_model_string
|
||||
|
||||
result = parse_model_string("cloudflare/workers-ai-model")
|
||||
assert result["provider_id"] == "cloudflare"
|
||||
assert result["model_id"] == "workers-ai-model"
|
||||
|
||||
|
||||
class TestLiteLLMIsolation:
|
||||
"""Tests to verify LiteLLM paths are not affected."""
|
||||
|
||||
def test_registry_does_not_import_litellm(self):
|
||||
"""Registry module should not import litellm."""
|
||||
import sys
|
||||
|
||||
# Clear any cached imports
|
||||
modules_before = set(sys.modules.keys())
|
||||
|
||||
# Import registry
|
||||
from praisonai.llm import registry
|
||||
|
||||
# Check litellm was not imported
|
||||
modules_after = set(sys.modules.keys())
|
||||
new_modules = modules_after - modules_before
|
||||
|
||||
litellm_modules = [m for m in new_modules if 'litellm' in m.lower()]
|
||||
assert len(litellm_modules) == 0, f"Registry imported litellm modules: {litellm_modules}"
|
||||
|
||||
def test_registry_only_imports_typing(self):
|
||||
"""Registry should only import from typing module."""
|
||||
import ast
|
||||
import inspect
|
||||
from praisonai.llm import registry
|
||||
|
||||
source = inspect.getsource(registry)
|
||||
tree = ast.parse(source)
|
||||
|
||||
imports = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
imports.append(alias.name)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
imports.append(node.module)
|
||||
|
||||
# Only typing should be imported
|
||||
assert imports == ['typing'], f"Unexpected imports: {imports}"
|
||||
Generated
+4
-4
@@ -4118,7 +4118,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.5.1" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.5.2" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4161,7 +4161,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4170,9 +4170,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/49/1534414aac662df69537784350c90f35f62dfd22950274a875d6924a63c3/praisonaiagents-0.5.1.tar.gz", hash = "sha256:f18cd248197e327a5ae595a9e4151bbc6b2030fcf51e188cc495ea869fb6dcc2", size = 774252, upload-time = "2025-12-29T02:13:02.645Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/e5/f0569d57595331894418c250cc40edb158a1476a4c7271448095ef3aa744/praisonaiagents-0.5.2.tar.gz", hash = "sha256:1d4f0616ca6a683b54edc84757ae9decaf14742acfa2398debdde65aef02ae32", size = 780477, upload-time = "2025-12-30T17:47:27.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/95/6bc3822fa9396be446155ed7461821a273f02d10fc8e0dfb8186a758f11e/praisonaiagents-0.5.1-py3-none-any.whl", hash = "sha256:cc9c524c7901ce1102a9580b78eb6cc71db6751c8dfe4566c102f4b93ab19a89", size = 721775, upload-time = "2025-12-29T02:13:00.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/7f/9b71b7c67a07e8a3085ab380a3fc2a8d69521543cae285ffb0c7462cebeb/praisonaiagents-0.5.2-py3-none-any.whl", hash = "sha256:a9fc263fe5664e3901ac2cee05e44a3cf0ea9e6fe732da786d89f2e862c8def1", size = 724488, upload-time = "2025-12-30T17:47:25.834Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user