diff --git a/autobot-frontend/src/components/autoresearch/ApprovalCard.vue b/autobot-frontend/src/components/autoresearch/ApprovalCard.vue new file mode 100644 index 000000000..e83482ee6 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/ApprovalCard.vue @@ -0,0 +1,85 @@ + + + + + + + + + + + Approval Required + + + + + + Baseline val_bpb: + {{ approval.metrics.baseline_val_bpb?.toFixed(4) ?? '---' }} + + + Result val_bpb: + {{ approval.metrics.result_val_bpb?.toFixed(4) ?? '---' }} + + + Improvement: + + {{ approval.metrics.improvement_pct.toFixed(2) }}% + + + + + + + Approve + + + Reject + + + + diff --git a/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue b/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue new file mode 100644 index 000000000..2ee78e4f0 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue @@ -0,0 +1,61 @@ + + + + + + + + + {{ stateLabel[experiment.state] ?? experiment.state }} + + + + {{ experiment.hypothesis || 'AutoResearch experiment' }} + + + + {{ experiment.result.val_bpb.toFixed(4) }} + + + + Details + + + diff --git a/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue new file mode 100644 index 000000000..973fc8862 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue @@ -0,0 +1,102 @@ + + + + + + + + + No experiments yet + + + + + + + {{ exp.state }} + + {{ formatTime(exp.created_at) }} + + + + {{ exp.hypothesis || 'No hypothesis' }} + + + + + val_bpb: {{ exp.result.val_bpb.toFixed(4) }} + + + {{ exp.result.wall_time_seconds.toFixed(0) }}s + + + {{ exp.result.tokens_per_second.toFixed(0) }} tok/s + + + + + emit('approve', sid, eid)" + @reject="(sid: string, eid: string) => emit('reject', sid, eid)" + /> + + + diff --git a/autobot-frontend/src/components/autoresearch/InsightsPanel.vue b/autobot-frontend/src/components/autoresearch/InsightsPanel.vue new file mode 100644 index 000000000..a06d5ab24 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/InsightsPanel.vue @@ -0,0 +1,79 @@ + + + + + + + + Experiment Insights + + + + + + Search + + + + + + No insights yet. Run experiments and trigger synthesis. + + + + + + + {{ (insight.confidence * 100).toFixed(0) }}% confidence + + + {{ insight.related_hyperparams.join(', ') }} + + + + {{ insight.statement }} + + + Based on {{ insight.supporting_experiments.length }} experiment(s) + + + + + diff --git a/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue b/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue new file mode 100644 index 000000000..bbbff3e5f --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue @@ -0,0 +1,135 @@ + + + + + + + + Prompt Optimizer + + + + + Target Agent + + + + Max Rounds + + + + Start Optimization + + + + + + + Status: {{ session.status }} + + + Round {{ session.rounds_completed }}/{{ session.max_rounds }} + + + Cancel + + + + Best score: {{ session.best_variant.final_score.toFixed(3) }} + + + + + + + + {{ variant.id.slice(0, 8) }} + Score: {{ variant.final_score.toFixed(3) }} + + + {{ variant.prompt_text.slice(0, 200) }}{{ variant.prompt_text.length > 200 ? '...' : '' }} + + + + + + + + Submit + + + + Review + + + + + diff --git a/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts b/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts new file mode 100644 index 000000000..af73c3799 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts @@ -0,0 +1,43 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ApprovalCard from '../ApprovalCard.vue' + +describe('ApprovalCard', () => { + const defaultProps = { + approval: { + sessionId: 's1', + experimentId: 'e1', + metrics: { + baseline_val_bpb: 5.0, + result_val_bpb: 4.5, + improvement_pct: 10.0, + }, + }, + } + + it('renders approval details', () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + expect(wrapper.text()).toContain('Approval Required') + expect(wrapper.text()).toContain('5.0000') + expect(wrapper.text()).toContain('4.5000') + expect(wrapper.text()).toContain('10.00%') + }) + + it('emits approve event on button click', async () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + await wrapper.find('button:first-of-type').trigger('click') + expect(wrapper.emitted('approve')).toBeTruthy() + expect(wrapper.emitted('approve')![0]).toEqual(['s1', 'e1']) + }) + + it('emits reject event on button click', async () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + const buttons = wrapper.findAll('button') + await buttons[1].trigger('click') + expect(wrapper.emitted('reject')).toBeTruthy() + }) +}) diff --git a/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts b/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts new file mode 100644 index 000000000..eef9274e8 --- /dev/null +++ b/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts @@ -0,0 +1,64 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useAutoResearch } from '../useAutoResearch' + +const mockGet = vi.fn() +const mockPost = vi.fn() + +vi.mock('../useApi', () => ({ + useApi: () => ({ + get: mockGet, + post: mockPost, + }), +})) + +describe('useAutoResearch', () => { + beforeEach(() => { + vi.clearAllMocks() + // Re-apply mocks (mockReset: true wipes vi.mock factories) + mockGet.mockReset() + mockPost.mockReset() + }) + + it('fetchExperiments populates experiments ref', async () => { + mockGet.mockResolvedValue({ + experiments: [{ id: 'e1', state: 'completed', hypothesis: 'test' }], + }) + + const { experiments, fetchExperiments } = useAutoResearch() + await fetchExperiments() + + expect(experiments.value).toHaveLength(1) + expect(experiments.value[0].id).toBe('e1') + }) + + it('fetchStats populates stats ref', async () => { + mockGet.mockResolvedValue({ + total_experiments: 10, + kept: 3, + best_val_bpb: 4.5, + }) + + const { stats, fetchStats } = useAutoResearch() + await fetchStats() + + expect(stats.value).not.toBeNull() + expect(stats.value!.total_experiments).toBe(10) + }) + + it('approveExperiment sends correct request', async () => { + mockPost.mockResolvedValue({}) + mockGet.mockResolvedValue({ approvals: [] }) + + const { approveExperiment } = useAutoResearch() + await approveExperiment('s1', 'e1') + + expect(mockPost).toHaveBeenCalledWith( + '/api/autoresearch/approvals/s1/e1', + { decision: 'approved' }, + ) + }) +}) diff --git a/autobot-frontend/src/composables/useAutoResearch.ts b/autobot-frontend/src/composables/useAutoResearch.ts new file mode 100644 index 000000000..49f342dde --- /dev/null +++ b/autobot-frontend/src/composables/useAutoResearch.ts @@ -0,0 +1,271 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { ref, type Ref } from 'vue' +import { useApi } from './useApi' + +// --- Types --- + +export interface ExperimentResult { + val_bpb: number | null + train_loss: number | null + val_loss: number | null + steps_completed: number + tokens_per_second: number | null + wall_time_seconds: number + error_message: string | null +} + +export interface Experiment { + id: string + hypothesis: string + description: string + state: string + hyperparams: Record + result: ExperimentResult | null + baseline_val_bpb: number | null + tags: string[] + created_at: number + started_at: number | null + completed_at: number | null +} + +export interface ExperimentStats { + total_experiments: number + completed: number + failed: number + kept: number + discarded: number + best_val_bpb: number | null + baseline_val_bpb: number | null + avg_wall_time: number + total_wall_time: number + improvement_trend: number[] +} + +export interface PromptVariant { + id: string + prompt_text: string + output: string + scores: Record + final_score: number + round_number: number + created_at: number +} + +export interface OptimizationSession { + id: string + status: string + rounds_completed: number + max_rounds: number + best_variant: PromptVariant | null + baseline_score: number + all_variants: PromptVariant[] +} + +export interface ApprovalRequest { + session_id: string + experiment_id: string + details: Record + requested_at: number + status: string +} + +export interface ExperimentInsight { + id: string + statement: string + confidence: number + supporting_experiments: string[] + related_hyperparams: string[] + synthesized_at: number + session_id: string | null +} + +// --- Composable --- + +export function useAutoResearch() { + const api = useApi() + + const experiments: Ref = ref([]) + const stats: Ref = ref(null) + const loading = ref(false) + const error: Ref = ref(null) + + const optimizerStatus: Ref = ref(null) + const variants: Ref = ref([]) + + const pendingApprovals: Ref = ref([]) + + const insights: Ref = ref([]) + + let pollTimer: ReturnType | null = null + + // --- Experiments --- + + async function fetchExperiments(params?: { + limit?: number + offset?: number + state?: string + }): Promise { + loading.value = true + error.value = null + try { + const query = new URLSearchParams() + if (params?.limit != null) query.set('limit', String(params.limit)) + if (params?.offset != null) query.set('offset', String(params.offset)) + if (params?.state) query.set('state', params.state) + const response = await api.get(`/api/autoresearch/experiments?${query}`) + experiments.value = response.experiments ?? [] + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } finally { + loading.value = false + } + } + + async function fetchStats(): Promise { + try { + const response = await api.get('/api/autoresearch/experiments/stats') + stats.value = response + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } + } + + // --- Prompt Optimizer --- + + async function fetchOptimizerStatus(): Promise { + try { + const response = await api.get('/api/autoresearch/prompt-optimizer/status') + optimizerStatus.value = response.session ?? null + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } + } + + async function startOptimization( + agentName: string, + maxRounds: number = 3, + ): Promise { + await api.post('/api/autoresearch/prompt-optimizer/start', { + agent_name: agentName, + max_rounds: maxRounds, + }) + await fetchOptimizerStatus() + } + + async function cancelOptimization(): Promise { + await api.post('/api/autoresearch/prompt-optimizer/cancel') + await fetchOptimizerStatus() + } + + async function fetchVariants(sessionId: string): Promise { + const response = await api.get( + `/api/autoresearch/prompt-optimizer/variants/${sessionId}`, + ) + variants.value = response.variants ?? [] + } + + async function scoreVariant( + variantId: string, + sessionId: string, + score: number, + comment: string = '', + ): Promise { + await api.post( + `/api/autoresearch/prompt-optimizer/variants/${variantId}/score?session_id=${sessionId}`, + { score, comment }, + ) + } + + // --- Approvals --- + + async function fetchPendingApprovals(): Promise { + const response = await api.get('/api/autoresearch/approvals/pending') + pendingApprovals.value = response.approvals ?? [] + } + + async function approveExperiment( + sessionId: string, + experimentId: string, + ): Promise { + await api.post( + `/api/autoresearch/approvals/${sessionId}/${experimentId}`, + { decision: 'approved' }, + ) + await fetchPendingApprovals() + } + + async function rejectExperiment( + sessionId: string, + experimentId: string, + ): Promise { + await api.post( + `/api/autoresearch/approvals/${sessionId}/${experimentId}`, + { decision: 'rejected' }, + ) + await fetchPendingApprovals() + } + + // --- Insights --- + + async function fetchInsights(minConfidence: number = 0): Promise { + const response = await api.get( + `/api/autoresearch/insights?min_confidence=${minConfidence}`, + ) + insights.value = response.insights ?? [] + } + + async function searchInsights(query: string): Promise { + const response = await api.get( + `/api/autoresearch/insights/search?q=${encodeURIComponent(query)}`, + ) + insights.value = response.insights ?? [] + } + + // --- Polling --- + + function startPolling(intervalMs: number = 10000): void { + stopPolling() + pollTimer = setInterval(async () => { + await Promise.all([ + fetchExperiments(), + fetchStats(), + fetchOptimizerStatus(), + fetchPendingApprovals(), + ]) + }, intervalMs) + } + + function stopPolling(): void { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + } + + return { + experiments, + stats, + loading, + error, + fetchExperiments, + fetchStats, + optimizerStatus, + startOptimization, + cancelOptimization, + variants, + fetchVariants, + scoreVariant, + pendingApprovals, + fetchPendingApprovals, + approveExperiment, + rejectExperiment, + insights, + fetchInsights, + searchInsights, + startPolling, + stopPolling, + } +} diff --git a/autobot-frontend/src/router/index.ts b/autobot-frontend/src/router/index.ts index e9ff90a23..e8bf786cc 100644 --- a/autobot-frontend/src/router/index.ts +++ b/autobot-frontend/src/router/index.ts @@ -538,6 +538,18 @@ const routes: RouteRecordRaw[] = [ requiresAuth: true } }, + // Issue #3201: AutoResearch Experiment Dashboard + { + path: '/experiments', + name: 'experiments', + component: () => import('@/views/ExperimentDashboard.vue'), + meta: { + title: 'Experiments', + icon: 'BeakerIcon', + description: 'AutoResearch experiment dashboard', + requiresAuth: true, + }, + }, // Issue #729: Infrastructure routes redirected to slm-admin // These routes are kept as redirects for backwards compatibility diff --git a/autobot-frontend/src/views/ExperimentDashboard.vue b/autobot-frontend/src/views/ExperimentDashboard.vue new file mode 100644 index 000000000..068484f09 --- /dev/null +++ b/autobot-frontend/src/views/ExperimentDashboard.vue @@ -0,0 +1,127 @@ + + + + + + + + Experiment Dashboard + + + + + {{ stats.total_experiments }} + Total Experiments + + + {{ stats.kept }} + Kept + + + {{ stats.discarded }} + Discarded + + + + {{ stats.best_val_bpb?.toFixed(4) ?? '---' }} + + Best val_bpb + + + + + + Loading experiments... + + + + + + + Experiment Timeline + + + + + + + + + + + +
+ {{ exp.hypothesis || 'No hypothesis' }} +
+ {{ insight.statement }} +
+ {{ variant.prompt_text.slice(0, 200) }}{{ variant.prompt_text.length > 200 ? '...' : '' }} +