@inflexa-ai/harness 0.15.1 → 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.
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +5 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +5 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn.d.ts +2 -0
- package/dist/app/chat-turn.js +11 -2
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +2 -1
- package/dist/citations/__fixtures__/resolver.d.ts +3 -0
- package/dist/citations/__fixtures__/resolver.js +9 -0
- package/dist/citations/aggregate.d.ts +3 -0
- package/dist/citations/aggregate.js +44 -0
- package/dist/citations/clients/arxiv.d.ts +6 -0
- package/dist/citations/clients/arxiv.js +34 -0
- package/dist/citations/clients/common.d.ts +11 -0
- package/dist/citations/clients/common.js +43 -0
- package/dist/citations/clients/crossref.d.ts +8 -0
- package/dist/citations/clients/crossref.js +98 -0
- package/dist/citations/clients/doi-registry.d.ts +3 -0
- package/dist/citations/clients/doi-registry.js +95 -0
- package/dist/citations/clients/pubmed.d.ts +6 -0
- package/dist/citations/clients/pubmed.js +83 -0
- package/dist/citations/clients/semantic-scholar.d.ts +6 -0
- package/dist/citations/clients/semantic-scholar.js +60 -0
- package/dist/citations/compare.d.ts +9 -0
- package/dist/citations/compare.js +72 -0
- package/dist/citations/match.d.ts +11 -0
- package/dist/citations/match.js +173 -0
- package/dist/citations/normalize.d.ts +31 -0
- package/dist/citations/normalize.js +187 -0
- package/dist/citations/plan.d.ts +2 -0
- package/dist/citations/plan.js +59 -0
- package/dist/citations/resolve.d.ts +28 -0
- package/dist/citations/resolve.js +238 -0
- package/dist/citations/types.d.ts +583 -0
- package/dist/citations/types.js +129 -0
- package/dist/contracts/chat-events.d.ts +16 -0
- package/dist/contracts/message.d.ts +13 -8
- package/dist/contracts/schemas/chat-events.d.ts +7 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/schemas/chat-parts.d.ts +8 -8
- package/dist/contracts/target-dossier.d.ts +20 -20
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +14 -10
- package/dist/index.js +20 -16
- package/dist/lib/cache.d.ts +8 -0
- package/dist/lib/cache.js +35 -0
- package/dist/lib/rate-limit.d.ts +35 -0
- package/dist/lib/rate-limit.js +121 -0
- package/dist/literature/sources/arxiv.d.ts +22 -0
- package/dist/literature/sources/arxiv.js +103 -0
- package/dist/literature/sources/http.d.ts +28 -0
- package/dist/literature/sources/http.js +97 -0
- package/dist/literature/sources/pubmed.d.ts +68 -0
- package/dist/literature/sources/pubmed.js +266 -0
- package/dist/literature/sources/semantic-scholar.d.ts +24 -0
- package/dist/literature/sources/semantic-scholar.js +71 -0
- package/dist/memory/content-to-cortex.d.ts +12 -11
- package/dist/memory/content-to-cortex.js +17 -14
- package/dist/memory/conversation-display-backfill.d.ts +25 -0
- package/dist/memory/conversation-display-backfill.js +97 -0
- package/dist/memory/conversation-display-recorder.d.ts +17 -0
- package/dist/memory/conversation-display-recorder.js +156 -0
- package/dist/memory/conversation-display-replay.d.ts +19 -0
- package/dist/memory/conversation-display-replay.js +39 -0
- package/dist/memory/conversation-display-storage.d.ts +86 -0
- package/dist/memory/conversation-display-storage.js +168 -0
- package/dist/memory/reconstruct-cards.d.ts +7 -1
- package/dist/memory/reconstruct-cards.js +5 -1
- package/dist/memory/thread-history.d.ts +46 -11
- package/dist/memory/thread-history.js +55 -13
- package/dist/memory/thread-store.d.ts +142 -25
- package/dist/memory/thread-store.js +228 -40
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +2 -0
- package/dist/runtime/assemble.d.ts +50 -4
- package/dist/runtime/assemble.js +47 -3
- package/dist/runtime/boot.d.ts +8 -2
- package/dist/runtime/boot.js +20 -2
- package/dist/state/init.js +50 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
- package/dist/tools/bio/keys.d.ts +1 -0
- package/dist/tools/bio/pubmed.js +4 -4
- package/dist/tools/lib/ncbi-utils.d.ts +3 -58
- package/dist/tools/lib/ncbi-utils.js +2 -286
- package/dist/tools/lib/pubmed-client.d.ts +5 -12
- package/dist/tools/lib/pubmed-client.js +20 -79
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/research/resolve-citation.d.ts +3 -0
- package/dist/tools/research/resolve-citation.js +15 -0
- package/dist/tools/research/search-arxiv.d.ts +3 -19
- package/dist/tools/research/search-arxiv.js +9 -105
- package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
- package/dist/tools/research/search-semantic-scholar.js +29 -88
- package/dist/tools/sandbox/list-available-refs.js +1 -2
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +5 -0
- package/dist/workflows/sandbox-step.js +1 -0
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
- package/dist/workflows/target-assessment/schemas.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
package/dist/state/init.js
CHANGED
|
@@ -255,6 +255,9 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
255
255
|
role TEXT,
|
|
256
256
|
content_jsonb JSONB,
|
|
257
257
|
message_envelope JSON,
|
|
258
|
+
-- Versioned AI SDK UIMessage projection for the whole turn. Present only
|
|
259
|
+
-- on the genuine-user-start row. Model-history reads never select it.
|
|
260
|
+
display_envelope JSONB,
|
|
258
261
|
tokens INTEGER NOT NULL,
|
|
259
262
|
reported_usage JSONB,
|
|
260
263
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
@@ -272,12 +275,57 @@ CREATE TABLE IF NOT EXISTS cortex_analysis_threads (
|
|
|
272
275
|
title TEXT,
|
|
273
276
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
274
277
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
275
|
-
deleted_at TIMESTAMPTZ
|
|
278
|
+
deleted_at TIMESTAMPTZ,
|
|
279
|
+
thread_type TEXT NOT NULL DEFAULT 'conversation',
|
|
280
|
+
-- The cascade on this self-reference is a BACKSTOP, never the deletion path.
|
|
281
|
+
-- messages carries no foreign key to this table, so a cascade firing on its
|
|
282
|
+
-- own drops a descendant's metadata row while leaving that descendant's
|
|
283
|
+
-- messages rows behind with nothing naming them, beyond the reach of any
|
|
284
|
+
-- later thread-scoped or analysis-scoped reclamation. purgeThread therefore
|
|
285
|
+
-- walks the subtree and deletes its messages rows explicitly, to the same
|
|
286
|
+
-- depth the cascade reaches.
|
|
287
|
+
parent_thread_id TEXT REFERENCES cortex_analysis_threads(thread_id) ON DELETE CASCADE,
|
|
288
|
+
parent_seq BIGINT
|
|
276
289
|
);
|
|
277
290
|
|
|
291
|
+
-- These three sit here, and not in the additive block further down, because the
|
|
292
|
+
-- index below reads one of them. CREATE TABLE IF NOT EXISTS adds no column to a
|
|
293
|
+
-- table that already exists, so on every database but a brand new one the column
|
|
294
|
+
-- arrives from an ALTER -- and the whole additive block runs after this text. An
|
|
295
|
+
-- index declared here over a column added there fails with 42703 on exactly the
|
|
296
|
+
-- databases a migration exists to serve. Additive with no backfill either way: an
|
|
297
|
+
-- existing row reads back as a root conversation thread with no anchor, which is
|
|
298
|
+
-- what it is.
|
|
299
|
+
ALTER TABLE cortex_analysis_threads
|
|
300
|
+
ADD COLUMN IF NOT EXISTS thread_type TEXT NOT NULL DEFAULT 'conversation';
|
|
301
|
+
|
|
302
|
+
ALTER TABLE cortex_analysis_threads
|
|
303
|
+
ADD COLUMN IF NOT EXISTS parent_thread_id TEXT REFERENCES cortex_analysis_threads(thread_id) ON DELETE CASCADE;
|
|
304
|
+
|
|
305
|
+
ALTER TABLE cortex_analysis_threads
|
|
306
|
+
ADD COLUMN IF NOT EXISTS parent_seq BIGINT;
|
|
307
|
+
|
|
278
308
|
CREATE INDEX IF NOT EXISTS idx_cortex_analysis_threads_analysis
|
|
279
309
|
ON cortex_analysis_threads(analysis_id) WHERE deleted_at IS NULL;
|
|
280
310
|
|
|
311
|
+
-- Not partial, unlike the analysis index above. Two of the three readers of
|
|
312
|
+
-- parent_thread_id carry no deleted_at predicate and cannot: the recursive
|
|
313
|
+
-- subtree walk in thread-store.ts must reach archived descendants, and the
|
|
314
|
+
-- referential trigger behind ON DELETE CASCADE is the database's own query.
|
|
315
|
+
-- Postgres uses a partial index only where it can prove the predicate holds, so
|
|
316
|
+
-- a partial index here leaves both to a sequential scan of the whole table.
|
|
317
|
+
-- Over 150k rows that measured 28 ms per walk and 20 ms per cascade, against
|
|
318
|
+
-- 0.06 ms and 0.10 ms with this one. The third reader, a listing filtered by
|
|
319
|
+
-- parent, keeps an index scan either way.
|
|
320
|
+
--
|
|
321
|
+
-- The old partial index goes by name: CREATE INDEX IF NOT EXISTS keeps whatever
|
|
322
|
+
-- definition already holds the name, so a database that built the partial one
|
|
323
|
+
-- would never see this.
|
|
324
|
+
DROP INDEX IF EXISTS idx_cortex_analysis_threads_parent;
|
|
325
|
+
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_analysis_threads_parent_fk
|
|
327
|
+
ON cortex_analysis_threads(parent_thread_id);
|
|
328
|
+
|
|
281
329
|
-- Working memory — the harness WorkingMemory module's table, one row per
|
|
282
330
|
-- analysis. The data column holds a JSONB object with four sections (goal,
|
|
283
331
|
-- constraints, hypotheses, findings). Section updates are a read-modify-write
|
|
@@ -475,6 +523,7 @@ export async function initCortexState(pool, injected) {
|
|
|
475
523
|
ALTER TABLE messages ALTER COLUMN message_envelope TYPE JSON USING message_envelope::text::json;
|
|
476
524
|
END IF;
|
|
477
525
|
END $$`,
|
|
526
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS display_envelope JSONB",
|
|
478
527
|
// Grant key an 'always' records the standing grant under. Nullable
|
|
479
528
|
// and unbackfilled: every new row writes it (command when absent), and
|
|
480
529
|
// orphaned legacy pending rows are swept to 'expired' before any answer
|
package/dist/state/schema.d.ts
CHANGED
|
@@ -46,10 +46,10 @@ export declare const ArtifactRowSchema: z.ZodObject<{
|
|
|
46
46
|
}, z.core.$strip>;
|
|
47
47
|
export type ArtifactRow = z.infer<typeof ArtifactRowSchema>;
|
|
48
48
|
export declare const RunStatus: z.ZodEnum<{
|
|
49
|
+
partial: "partial";
|
|
49
50
|
running: "running";
|
|
50
51
|
completed: "completed";
|
|
51
52
|
failed: "failed";
|
|
52
|
-
partial: "partial";
|
|
53
53
|
canceled: "canceled";
|
|
54
54
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
55
55
|
}>;
|
|
@@ -77,10 +77,10 @@ export declare const CortexRunRowSchema: z.ZodObject<{
|
|
|
77
77
|
threadId: z.ZodNullable<z.ZodString>;
|
|
78
78
|
workflowName: z.ZodString;
|
|
79
79
|
status: z.ZodEnum<{
|
|
80
|
+
partial: "partial";
|
|
80
81
|
running: "running";
|
|
81
82
|
completed: "completed";
|
|
82
83
|
failed: "failed";
|
|
83
|
-
partial: "partial";
|
|
84
84
|
canceled: "canceled";
|
|
85
85
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
86
86
|
}>;
|
|
@@ -51,7 +51,7 @@ export declare const genePreclinicalProfileTool: import("../define-tool.js").Too
|
|
|
51
51
|
geneSymbol: string;
|
|
52
52
|
include?: ("expression" | "knockout")[] | undefined;
|
|
53
53
|
species?: ("homo_sapiens" | "mus_musculus" | "rattus_norvegicus" | "canis_lupus_familiaris" | "macaca_mulatta")[] | undefined;
|
|
54
|
-
minRank?: "
|
|
54
|
+
minRank?: "absent" | "high" | "medium" | "low" | undefined;
|
|
55
55
|
tissueLimit?: number | undefined;
|
|
56
56
|
phenotypeLimit?: number | undefined;
|
|
57
57
|
}, PreclinicalOutput>;
|
package/dist/tools/bio/keys.d.ts
CHANGED
package/dist/tools/bio/pubmed.js
CHANGED
|
@@ -140,16 +140,16 @@ export function createPubMedTool(deps) {
|
|
|
140
140
|
return `fulltext ${input.pmcId ?? ""}`;
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
execute: async (input) => {
|
|
143
|
+
execute: async (input, context) => {
|
|
144
144
|
switch (input.action) {
|
|
145
145
|
case "search":
|
|
146
146
|
return ok(await searchPubmed(deps.ncbiApiKey, input.query, {
|
|
147
147
|
maxResults: input.maxResults ?? DEFAULTS.searchResults,
|
|
148
148
|
...(input.sort ? { sort: input.sort } : {}),
|
|
149
149
|
...(input.dateRange ? { dateRange: input.dateRange } : {}),
|
|
150
|
-
}));
|
|
150
|
+
}, context.signal));
|
|
151
151
|
case "details": {
|
|
152
|
-
const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids);
|
|
152
|
+
const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids, context.signal);
|
|
153
153
|
return ok({
|
|
154
154
|
articles: boundArticleDetails(articles, {
|
|
155
155
|
maxAuthors: input.maxAuthors ?? DEFAULTS.maxAuthors,
|
|
@@ -162,7 +162,7 @@ export function createPubMedTool(deps) {
|
|
|
162
162
|
const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId, {
|
|
163
163
|
...(input.sections ? { sections: input.sections } : {}),
|
|
164
164
|
maxChars: input.maxChars ?? DEFAULTS.maxChars,
|
|
165
|
-
});
|
|
165
|
+
}, context.signal);
|
|
166
166
|
// "Not open-access" is an expected outcome — a data variant, not an error.
|
|
167
167
|
if (!parsed)
|
|
168
168
|
return ok({ pmcId: input.pmcId, available: false });
|
|
@@ -1,58 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Provides base URL constants, API key injection, and XML-to-article
|
|
5
|
-
* mapping functions for PubMed and PMC DTD structures.
|
|
6
|
-
*/
|
|
7
|
-
export declare const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
8
|
-
export declare const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
|
|
9
|
-
/**
|
|
10
|
-
* Append `api_key` and optional extra params to a base URL.
|
|
11
|
-
* Omits the key when `apiKey` is undefined.
|
|
12
|
-
*/
|
|
13
|
-
export declare function ncbiUrl(apiKey: string | undefined, base: string, params: Record<string, string | number | undefined>): string;
|
|
14
|
-
export interface PubMedSummary {
|
|
15
|
-
pmid: string;
|
|
16
|
-
title: string;
|
|
17
|
-
journal: string;
|
|
18
|
-
year: string;
|
|
19
|
-
authors: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Parse esummary XML into PubMedSummary records.
|
|
23
|
-
*/
|
|
24
|
-
export declare function parseEsummary(xml: string): PubMedSummary[];
|
|
25
|
-
export interface ArticleDetail {
|
|
26
|
-
pmid: string;
|
|
27
|
-
title: string;
|
|
28
|
-
abstract: string;
|
|
29
|
-
authors: string[];
|
|
30
|
-
journal: string;
|
|
31
|
-
year: string;
|
|
32
|
-
doi: string;
|
|
33
|
-
meshTerms: string[];
|
|
34
|
-
pmcId: string | null;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Parse efetch PubMed XML into ArticleDetail records.
|
|
38
|
-
* `pmcId` is left null here — resolved separately via ID Converter.
|
|
39
|
-
*/
|
|
40
|
-
export declare function parseEfetch(xml: string): ArticleDetail[];
|
|
41
|
-
/**
|
|
42
|
-
* Parse ID Converter JSON response into a PMID → PMC ID map.
|
|
43
|
-
* Returns only entries that have a PMC ID.
|
|
44
|
-
*/
|
|
45
|
-
export declare function parseIdConvResponse(data: unknown): Map<string, string>;
|
|
46
|
-
export interface ArticleSection {
|
|
47
|
-
heading: string;
|
|
48
|
-
text: string;
|
|
49
|
-
}
|
|
50
|
-
export interface FullTextResult {
|
|
51
|
-
fullText: string;
|
|
52
|
-
sections: ArticleSection[];
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Parse PMC efetch XML into plain text with section structure.
|
|
56
|
-
* Omits figures, tables, and supplementary material markup.
|
|
57
|
-
*/
|
|
58
|
-
export declare function parsePmcFullText(xml: string): FullTextResult | null;
|
|
1
|
+
/** @deprecated Import NCBI authority contracts from literature/sources/pubmed. */
|
|
2
|
+
export { NCBI_BASE, NCBI_IDCONV, ncbiUrl, parseEfetch, parseEsummary, parseIdConvResponse, parsePmcFullText } from "../../literature/sources/pubmed.js";
|
|
3
|
+
export type { ArticleDetail, ArticleSection, FullTextResult, PubMedSummary } from "../../literature/sources/pubmed.js";
|
|
@@ -1,286 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Provides base URL constants, API key injection, and XML-to-article
|
|
5
|
-
* mapping functions for PubMed and PMC DTD structures.
|
|
6
|
-
*/
|
|
7
|
-
import { XMLParser } from "fast-xml-parser";
|
|
8
|
-
// ── NCBI E-utilities base URLs ──────────────────────────────────────
|
|
9
|
-
export const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
10
|
-
export const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
|
|
11
|
-
// ── API key injection ───────────────────────────────────────────────
|
|
12
|
-
/**
|
|
13
|
-
* Append `api_key` and optional extra params to a base URL.
|
|
14
|
-
* Omits the key when `apiKey` is undefined.
|
|
15
|
-
*/
|
|
16
|
-
export function ncbiUrl(apiKey, base, params) {
|
|
17
|
-
const url = new URL(base);
|
|
18
|
-
if (apiKey) {
|
|
19
|
-
url.searchParams.set("api_key", apiKey);
|
|
20
|
-
}
|
|
21
|
-
for (const [k, v] of Object.entries(params)) {
|
|
22
|
-
if (v !== undefined) {
|
|
23
|
-
url.searchParams.set(k, String(v));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return url.toString();
|
|
27
|
-
}
|
|
28
|
-
// ── XML parser (shared, configured once) ────────────────────────────
|
|
29
|
-
const xmlParser = new XMLParser({
|
|
30
|
-
ignoreAttributes: false,
|
|
31
|
-
attributeNamePrefix: "@_",
|
|
32
|
-
isArray: (name) => ["PubmedArticle", "Author", "MeshHeading", "AbstractText", "sec", "p", "record"].includes(name),
|
|
33
|
-
textNodeName: "#text",
|
|
34
|
-
});
|
|
35
|
-
function getItemValue(items, name) {
|
|
36
|
-
if (!items)
|
|
37
|
-
return "";
|
|
38
|
-
const arr = Array.isArray(items) ? items : [items];
|
|
39
|
-
const item = arr.find((i) => i["@_Name"] === name);
|
|
40
|
-
return item?.["#text"] ?? "";
|
|
41
|
-
}
|
|
42
|
-
function getAuthorsFromDocSum(items) {
|
|
43
|
-
if (!items)
|
|
44
|
-
return "";
|
|
45
|
-
const arr = Array.isArray(items) ? items : [items];
|
|
46
|
-
const authorList = arr.find((i) => i["@_Name"] === "AuthorList");
|
|
47
|
-
if (!authorList?.Item)
|
|
48
|
-
return "";
|
|
49
|
-
const authors = Array.isArray(authorList.Item) ? authorList.Item : [authorList.Item];
|
|
50
|
-
const names = authors.map((a) => a["#text"] ?? "").filter(Boolean);
|
|
51
|
-
if (names.length === 0)
|
|
52
|
-
return "";
|
|
53
|
-
if (names.length <= 3)
|
|
54
|
-
return names.join(", ");
|
|
55
|
-
return `${names[0]} et al.`;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Parse esummary XML into PubMedSummary records.
|
|
59
|
-
*/
|
|
60
|
-
export function parseEsummary(xml) {
|
|
61
|
-
const parsed = xmlParser.parse(xml);
|
|
62
|
-
const result = parsed?.eSummaryResult ?? parsed;
|
|
63
|
-
const docSums = result?.DocSum;
|
|
64
|
-
if (!docSums)
|
|
65
|
-
return [];
|
|
66
|
-
const docs = Array.isArray(docSums) ? docSums : [docSums];
|
|
67
|
-
return docs.map((doc) => {
|
|
68
|
-
const items = doc.Item;
|
|
69
|
-
const pubDate = getItemValue(items, "PubDate");
|
|
70
|
-
return {
|
|
71
|
-
pmid: String(doc.Id ?? ""),
|
|
72
|
-
title: getItemValue(items, "Title"),
|
|
73
|
-
journal: getItemValue(items, "FullJournalName") || getItemValue(items, "Source"),
|
|
74
|
-
year: pubDate ? String(pubDate).slice(0, 4) : "",
|
|
75
|
-
authors: getAuthorsFromDocSum(items),
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Parse efetch PubMed XML into ArticleDetail records.
|
|
81
|
-
* `pmcId` is left null here — resolved separately via ID Converter.
|
|
82
|
-
*/
|
|
83
|
-
export function parseEfetch(xml) {
|
|
84
|
-
const parsed = xmlParser.parse(xml);
|
|
85
|
-
const articleSet = parsed?.PubmedArticleSet;
|
|
86
|
-
if (!articleSet)
|
|
87
|
-
return [];
|
|
88
|
-
const articles = Array.isArray(articleSet.PubmedArticle) ? articleSet.PubmedArticle : articleSet.PubmedArticle ? [articleSet.PubmedArticle] : [];
|
|
89
|
-
return articles.map((entry) => {
|
|
90
|
-
const article = entry;
|
|
91
|
-
const medlineCitation = article.MedlineCitation;
|
|
92
|
-
const articleData = medlineCitation?.Article;
|
|
93
|
-
// PMID
|
|
94
|
-
const pmidObj = medlineCitation?.PMID;
|
|
95
|
-
const pmid = typeof pmidObj === "object" && pmidObj !== null ? String(pmidObj["#text"] ?? "") : String(pmidObj ?? "");
|
|
96
|
-
// Title
|
|
97
|
-
const title = String(articleData?.ArticleTitle ?? "");
|
|
98
|
-
// Abstract
|
|
99
|
-
const abstractObj = articleData?.Abstract;
|
|
100
|
-
let abstract = "";
|
|
101
|
-
if (abstractObj?.AbstractText) {
|
|
102
|
-
const texts = Array.isArray(abstractObj.AbstractText) ? abstractObj.AbstractText : [abstractObj.AbstractText];
|
|
103
|
-
abstract = texts
|
|
104
|
-
.map((t) => {
|
|
105
|
-
if (typeof t === "string")
|
|
106
|
-
return t;
|
|
107
|
-
if (typeof t === "object" && t !== null) {
|
|
108
|
-
const label = t["@_Label"];
|
|
109
|
-
const text = t["#text"] ?? "";
|
|
110
|
-
return label ? `${label}: ${text}` : String(text);
|
|
111
|
-
}
|
|
112
|
-
return String(t);
|
|
113
|
-
})
|
|
114
|
-
.join("\n\n");
|
|
115
|
-
}
|
|
116
|
-
// Authors
|
|
117
|
-
const authorListObj = articleData?.AuthorList;
|
|
118
|
-
const rawAuthors = authorListObj?.Author;
|
|
119
|
-
const authorArr = Array.isArray(rawAuthors) ? rawAuthors : rawAuthors ? [rawAuthors] : [];
|
|
120
|
-
const authors = authorArr.map((a) => {
|
|
121
|
-
if (a.CollectiveName)
|
|
122
|
-
return a.CollectiveName;
|
|
123
|
-
return [a.ForeName ?? a.Initials, a.LastName].filter(Boolean).join(" ");
|
|
124
|
-
});
|
|
125
|
-
// Journal + year
|
|
126
|
-
const journalObj = articleData?.Journal;
|
|
127
|
-
const journal = String(journalObj?.Title ?? journalObj?.ISOAbbreviation ?? "");
|
|
128
|
-
const journalIssue = journalObj?.JournalIssue;
|
|
129
|
-
const pubDate = journalIssue?.PubDate;
|
|
130
|
-
const year = String(pubDate?.Year ?? "");
|
|
131
|
-
// DOI
|
|
132
|
-
let doi = "";
|
|
133
|
-
const articleIdList = article.PubmedData?.ArticleIdList;
|
|
134
|
-
if (articleIdList?.ArticleId) {
|
|
135
|
-
const ids = Array.isArray(articleIdList.ArticleId) ? articleIdList.ArticleId : [articleIdList.ArticleId];
|
|
136
|
-
for (const idObj of ids) {
|
|
137
|
-
if (typeof idObj === "object" && idObj !== null) {
|
|
138
|
-
const typed = idObj;
|
|
139
|
-
if (typed["@_IdType"] === "doi") {
|
|
140
|
-
doi = String(typed["#text"] ?? "");
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// MeSH terms
|
|
146
|
-
const meshList = medlineCitation?.MeshHeadingList;
|
|
147
|
-
const meshHeadings = meshList?.MeshHeading
|
|
148
|
-
? Array.isArray(meshList.MeshHeading)
|
|
149
|
-
? meshList.MeshHeading
|
|
150
|
-
: [meshList.MeshHeading]
|
|
151
|
-
: [];
|
|
152
|
-
const meshTerms = meshHeadings
|
|
153
|
-
.map((h) => {
|
|
154
|
-
const desc = h.DescriptorName;
|
|
155
|
-
if (typeof desc === "string")
|
|
156
|
-
return desc;
|
|
157
|
-
if (typeof desc === "object" && desc !== null)
|
|
158
|
-
return desc["#text"] ?? "";
|
|
159
|
-
return "";
|
|
160
|
-
})
|
|
161
|
-
.filter(Boolean);
|
|
162
|
-
return {
|
|
163
|
-
pmid,
|
|
164
|
-
title,
|
|
165
|
-
abstract,
|
|
166
|
-
authors,
|
|
167
|
-
journal,
|
|
168
|
-
year,
|
|
169
|
-
doi,
|
|
170
|
-
meshTerms,
|
|
171
|
-
pmcId: null, // resolved separately via ID Converter
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
// ── NCBI ID Converter mapping ───────────────────────────────────────
|
|
176
|
-
/**
|
|
177
|
-
* Parse ID Converter JSON response into a PMID → PMC ID map.
|
|
178
|
-
* Returns only entries that have a PMC ID.
|
|
179
|
-
*/
|
|
180
|
-
export function parseIdConvResponse(data) {
|
|
181
|
-
const map = new Map();
|
|
182
|
-
const typed = data;
|
|
183
|
-
if (!typed?.records)
|
|
184
|
-
return map;
|
|
185
|
-
for (const rec of typed.records) {
|
|
186
|
-
if (rec.pmid && rec.pmcid) {
|
|
187
|
-
map.set(rec.pmid, rec.pmcid);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return map;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Parse PMC efetch XML into plain text with section structure.
|
|
194
|
-
* Omits figures, tables, and supplementary material markup.
|
|
195
|
-
*/
|
|
196
|
-
export function parsePmcFullText(xml) {
|
|
197
|
-
const parsed = xmlParser.parse(xml);
|
|
198
|
-
const articleSet = parsed?.["pmc-articleset"];
|
|
199
|
-
const article = articleSet?.article;
|
|
200
|
-
if (!article)
|
|
201
|
-
return null;
|
|
202
|
-
const body = article.body;
|
|
203
|
-
if (!body)
|
|
204
|
-
return null;
|
|
205
|
-
const sections = [];
|
|
206
|
-
const rawSecs = body.sec ? (Array.isArray(body.sec) ? body.sec : [body.sec]) : [];
|
|
207
|
-
for (const sec of rawSecs) {
|
|
208
|
-
extractSection(sec, sections);
|
|
209
|
-
}
|
|
210
|
-
// If no sections found, try to extract body text directly
|
|
211
|
-
if (sections.length === 0) {
|
|
212
|
-
const bodyText = extractParagraphs(body);
|
|
213
|
-
if (bodyText) {
|
|
214
|
-
sections.push({ heading: "", text: bodyText });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const fullText = sections.map((s) => (s.heading ? `## ${s.heading}\n\n${s.text}` : s.text)).join("\n\n");
|
|
218
|
-
return { fullText, sections };
|
|
219
|
-
}
|
|
220
|
-
function extractSection(sec, out) {
|
|
221
|
-
if (!sec || typeof sec !== "object")
|
|
222
|
-
return;
|
|
223
|
-
const s = sec;
|
|
224
|
-
const heading = typeof s.title === "string" ? s.title : "";
|
|
225
|
-
const text = extractParagraphs(s);
|
|
226
|
-
if (text) {
|
|
227
|
-
out.push({ heading, text });
|
|
228
|
-
}
|
|
229
|
-
// Recurse into nested sections
|
|
230
|
-
if (s.sec) {
|
|
231
|
-
const nested = Array.isArray(s.sec) ? s.sec : [s.sec];
|
|
232
|
-
for (const sub of nested) {
|
|
233
|
-
extractSection(sub, out);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function extractParagraphs(container) {
|
|
238
|
-
const paragraphs = container.p;
|
|
239
|
-
if (!paragraphs)
|
|
240
|
-
return "";
|
|
241
|
-
const pArr = Array.isArray(paragraphs) ? paragraphs : [paragraphs];
|
|
242
|
-
return pArr
|
|
243
|
-
.map((p) => {
|
|
244
|
-
if (typeof p === "string")
|
|
245
|
-
return p;
|
|
246
|
-
if (typeof p === "object" && p !== null) {
|
|
247
|
-
return flattenTextContent(p);
|
|
248
|
-
}
|
|
249
|
-
return String(p);
|
|
250
|
-
})
|
|
251
|
-
.filter(Boolean)
|
|
252
|
-
.join("\n\n");
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Recursively extract text content from a parsed XML node,
|
|
256
|
-
* ignoring markup like <fig>, <table-wrap>, <xref>, etc.
|
|
257
|
-
*/
|
|
258
|
-
function flattenTextContent(node) {
|
|
259
|
-
if (typeof node === "string")
|
|
260
|
-
return node;
|
|
261
|
-
if (typeof node === "number")
|
|
262
|
-
return String(node);
|
|
263
|
-
if (!node || typeof node !== "object")
|
|
264
|
-
return "";
|
|
265
|
-
const obj = node;
|
|
266
|
-
const parts = [];
|
|
267
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
268
|
-
// Skip attributes and figure/table elements
|
|
269
|
-
if (key.startsWith("@_"))
|
|
270
|
-
continue;
|
|
271
|
-
if (key === "fig" || key === "table-wrap" || key === "supplementary-material")
|
|
272
|
-
continue;
|
|
273
|
-
if (key === "#text") {
|
|
274
|
-
parts.push(String(value));
|
|
275
|
-
}
|
|
276
|
-
else if (Array.isArray(value)) {
|
|
277
|
-
for (const item of value) {
|
|
278
|
-
parts.push(flattenTextContent(item));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
parts.push(flattenTextContent(value));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return parts.filter(Boolean).join("");
|
|
286
|
-
}
|
|
1
|
+
/** @deprecated Import NCBI authority contracts from literature/sources/pubmed. */
|
|
2
|
+
export { NCBI_BASE, NCBI_IDCONV, ncbiUrl, parseEfetch, parseEsummary, parseIdConvResponse, parsePmcFullText } from "../../literature/sources/pubmed.js";
|
|
@@ -4,18 +4,11 @@
|
|
|
4
4
|
* Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
|
|
5
5
|
* pubmed-index Phase-1 collector.
|
|
6
6
|
*/
|
|
7
|
-
import { type ArticleDetail, type ArticleSection, type
|
|
7
|
+
import { type ArticleDetail, type ArticleSection, type FullTextResult, type PubmedSearchOptions, type PubMedSummary } from "../../literature/sources/pubmed.js";
|
|
8
8
|
export type { PubMedSummary, ArticleDetail, ArticleSection, FullTextResult };
|
|
9
|
-
export
|
|
10
|
-
maxResults?: number;
|
|
11
|
-
sort?: "relevance" | "date";
|
|
12
|
-
dateRange?: {
|
|
13
|
-
from: string;
|
|
14
|
-
to: string;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
9
|
+
export type SearchOptions = PubmedSearchOptions;
|
|
17
10
|
/** PubMed search via esearch + esummary. */
|
|
18
|
-
export declare function searchPubmed(ncbiApiKey: string | undefined, query: string, options?: SearchOptions): Promise<{
|
|
11
|
+
export declare function searchPubmed(ncbiApiKey: string | undefined, query: string, options?: SearchOptions, signal?: AbortSignal): Promise<{
|
|
19
12
|
totalFound: number;
|
|
20
13
|
results: PubMedSummary[];
|
|
21
14
|
}>;
|
|
@@ -36,7 +29,7 @@ export interface BoundedArticleDetail extends ArticleDetail {
|
|
|
36
29
|
meshTermCount: number;
|
|
37
30
|
}
|
|
38
31
|
/** Fetch full PubMed article details (efetch + ID Converter for PMC). */
|
|
39
|
-
export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[]): Promise<{
|
|
32
|
+
export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[], signal?: AbortSignal): Promise<{
|
|
40
33
|
articles: ArticleDetail[];
|
|
41
34
|
notFound: string[];
|
|
42
35
|
}>;
|
|
@@ -76,6 +69,6 @@ export interface BoundedFullText {
|
|
|
76
69
|
* Methods is worse than none) and admitted in document order until the budget
|
|
77
70
|
* would be exceeded.
|
|
78
71
|
*/
|
|
79
|
-
export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions): Promise<BoundedFullText | null>;
|
|
72
|
+
export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions, signal?: AbortSignal): Promise<BoundedFullText | null>;
|
|
80
73
|
/** Apply the section filter and character budget to a parsed full text. */
|
|
81
74
|
export declare function boundFullText(parsed: FullTextResult, options?: FullTextOptions): BoundedFullText;
|
|
@@ -4,76 +4,27 @@
|
|
|
4
4
|
* Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
|
|
5
5
|
* pubmed-index Phase-1 collector.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
const IdConvResponseSchema = z.object({
|
|
16
|
-
records: z.array(z.object({ pmid: z.string().optional(), pmcid: z.string().optional() })).optional(),
|
|
17
|
-
});
|
|
18
|
-
/** PubMed search via esearch + esummary. */
|
|
19
|
-
export async function searchPubmed(ncbiApiKey, query, options = {}) {
|
|
20
|
-
const maxResults = options.maxResults ?? 10;
|
|
21
|
-
const sort = options.sort ?? "relevance";
|
|
22
|
-
const esearchParams = {
|
|
23
|
-
db: "pubmed",
|
|
24
|
-
term: query,
|
|
25
|
-
retmax: maxResults,
|
|
26
|
-
retmode: "json",
|
|
27
|
-
sort: sort === "date" ? "pub+date" : "relevance",
|
|
28
|
-
};
|
|
29
|
-
if (options.dateRange) {
|
|
30
|
-
esearchParams.mindate = options.dateRange.from;
|
|
31
|
-
esearchParams.maxdate = options.dateRange.to;
|
|
32
|
-
esearchParams.datetype = "pdat";
|
|
33
|
-
}
|
|
34
|
-
const searchUrl = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/esearch.fcgi`, esearchParams);
|
|
35
|
-
const searchResult = await apiFetchValidated(searchUrl, EsearchResponseSchema);
|
|
36
|
-
if (searchResult.isErr())
|
|
37
|
-
throw new Error(describeApiError(searchResult.error));
|
|
38
|
-
const idList = searchResult.value.esearchresult?.idlist ?? [];
|
|
39
|
-
const totalFound = parseInt(searchResult.value.esearchresult?.count ?? "0", 10);
|
|
40
|
-
if (idList.length === 0)
|
|
41
|
-
return { totalFound, results: [] };
|
|
42
|
-
const summaryUrl = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/esummary.fcgi`, {
|
|
43
|
-
db: "pubmed",
|
|
44
|
-
id: idList.join(","),
|
|
45
|
-
retmode: "xml",
|
|
7
|
+
import { createPubmedSource, } from "../../literature/sources/pubmed.js";
|
|
8
|
+
function pubmedSource(ncbiApiKey) {
|
|
9
|
+
return createPubmedSource({
|
|
10
|
+
...(ncbiApiKey === undefined ? {} : { apiKey: ncbiApiKey }),
|
|
11
|
+
maxRetries: 3,
|
|
12
|
+
retryDelayMs: 1_000,
|
|
13
|
+
timeoutMs: 90_000,
|
|
46
14
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
15
|
+
}
|
|
16
|
+
function unwrap(result) {
|
|
17
|
+
if (result.status !== "ok")
|
|
18
|
+
throw new Error(result.detail);
|
|
19
|
+
return result.value;
|
|
20
|
+
}
|
|
21
|
+
/** PubMed search via esearch + esummary. */
|
|
22
|
+
export async function searchPubmed(ncbiApiKey, query, options = {}, signal) {
|
|
23
|
+
return unwrap(await pubmedSource(ncbiApiKey).search(query, options, signal));
|
|
51
24
|
}
|
|
52
25
|
/** Fetch full PubMed article details (efetch + ID Converter for PMC). */
|
|
53
|
-
export async function getArticleDetails(ncbiApiKey, pmids) {
|
|
54
|
-
|
|
55
|
-
const efetchAsync = apiFetch(ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
|
|
56
|
-
db: "pubmed",
|
|
57
|
-
id: idString,
|
|
58
|
-
rettype: "xml",
|
|
59
|
-
retmode: "xml",
|
|
60
|
-
}), { parseAs: "text" });
|
|
61
|
-
const idConvAsync = apiFetchValidated(ncbiUrl(ncbiApiKey, NCBI_IDCONV, { ids: idString, format: "json" }), IdConvResponseSchema);
|
|
62
|
-
// Both requests run concurrently; each Result is awaited and handled below.
|
|
63
|
-
const efetchResult = await efetchAsync;
|
|
64
|
-
const idConvResult = await idConvAsync;
|
|
65
|
-
if (efetchResult.isErr())
|
|
66
|
-
throw new Error(describeApiError(efetchResult.error));
|
|
67
|
-
const articles = parseEfetch(efetchResult.value);
|
|
68
|
-
if (idConvResult.isOk()) {
|
|
69
|
-
const pmcMap = parseIdConvResponse(idConvResult.value);
|
|
70
|
-
for (const article of articles) {
|
|
71
|
-
article.pmcId = pmcMap.get(article.pmid) ?? null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const foundPmids = new Set(articles.map((a) => a.pmid));
|
|
75
|
-
const notFound = pmids.filter((id) => !foundPmids.has(id));
|
|
76
|
-
return { articles, notFound };
|
|
26
|
+
export async function getArticleDetails(ncbiApiKey, pmids, signal) {
|
|
27
|
+
return unwrap(await pubmedSource(ncbiApiKey).articleDetails(pmids, signal));
|
|
77
28
|
}
|
|
78
29
|
/** Trim the two unbounded list fields on each article, recording their true sizes. */
|
|
79
30
|
export function boundArticleDetails(articles, options = {}) {
|
|
@@ -97,18 +48,8 @@ export function boundArticleDetails(articles, options = {}) {
|
|
|
97
48
|
* Methods is worse than none) and admitted in document order until the budget
|
|
98
49
|
* would be exceeded.
|
|
99
50
|
*/
|
|
100
|
-
export async function getArticleFullText(ncbiApiKey, pmcId, options = {}) {
|
|
101
|
-
const
|
|
102
|
-
const url = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
|
|
103
|
-
db: "pmc",
|
|
104
|
-
id: numericId,
|
|
105
|
-
rettype: "xml",
|
|
106
|
-
retmode: "xml",
|
|
107
|
-
});
|
|
108
|
-
const result = await apiFetch(url, { parseAs: "text" });
|
|
109
|
-
if (result.isErr())
|
|
110
|
-
throw new Error(describeApiError(result.error));
|
|
111
|
-
const parsed = parsePmcFullText(result.value);
|
|
51
|
+
export async function getArticleFullText(ncbiApiKey, pmcId, options = {}, signal) {
|
|
52
|
+
const parsed = unwrap(await pubmedSource(ncbiApiKey).fetchFullText(pmcId, signal));
|
|
112
53
|
if (!parsed)
|
|
113
54
|
return null;
|
|
114
55
|
return boundFullText(parsed, options);
|
|
@@ -23,7 +23,7 @@ import { finalText, runAgent } from "../../loop/run-agent.js";
|
|
|
23
23
|
import { passthroughStep } from "../../loop/run-step.js";
|
|
24
24
|
import { defineTool } from "../define-tool.js";
|
|
25
25
|
// Cross-domain search tools the analogical-reasoner uses.
|
|
26
|
-
import {
|
|
26
|
+
import { createSearchSemanticScholarTool } from "./search-semantic-scholar.js";
|
|
27
27
|
import { searchArxivTool } from "./search-arxiv.js";
|
|
28
28
|
import { createSearchGithubReposTool } from "./search-github-repos.js";
|
|
29
29
|
// Biology literature — reused for analogies that land back in biology.
|
|
@@ -156,7 +156,7 @@ function buildConversionPrompt(rawOutput) {
|
|
|
156
156
|
export function createGenerateAnalogyReportTool(deps) {
|
|
157
157
|
const ncbi = createNcbiTools(deps.bioKeys);
|
|
158
158
|
const reasonerTools = [
|
|
159
|
-
|
|
159
|
+
createSearchSemanticScholarTool({ ...(deps.bioKeys.semanticScholar === undefined ? {} : { apiKey: deps.bioKeys.semanticScholar }) }),
|
|
160
160
|
searchArxivTool,
|
|
161
161
|
createSearchGithubReposTool({ githubToken: deps.bioKeys.github }),
|
|
162
162
|
ncbi.pubmed,
|