Research Agents: Lets users define research agents that answer chat questions from SEC filings, uploaded documents, and the web, with every figure cited.
| 1 | import type { BackendContext } from '@jaspers-ai/sdk' |
| 2 | import { openTickers } from './chats.ts' |
| 3 | import { publishDocs } from './doc-files.ts' |
| 4 | import { errorText } from './errors.ts' |
| 5 | import { SERVER } from './research.ts' |
| 6 | import { companies } from './sec-files.ts' |
| 7 | import { companyStatuses } from './sec.ts' |
| 8 | |
| 9 | // What the view shows of the research store, published while the plugin runs: whether the Terminal |
| 10 | // is signed in to it (the connection is ready), and each company the open conversations touch, with |
| 11 | // how far its reading and embedding are. Polled every two seconds while anything is moving, every |
| 12 | // ten while nothing is. |
| 13 | |
| 14 | const BUSY_MS = 2_000 |
| 15 | const IDLE_MS = 10_000 |
| 16 | |
| 17 | /** What the view gates on: the store is reached as the account. */ |
| 18 | export interface Setup { |
| 19 | signedIn: boolean |
| 20 | /** The connection's status when it is not ready: needs-sign-in, error, starting. */ |
| 21 | status: string |
| 22 | error: string | null |
| 23 | } |
| 24 | |
| 25 | let watching = false |
| 26 | /** Whether the account's documents have been listed since the store last answered: once, at the first ready. */ |
| 27 | let docsListed = false |
| 28 | |
| 29 | export function watchStatus(ctx: BackendContext): void { |
| 30 | if (watching) return |
| 31 | watching = true |
| 32 | const tick = async (): Promise<void> => { |
| 33 | let busy = true |
| 34 | try { |
| 35 | busy = await publishStatus(ctx) |
| 36 | } catch (err) { |
| 37 | console.warn(`status: ${errorText(err, 'failed')}`) |
| 38 | } |
| 39 | setTimeout(() => void tick(), busy ? BUSY_MS : IDLE_MS) |
| 40 | } |
| 41 | void tick() |
| 42 | } |
| 43 | |
| 44 | /** Publishes the setup and the companies; true while anything is still moving. */ |
| 45 | export async function publishStatus(ctx: BackendContext): Promise<boolean> { |
| 46 | const [connection] = await ctx.tools.connections({ connections: [SERVER] }) |
| 47 | const status = connection?.status ?? 'starting' |
| 48 | if (status !== 'ready') { |
| 49 | docsListed = false |
| 50 | ctx.live.set('setup', { signedIn: false, status, error: null } satisfies Setup) |
| 51 | ctx.live.set('sec', []) |
| 52 | return status === 'starting' |
| 53 | } |
| 54 | ctx.live.set('setup', { signedIn: true, status, error: null } satisfies Setup) |
| 55 | if (!docsListed) { |
| 56 | // The account's documents live in the store: listed as soon as it answers, then after each change. |
| 57 | await publishDocs(ctx) |
| 58 | docsListed = true |
| 59 | } |
| 60 | const held = await companies(ctx, openTickers()) |
| 61 | ctx.live.set('sec', companyStatuses(held)) |
| 62 | return held.some( |
| 63 | (c) => c.unembedded > 0 || (c.job !== null && ['queued', 'listing', 'reading'].includes(c.job.state)), |
| 64 | ) |
| 65 | } |