Jaspers Hub

jaspers / agents

pluginOfficialReviewed★ 0↓ 1

Research Agents: Lets you define research agents that chat and answer with citations from SEC filings, your documents, and the web, via Jaspers' research-store service.

2.6 KB
1import type { AgentSearch } from './agents.ts'
2
3// One search as the store is asked it: the agent's own hold, then what the model asked within it.
4// The agent's documents are its scope; a company the model names narrows the search to it. Pure, so the
5// merge is tested.
6
7/** What the model may ask of a search, beside its words. */
8export interface Asked {
9 ticker?: string
10 /** One document by its id, like doc:notes.pdf. */
11 document?: string
12 form?: string
13 fiscal_year?: number
14 fiscal_period?: string
15 section?: string
16 filed_after?: string
17 filed_before?: string
18}
19
20export type Merged = { args: Record<string, unknown> } | { refused: string }
21
22export function mergeSearch(query: string, pinned: AgentSearch, asked: Asked): Merged {
23 const args: Record<string, unknown> = { query }
24 if (pinned.documents.length) args['scope_documents'] = pinned.documents
25 if (pinned.origins.length) args['origins'] = pinned.origins
26
27 const ticker = asked.ticker?.trim().toUpperCase()
28 if (ticker) args['tickers'] = [ticker]
29 const document = asked.document?.trim()
30 if (document) args['documents'] = [document]
31 const form = asked.form?.trim().toUpperCase()
32 if (form) {
33 if (pinned.forms.length && !pinned.forms.includes(form)) {
34 return { refused: `${form} is not one of the forms you read: ${pinned.forms.join(', ')}.` }
35 }
36 args['forms'] = [form]
37 } else if (pinned.forms.length) {
38 args['forms'] = pinned.forms
39 }
40 if (typeof asked.fiscal_year === 'number' && Number.isInteger(asked.fiscal_year))
41 args['fiscal_years'] = [asked.fiscal_year]
42 if (asked.fiscal_period?.trim()) args['fiscal_period'] = asked.fiscal_period.trim().toUpperCase()
43 if (asked.section?.trim()) args['section'] = asked.section.trim()
44 const after = [pinned.since, asked.filed_after?.trim()]
45 .filter((d): d is string => Boolean(d))
46 .sort()
47 .at(-1)
48 if (after) args['filed_after'] = after
49 if (asked.filed_before?.trim()) args['filed_before'] = asked.filed_before.trim()
50 return { args }
51}
52
53/** What the model asked, read the forgiving way: a number given as text is a number. */
54export function askedOf(input: Record<string, unknown>): Asked {
55 const text = (key: string): string | undefined =>
56 typeof input[key] === 'string' ? (input[key] as string) : undefined
57 const year =
58 typeof input['fiscal_year'] === 'number' ? input['fiscal_year'] : Number.parseInt(text('fiscal_year') ?? '', 10)
59 return {
60 ticker: text('ticker'),
61 document: text('document'),
62 form: text('form'),
63 fiscal_year: Number.isFinite(year) ? (year as number) : undefined,
64 fiscal_period: text('fiscal_period'),
65 section: text('section'),
66 filed_after: text('filed_after'),
67 filed_before: text('filed_before'),
68 }
69}