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.5 KB
1import type { BackendContext } from '@jaspers-ai/sdk'
2import { docName, isReadable, READ_TYPES, type Doc } from './docs.ts'
3import { 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
9function asDoc(stored: StoredDoc): Doc {
10 return { name: stored.title, id: stored.id, size: stored.chars, modified: Date.parse(stored.added_at) || 0 }
11}
12
13export 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 */
25export 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. */
44export 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
61export 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}