feat: implement API client and SSE connection for trading agents

- Added api-client module with functions for creating and listing runs, and fetching settings.
- Introduced SSE connection handling for real-time updates with event listeners for agent and run events.
- Created types for agents and runs to support the new API structure.
- Updated .gitignore to ensure proper exclusion of Next.js library directories.

Made-with: Cursor
This commit is contained in:
Ali AL OGAILI 2026-03-23 05:43:00 +01:00
parent 0690f628ab
commit 723069b958
7 changed files with 191 additions and 2 deletions

5
.gitignore vendored
View File

@ -17,8 +17,9 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
# Root-only: unanchored `lib/` would ignore Next.js `ui/lib/`
/lib/
/lib64/
parts/
sdist/
var/

View File

@ -0,0 +1,28 @@
import { createRun, listRuns, getSettings } from '@/lib/api-client'
global.fetch = jest.fn()
beforeEach(() => jest.clearAllMocks())
test('createRun POSTs to /api/runs and returns run summary', async () => {
const mockRun = { id: 'abc123', ticker: 'NVDA', status: 'queued' }
;(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockRun,
})
const result = await createRun({
ticker: 'NVDA', date: '2024-05-10',
llm_provider: 'openai', deep_think_llm: 'gpt-5.2',
quick_think_llm: 'gpt-5-mini', max_debate_rounds: 1,
max_risk_discuss_rounds: 1,
})
expect(fetch).toHaveBeenCalledWith('/api/runs', expect.objectContaining({ method: 'POST' }))
expect(result.id).toBe('abc123')
})
test('listRuns GETs /api/runs', async () => {
;(fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => [] })
const result = await listRuns()
expect(fetch).toHaveBeenCalledWith('/api/runs')
expect(Array.isArray(result)).toBe(true)
})

33
ui/lib/api-client.ts Normal file
View File

@ -0,0 +1,33 @@
import type { RunConfig, RunSummary } from './types/run'
import type { Settings } from './types/settings'
const API = process.env.NEXT_PUBLIC_API_URL ?? ''
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = init
? await fetch(`${API}${path}`, {
headers: { 'Content-Type': 'application/json' },
...init,
})
: await fetch(`${API}${path}`)
if (!res.ok) throw new Error(`API error ${res.status}: ${path}`)
return res.json() as Promise<T>
}
export const createRun = (config: RunConfig): Promise<RunSummary> =>
apiFetch('/api/runs', { method: 'POST', body: JSON.stringify(config) })
export const listRuns = (): Promise<RunSummary[]> =>
apiFetch('/api/runs')
export const getRun = (id: string): Promise<RunSummary> =>
apiFetch(`/api/runs/${id}`)
export const getSettings = (): Promise<Settings> =>
apiFetch('/api/settings')
export const updateSettings = (settings: Settings): Promise<Settings> =>
apiFetch('/api/settings', { method: 'PUT', body: JSON.stringify(settings) })
export const getRunStreamUrl = (id: string): string =>
`${process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'}/api/runs/${id}/stream`

42
ui/lib/sse.ts Normal file
View File

@ -0,0 +1,42 @@
export type SSEHandlers = {
onAgentStart?: (data: { step: string; turn: number }) => void
onAgentComplete?: (data: { step: string; turn: number; report: string }) => void
onRunComplete?: (data: { decision: string; run_id: string }) => void
onRunError?: (data: { message: string }) => void
onOpen?: () => void
}
export function createSSEConnection(url: string, handlers: SSEHandlers): () => void {
const source = new EventSource(url)
source.onopen = () => handlers.onOpen?.()
source.onerror = () => {
handlers.onRunError?.({ message: 'SSE connection error' })
source.close()
}
source.addEventListener('agent:start', (e: MessageEvent) => {
try { handlers.onAgentStart?.(JSON.parse(e.data)) }
catch { handlers.onRunError?.({ message: 'Failed to parse event data' }) }
})
source.addEventListener('agent:complete', (e: MessageEvent) => {
try { handlers.onAgentComplete?.(JSON.parse(e.data)) }
catch { handlers.onRunError?.({ message: 'Failed to parse event data' }) }
})
source.addEventListener('run:complete', (e: MessageEvent) => {
try { handlers.onRunComplete?.(JSON.parse(e.data)) }
catch { handlers.onRunError?.({ message: 'Failed to parse event data' }) }
source.close()
})
source.addEventListener('run:error', (e: MessageEvent) => {
try { handlers.onRunError?.(JSON.parse(e.data)) }
catch { handlers.onRunError?.({ message: 'Failed to parse event data' }) }
source.close()
})
return () => source.close()
}

6
ui/lib/types/agents.ts Normal file
View File

@ -0,0 +1,6 @@
import type { AgentStep } from './run'
export type Decision = 'BUY' | 'SELL' | 'HOLD'
export type DebateTurn = { speaker: string; text: string }
export type PhaseReport = { step: AgentStep; content: string }
export type StepStatus = 'pending' | 'running' | 'done'

72
ui/lib/types/run.ts Normal file
View File

@ -0,0 +1,72 @@
export type AgentStep =
| 'market_analyst'
| 'news_analyst'
| 'fundamentals_analyst'
| 'social_analyst'
| 'bull_researcher'
| 'bear_researcher'
| 'research_manager'
| 'trader'
| 'aggressive_analyst'
| 'conservative_analyst'
| 'neutral_analyst'
| 'risk_judge'
export const AGENT_STEPS: AgentStep[] = [
'market_analyst', 'news_analyst', 'fundamentals_analyst', 'social_analyst',
'bull_researcher', 'bear_researcher', 'research_manager',
'trader',
'aggressive_analyst', 'conservative_analyst', 'neutral_analyst', 'risk_judge',
]
export const AGENT_STEP_LABELS: Record<AgentStep, string> = {
market_analyst: 'Market',
news_analyst: 'News',
fundamentals_analyst: 'Fundamentals',
social_analyst: 'Social',
bull_researcher: 'Bull Researcher',
bear_researcher: 'Bear Researcher',
research_manager: 'Research Manager',
trader: 'Trader',
aggressive_analyst: 'Aggressive',
conservative_analyst: 'Conservative',
neutral_analyst: 'Neutral',
risk_judge: 'Risk Judge',
}
export const STEP_PHASE: Record<AgentStep, 'analysts' | 'researchers' | 'trader' | 'risk'> = {
market_analyst: 'analysts',
news_analyst: 'analysts',
fundamentals_analyst: 'analysts',
social_analyst: 'analysts',
bull_researcher: 'researchers',
bear_researcher: 'researchers',
research_manager: 'researchers',
trader: 'trader',
aggressive_analyst: 'risk',
conservative_analyst: 'risk',
neutral_analyst: 'risk',
risk_judge: 'risk',
}
export type RunStatus = 'queued' | 'running' | 'complete' | 'error'
export type RunConfig = {
ticker: string
date: string
llm_provider: string
deep_think_llm: string
quick_think_llm: string
max_debate_rounds: number
max_risk_discuss_rounds: number
enabled_analysts?: string[]
}
export type RunSummary = {
id: string
ticker: string
date: string
status: RunStatus
decision: 'BUY' | 'SELL' | 'HOLD' | null
created_at: string
}

7
ui/lib/types/settings.ts Normal file
View File

@ -0,0 +1,7 @@
export type Settings = {
deep_think_llm: string
quick_think_llm: string
llm_provider: string
max_debate_rounds: number
max_risk_discuss_rounds: number
}