feat: parse chief_analyst report in useRunStream reducer
This commit is contained in:
parent
9a46424e61
commit
d3d69b2d56
|
|
@ -174,3 +174,78 @@ test('completed-run hydration populates tokens from getRun().token_usage without
|
||||||
expect(result.current.tokensByStep['market_analyst']).toEqual({ in: 1200, out: 400 })
|
expect(result.current.tokensByStep['market_analyst']).toEqual({ in: 1200, out: 400 })
|
||||||
expect(result.current.tokensTotal).toEqual({ in: 1200, out: 400 })
|
expect(result.current.tokensTotal).toEqual({ in: 1200, out: 400 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('AGENT_COMPLETE for chief_analyst parses JSON into chiefAnalystReport', async () => {
|
||||||
|
const { getRun } = jest.requireMock('@/lib/api-client')
|
||||||
|
const { createSSEConnection } = jest.requireMock('@/lib/sse')
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
getRun.mockResolvedValueOnce({
|
||||||
|
id: 'ca1', ticker: 'AAPL', date: '2024-01-15', status: 'queued',
|
||||||
|
decision: null, created_at: '2024-01-15T00:00:00Z',
|
||||||
|
config: null, reports: {}, error: null, token_usage: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const reportPayload = JSON.stringify({
|
||||||
|
verdict: 'BUY',
|
||||||
|
catalyst: 'Strong Q4 earnings',
|
||||||
|
execution: 'Enter at market, SL at 180',
|
||||||
|
tail_risk: 'Rate hike risk',
|
||||||
|
})
|
||||||
|
|
||||||
|
createSSEConnection.mockImplementationOnce(
|
||||||
|
(_url: string, handlers: Record<string, (d: unknown) => void>) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handlers.onAgentStart?.({ step: 'chief_analyst', turn: 0 })
|
||||||
|
handlers.onAgentComplete?.({ step: 'chief_analyst', turn: 0, report: reportPayload })
|
||||||
|
handlers.onRunComplete?.({ decision: 'BUY', run_id: 'ca1' })
|
||||||
|
}, 0)
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRunStream('ca1'))
|
||||||
|
await act(async () => { await new Promise((r) => setTimeout(r, 10)) })
|
||||||
|
|
||||||
|
expect(result.current.chiefAnalystReport).toEqual({
|
||||||
|
verdict: 'BUY',
|
||||||
|
catalyst: 'Strong Q4 earnings',
|
||||||
|
execution: 'Enter at market, SL at 180',
|
||||||
|
tail_risk: 'Rate hike risk',
|
||||||
|
})
|
||||||
|
// Raw JSON must also land in reports['chief_analyst']
|
||||||
|
expect(result.current.reports['chief_analyst']).toEqual([reportPayload])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AGENT_COMPLETE for chief_analyst with invalid JSON sets chiefAnalystReport to null', async () => {
|
||||||
|
const { getRun } = jest.requireMock('@/lib/api-client')
|
||||||
|
const { createSSEConnection } = jest.requireMock('@/lib/sse')
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
getRun.mockResolvedValueOnce({
|
||||||
|
id: 'ca2', ticker: 'AAPL', date: '2024-01-15', status: 'queued',
|
||||||
|
decision: null, created_at: '2024-01-15T00:00:00Z',
|
||||||
|
config: null, reports: {}, error: null, token_usage: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
createSSEConnection.mockImplementationOnce(
|
||||||
|
(_url: string, handlers: Record<string, (d: unknown) => void>) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handlers.onAgentStart?.({ step: 'chief_analyst', turn: 0 })
|
||||||
|
handlers.onAgentComplete?.({ step: 'chief_analyst', turn: 0, report: 'not-valid-json' })
|
||||||
|
handlers.onRunComplete?.({ decision: 'HOLD', run_id: 'ca2' })
|
||||||
|
}, 0)
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRunStream('ca2'))
|
||||||
|
await act(async () => { await new Promise((r) => setTimeout(r, 10)) })
|
||||||
|
|
||||||
|
expect(result.current.chiefAnalystReport).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('initial chiefAnalystReport is null', () => {
|
||||||
|
const { result } = renderHook(() => useRunStream('abc'))
|
||||||
|
expect(result.current.chiefAnalystReport).toBeNull()
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { createSSEConnection } from '@/lib/sse'
|
||||||
import { getRun, getRunStreamUrl } from '@/lib/api-client'
|
import { getRun, getRunStreamUrl } from '@/lib/api-client'
|
||||||
import { AGENT_STEPS } from '@/lib/types/run'
|
import { AGENT_STEPS } from '@/lib/types/run'
|
||||||
import type { AgentStep } from '@/lib/types/run'
|
import type { AgentStep } from '@/lib/types/run'
|
||||||
|
import type { ChiefAnalystReport } from '@/lib/types/agents'
|
||||||
import type { RunStreamState, TokenCount } from '../types'
|
import type { RunStreamState, TokenCount } from '../types'
|
||||||
|
|
||||||
const zeroTokens = (): TokenCount => ({ in: 0, out: 0 })
|
const zeroTokens = (): TokenCount => ({ in: 0, out: 0 })
|
||||||
|
|
@ -11,11 +12,12 @@ const zeroTokens = (): TokenCount => ({ in: 0, out: 0 })
|
||||||
const initialState: RunStreamState = {
|
const initialState: RunStreamState = {
|
||||||
status: 'connecting',
|
status: 'connecting',
|
||||||
steps: Object.fromEntries(AGENT_STEPS.map((s) => [s, 'pending'])) as RunStreamState['steps'],
|
steps: Object.fromEntries(AGENT_STEPS.map((s) => [s, 'pending'])) as RunStreamState['steps'],
|
||||||
reports: Object.fromEntries(AGENT_STEPS.map((s) => [s, []])) as RunStreamState['reports'],
|
reports: Object.fromEntries(AGENT_STEPS.map((s) => [s, []])) as unknown as RunStreamState['reports'],
|
||||||
tokensByStep: Object.fromEntries(AGENT_STEPS.map((s) => [s, zeroTokens()])) as RunStreamState['tokensByStep'],
|
tokensByStep: Object.fromEntries(AGENT_STEPS.map((s) => [s, zeroTokens()])) as RunStreamState['tokensByStep'],
|
||||||
tokensTotal: zeroTokens(),
|
tokensTotal: zeroTokens(),
|
||||||
verdict: null,
|
verdict: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
chiefAnalystReport: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
|
|
@ -38,6 +40,13 @@ function reducer(state: RunStreamState, action: Action): RunStreamState {
|
||||||
const dIn = action.tokens_in ?? 0
|
const dIn = action.tokens_in ?? 0
|
||||||
const dOut = action.tokens_out ?? 0
|
const dOut = action.tokens_out ?? 0
|
||||||
const prev = state.tokensByStep[action.step] ?? zeroTokens()
|
const prev = state.tokensByStep[action.step] ?? zeroTokens()
|
||||||
|
|
||||||
|
let chiefAnalystReport: ChiefAnalystReport | null = state.chiefAnalystReport
|
||||||
|
if (action.step === 'chief_analyst') {
|
||||||
|
try { chiefAnalystReport = JSON.parse(action.report) as ChiefAnalystReport }
|
||||||
|
catch { chiefAnalystReport = null }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
steps: { ...state.steps, [action.step]: 'done' },
|
steps: { ...state.steps, [action.step]: 'done' },
|
||||||
|
|
@ -53,6 +62,7 @@ function reducer(state: RunStreamState, action: Action): RunStreamState {
|
||||||
in: state.tokensTotal.in + dIn,
|
in: state.tokensTotal.in + dIn,
|
||||||
out: state.tokensTotal.out + dOut,
|
out: state.tokensTotal.out + dOut,
|
||||||
},
|
},
|
||||||
|
chiefAnalystReport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue