@inflexa-ai/harness 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/agents/conversation-agent.d.ts +3 -0
  2. package/dist/agents/conversation-agent.js +5 -2
  3. package/dist/agents/sandbox/shared.d.ts +3 -0
  4. package/dist/agents/sandbox/shared.js +5 -0
  5. package/dist/agents/sandbox/types.d.ts +1 -1
  6. package/dist/app/chat-turn.d.ts +2 -0
  7. package/dist/app/chat-turn.js +11 -2
  8. package/dist/app/synthesize-run.d.ts +3 -0
  9. package/dist/app/synthesize-run.js +2 -1
  10. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  11. package/dist/citations/__fixtures__/resolver.js +9 -0
  12. package/dist/citations/aggregate.d.ts +3 -0
  13. package/dist/citations/aggregate.js +44 -0
  14. package/dist/citations/clients/arxiv.d.ts +6 -0
  15. package/dist/citations/clients/arxiv.js +34 -0
  16. package/dist/citations/clients/common.d.ts +11 -0
  17. package/dist/citations/clients/common.js +43 -0
  18. package/dist/citations/clients/crossref.d.ts +8 -0
  19. package/dist/citations/clients/crossref.js +98 -0
  20. package/dist/citations/clients/doi-registry.d.ts +3 -0
  21. package/dist/citations/clients/doi-registry.js +95 -0
  22. package/dist/citations/clients/pubmed.d.ts +6 -0
  23. package/dist/citations/clients/pubmed.js +83 -0
  24. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  25. package/dist/citations/clients/semantic-scholar.js +60 -0
  26. package/dist/citations/compare.d.ts +9 -0
  27. package/dist/citations/compare.js +72 -0
  28. package/dist/citations/match.d.ts +11 -0
  29. package/dist/citations/match.js +173 -0
  30. package/dist/citations/normalize.d.ts +31 -0
  31. package/dist/citations/normalize.js +187 -0
  32. package/dist/citations/plan.d.ts +2 -0
  33. package/dist/citations/plan.js +59 -0
  34. package/dist/citations/resolve.d.ts +28 -0
  35. package/dist/citations/resolve.js +238 -0
  36. package/dist/citations/types.d.ts +583 -0
  37. package/dist/citations/types.js +129 -0
  38. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  39. package/dist/contracts/target-dossier.d.ts +20 -20
  40. package/dist/execution/run-synthesis.d.ts +3 -0
  41. package/dist/execution/run-synthesis.js +1 -0
  42. package/dist/index.d.ts +6 -1
  43. package/dist/index.js +3 -0
  44. package/dist/lib/cache.d.ts +8 -0
  45. package/dist/lib/cache.js +35 -0
  46. package/dist/lib/rate-limit.d.ts +35 -0
  47. package/dist/lib/rate-limit.js +121 -0
  48. package/dist/literature/sources/arxiv.d.ts +22 -0
  49. package/dist/literature/sources/arxiv.js +103 -0
  50. package/dist/literature/sources/http.d.ts +28 -0
  51. package/dist/literature/sources/http.js +97 -0
  52. package/dist/literature/sources/pubmed.d.ts +68 -0
  53. package/dist/literature/sources/pubmed.js +266 -0
  54. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  55. package/dist/literature/sources/semantic-scholar.js +71 -0
  56. package/dist/memory/thread-store.d.ts +142 -25
  57. package/dist/memory/thread-store.js +228 -40
  58. package/dist/prompts/literature-reviewer.d.ts +1 -1
  59. package/dist/prompts/literature-reviewer.js +2 -0
  60. package/dist/runtime/assemble.d.ts +50 -4
  61. package/dist/runtime/assemble.js +47 -3
  62. package/dist/runtime/boot.js +7 -1
  63. package/dist/state/init.js +46 -1
  64. package/dist/state/schema.d.ts +2 -2
  65. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  66. package/dist/tools/bio/keys.d.ts +1 -0
  67. package/dist/tools/bio/pubmed.js +4 -4
  68. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  69. package/dist/tools/lib/ncbi-utils.js +2 -286
  70. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  71. package/dist/tools/lib/pubmed-client.js +20 -79
  72. package/dist/tools/research/generate-analogy-report.js +2 -2
  73. package/dist/tools/research/index.d.ts +1 -0
  74. package/dist/tools/research/index.js +1 -0
  75. package/dist/tools/research/inspect-run.d.ts +1 -1
  76. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  77. package/dist/tools/research/literature-reviewer.js +2 -0
  78. package/dist/tools/research/resolve-citation.d.ts +3 -0
  79. package/dist/tools/research/resolve-citation.js +15 -0
  80. package/dist/tools/research/search-arxiv.d.ts +3 -19
  81. package/dist/tools/research/search-arxiv.js +9 -105
  82. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  83. package/dist/tools/research/search-semantic-scholar.js +29 -88
  84. package/dist/tools/sandbox/list-available-refs.js +1 -2
  85. package/dist/workflows/execute-analysis.d.ts +3 -0
  86. package/dist/workflows/execute-analysis.js +1 -0
  87. package/dist/workflows/sandbox-step.d.ts +5 -0
  88. package/dist/workflows/sandbox-step.js +1 -0
  89. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
  90. package/dist/workflows/target-assessment/schemas.d.ts +2 -2
  91. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
  92. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  93. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { requestText } from "./http.js";
2
+ const BASE_URL = "https://export.arxiv.org/api/query";
3
+ const stripCdata = (value) => value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
4
+ const decodeXmlEntities = (value) => value
5
+ .replace(/&lt;/g, "<")
6
+ .replace(/&gt;/g, ">")
7
+ .replace(/&quot;/g, '"')
8
+ .replace(/&apos;/g, "'")
9
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hexadecimal) => String.fromCodePoint(parseInt(hexadecimal, 16)))
10
+ .replace(/&#(\d+);/g, (_, decimal) => String.fromCodePoint(parseInt(decimal, 10)))
11
+ .replace(/&amp;/g, "&");
12
+ const collapseWhitespace = (value) => decodeXmlEntities(stripCdata(value)).replace(/\s+/g, " ").trim();
13
+ function extractEntries(xml) {
14
+ const expression = /<entry\b[^>]*>([\s\S]*?)<\/entry>/g;
15
+ const entries = [];
16
+ let match;
17
+ while ((match = expression.exec(xml)) !== null)
18
+ entries.push(match[1]);
19
+ return entries;
20
+ }
21
+ function firstMatch(entry, tag) {
22
+ return entry.match(new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)</${tag}>`))?.[1];
23
+ }
24
+ function allMatches(entry, tag) {
25
+ const expression = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)</${tag}>`, "g");
26
+ const values = [];
27
+ let match;
28
+ while ((match = expression.exec(entry)) !== null)
29
+ values.push(match[1]);
30
+ return values;
31
+ }
32
+ function linkAttribute(entry, predicate) {
33
+ const expression = /<link\b([^>]*)\/>/g;
34
+ let match;
35
+ while ((match = expression.exec(entry)) !== null) {
36
+ const attributes = match[1];
37
+ if (predicate(attributes))
38
+ return attributes.match(/href="([^"]+)"/)?.[1];
39
+ }
40
+ return undefined;
41
+ }
42
+ function categoryTerms(entry) {
43
+ const expression = /<category\b([^>]*)\/>/g;
44
+ const terms = [];
45
+ let match;
46
+ while ((match = expression.exec(entry)) !== null) {
47
+ const term = match[1].match(/term="([^"]+)"/)?.[1];
48
+ if (term !== undefined)
49
+ terms.push(term);
50
+ }
51
+ return terms;
52
+ }
53
+ export function parseArxivAtom(xml) {
54
+ return extractEntries(xml).flatMap((entry) => {
55
+ const rawId = firstMatch(entry, "id");
56
+ const title = firstMatch(entry, "title");
57
+ if (rawId === undefined || title === undefined)
58
+ return [];
59
+ const idMatch = rawId.match(/abs\/(.+)$/);
60
+ const authors = allMatches(entry, "author")
61
+ .map((author) => firstMatch(author, "name"))
62
+ .filter((name) => name !== undefined)
63
+ .map(collapseWhitespace);
64
+ const htmlUrl = linkAttribute(entry, (attributes) => /rel="alternate"/.test(attributes) && /type="text\/html"/.test(attributes));
65
+ const pdfUrl = linkAttribute(entry, (attributes) => /title="pdf"/.test(attributes));
66
+ return [
67
+ {
68
+ id: idMatch?.[1] ?? collapseWhitespace(rawId),
69
+ title: collapseWhitespace(title),
70
+ abstract: collapseWhitespace(firstMatch(entry, "summary") ?? ""),
71
+ authors,
72
+ published: collapseWhitespace(firstMatch(entry, "published") ?? ""),
73
+ categories: categoryTerms(entry),
74
+ url: htmlUrl ?? collapseWhitespace(rawId),
75
+ ...(pdfUrl === undefined ? {} : { pdfUrl }),
76
+ },
77
+ ];
78
+ });
79
+ }
80
+ export function createArxivSource(options = {}) {
81
+ async function fetchPapers(params, signal) {
82
+ const result = await requestText(`${BASE_URL}?${params}`, options, signal);
83
+ if (result.status !== "ok")
84
+ return result;
85
+ if (!/<feed\b/.test(result.value))
86
+ return { status: "unavailable", detail: "response schema mismatch: missing Atom feed" };
87
+ return { status: "ok", value: parseArxivAtom(result.value) };
88
+ }
89
+ return {
90
+ search: (input, signal) => {
91
+ const categoryQuery = input.categories !== undefined && input.categories.length > 0
92
+ ? ` AND (${input.categories.map((category) => `cat:${category}`).join(" OR ")})`
93
+ : "";
94
+ return fetchPapers(new URLSearchParams({
95
+ search_query: `all:${input.query}${categoryQuery}`,
96
+ max_results: String(input.limit),
97
+ sortBy: "relevance",
98
+ sortOrder: "descending",
99
+ }), signal);
100
+ },
101
+ lookupExact: (id, limit, signal) => fetchPapers(new URLSearchParams({ id_list: id, max_results: String(limit) }), signal),
102
+ };
103
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ export interface SourceHttpOptions {
3
+ readonly fetch?: typeof globalThis.fetch;
4
+ /**
5
+ * Admission gate wrapped around each network attempt — a rate limiter, a
6
+ * concurrency semaphore, or both. The request timeout is armed *inside* it,
7
+ * so time an attempt spends queueing is never charged against the timeout.
8
+ */
9
+ readonly schedule?: <T>(operation: () => Promise<T>, signal?: AbortSignal) => Promise<T>;
10
+ readonly timeoutMs?: number;
11
+ readonly maxRetries?: number;
12
+ readonly retryDelayMs?: number;
13
+ /** Ceiling on any single retry wait, including one a server asks for via `Retry-After`. */
14
+ readonly maxRetryDelayMs?: number;
15
+ readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
16
+ }
17
+ export type SourceHttpResult<T> = {
18
+ readonly status: "ok";
19
+ readonly value: T;
20
+ } | {
21
+ readonly status: "no_data";
22
+ readonly detail: string;
23
+ } | {
24
+ readonly status: "unavailable";
25
+ readonly detail: string;
26
+ };
27
+ export declare function requestJson<S extends z.ZodType>(url: string, schema: S, options: SourceHttpOptions, signal?: AbortSignal, init?: RequestInit): Promise<SourceHttpResult<z.infer<S>>>;
28
+ export declare function requestText(url: string, options: SourceHttpOptions, signal?: AbortSignal, init?: RequestInit): Promise<SourceHttpResult<string>>;
@@ -0,0 +1,97 @@
1
+ const RETRYABLE_STATUSES = new Set([429, 503]);
2
+ function cancellationReason(signal) {
3
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
4
+ }
5
+ async function delay(ms, signal) {
6
+ if (signal?.aborted)
7
+ throw cancellationReason(signal);
8
+ await new Promise((resolve, reject) => {
9
+ const onAbort = () => {
10
+ clearTimeout(timeout);
11
+ reject(cancellationReason(signal));
12
+ };
13
+ const timeout = setTimeout(() => {
14
+ signal?.removeEventListener("abort", onAbort);
15
+ resolve();
16
+ }, ms);
17
+ signal?.addEventListener("abort", onAbort, { once: true });
18
+ });
19
+ }
20
+ function retryAfterMilliseconds(response) {
21
+ const value = response.headers.get("retry-after")?.trim();
22
+ if (!value)
23
+ return undefined;
24
+ const seconds = Number(value);
25
+ if (Number.isFinite(seconds) && seconds >= 0)
26
+ return seconds * 1000;
27
+ const date = Date.parse(value);
28
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
29
+ }
30
+ async function request(url, options, signal, init) {
31
+ if (signal?.aborted)
32
+ throw cancellationReason(signal);
33
+ const timeoutMs = options.timeoutMs ?? 15_000;
34
+ const maxRetries = options.maxRetries ?? 0;
35
+ const retryDelayMs = options.retryDelayMs ?? 1_000;
36
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? Number.POSITIVE_INFINITY;
37
+ const schedule = options.schedule ?? ((operation) => operation());
38
+ const sleep = options.sleep ?? delay;
39
+ const fetcher = options.fetch ?? globalThis.fetch;
40
+ const backoff = (attempt) => Math.min(maxRetryDelayMs, retryDelayMs * 2 ** attempt);
41
+ for (let attempt = 0;; attempt += 1) {
42
+ let timeout;
43
+ try {
44
+ const response = await schedule(async () => {
45
+ timeout = AbortSignal.timeout(timeoutMs);
46
+ const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
47
+ return await fetcher(url, { ...init, signal: combined });
48
+ }, signal);
49
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < maxRetries) {
50
+ await sleep(Math.min(maxRetryDelayMs, retryAfterMilliseconds(response) ?? backoff(attempt)), signal);
51
+ continue;
52
+ }
53
+ if (response.status === 404 || response.status === 400 || response.status === 406) {
54
+ return { status: "no_data", detail: `HTTP ${response.status}` };
55
+ }
56
+ if (!response.ok)
57
+ return { status: "unavailable", detail: `HTTP ${response.status}` };
58
+ return { status: "ok", value: response };
59
+ }
60
+ catch (error) {
61
+ if (signal?.aborted)
62
+ throw cancellationReason(signal);
63
+ if (timeout?.aborted)
64
+ return { status: "unavailable", detail: `request timed out after ${timeoutMs}ms` };
65
+ if (attempt < maxRetries) {
66
+ await sleep(backoff(attempt), signal);
67
+ continue;
68
+ }
69
+ return { status: "unavailable", detail: error instanceof Error ? error.message : String(error) };
70
+ }
71
+ }
72
+ }
73
+ export async function requestJson(url, schema, options, signal, init = {}) {
74
+ const response = await request(url, options, signal, init);
75
+ if (response.status !== "ok")
76
+ return response;
77
+ try {
78
+ const parsed = schema.safeParse(await response.value.json());
79
+ if (!parsed.success)
80
+ return { status: "unavailable", detail: `response schema mismatch: ${parsed.error.issues[0]?.message ?? "unknown"}` };
81
+ return { status: "ok", value: parsed.data };
82
+ }
83
+ catch (error) {
84
+ return { status: "unavailable", detail: `invalid JSON response: ${error instanceof Error ? error.message : String(error)}` };
85
+ }
86
+ }
87
+ export async function requestText(url, options, signal, init = {}) {
88
+ const response = await request(url, options, signal, init);
89
+ if (response.status !== "ok")
90
+ return response;
91
+ try {
92
+ return { status: "ok", value: await response.value.text() };
93
+ }
94
+ catch (error) {
95
+ return { status: "unavailable", detail: `invalid text response: ${error instanceof Error ? error.message : String(error)}` };
96
+ }
97
+ }
@@ -0,0 +1,68 @@
1
+ import { type SourceHttpOptions, type SourceHttpResult } from "./http.js";
2
+ export declare const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
3
+ export declare const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
4
+ export interface PubmedSourceOptions extends SourceHttpOptions {
5
+ readonly apiKey?: string;
6
+ }
7
+ export interface PubmedSearchOptions {
8
+ readonly maxResults?: number;
9
+ readonly sort?: "relevance" | "date";
10
+ readonly dateRange?: {
11
+ readonly from: string;
12
+ readonly to: string;
13
+ };
14
+ }
15
+ export interface PubmedCitationFields {
16
+ readonly venue?: string;
17
+ readonly year?: string | number;
18
+ readonly volume?: string;
19
+ readonly firstPage?: string;
20
+ readonly firstAuthor?: string;
21
+ readonly key: string;
22
+ }
23
+ export interface PubMedSummary {
24
+ pmid: string;
25
+ title: string;
26
+ journal: string;
27
+ year: string;
28
+ authors: string;
29
+ }
30
+ export interface ArticleDetail {
31
+ pmid: string;
32
+ title: string;
33
+ abstract: string;
34
+ authors: string[];
35
+ journal: string;
36
+ year: string;
37
+ doi: string;
38
+ meshTerms: string[];
39
+ pmcId: string | null;
40
+ }
41
+ export interface ArticleSection {
42
+ heading: string;
43
+ text: string;
44
+ }
45
+ export interface FullTextResult {
46
+ fullText: string;
47
+ sections: ArticleSection[];
48
+ }
49
+ export interface PubmedSource {
50
+ search(query: string, options?: PubmedSearchOptions, signal?: AbortSignal): Promise<SourceHttpResult<{
51
+ totalFound: number;
52
+ results: PubMedSummary[];
53
+ }>>;
54
+ searchIds(query: string, maxResults: number, signal?: AbortSignal): Promise<SourceHttpResult<string[]>>;
55
+ matchCitation(fields: PubmedCitationFields, signal?: AbortSignal): Promise<SourceHttpResult<string[]>>;
56
+ fetchArticles(pmids: readonly string[], signal?: AbortSignal): Promise<SourceHttpResult<ArticleDetail[]>>;
57
+ articleDetails(pmids: readonly string[], signal?: AbortSignal): Promise<SourceHttpResult<{
58
+ articles: ArticleDetail[];
59
+ notFound: string[];
60
+ }>>;
61
+ fetchFullText(pmcId: string, signal?: AbortSignal): Promise<SourceHttpResult<FullTextResult | null>>;
62
+ }
63
+ export declare function ncbiUrl(apiKey: string | undefined, base: string, params: Record<string, string | number | undefined>): string;
64
+ export declare function parseEsummary(xml: string): PubMedSummary[];
65
+ export declare function parseEfetch(xml: string): ArticleDetail[];
66
+ export declare function parseIdConvResponse(data: unknown): Map<string, string>;
67
+ export declare function parsePmcFullText(xml: string): FullTextResult | null;
68
+ export declare function createPubmedSource(options?: PubmedSourceOptions): PubmedSource;
@@ -0,0 +1,266 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ import { z } from "zod";
3
+ import { requestJson, requestText } from "./http.js";
4
+ export const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
5
+ export const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
6
+ const EsearchResponseSchema = z.object({
7
+ esearchresult: z.object({ idlist: z.array(z.string()).optional(), count: z.string().optional() }).optional(),
8
+ });
9
+ const IdConvResponseSchema = z.object({
10
+ records: z.array(z.object({ pmid: z.string().optional(), pmcid: z.string().optional() })).optional(),
11
+ });
12
+ export function ncbiUrl(apiKey, base, params) {
13
+ const url = new URL(base);
14
+ if (apiKey !== undefined)
15
+ url.searchParams.set("api_key", apiKey);
16
+ for (const [key, value] of Object.entries(params))
17
+ if (value !== undefined)
18
+ url.searchParams.set(key, String(value));
19
+ return url.toString();
20
+ }
21
+ const xmlParser = new XMLParser({
22
+ ignoreAttributes: false,
23
+ attributeNamePrefix: "@_",
24
+ isArray: (name) => ["PubmedArticle", "Author", "MeshHeading", "AbstractText", "sec", "p", "record"].includes(name),
25
+ textNodeName: "#text",
26
+ });
27
+ function getItemValue(items, name) {
28
+ if (items === undefined)
29
+ return "";
30
+ const item = (Array.isArray(items) ? items : [items]).find((candidate) => candidate["@_Name"] === name);
31
+ return item?.["#text"] ?? "";
32
+ }
33
+ function getAuthorsFromDocSum(items) {
34
+ if (items === undefined)
35
+ return "";
36
+ const authorList = (Array.isArray(items) ? items : [items]).find((item) => item["@_Name"] === "AuthorList");
37
+ if (authorList?.Item === undefined)
38
+ return "";
39
+ const names = (Array.isArray(authorList.Item) ? authorList.Item : [authorList.Item]).map((author) => author["#text"] ?? "").filter(Boolean);
40
+ if (names.length <= 3)
41
+ return names.join(", ");
42
+ return `${names[0]} et al.`;
43
+ }
44
+ export function parseEsummary(xml) {
45
+ const parsed = xmlParser.parse(xml);
46
+ const docSums = (parsed?.eSummaryResult ?? parsed)?.DocSum;
47
+ if (docSums === undefined)
48
+ return [];
49
+ const documents = Array.isArray(docSums) ? docSums : [docSums];
50
+ return documents.map((document) => {
51
+ const publicationDate = getItemValue(document.Item, "PubDate");
52
+ return {
53
+ pmid: String(document.Id ?? ""),
54
+ title: getItemValue(document.Item, "Title"),
55
+ journal: getItemValue(document.Item, "FullJournalName") || getItemValue(document.Item, "Source"),
56
+ year: publicationDate.slice(0, 4),
57
+ authors: getAuthorsFromDocSum(document.Item),
58
+ };
59
+ });
60
+ }
61
+ export function parseEfetch(xml) {
62
+ const articleSet = xmlParser.parse(xml)?.PubmedArticleSet;
63
+ if (articleSet === undefined)
64
+ return [];
65
+ const entries = Array.isArray(articleSet.PubmedArticle)
66
+ ? articleSet.PubmedArticle
67
+ : articleSet.PubmedArticle === undefined
68
+ ? []
69
+ : [articleSet.PubmedArticle];
70
+ return entries.map((entry) => {
71
+ const article = entry;
72
+ const citation = article.MedlineCitation;
73
+ const articleData = citation?.Article;
74
+ const pmidValue = citation?.PMID;
75
+ const pmid = typeof pmidValue === "object" && pmidValue !== null ? String(pmidValue["#text"] ?? "") : String(pmidValue ?? "");
76
+ const abstractValue = articleData?.Abstract?.AbstractText;
77
+ const abstract = (abstractValue === undefined ? [] : Array.isArray(abstractValue) ? abstractValue : [abstractValue])
78
+ .map((value) => {
79
+ if (typeof value === "string")
80
+ return value;
81
+ if (typeof value !== "object" || value === null)
82
+ return String(value);
83
+ const object = value;
84
+ const text = String(object["#text"] ?? "");
85
+ return object["@_Label"] === undefined ? text : `${String(object["@_Label"])}: ${text}`;
86
+ })
87
+ .join("\n\n");
88
+ const authorValue = articleData?.AuthorList?.Author;
89
+ const authors = (authorValue === undefined ? [] : Array.isArray(authorValue) ? authorValue : [authorValue]).map((author) => author.CollectiveName ?? [author.ForeName ?? author.Initials, author.LastName].filter(Boolean).join(" "));
90
+ const journalData = articleData?.Journal;
91
+ const publicationDate = journalData?.JournalIssue?.PubDate;
92
+ let doi = "";
93
+ const articleIds = article.PubmedData?.ArticleIdList?.ArticleId;
94
+ for (const id of articleIds === undefined ? [] : Array.isArray(articleIds) ? articleIds : [articleIds]) {
95
+ if (typeof id === "object" && id !== null && id["@_IdType"] === "doi") {
96
+ doi = String(id["#text"] ?? "");
97
+ }
98
+ }
99
+ const meshValue = citation?.MeshHeadingList?.MeshHeading;
100
+ const meshTerms = (meshValue === undefined ? [] : Array.isArray(meshValue) ? meshValue : [meshValue])
101
+ .map((heading) => {
102
+ const descriptor = heading.DescriptorName;
103
+ return typeof descriptor === "string" ? descriptor : (descriptor?.["#text"] ?? "");
104
+ })
105
+ .filter(Boolean);
106
+ return {
107
+ pmid,
108
+ title: String(articleData?.ArticleTitle ?? ""),
109
+ abstract,
110
+ authors,
111
+ journal: String(journalData?.Title ?? journalData?.ISOAbbreviation ?? ""),
112
+ year: String(publicationDate?.Year ?? ""),
113
+ doi,
114
+ meshTerms,
115
+ pmcId: null,
116
+ };
117
+ });
118
+ }
119
+ export function parseIdConvResponse(data) {
120
+ const result = new Map();
121
+ const records = data?.records ?? [];
122
+ for (const record of records)
123
+ if (record.pmid !== undefined && record.pmcid !== undefined)
124
+ result.set(record.pmid, record.pmcid);
125
+ return result;
126
+ }
127
+ function flattenTextContent(node) {
128
+ if (typeof node === "string" || typeof node === "number")
129
+ return String(node);
130
+ if (node === null || typeof node !== "object")
131
+ return "";
132
+ return Object.entries(node)
133
+ .flatMap(([key, value]) => {
134
+ if (key.startsWith("@_") || key === "fig" || key === "table-wrap" || key === "supplementary-material")
135
+ return [];
136
+ if (key === "#text")
137
+ return [String(value)];
138
+ return Array.isArray(value) ? value.map(flattenTextContent) : [flattenTextContent(value)];
139
+ })
140
+ .filter(Boolean)
141
+ .join("");
142
+ }
143
+ function extractParagraphs(container) {
144
+ const paragraphs = container.p;
145
+ if (paragraphs === undefined)
146
+ return "";
147
+ return (Array.isArray(paragraphs) ? paragraphs : [paragraphs])
148
+ .map((paragraph) => (typeof paragraph === "string" ? paragraph : flattenTextContent(paragraph)))
149
+ .filter(Boolean)
150
+ .join("\n\n");
151
+ }
152
+ function extractSection(section, output) {
153
+ if (section === null || typeof section !== "object")
154
+ return;
155
+ const object = section;
156
+ const text = extractParagraphs(object);
157
+ if (text)
158
+ output.push({ heading: typeof object.title === "string" ? object.title : "", text });
159
+ const nested = object.sec;
160
+ for (const child of nested === undefined ? [] : Array.isArray(nested) ? nested : [nested])
161
+ extractSection(child, output);
162
+ }
163
+ export function parsePmcFullText(xml) {
164
+ const article = xmlParser.parse(xml)?.["pmc-articleset"]?.article;
165
+ const body = article?.body;
166
+ if (body === undefined)
167
+ return null;
168
+ const sections = [];
169
+ for (const section of body.sec === undefined ? [] : Array.isArray(body.sec) ? body.sec : [body.sec])
170
+ extractSection(section, sections);
171
+ if (sections.length === 0) {
172
+ const text = extractParagraphs(body);
173
+ if (text)
174
+ sections.push({ heading: "", text });
175
+ }
176
+ return {
177
+ fullText: sections.map((section) => (section.heading ? `## ${section.heading}\n\n${section.text}` : section.text)).join("\n\n"),
178
+ sections,
179
+ };
180
+ }
181
+ export function createPubmedSource(options = {}) {
182
+ const url = (endpoint, params) => ncbiUrl(options.apiKey, endpoint, params);
183
+ async function searchIdsWithTotal(query, searchOptions, signal) {
184
+ const params = {
185
+ db: "pubmed",
186
+ term: query,
187
+ retmax: searchOptions.maxResults ?? 10,
188
+ retmode: "json",
189
+ sort: searchOptions.sort === "date" ? "pub+date" : "relevance",
190
+ };
191
+ if (searchOptions.dateRange !== undefined) {
192
+ params.mindate = searchOptions.dateRange.from;
193
+ params.maxdate = searchOptions.dateRange.to;
194
+ params.datetype = "pdat";
195
+ }
196
+ const result = await requestJson(url(`${NCBI_BASE}/esearch.fcgi`, params), EsearchResponseSchema, options, signal);
197
+ if (result.status !== "ok")
198
+ return result;
199
+ return {
200
+ status: "ok",
201
+ value: {
202
+ totalFound: parseInt(result.value.esearchresult?.count ?? "0", 10),
203
+ ids: result.value.esearchresult?.idlist ?? [],
204
+ },
205
+ };
206
+ }
207
+ async function fetchArticles(pmids, signal) {
208
+ const result = await requestText(url(`${NCBI_BASE}/efetch.fcgi`, { db: "pubmed", id: pmids.join(","), rettype: "xml", retmode: "xml" }), options, signal);
209
+ if (result.status !== "ok")
210
+ return result;
211
+ if (!/<PubmedArticleSet\b/.test(result.value))
212
+ return { status: "unavailable", detail: "response schema mismatch: missing PubmedArticleSet" };
213
+ return { status: "ok", value: parseEfetch(result.value) };
214
+ }
215
+ return {
216
+ async search(query, searchOptions = {}, signal) {
217
+ const search = await searchIdsWithTotal(query, searchOptions, signal);
218
+ if (search.status !== "ok")
219
+ return search;
220
+ if (search.value.ids.length === 0)
221
+ return { status: "ok", value: { totalFound: search.value.totalFound, results: [] } };
222
+ const summary = await requestText(url(`${NCBI_BASE}/esummary.fcgi`, { db: "pubmed", id: search.value.ids.join(","), retmode: "xml" }), options, signal);
223
+ if (summary.status !== "ok")
224
+ return summary;
225
+ return { status: "ok", value: { totalFound: search.value.totalFound, results: parseEsummary(summary.value) } };
226
+ },
227
+ async searchIds(query, maxResults, signal) {
228
+ const result = await searchIdsWithTotal(query, { maxResults }, signal);
229
+ return result.status === "ok" ? { status: "ok", value: result.value.ids } : result;
230
+ },
231
+ async matchCitation(fields, signal) {
232
+ const bdata = [fields.venue ?? "", fields.year ?? "", fields.volume ?? "", fields.firstPage ?? "", fields.firstAuthor ?? "", fields.key, ""].join("|");
233
+ const result = await requestText(url(`${NCBI_BASE}/ecitmatch.cgi`, { db: "pubmed", rettype: "xml", bdata }), options, signal);
234
+ if (result.status !== "ok")
235
+ return result;
236
+ const pmids = result.value
237
+ .trim()
238
+ .split(/\r?\n/)
239
+ .flatMap((line) => {
240
+ const value = line.split("|").at(-1)?.trim();
241
+ return value !== undefined && /^\d+$/.test(value) ? [value] : [];
242
+ });
243
+ return { status: "ok", value: pmids };
244
+ },
245
+ fetchArticles,
246
+ async articleDetails(pmids, signal) {
247
+ const [articlesResult, conversionResult] = await Promise.all([
248
+ fetchArticles(pmids, signal),
249
+ requestJson(url(NCBI_IDCONV, { ids: pmids.join(","), format: "json" }), IdConvResponseSchema, options, signal),
250
+ ]);
251
+ if (articlesResult.status !== "ok")
252
+ return articlesResult;
253
+ if (conversionResult.status === "ok") {
254
+ const conversions = parseIdConvResponse(conversionResult.value);
255
+ for (const article of articlesResult.value)
256
+ article.pmcId = conversions.get(article.pmid) ?? null;
257
+ }
258
+ const found = new Set(articlesResult.value.map((article) => article.pmid));
259
+ return { status: "ok", value: { articles: articlesResult.value, notFound: pmids.filter((pmid) => !found.has(pmid)) } };
260
+ },
261
+ async fetchFullText(pmcId, signal) {
262
+ const result = await requestText(url(`${NCBI_BASE}/efetch.fcgi`, { db: "pmc", id: pmcId.replace(/^PMC/i, ""), rettype: "xml", retmode: "xml" }), options, signal);
263
+ return result.status === "ok" ? { status: "ok", value: parsePmcFullText(result.value) } : result;
264
+ },
265
+ };
266
+ }
@@ -0,0 +1,24 @@
1
+ import { type SourceHttpOptions, type SourceHttpResult } from "./http.js";
2
+ export interface SemanticScholarPaper {
3
+ id: string;
4
+ title?: string;
5
+ abstract?: string;
6
+ year?: number;
7
+ venue?: string;
8
+ citationCount?: number;
9
+ url?: string;
10
+ authors: string[];
11
+ externalIds?: Record<string, string>;
12
+ }
13
+ export interface SemanticScholarSearchPaper extends SemanticScholarPaper {
14
+ title: string;
15
+ }
16
+ export interface SemanticScholarSourceOptions extends SourceHttpOptions {
17
+ readonly apiKey?: string;
18
+ }
19
+ export interface SemanticScholarSource {
20
+ search(query: string, limit: number, signal?: AbortSignal): Promise<SourceHttpResult<SemanticScholarSearchPaper[]>>;
21
+ lookupIdentifier(identifier: string, signal?: AbortSignal): Promise<SourceHttpResult<SemanticScholarPaper>>;
22
+ }
23
+ export declare function parseSemanticScholarResponse(raw: unknown): SemanticScholarSearchPaper[];
24
+ export declare function createSemanticScholarSource(options?: SemanticScholarSourceOptions): SemanticScholarSource;
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import { requestJson } from "./http.js";
3
+ const BASE_URL = "https://api.semanticscholar.org/graph/v1";
4
+ const FIELDS = "paperId,title,abstract,year,venue,citationCount,url,authors,externalIds";
5
+ const WirePaperSchema = z
6
+ .object({
7
+ paperId: z.string().optional(),
8
+ title: z.string().nullable().optional(),
9
+ abstract: z.string().nullable().optional(),
10
+ year: z.number().nullable().optional(),
11
+ venue: z.string().nullable().optional(),
12
+ citationCount: z.number().nullable().optional(),
13
+ url: z.string().nullable().optional(),
14
+ authors: z.array(z.object({ name: z.string().nullable().optional() })).optional(),
15
+ externalIds: z.record(z.string(), z.unknown()).nullable().optional(),
16
+ })
17
+ .passthrough();
18
+ const ExactPaperSchema = WirePaperSchema.extend({ paperId: z.string() });
19
+ const SearchResponseSchema = z.object({ data: z.array(WirePaperSchema).optional() });
20
+ function stringExternalIds(value) {
21
+ if (value === null || value === undefined)
22
+ return undefined;
23
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
24
+ return entries.length === 0 ? undefined : Object.fromEntries(entries);
25
+ }
26
+ function mapPaper(raw) {
27
+ if (raw.paperId === undefined || raw.paperId.trim() === "")
28
+ return undefined;
29
+ const authors = raw.authors?.flatMap((author) => (typeof author.name === "string" && author.name.trim() ? [author.name] : [])) ?? [];
30
+ const externalIds = stringExternalIds(raw.externalIds);
31
+ return {
32
+ id: raw.paperId,
33
+ ...(typeof raw.title === "string" ? { title: raw.title } : {}),
34
+ ...(typeof raw.abstract === "string" ? { abstract: raw.abstract } : {}),
35
+ ...(typeof raw.year === "number" ? { year: raw.year } : {}),
36
+ ...(typeof raw.venue === "string" ? { venue: raw.venue } : {}),
37
+ ...(typeof raw.citationCount === "number" ? { citationCount: raw.citationCount } : {}),
38
+ ...(typeof raw.url === "string" ? { url: raw.url } : {}),
39
+ authors,
40
+ ...(externalIds === undefined ? {} : { externalIds }),
41
+ };
42
+ }
43
+ export function parseSemanticScholarResponse(raw) {
44
+ const parsed = SearchResponseSchema.safeParse(raw);
45
+ if (!parsed.success)
46
+ return [];
47
+ return (parsed.data.data ?? []).flatMap((paper) => {
48
+ const mapped = mapPaper(paper);
49
+ return mapped?.title === undefined ? [] : [mapped];
50
+ });
51
+ }
52
+ export function createSemanticScholarSource(options = {}) {
53
+ const headers = options.apiKey === undefined ? {} : { "x-api-key": options.apiKey };
54
+ return {
55
+ async search(query, limit, signal) {
56
+ const params = new URLSearchParams({ query, limit: String(limit), fields: FIELDS });
57
+ const result = await requestJson(`${BASE_URL}/paper/search?${params}`, SearchResponseSchema, options, signal, { headers });
58
+ if (result.status !== "ok")
59
+ return result;
60
+ return { status: "ok", value: parseSemanticScholarResponse(result.value) };
61
+ },
62
+ async lookupIdentifier(identifier, signal) {
63
+ const url = `${BASE_URL}/paper/${encodeURIComponent(identifier)}?fields=${encodeURIComponent(FIELDS)}`;
64
+ const result = await requestJson(url, ExactPaperSchema, options, signal, { headers });
65
+ if (result.status !== "ok")
66
+ return result;
67
+ const paper = mapPaper(result.value);
68
+ return paper === undefined ? { status: "unavailable", detail: "response schema mismatch: missing paperId" } : { status: "ok", value: paper };
69
+ },
70
+ };
71
+ }