@flatfile/improv
Version:
A powerful TypeScript library for building AI agents with multi-threaded conversations, tool execution, and event handling capabilities
653 lines (532 loc) • 17.7 kB
Markdown
# Agent vs Solo: Choosing the Right Pattern
## Overview
Improv provides two primary interfaces for interacting with LLMs:
1. **Solo**: For simple, one-off model calls with structured outputs
2. **Agent**: For complex, multi-step workflows with tool calling
## When to Use Solo
Solo is designed for **deterministic, single-response tasks** where you need:
- ✅ Classification into predefined categories
- ✅ Structured data extraction from text
- ✅ Simple transformations with type-safe outputs
- ✅ Retry and fallback mechanisms for reliability
- ✅ Consistent, predictable responses
### Solo Examples
```typescript
// Sentiment classification
const sentiment = await classify(
"I love this product!",
["positive", "negative", "neutral"],
driver
);
// Data extraction with schema
const PersonSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number()
});
const person = await extract(
"John Doe (john@example.com) is 30 years old",
PersonSchema,
driver
);
// Intent classification with detailed output
const solo = new Solo({
driver,
outputSchema: z.object({
intent: z.enum(["order_status", "complaint", "inquiry"]),
confidence: z.number(),
entities: z.record(z.string())
})
});
const result = await solo.ask("Where is my order #12345?");
```
### Solo Features
- **Structured Output**: Use Zod schemas to ensure type-safe responses
- **Retry Logic**: Automatic retries with exponential backoff
- **Fallback Drivers**: Switch to backup models if primary fails
- **Low Temperature**: Optimized for consistent, deterministic outputs
- **Schema Validation**: Runtime validation of LLM responses
## When to Use Agent
Agent is designed for **complex, iterative workflows** where you need:
- ✅ Tool calling and execution
- ✅ Multi-step reasoning processes
- ✅ Dynamic decision making
- ✅ Conversation state management
- ✅ Completion detection through evaluators
### Agent Examples
```typescript
// Agent with tools and evaluators
const agent = new Agent({
tools: [databaseTool, calculatorTool, emailTool],
evaluators: [threeKeyedLockEvaluator()],
driver
});
const thread = agent.createThread({
prompt: "Process all pending orders and send confirmation emails"
});
await thread.send(); // Will loop through tools until complete
```
### Agent Features
- **Tool Execution**: Define and execute custom tools
- **Evaluators**: Control flow and completion detection
- **Thread Management**: Maintain conversation state
- **Knowledge & Instructions**: Provide context and behavior rules
- **Memory System**: Persist and recall past conversations
## Decision Matrix
| Use Case | Solo | Agent |
|----------|------|-------|
| Classify customer sentiment | ✅ | ❌ |
| Extract entities from text | ✅ | ❌ |
| Generate structured JSON | ✅ | ❌ |
| Process multiple items with tools | ❌ | ✅ |
| Research task requiring web search | ❌ | ✅ |
| Multi-step workflow automation | ❌ | ✅ |
| Simple Q&A with consistent format | ✅ | ❌ |
| Complex reasoning with iterations | ❌ | ✅ |
## Complementary Patterns
### 1. **Agent as Orchestra, Solo as Instruments**
Use Agent for complex workflow orchestration while Solo handles specific, isolated tasks.
```typescript
import { Agent, Solo, OpenAIThreadDriver } from 'improv';
class ContentModerationSystem {
private agent: Agent;
private classifierSolo: Solo;
private summarizerSolo: Solo;
constructor(driver: ThreadDriver) {
// Agent handles the overall moderation workflow
this.agent = new Agent({
driver,
systemPrompt: "You are a content moderation coordinator. Use tools to analyze and make decisions about content.",
tools: [
this.createClassificationTool(),
this.createSummaryTool(),
this.createDecisionTool()
]
});
// Solo instances for specific classification tasks
this.classifierSolo = new Solo({
driver,
systemPrompt: "Classify content as: safe, questionable, harmful, spam. Respond with only the classification."
});
this.summarizerSolo = new Solo({
driver,
systemPrompt: "Summarize content in exactly one sentence focusing on key concerns for moderation."
});
}
private createClassificationTool() {
return {
name: "classify_content",
description: "Classify content using specialized model",
parameters: z.object({ content: z.string() }),
executeTool: async ({ content }) => {
// Solo handles the specialized classification
const result = await this.classifierSolo.ask(content);
return { classification: result.content.trim() };
}
};
}
}
```
### 2. **Agent for Conversations, Solo for Analysis**
Agent maintains conversation state while Solo performs stateless analysis tasks.
```typescript
class CustomerSupportSystem {
private conversationAgent: Agent;
private sentimentSolo: Solo;
private urgencySolo: Solo;
async handleCustomerMessage(conversationId: string, message: string) {
// Solo quickly analyzes the incoming message
const [sentiment, urgency] = await Promise.all([
this.sentimentSolo.ask(`Analyze sentiment of: "${message}"`),
this.urgencySolo.ask(`Rate urgency 1-10 for: "${message}"`)
]);
// Agent handles the conversation with enriched context
const agent = this.getOrCreateAgent(conversationId);
agent.addKnowledge({
fact: `Current message sentiment: ${sentiment.content}`,
source: "sentiment_analysis"
});
const thread = agent.createThread({ prompt: message });
return thread.send();
}
}
```
### 3. **Agent with Solo-Powered Tools**
Tools within an Agent can use Solo for their own LLM operations.
```typescript
class ResearchAgent {
constructor(driver: ThreadDriver) {
const webSearchSolo = new Solo({
driver,
systemPrompt: "Extract key information from search results. Be concise and factual."
});
const agent = new Agent({
driver,
tools: [{
name: "web_search",
description: "Search the web and summarize results",
parameters: z.object({ query: z.string() }),
executeTool: async ({ query }) => {
// Fetch search results (pseudo-code)
const searchResults = await fetchSearchResults(query);
// Use Solo to analyze and summarize
const analysis = await webSearchSolo.ask(
`Summarize these search results for query "${query}":\n${searchResults}`
);
return { summary: analysis.content, source: "web_search" };
}
}]
});
}
}
```
### 4. **Microservice Architecture**
Different services use Solo for their specific LLM needs while a main service uses Agent for orchestration.
```typescript
// Email Service
class EmailService {
private subjectSolo: Solo;
private bodySolo: Solo;
async generateEmail(context: EmailContext) {
const [subject, body] = await Promise.all([
this.subjectSolo.ask(`Generate email subject for: ${context.purpose}`),
this.bodySolo.ask(`Generate email body for: ${JSON.stringify(context)}`)
]);
return { subject: subject.content, body: body.content };
}
}
// Main Orchestration Service
class WorkflowService {
private orchestrationAgent: Agent;
private emailService: EmailService;
async processWorkflow(workflowData: any) {
// Agent coordinates the overall workflow
const thread = this.orchestrationAgent.createThread({
prompt: `Process this workflow: ${JSON.stringify(workflowData)}`
});
// Agent can call tools that internally use other services' Solo instances
return thread.send();
}
}
```
## Implementation Patterns
### Solo Pattern: Classification Pipeline
```typescript
// Create a reusable classifier
class CustomerServiceClassifier {
private solo: Solo<{
category: string;
urgency: "low" | "medium" | "high";
requiresHuman: boolean;
}>;
constructor(driver: ThreadDriver) {
this.solo = new Solo({
driver,
outputSchema: z.object({
category: z.string(),
urgency: z.enum(["low", "medium", "high"]),
requiresHuman: z.boolean()
}),
retry: {
maxAttempts: 3,
exponentialBackoff: true
},
temperature: 0.1 // Low for consistency
});
}
async classify(message: string) {
return await this.solo.ask(message);
}
}
```
### Agent Pattern: Workflow Automation
```typescript
// Create a workflow agent
class OrderProcessingAgent extends Agent {
constructor(driver: ThreadDriver) {
super({
driver,
tools: [
databaseQueryTool,
inventoryCheckTool,
paymentProcessTool,
emailSenderTool
],
instructions: [
{
instruction: "Process orders in chronological order",
priority: 1
},
{
instruction: "Check inventory before confirming payment",
priority: 2
}
],
evaluators: [
threeKeyedLockEvaluator({
evalPrompt: "Are there more orders to process?",
exitPrompt: "Summarize all processed orders"
})
]
});
}
}
## Best Practices
### Solo Best Practices
1. **Use Specific Schemas**: Define precise Zod schemas for expected outputs
2. **Set Low Temperature**: Use 0.1-0.3 for classification tasks
3. **Implement Fallbacks**: Have backup drivers for critical operations
4. **Cache Results**: Solo outputs are deterministic and cacheable
5. **Batch Similar Requests**: Process multiple classifications together
### Agent Best Practices
1. **Define Clear Tools**: Each tool should have a single responsibility
2. **Use Evaluators**: Implement proper completion detection
3. **Manage State**: Track progress through agent memory
4. **Set Appropriate Limits**: Configure maxSteps to prevent infinite loops
5. **Monitor Events**: Subscribe to agent events for debugging
## Migration Guide
### From Basic LLM Calls to Solo
Before:
```typescript
// Direct API call with manual parsing
const response = await openai.chat.completions.create({
messages: [{ role: "user", content: "Classify: " + text }],
model: "gpt-4"
});
const result = JSON.parse(response.choices[0].message.content);
```
After:
```typescript
// Type-safe with automatic retry
const result = await classify(
text,
["category1", "category2", "category3"],
driver
);
```
### From Complex Scripts to Agent
Before:
```typescript
// Manual orchestration
async function processOrders() {
const orders = await getOrders();
for (const order of orders) {
const inventory = await checkInventory(order);
if (inventory.available) {
await processPayment(order);
await sendEmail(order);
}
}
}
```
After:
```typescript
// Agent with tools and evaluators
const agent = new OrderAgent({ driver, tools: [...] });
const thread = agent.createThread({
prompt: "Process all pending orders"
});
await thread.send(); // Handles all logic internally
```
## Performance Considerations
### Solo Performance
- **Latency**: Single API call (plus retries if needed)
- **Token Usage**: Minimal - only classification/extraction prompts
- **Caching**: Responses are deterministic and cacheable
- **Concurrency**: Can process many classifications in parallel
### Agent Performance
- **Latency**: Multiple API calls for tool execution
- **Token Usage**: Higher due to conversation context
- **State Management**: Maintains thread history
- **Concurrency**: Limited by stateful nature of conversations
## Solo Optimizations
### 1. **Driver Reuse and Connection Pooling**
```typescript
class OptimizedSoloPool {
private drivers: Map<string, ThreadDriver> = new Map();
private soloInstances: Map<string, Solo> = new Map();
getOrCreateSolo(purpose: string, systemPrompt: string): Solo {
const key = `${purpose}-${hashString(systemPrompt)}`;
if (!this.soloInstances.has(key)) {
const driver = this.getOrCreateDriver(purpose);
this.soloInstances.set(key, new Solo({ driver, systemPrompt }));
}
return this.soloInstances.get(key)!;
}
private getOrCreateDriver(purpose: string): ThreadDriver {
if (!this.drivers.has(purpose)) {
// Optimize driver settings based on purpose
const config = this.getOptimalConfig(purpose);
this.drivers.set(purpose, new OpenAIThreadDriver(config));
}
return this.drivers.get(purpose)!;
}
private getOptimalConfig(purpose: string): OpenAIConfig {
switch (purpose) {
case 'classification':
return { model: 'gpt-4o-mini', temperature: 0.1, maxTokens: 50 };
case 'creative':
return { model: 'gpt-4o', temperature: 0.9, maxTokens: 1000 };
case 'analysis':
return { model: 'gpt-4o', temperature: 0.3, maxTokens: 500 };
default:
return { model: 'gpt-4o-mini', temperature: 0.7 };
}
}
}
```
### 2. **Caching for Similar Requests**
```typescript
class CachedSolo {
private cache = new Map<string, { response: string; timestamp: number }>();
private solo: Solo;
private cacheTTL = 5 * 60 * 1000; // 5 minutes
constructor(options: SoloOptions) {
this.solo = new Solo(options);
}
async ask(prompt: string): Promise<string> {
const cacheKey = this.hashPrompt(prompt);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.response;
}
const response = await this.solo.ask(prompt);
this.cache.set(cacheKey, {
response: response.content,
timestamp: Date.now()
});
return response.content;
}
private hashPrompt(prompt: string): string {
// Simple hash function - use crypto in production
return btoa(prompt).slice(0, 32);
}
}
```
### 3. **Batch Processing Optimization**
```typescript
class BatchSolo {
private solo: Solo;
private batchSize = 10;
private processingQueue: Array<{
prompt: string;
resolve: (value: string) => void;
reject: (error: Error) => void;
}> = [];
async ask(prompt: string): Promise<string> {
return new Promise((resolve, reject) => {
this.processingQueue.push({ prompt, resolve, reject });
this.processBatch();
});
}
private async processBatch() {
if (this.processingQueue.length < this.batchSize) {
return; // Wait for more items
}
const batch = this.processingQueue.splice(0, this.batchSize);
// Process batch items in parallel
const promises = batch.map(async ({ prompt, resolve, reject }) => {
try {
const response = await this.solo.ask(prompt);
resolve(response.content);
} catch (error) {
reject(error as Error);
}
});
await Promise.all(promises);
}
}
```
### 4. **Specialized Solo Variants**
```typescript
// Ultra-fast classification
class QuickClassifierSolo extends Solo {
constructor(driver: ThreadDriver, categories: string[]) {
super({
driver,
systemPrompt: `Classify input as one of: ${categories.join(', ')}.
Respond with ONLY the category name, nothing else.`,
maxSteps: 1 // No tool calling needed
});
}
async classify(text: string): Promise<string> {
const response = await this.ask(text);
return response.content.trim().toLowerCase();
}
}
// Streaming content generator
class StreamingContentSolo extends Solo {
constructor(driver: ThreadDriver, contentType: string) {
super({
driver,
systemPrompt: `Generate ${contentType} content. Be creative and engaging.`
});
}
async *generateContent(prompt: string): AsyncGenerator<string, void> {
yield* this.stream(prompt);
}
}
// JSON-only response Solo
class JSONSolo extends Solo {
constructor(driver: ThreadDriver, schema: string) {
super({
driver,
systemPrompt: `Always respond with valid JSON matching this schema: ${schema}.
Never include explanations, only JSON.`
});
}
async askJSON<T>(prompt: string): Promise<T> {
const response = await this.ask(prompt);
try {
return JSON.parse(response.content);
} catch (error) {
throw new Error(`Invalid JSON response: ${response.content}`);
}
}
}
```
### 5. **Performance Monitoring**
```typescript
class MonitoredSolo extends Solo {
private metrics = {
totalCalls: 0,
averageLatency: 0,
errorRate: 0,
cacheHitRate: 0
};
async ask(prompt: string): Promise<SoloResponse> {
const startTime = Date.now();
this.metrics.totalCalls++;
try {
const response = await super.ask(prompt);
// Update metrics
const latency = Date.now() - startTime;
this.updateLatencyMetric(latency);
return response;
} catch (error) {
this.metrics.errorRate = this.calculateErrorRate();
throw error;
}
}
getMetrics() {
return { ...this.metrics };
}
private updateLatencyMetric(latency: number) {
this.metrics.averageLatency =
(this.metrics.averageLatency * (this.metrics.totalCalls - 1) + latency) /
this.metrics.totalCalls;
}
}
```
## Summary
Choose **Solo** when you need:
- Simple, deterministic outputs
- Type-safe structured data
- High reliability with retries
- Consistent classification results
Choose **Agent** when you need:
- Complex multi-step workflows
- Dynamic tool execution
- Iterative problem solving
- Stateful conversations
Both patterns can be combined - use Solo for classification steps within Agent workflows for the best of both worlds.