Research Agents: Agents you define that answer research chats from SEC filings, your documents, and the web, citing every passage.
| 1 | import type { BackendContext } from '@jaspers-ai/sdk' |
| 2 | import { docName, isReadable, READ_TYPES, type Doc } from './docs.ts' |
| 3 | import { research, type StoredDoc } from './research.ts' |
| 4 | |
| 5 | // Documents: what the user uploads, and the pages agents read. Each lives in the research store, the |
| 6 | // account's own, cut by its pages or headings; nothing is kept on this machine. The list is the live |
| 7 | // value `docs`, published again after every change. |
| 8 | |
| 9 | function asDoc(stored: StoredDoc): Doc { |
| 10 | return { name: stored.title, id: stored.id, size: stored.chars, modified: Date.parse(stored.added_at) || 0 } |
| 11 | } |
| 12 | |
| 13 | export async function publishDocs(ctx: BackendContext): Promise<Doc[]> { |
| 14 | const docs = (await research<StoredDoc[]>(ctx, 'documents', {})) |
| 15 | .map(asDoc) |
| 16 | .sort((a, b) => a.name.localeCompare(b.name)) |
| 17 | ctx.live.set('docs', docs) |
| 18 | return docs |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Puts a document's text in the store under its name; the same name again replaces it. A PDF's text |
| 23 | * is read in the view, so only the text comes here. |
| 24 | */ |
| 25 | export async function uploadDoc( |
| 26 | ctx: BackendContext, |
| 27 | upload: { name: string; text: string }, |
| 28 | ): Promise<{ id: string; title: string; said: string }> { |
| 29 | const title = docName(upload.name) |
| 30 | if (!isReadable(title)) { |
| 31 | throw new Error(`${upload.name} is not read yet: the files read are ${READ_TYPES.join(', ')}.`) |
| 32 | } |
| 33 | if (!upload.text.trim()) throw new Error(`${upload.name} has no text.`) |
| 34 | const put = await research<{ id: string; chunks: number }>(ctx, 'index_document', { |
| 35 | origin: 'upload', |
| 36 | title, |
| 37 | text: upload.text, |
| 38 | }) |
| 39 | await publishDocs(ctx) |
| 40 | return { id: put.id, title, said: `Indexed ${title}: ${put.chunks} ${put.chunks === 1 ? 'chunk' : 'chunks'}.` } |
| 41 | } |
| 42 | |
| 43 | /** A public page an agent asked for, read by the app and put in the store as a web page under its address. */ |
| 44 | export async function savePage( |
| 45 | ctx: BackendContext, |
| 46 | url: string, |
| 47 | signal: AbortSignal, |
| 48 | ): Promise<{ url: string; doc: string }> { |
| 49 | const page = await ctx.web.fetch(url, { signal }) |
| 50 | if (!page.text.trim()) throw new Error(`${page.url} has no text: a page drawn by script reads empty here.`) |
| 51 | const put = await research<{ id: string }>( |
| 52 | ctx, |
| 53 | 'index_document', |
| 54 | { origin: 'web', title: page.url, text: page.text, url: page.url }, |
| 55 | signal, |
| 56 | ) |
| 57 | await publishDocs(ctx) |
| 58 | return { url: page.url, doc: put.id } |
| 59 | } |
| 60 | |
| 61 | export async function deleteDoc(ctx: BackendContext, id: string): Promise<string> { |
| 62 | const { deleted } = await research<{ deleted: number }>(ctx, 'delete', { documents: [id] }) |
| 63 | await publishDocs(ctx) |
| 64 | return deleted ? 'Deleted.' : 'Already gone.' |
| 65 | } |