140 lines · 5.3 KB · TypeScript
RawDownload
import z from "zod"import { Tool } from "./tool"import { registry } from "../science/connectors"import type { ConnectorHit } from "../science/connectors" /** * Small, database-agnostic surface over the scientific connector registry. * * There are intentionally only TWO tools regardless of how many databases are * registered — the model picks a `db` id from `science_list_dbs` and searches * through `science_search`. This keeps the tool count flat as connectors grow. */ export const ScienceListDbsTool = Tool.define("science_list_dbs", {  description: [    "List the scientific databases available to search via `science_search`.",    "Returns each database's id, name, domain, and description.",    "Call this first to discover which `db` id to pass to `science_search`.",  ].join("\n"),  parameters: z.object({    domain: z      .string()      .optional()      .describe("Optional domain filter (e.g. 'chemistry', 'biology', 'literature', 'structure')"),  }),  async execute(params, _ctx) {    const entries = registry.catalog().filter((e) => !params.domain || e.domain === params.domain)    if (!entries.length) {      return {        title: "Scientific databases",        output: params.domain          ? `No databases registered for domain "${params.domain}".`          : "No scientific databases are registered yet.",        metadata: { count: 0, domains: [] as string[] },      }    }     const byDomain = new Map<string, typeof entries>()    for (const e of entries) {      const list = byDomain.get(e.domain) ?? []      list.push(e)      byDomain.set(e.domain, list)    }     const sections = [...byDomain.entries()].map(([domain, list]) => {      const rows = list.map((e) => `- **${e.id}** (${e.name}) — ${e.description}`)      return `### ${domain}\n${rows.join("\n")}`    })     return {      title: `Scientific databases (${entries.length})`,      output: sections.join("\n\n"),      metadata: { count: entries.length, domains: [...byDomain.keys()] },    }  },}) export const ScienceSearchTool = Tool.define("science_search", {  description: [    "Search a scientific database registered in the connector registry.",    "Pass a `db` id (from `science_list_dbs`) and a `query`.",    "Returns normalized hits: id, title, summary, and URL.",  ].join("\n"),  parameters: z.object({    db: z.string().describe("Database id to search (from science_list_dbs, e.g. 'uniprot', 'arxiv')"),    query: z.string().describe("Search query in the database's native syntax"),    limit: z.number().default(10).describe("Max results (1-50)"),    organism: z.string().optional().describe("Optional organism/taxon filter where supported"),  }),  async execute(params, ctx) {    const connector = registry.get(params.db)    if (!connector) {      const available = registry        .catalog()        .map((e) => e.id)        .join(", ")      return {        title: "Unknown database",        output: `No database "${params.db}". Available: ${available || "(none registered)"}. Use science_list_dbs.`,        metadata: { error: "unknown_db" } as Record<string, unknown>,      }    }     const limit = Math.min(Math.max(params.limit, 1), 50)    let hits: ConnectorHit[]    try {      hits = await connector.search(params.query, {        limit,        organism: params.organism,        signal: ctx.abort,      })    } catch (err) {      // A source error is NOT the same as "no results" — surface it as an      // actionable, degraded result instead of throwing a raw `HTTP 429` string.      if (ctx.abort.aborted) throw err      const message = err instanceof Error ? err.message : String(err)      const rateLimited = /\b(429|503|408)\b/.test(message) || /rate.?limit/i.test(message)      const guidance = rateLimited        ? `${connector.name} is rate limiting requests. Wait a few seconds, then retry${            connector.id === "arxiv" ? " (arXiv allows ~1 request every 3s)" : ""          }.`        : `${connector.name} returned an error: ${message}`      return {        title: `${connector.name} temporarily unavailable — ${rateLimited ? "rate limited, retry shortly" : "source error"}`,        output: [`Could not complete the search for "${params.query}".`, guidance].join("\n"),        metadata: {          db: connector.id,          count: 0,          error: rateLimited ? "rate_limited" : "source_error",          message,        } as Record<string, unknown>,      }    }     if (!hits.length) {      return {        title: `${connector.name}: ${params.query}`,        output: `No results for "${params.query}" in ${connector.name}.`,        metadata: { db: connector.id, count: 0 } as Record<string, unknown>,      }    }     const rows = hits.map((h) => {      const lines = [`## ${h.title}`, `**id**: ${h.id}${h.score !== undefined ? ` · score: ${h.score}` : ""}`]      if (h.url) lines.push(`**url**: ${h.url}`)      if (h.summary) lines.push(h.summary)      return lines.join("\n")    })     return {      title: `${connector.name}: ${params.query}`,      output: [`**${connector.name}** — ${hits.length} result(s):`, "", rows.join("\n\n---\n\n")].join("\n"),      metadata: { db: connector.id, count: hits.length } as Record<string, unknown>,    }  },}) export const ScienceTools = [ScienceListDbsTool, ScienceSearchTool] export const SCIENCE_TOOL_IDS = new Set(["science_list_dbs", "science_search"])