379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
/**
|
|
* Multi-Perspective Code Review
|
|
*
|
|
* Demonstrates:
|
|
* - Dependency chain: generator produces code, three reviewers depend on it
|
|
* - Parallel execution: security, performance, and style reviewers run concurrently
|
|
* - Structured output: synthesizer returns a Zod-validated list of findings
|
|
* - Shared memory: each agent's output is automatically stored and injected
|
|
* into downstream agents' prompts by the framework
|
|
*
|
|
* Flow:
|
|
* generator → [security-reviewer, performance-reviewer, style-reviewer] (parallel) → synthesizer
|
|
*
|
|
* Run:
|
|
* npx tsx examples/patterns/multi-perspective-code-review.ts
|
|
*
|
|
* Prerequisites:
|
|
* If LLM_PROVIDER is unset, this example auto-selects the first available key
|
|
* in this fixed order: Gemini → Groq → OpenRouter → Anthropic.
|
|
* This precedence is this example's implementation choice for satisfying
|
|
* "default to whichever key is present".
|
|
* Override with LLM_PROVIDER=gemini|groq|openrouter|anthropic.
|
|
*
|
|
* Supported env vars:
|
|
* - Gemini: GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_AI_STUDIO_API_KEY
|
|
* - Groq: GROQ_API_KEY
|
|
* - OpenRouter: OPENROUTER_API_KEY
|
|
* - Anthropic: ANTHROPIC_API_KEY
|
|
*
|
|
* Anthropic support is kept for backward compatibility with the original
|
|
* example. It is not part of the free-provider path.
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
import { OpenMultiAgent } from '../../src/index.js'
|
|
import type { AgentConfig, OrchestratorEvent } from '../../src/types.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API spec to implement
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const API_SPEC = `POST /users endpoint that:
|
|
- Accepts JSON body with name (string, required), email (string, required), age (number, optional)
|
|
- Validates all fields
|
|
- Inserts into a PostgreSQL database
|
|
- Returns 201 with the created user or 400/500 on error`
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Structured output schema
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const ReviewFinding = z.object({
|
|
priority: z.enum(['critical', 'high', 'medium', 'low']),
|
|
category: z.enum(['security', 'performance', 'style']),
|
|
issue: z.string().describe('A concise description of the code review finding'),
|
|
fix_hint: z.string().describe('A short, actionable suggestion for fixing the issue'),
|
|
})
|
|
|
|
const ReviewFindings = z.array(ReviewFinding)
|
|
|
|
type ProviderId = 'anthropic' | 'gemini' | 'groq' | 'openrouter'
|
|
type ProviderConfig = Pick<AgentConfig, 'provider' | 'model' | 'apiKey' | 'baseURL'>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getGeminiApiKey(): string | undefined {
|
|
return (
|
|
process.env.GEMINI_API_KEY ??
|
|
process.env.GOOGLE_API_KEY ??
|
|
process.env.GOOGLE_AI_STUDIO_API_KEY
|
|
)
|
|
}
|
|
|
|
function inferProvider(): ProviderId {
|
|
if (getGeminiApiKey()) return 'gemini'
|
|
if (process.env.GROQ_API_KEY) return 'groq'
|
|
if (process.env.OPENROUTER_API_KEY) return 'openrouter'
|
|
if (process.env.ANTHROPIC_API_KEY) return 'anthropic'
|
|
|
|
throw new Error(
|
|
'No supported API key found. Set GEMINI_API_KEY / GOOGLE_API_KEY / GOOGLE_AI_STUDIO_API_KEY, ' +
|
|
'GROQ_API_KEY, OPENROUTER_API_KEY, or ANTHROPIC_API_KEY.',
|
|
)
|
|
}
|
|
|
|
function getSelectedProvider(): ProviderId {
|
|
const requested = process.env.LLM_PROVIDER?.trim().toLowerCase()
|
|
if (!requested) return inferProvider()
|
|
|
|
if (
|
|
requested === 'anthropic' ||
|
|
requested === 'gemini' ||
|
|
requested === 'groq' ||
|
|
requested === 'openrouter'
|
|
) {
|
|
return requested
|
|
}
|
|
|
|
throw new Error(
|
|
`Unsupported LLM_PROVIDER="${process.env.LLM_PROVIDER}". ` +
|
|
'Use one of: gemini, groq, openrouter, anthropic.',
|
|
)
|
|
}
|
|
|
|
function getProviderConfigs(provider: ProviderId): {
|
|
defaultModel: string
|
|
defaultProvider: 'anthropic' | 'gemini' | 'openai'
|
|
fast: ProviderConfig
|
|
strong: ProviderConfig
|
|
} {
|
|
switch (provider) {
|
|
case 'gemini': {
|
|
const apiKey = getGeminiApiKey()
|
|
if (!apiKey) {
|
|
throw new Error(
|
|
'LLM_PROVIDER=gemini requires GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_AI_STUDIO_API_KEY.',
|
|
)
|
|
}
|
|
|
|
return {
|
|
defaultModel: 'gemini-2.5-flash',
|
|
defaultProvider: 'gemini',
|
|
fast: {
|
|
provider: 'gemini',
|
|
model: 'gemini-2.5-flash',
|
|
apiKey,
|
|
},
|
|
strong: {
|
|
provider: 'gemini',
|
|
model: 'gemini-2.5-flash',
|
|
apiKey,
|
|
},
|
|
}
|
|
}
|
|
|
|
case 'groq': {
|
|
const apiKey = process.env.GROQ_API_KEY
|
|
if (!apiKey) {
|
|
throw new Error('LLM_PROVIDER=groq requires GROQ_API_KEY.')
|
|
}
|
|
|
|
return {
|
|
defaultModel: 'llama-3.3-70b-versatile',
|
|
defaultProvider: 'openai',
|
|
fast: {
|
|
provider: 'openai',
|
|
model: 'llama-3.3-70b-versatile',
|
|
apiKey,
|
|
baseURL: 'https://api.groq.com/openai/v1',
|
|
},
|
|
strong: {
|
|
provider: 'openai',
|
|
model: 'llama-3.3-70b-versatile',
|
|
apiKey,
|
|
baseURL: 'https://api.groq.com/openai/v1',
|
|
},
|
|
}
|
|
}
|
|
|
|
case 'openrouter': {
|
|
const apiKey = process.env.OPENROUTER_API_KEY
|
|
if (!apiKey) {
|
|
throw new Error('LLM_PROVIDER=openrouter requires OPENROUTER_API_KEY.')
|
|
}
|
|
|
|
return {
|
|
defaultModel: 'google/gemini-2.5-flash',
|
|
defaultProvider: 'openai',
|
|
fast: {
|
|
provider: 'openai',
|
|
model: 'google/gemini-2.5-flash',
|
|
apiKey,
|
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
},
|
|
strong: {
|
|
provider: 'openai',
|
|
model: 'google/gemini-2.5-flash',
|
|
apiKey,
|
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
},
|
|
}
|
|
}
|
|
|
|
case 'anthropic':
|
|
default:
|
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
throw new Error('LLM_PROVIDER=anthropic requires ANTHROPIC_API_KEY.')
|
|
}
|
|
|
|
return {
|
|
defaultModel: 'claude-sonnet-4-6',
|
|
defaultProvider: 'anthropic',
|
|
fast: {
|
|
provider: 'anthropic',
|
|
model: 'claude-sonnet-4-6',
|
|
},
|
|
strong: {
|
|
provider: 'anthropic',
|
|
model: 'claude-sonnet-4-6',
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
const selectedProvider = getSelectedProvider()
|
|
const providerConfigs = getProviderConfigs(selectedProvider)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Agents
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const generator: AgentConfig = {
|
|
name: 'generator',
|
|
...providerConfigs.fast,
|
|
systemPrompt: `You are a Node.js backend developer. Given an API spec, write a complete
|
|
Express route handler. Include imports, validation, database query, and error handling.
|
|
Output only the code, no explanation. Keep it under 80 lines.`,
|
|
maxTurns: 2,
|
|
}
|
|
|
|
const securityReviewer: AgentConfig = {
|
|
name: 'security-reviewer',
|
|
...providerConfigs.fast,
|
|
systemPrompt: `You are a security reviewer. Review the code provided in context and check
|
|
for OWASP top 10 vulnerabilities: SQL injection, XSS, broken authentication,
|
|
sensitive data exposure, etc. Write your findings as a markdown checklist.
|
|
Keep it to 150-200 words.`,
|
|
maxTurns: 2,
|
|
}
|
|
|
|
const performanceReviewer: AgentConfig = {
|
|
name: 'performance-reviewer',
|
|
...providerConfigs.fast,
|
|
systemPrompt: `You are a performance reviewer. Review the code provided in context and check
|
|
for N+1 queries, memory leaks, blocking calls, missing connection pooling, and
|
|
inefficient patterns. Write your findings as a markdown checklist.
|
|
Keep it to 150-200 words.`,
|
|
maxTurns: 2,
|
|
}
|
|
|
|
const styleReviewer: AgentConfig = {
|
|
name: 'style-reviewer',
|
|
...providerConfigs.fast,
|
|
systemPrompt: `You are a code style reviewer. Review the code provided in context and check
|
|
naming conventions, function structure, readability, error message clarity, and
|
|
consistency. Write your findings as a markdown checklist.
|
|
Keep it to 150-200 words.`,
|
|
maxTurns: 2,
|
|
}
|
|
|
|
const synthesizer: AgentConfig = {
|
|
name: 'synthesizer',
|
|
...providerConfigs.strong,
|
|
systemPrompt: `You are a lead engineer synthesizing code review feedback. Review all
|
|
the feedback and original code provided in context. Produce a deduplicated list of
|
|
code review findings as JSON.
|
|
|
|
Rules:
|
|
- Output ONLY a JSON array matching the provided schema.
|
|
- Merge overlapping reviewer comments into a single finding when they describe the same issue.
|
|
- Use category "security", "performance", or "style" only.
|
|
- Use priority "critical", "high", "medium", or "low" only.
|
|
- issue should describe the problem, not the fix.
|
|
- fix_hint should be specific and actionable.
|
|
- If the code looks clean, return an empty JSON array.`,
|
|
maxTurns: 2,
|
|
outputSchema: ReviewFindings,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Orchestrator + team
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function handleProgress(event: OrchestratorEvent): void {
|
|
if (event.type === 'task_start') {
|
|
console.log(` [START] ${event.task ?? '?'} → ${event.agent ?? '?'}`)
|
|
}
|
|
if (event.type === 'task_complete') {
|
|
const success = (event.data as { success?: boolean })?.success ?? true
|
|
console.log(` [DONE] ${event.task ?? '?'} (${success ? 'OK' : 'FAIL'})`)
|
|
}
|
|
}
|
|
|
|
const orchestrator = new OpenMultiAgent({
|
|
defaultModel: providerConfigs.defaultModel,
|
|
defaultProvider: providerConfigs.defaultProvider,
|
|
onProgress: handleProgress,
|
|
})
|
|
|
|
const team = orchestrator.createTeam('code-review-team', {
|
|
name: 'code-review-team',
|
|
agents: [generator, securityReviewer, performanceReviewer, styleReviewer, synthesizer],
|
|
sharedMemory: true,
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tasks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const tasks = [
|
|
{
|
|
title: 'Generate code',
|
|
description: `Write a Node.js Express route handler for this API spec:\n\n${API_SPEC}`,
|
|
assignee: 'generator',
|
|
},
|
|
{
|
|
title: 'Security review',
|
|
description: 'Review the generated code for security vulnerabilities.',
|
|
assignee: 'security-reviewer',
|
|
dependsOn: ['Generate code'],
|
|
},
|
|
{
|
|
title: 'Performance review',
|
|
description: 'Review the generated code for performance issues.',
|
|
assignee: 'performance-reviewer',
|
|
dependsOn: ['Generate code'],
|
|
},
|
|
{
|
|
title: 'Style review',
|
|
description: 'Review the generated code for style and readability.',
|
|
assignee: 'style-reviewer',
|
|
dependsOn: ['Generate code'],
|
|
},
|
|
{
|
|
title: 'Synthesize feedback',
|
|
description: 'Synthesize all review feedback and the original code into a unified, prioritized structured findings array.',
|
|
assignee: 'synthesizer',
|
|
dependsOn: ['Security review', 'Performance review', 'Style review'],
|
|
},
|
|
]
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Run
|
|
// ---------------------------------------------------------------------------
|
|
|
|
console.log('Multi-Perspective Code Review')
|
|
console.log('='.repeat(60))
|
|
console.log(`Provider: ${selectedProvider}`)
|
|
console.log(`Spec: ${API_SPEC.split('\n')[0]}`)
|
|
console.log('Pipeline: generator → 3 reviewers (parallel) → synthesizer')
|
|
console.log('='.repeat(60))
|
|
console.log()
|
|
|
|
const result = await orchestrator.runTasks(team, tasks)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Output
|
|
// ---------------------------------------------------------------------------
|
|
|
|
console.log('\n' + '='.repeat(60))
|
|
console.log(`Overall success: ${result.success}`)
|
|
console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
|
|
console.log()
|
|
|
|
for (const [name, r] of result.agentResults) {
|
|
const icon = r.success ? 'OK ' : 'FAIL'
|
|
const tokens = `in:${r.tokenUsage.input_tokens} out:${r.tokenUsage.output_tokens}`
|
|
console.log(` [${icon}] ${name.padEnd(22)} ${tokens}`)
|
|
}
|
|
|
|
const synthResult = result.agentResults.get('synthesizer')
|
|
if (synthResult?.structured) {
|
|
console.log('\n' + '='.repeat(60))
|
|
console.log('STRUCTURED REVIEW FINDINGS')
|
|
console.log('='.repeat(60))
|
|
console.log()
|
|
console.log(JSON.stringify(synthResult.structured, null, 2))
|
|
} else if (synthResult) {
|
|
console.log('\n' + '='.repeat(60))
|
|
console.log('SYNTHESIZER OUTPUT FAILED SCHEMA VALIDATION OR DID NOT PRODUCE VALID JSON')
|
|
console.log('='.repeat(60))
|
|
console.log()
|
|
console.log(synthResult.output.slice(0, 1200))
|
|
}
|
|
|
|
console.log('\nDone.')
|