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:
parent
0690f628ab
commit
723069b958
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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`
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue