feat: agent variants — migration, API, service integration, frontend, tests
- Migration 027: agent_variants table with single-active enforcement, variant_id column on agent_performance_log - API: full CRUD, clone from agent/variant, activate/deactivate, per-variant performance metrics and history endpoints - Services: extractor, event classifier, thesis rewriter all wired to AgentConfigResolver with variant override support - Frontend: variant list, comparison view, create/edit/clone forms, activate/delete actions on Agents page - Tests: API tests + 5 property-based tests (single-active invariant, clone preservation, config resolution, slug determinism, update idempotence) - Spec files for agent-variants feature
This commit is contained in:
@@ -54,6 +54,25 @@ export const mockCorporateDecisions = [
|
||||
{ catalyst_type: 'm_and_a', date: '2026-03-15T00:00:00Z', summary: 'Acquisition of AI startup for $2B', trend_direction: 'bullish', trend_strength: 0.7, sample_count: 5, pattern_confidence: 0.68, document_id: 'd1' },
|
||||
];
|
||||
|
||||
export const mockAgents = [
|
||||
{ id: 'agent-1', name: 'Document Extractor', slug: 'document-extractor', purpose: 'Extract structured data from documents', model_provider: 'ollama', model_name: 'llama3.1:8b', system_prompt: 'You are a document extractor.', user_prompt_template: 'Extract from: {doc}', prompt_version: 'v1', schema_version: '1.0', temperature: 0.0, max_tokens: 32768, timeout_seconds: 120, max_retries: 2, active: true, source: 'system', created_at: '2026-04-01T00:00:00Z', updated_at: '2026-04-01T00:00:00Z' },
|
||||
{ id: 'agent-2', name: 'Event Classifier', slug: 'event-classifier', purpose: 'Classify global events', model_provider: 'ollama', model_name: 'llama3.1:8b', system_prompt: 'You classify events.', user_prompt_template: 'Classify: {event}', prompt_version: 'v1', schema_version: '1.0', temperature: 0.1, max_tokens: 16384, timeout_seconds: 60, max_retries: 3, active: true, source: 'system', created_at: '2026-04-02T00:00:00Z', updated_at: '2026-04-02T00:00:00Z' },
|
||||
];
|
||||
|
||||
export const mockAgentVariants = [
|
||||
{ id: 'var-1', agent_id: 'agent-1', variant_name: 'GPT-4o Variant', variant_slug: 'gpt-4o-variant', description: 'Uses GPT-4o for extraction', model_provider: 'openai', model_name: 'gpt-4o', system_prompt: 'You are a document extractor.', user_prompt_template: 'Extract from: {doc}', prompt_version: 'v2', temperature: 0.2, max_tokens: 65536, context_window: 128000, input_token_limit: 100000, token_budget: 500000, timeout_seconds: 90, max_retries: 3, is_active: true, created_at: '2026-04-05T00:00:00Z', updated_at: '2026-04-05T00:00:00Z' },
|
||||
{ id: 'var-2', agent_id: 'agent-1', variant_name: 'Mistral Variant', variant_slug: 'mistral-variant', description: 'Uses Mistral for extraction', model_provider: 'ollama', model_name: 'mistral:7b', system_prompt: 'You are a document extractor.', user_prompt_template: 'Extract from: {doc}', prompt_version: 'v1', temperature: 0.0, max_tokens: 32768, context_window: 0, input_token_limit: 0, token_budget: 0, timeout_seconds: 120, max_retries: 2, is_active: false, created_at: '2026-04-06T00:00:00Z', updated_at: '2026-04-06T00:00:00Z' },
|
||||
];
|
||||
|
||||
export const mockVariantPerformance = {
|
||||
total_invocations: 50, successes: 45, failures: 5, avg_duration_ms: 1200, p95_duration_ms: 2500, avg_confidence: 0.85, avg_retries: 0.3, total_input_tokens: 50000, total_output_tokens: 15000, success_rate: 0.9,
|
||||
};
|
||||
|
||||
export const mockVariantPerfHistory = [
|
||||
{ hour: '2026-04-10T10:00:00Z', invocations: 10, successes: 9, avg_duration_ms: 1100, avg_confidence: 0.88 },
|
||||
{ hour: '2026-04-10T11:00:00Z', invocations: 12, successes: 11, avg_duration_ms: 1300, avg_confidence: 0.82 },
|
||||
];
|
||||
|
||||
export const handlers = [
|
||||
// Query API (proxied at /api/)
|
||||
http.get('/api/companies', () => HttpResponse.json(mockCompanies)),
|
||||
@@ -142,6 +161,54 @@ export const handlers = [
|
||||
}),
|
||||
http.get('/api/trends/:id/projection', () => HttpResponse.json(mockTrendProjection)),
|
||||
|
||||
// Agents
|
||||
http.get('/api/agents', () => HttpResponse.json(mockAgents)),
|
||||
http.get('/api/agents/:agent_id', ({ params }) => {
|
||||
const a = mockAgents.find((a) => a.id === params.agent_id);
|
||||
return a ? HttpResponse.json(a) : new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
http.get('/api/agents/:agent_id/performance', () => HttpResponse.json(mockVariantPerformance)),
|
||||
http.get('/api/agents/:agent_id/performance/history', () => HttpResponse.json(mockVariantPerfHistory)),
|
||||
|
||||
// Agent Variants
|
||||
http.get('/api/agents/:agent_id/variants', ({ params }) =>
|
||||
HttpResponse.json(mockAgentVariants.filter((v) => v.agent_id === params.agent_id)),
|
||||
),
|
||||
http.get('/api/agents/:agent_id/variants/:variant_id', ({ params }) => {
|
||||
const v = mockAgentVariants.find((v) => v.id === params.variant_id && v.agent_id === params.agent_id);
|
||||
return v ? HttpResponse.json(v) : new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
http.post('/api/agents/:agent_id/variants', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ id: 'var-new', agent_id: params.agent_id, variant_name: body.variant_name, variant_slug: body.variant_slug ?? 'new-variant', is_active: false, ...body, created_at: '2026-04-10T00:00:00Z', updated_at: '2026-04-10T00:00:00Z' }, { status: 201 });
|
||||
}),
|
||||
http.post('/api/agents/:agent_id/clone', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ id: 'var-cloned', agent_id: params.agent_id, variant_name: body.variant_name, variant_slug: 'cloned-variant', is_active: false, ...body, created_at: '2026-04-10T00:00:00Z', updated_at: '2026-04-10T00:00:00Z' }, { status: 201 });
|
||||
}),
|
||||
http.post('/api/agents/:agent_id/variants/:variant_id/clone', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ id: 'var-clone2', agent_id: params.agent_id, variant_name: body.variant_name, variant_slug: 'clone2', is_active: false, ...body, created_at: '2026-04-10T00:00:00Z', updated_at: '2026-04-10T00:00:00Z' }, { status: 201 });
|
||||
}),
|
||||
http.put('/api/agents/:agent_id/variants/:variant_id', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const v = mockAgentVariants.find((v) => v.id === params.variant_id);
|
||||
return v ? HttpResponse.json({ ...v, ...body, updated_at: '2026-04-10T12:00:00Z' }) : new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
http.delete('/api/agents/:agent_id/variants/:variant_id', ({ params }) => {
|
||||
const v = mockAgentVariants.find((v) => v.id === params.variant_id);
|
||||
if (!v) return new HttpResponse(null, { status: 404 });
|
||||
if (v.is_active) return HttpResponse.json({ detail: 'Cannot delete active variant' }, { status: 400 });
|
||||
return HttpResponse.json({ status: 'deleted' });
|
||||
}),
|
||||
http.post('/api/agents/:agent_id/variants/:variant_id/activate', ({ params }) => {
|
||||
const v = mockAgentVariants.find((v) => v.id === params.variant_id);
|
||||
return v ? HttpResponse.json({ ...v, is_active: true }) : new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
http.post('/api/agents/:agent_id/variants/deactivate', () => HttpResponse.json({ deactivated: true })),
|
||||
http.get('/api/agents/:agent_id/variants/:variant_id/performance', () => HttpResponse.json(mockVariantPerformance)),
|
||||
http.get('/api/agents/:agent_id/variants/:variant_id/performance/history', () => HttpResponse.json(mockVariantPerfHistory)),
|
||||
|
||||
// Competitive intelligence endpoints
|
||||
http.get('/registry/companies/:id/competitors', () => HttpResponse.json(mockCompetitors)),
|
||||
http.post('/registry/companies/:id/competitors/infer', () => HttpResponse.json(mockCompetitors)),
|
||||
|
||||
@@ -168,3 +168,43 @@ describe('Global Events page', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agents page', () => {
|
||||
it('renders agent list in sidebar', async () => {
|
||||
renderRoute('/agents');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Document Extractor')).toBeInTheDocument();
|
||||
expect(screen.getByText('Event Classifier')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders variant list when an agent is selected', async () => {
|
||||
renderRoute('/agents');
|
||||
await waitFor(() => expect(screen.getByText('Document Extractor')).toBeInTheDocument());
|
||||
|
||||
await userEvent.click(screen.getByText('Document Extractor'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GPT-4o Variant')).toBeInTheDocument();
|
||||
expect(screen.getByText('Mistral Variant')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows comparison view when multiple variants are checked', async () => {
|
||||
renderRoute('/agents');
|
||||
await waitFor(() => expect(screen.getByText('Document Extractor')).toBeInTheDocument());
|
||||
|
||||
await userEvent.click(screen.getByText('Document Extractor'));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('GPT-4o Variant')).toBeInTheDocument());
|
||||
|
||||
// Select both variant checkboxes
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
await userEvent.click(checkboxes[0]);
|
||||
await userEvent.click(checkboxes[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Variant Comparison/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user