@inflexa-ai/harness 0.13.0 → 0.14.1
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.js +23 -16
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.js +7 -13
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +20 -1
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +91 -25
- package/dist/loop/run-to-terminal.d.ts +22 -4
- package/dist/loop/run-to-terminal.js +20 -4
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/thread-history.js +34 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +15 -6
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.js +15 -20
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/state/init.js +37 -2
- package/dist/state/runs.d.ts +39 -0
- package/dist/state/runs.js +103 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/execute-analysis.d.ts +1 -0
- package/dist/tools/execute-analysis.js +5 -5
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +405 -85
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +63 -0
- package/dist/workflows/execute-analysis.js +73 -2
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lookupAnnotation — one tool over the functional-annotation vocabularies:
|
|
3
|
+
* Gene Ontology (QuickGO) and the pathway databases (KEGG, Reactome).
|
|
4
|
+
*
|
|
5
|
+
* These were two tools asking one question — "what is this gene or term known
|
|
6
|
+
* to do?" — against different vocabularies. `search_pathway` already treated
|
|
7
|
+
* the corpus as a parameter (`source: kegg | reactome | both`) rather than a
|
|
8
|
+
* tool boundary; this extends the same shape to GO.
|
|
9
|
+
*
|
|
10
|
+
* The input is a flat object with a `vocabulary` discriminator — not a
|
|
11
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
12
|
+
* a top-level `"type":"object"`). Per-vocabulary parameters are optional in the
|
|
13
|
+
* schema and made conditionally required by `.refine`, so a malformed call
|
|
14
|
+
* fails at the loop boundary naming the missing field instead of reaching an
|
|
15
|
+
* API request that cannot be built.
|
|
16
|
+
*/
|
|
17
|
+
import { ok } from "neverthrow";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { defineTool } from "../define-tool.js";
|
|
20
|
+
import { apiFetchValidated, describeApiError } from "../lib/api-utils.js";
|
|
21
|
+
import { searchPathways } from "../lib/pathway-client.js";
|
|
22
|
+
const QUICKGO_BASE = "https://www.ebi.ac.uk/QuickGO/services";
|
|
23
|
+
const HEADERS = { Accept: "application/json" };
|
|
24
|
+
const DEFAULT_LIMIT = 10;
|
|
25
|
+
// QuickGO term response, validated at the fetch boundary. The `.transform`
|
|
26
|
+
// carries the (context-free) rename from the raw wire fields to the `GoTerm[]`
|
|
27
|
+
// we return, so parsing IS both the validation and the normalization — there
|
|
28
|
+
// is no separate raw interface or mapper to keep in sync.
|
|
29
|
+
const GoTermResponseSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
results: z
|
|
32
|
+
.array(z.object({
|
|
33
|
+
id: z.string().optional(),
|
|
34
|
+
name: z.string().optional(),
|
|
35
|
+
definition: z.object({ text: z.string().optional() }).nullable().optional(),
|
|
36
|
+
aspect: z.string().optional(),
|
|
37
|
+
}))
|
|
38
|
+
.optional(),
|
|
39
|
+
})
|
|
40
|
+
.transform((res) => (res.results ?? []).map((t) => ({
|
|
41
|
+
id: t.id ?? "",
|
|
42
|
+
name: t.name ?? "",
|
|
43
|
+
definition: t.definition?.text ?? undefined,
|
|
44
|
+
aspect: t.aspect ?? undefined,
|
|
45
|
+
})));
|
|
46
|
+
const GoAnnotationResponseSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
results: z
|
|
49
|
+
.array(z.object({
|
|
50
|
+
geneProductId: z.string().optional(),
|
|
51
|
+
goId: z.string().optional(),
|
|
52
|
+
goName: z.string().optional(),
|
|
53
|
+
goAspect: z.string().optional(),
|
|
54
|
+
goEvidence: z.string().optional(),
|
|
55
|
+
qualifier: z.string().optional(),
|
|
56
|
+
}))
|
|
57
|
+
.optional(),
|
|
58
|
+
})
|
|
59
|
+
.transform((res) => (res.results ?? []).map((a) => ({
|
|
60
|
+
geneProductId: a.geneProductId ?? "",
|
|
61
|
+
goId: a.goId ?? "",
|
|
62
|
+
goName: a.goName ?? "",
|
|
63
|
+
aspect: a.goAspect ?? undefined,
|
|
64
|
+
evidenceCode: a.goEvidence ?? undefined,
|
|
65
|
+
qualifier: a.qualifier ?? undefined,
|
|
66
|
+
})));
|
|
67
|
+
const inputSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
vocabulary: z
|
|
70
|
+
.enum(["go", "kegg", "reactome", "pathways"])
|
|
71
|
+
.describe("'go' — Gene Ontology via QuickGO (EBI); needs ONE of goId, query, or geneProductId. Returns terms[] { id, name, definition, aspect } " +
|
|
72
|
+
"and/or annotations[] { geneProductId, goId, goName, aspect, evidenceCode, qualifier }. " +
|
|
73
|
+
"'kegg' / 'reactome' — one pathway database; needs query. 'pathways' — both, in parallel; needs query. " +
|
|
74
|
+
"Returns pathways[] { id, name, source, url, genes? }."),
|
|
75
|
+
query: z
|
|
76
|
+
.string()
|
|
77
|
+
.min(1)
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Free-text term. Required for the pathway vocabularies ('apoptosis', 'MAPK signaling'); for 'go' it is one of the three accepted inputs."),
|
|
80
|
+
goId: z
|
|
81
|
+
.string()
|
|
82
|
+
.regex(/^GO:\d{7}$/)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("'go' only. A specific GO term ID, e.g. 'GO:0008150'."),
|
|
85
|
+
geneProductId: z.string().min(1).optional().describe("'go' only. A UniProt accession, e.g. 'P04637', for that protein's GO annotations."),
|
|
86
|
+
taxonId: z.number().int().optional().describe("'go' only, with geneProductId. NCBI Taxon ID filter (9606 = human, 10090 = mouse)."),
|
|
87
|
+
organism: z.string().optional().describe("Pathway vocabularies only. KEGG organism code — 'hsa' (human, default), 'mmu' (mouse); mapped for Reactome."),
|
|
88
|
+
includeGenes: z
|
|
89
|
+
.boolean()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Pathway vocabularies only. Default FALSE — member-gene lists cost one request per pathway and run to hundreds of symbols. True only when " +
|
|
92
|
+
"you need the members, not the pathway identities."),
|
|
93
|
+
limit: z
|
|
94
|
+
.number()
|
|
95
|
+
.int()
|
|
96
|
+
.min(1)
|
|
97
|
+
.max(100)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(`Max records per vocabulary queried (default ${DEFAULT_LIMIT}, max 100); with 'pathways' it applies to KEGG and Reactome separately.`),
|
|
100
|
+
})
|
|
101
|
+
.refine((d) => d.vocabulary === "go" || (d.query !== undefined && d.query.trim().length > 0), {
|
|
102
|
+
message: "query is required for vocabulary 'kegg', 'reactome' and 'pathways' — the pathway term to search for",
|
|
103
|
+
path: ["query"],
|
|
104
|
+
})
|
|
105
|
+
.refine((d) => d.vocabulary !== "go" || d.goId !== undefined || d.query !== undefined || d.geneProductId !== undefined, {
|
|
106
|
+
message: "vocabulary 'go' needs at least one of goId (term by ID), query (free-text term search), or geneProductId (a protein's annotations)",
|
|
107
|
+
path: ["goId"],
|
|
108
|
+
})
|
|
109
|
+
.refine((d) => d.vocabulary === "go" || (d.goId === undefined && d.geneProductId === undefined && d.taxonId === undefined), {
|
|
110
|
+
message: "goId, geneProductId and taxonId belong to vocabulary 'go' — the pathway vocabularies take query + organism",
|
|
111
|
+
path: ["vocabulary"],
|
|
112
|
+
})
|
|
113
|
+
.refine((d) => d.vocabulary !== "go" || (d.organism === undefined && d.includeGenes === undefined), {
|
|
114
|
+
message: "organism and includeGenes belong to the pathway vocabularies — vocabulary 'go' filters annotations with taxonId instead",
|
|
115
|
+
path: ["vocabulary"],
|
|
116
|
+
});
|
|
117
|
+
export const lookupAnnotationTool = defineTool({
|
|
118
|
+
id: "lookup_annotation",
|
|
119
|
+
description: "Functional annotation for a gene, protein or term — Gene Ontology (QuickGO) and the pathway databases (KEGG, Reactome) behind one `vocabulary`. " +
|
|
120
|
+
"Names what a gene is known to do, resolves a GO or pathway identifier, or finds the pathways a term belongs to.\n" +
|
|
121
|
+
"This is annotation LOOKUP, not enrichment: to test whether a gene SET is over-represented in GO/KEGG/Reactome terms use " +
|
|
122
|
+
"search_interactions({action:'enrichment'}), which does the statistics.\n" +
|
|
123
|
+
"An empty terms / annotations / pathways array is valid no-data — do not retry the same input.",
|
|
124
|
+
inputSchema,
|
|
125
|
+
execute: async ({ vocabulary, query, goId, geneProductId, taxonId, organism, includeGenes, limit }) => {
|
|
126
|
+
const cap = limit ?? DEFAULT_LIMIT;
|
|
127
|
+
if (vocabulary !== "go") {
|
|
128
|
+
const pathways = await searchPathways(query, {
|
|
129
|
+
source: vocabulary === "pathways" ? "both" : vocabulary,
|
|
130
|
+
organism: organism ?? "hsa",
|
|
131
|
+
includeGenes: includeGenes ?? false,
|
|
132
|
+
maxResults: cap,
|
|
133
|
+
});
|
|
134
|
+
return ok({ pathways });
|
|
135
|
+
}
|
|
136
|
+
const tasks = [];
|
|
137
|
+
let terms;
|
|
138
|
+
let annotations;
|
|
139
|
+
if (goId) {
|
|
140
|
+
tasks.push((async () => {
|
|
141
|
+
const res = await apiFetchValidated(`${QUICKGO_BASE}/ontology/go/terms/${encodeURIComponent(goId)}`, GoTermResponseSchema, {
|
|
142
|
+
headers: HEADERS,
|
|
143
|
+
});
|
|
144
|
+
if (res.isErr())
|
|
145
|
+
throw new Error(`GO lookup: ${describeApiError(res.error)}`);
|
|
146
|
+
terms = res.value;
|
|
147
|
+
})());
|
|
148
|
+
}
|
|
149
|
+
if (query) {
|
|
150
|
+
tasks.push((async () => {
|
|
151
|
+
const params = new URLSearchParams({ query, limit: String(cap) });
|
|
152
|
+
const res = await apiFetchValidated(`${QUICKGO_BASE}/ontology/go/search?${params}`, GoTermResponseSchema, { headers: HEADERS });
|
|
153
|
+
if (res.isErr())
|
|
154
|
+
throw new Error(`GO search: ${describeApiError(res.error)}`);
|
|
155
|
+
const searchTerms = res.value;
|
|
156
|
+
terms = terms ? [...terms, ...searchTerms] : searchTerms;
|
|
157
|
+
})());
|
|
158
|
+
}
|
|
159
|
+
if (geneProductId) {
|
|
160
|
+
tasks.push((async () => {
|
|
161
|
+
const params = new URLSearchParams({ geneProductId, limit: String(cap) });
|
|
162
|
+
if (taxonId)
|
|
163
|
+
params.set("taxonId", String(taxonId));
|
|
164
|
+
const res = await apiFetchValidated(`${QUICKGO_BASE}/annotation/search?${params}`, GoAnnotationResponseSchema, { headers: HEADERS });
|
|
165
|
+
if (res.isErr())
|
|
166
|
+
throw new Error(`Annotations: ${describeApiError(res.error)}`);
|
|
167
|
+
annotations = res.value;
|
|
168
|
+
})());
|
|
169
|
+
}
|
|
170
|
+
await Promise.all(tasks);
|
|
171
|
+
return ok({ terms, annotations });
|
|
172
|
+
},
|
|
173
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opentargets — one tool over the Open Targets Platform: target-disease
|
|
3
|
-
* association evidence
|
|
4
|
-
*
|
|
3
|
+
* association evidence, read from a gene or from a disease.
|
|
4
|
+
*
|
|
5
|
+
* The curated safety liabilities this tool used to serve now belong to
|
|
6
|
+
* `target_safety`, which pairs them with the local secondary-pharmacology panel
|
|
7
|
+
* and resolves the Ensembl id they need.
|
|
5
8
|
*
|
|
6
9
|
* The input is a flat object with an `action` discriminator — not a
|
|
7
10
|
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
@@ -15,22 +18,15 @@
|
|
|
15
18
|
* result rather than an error. Naming the two identifiers apart is what makes
|
|
16
19
|
* that contract visible at the call site.
|
|
17
20
|
*/
|
|
18
|
-
import { type Association, type
|
|
21
|
+
import { type Association, type TargetInfo } from "../lib/opentargets-client.js";
|
|
19
22
|
type OpenTargetsOutput = {
|
|
20
23
|
targetInfo: TargetInfo | null;
|
|
21
24
|
associations: Association[];
|
|
22
25
|
} | {
|
|
23
26
|
associations: Association[];
|
|
24
|
-
} | {
|
|
25
|
-
found: false;
|
|
26
|
-
ensemblId: string;
|
|
27
|
-
} | {
|
|
28
|
-
found: true;
|
|
29
|
-
targetSymbol: string;
|
|
30
|
-
safetyLiabilities: SafetyLiability[];
|
|
31
27
|
};
|
|
32
28
|
export declare const openTargetsTool: import("../define-tool.js").Tool<{
|
|
33
|
-
action: "target" | "disease"
|
|
29
|
+
action: "target" | "disease";
|
|
34
30
|
ensemblId?: string | undefined;
|
|
35
31
|
efoId?: string | undefined;
|
|
36
32
|
limit?: number | undefined;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opentargets — one tool over the Open Targets Platform: target-disease
|
|
3
|
-
* association evidence
|
|
4
|
-
*
|
|
3
|
+
* association evidence, read from a gene or from a disease.
|
|
4
|
+
*
|
|
5
|
+
* The curated safety liabilities this tool used to serve now belong to
|
|
6
|
+
* `target_safety`, which pairs them with the local secondary-pharmacology panel
|
|
7
|
+
* and resolves the Ensembl id they need.
|
|
5
8
|
*
|
|
6
9
|
* The input is a flat object with an `action` discriminator — not a
|
|
7
10
|
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
@@ -18,32 +21,30 @@
|
|
|
18
21
|
import { ok } from "neverthrow";
|
|
19
22
|
import { z } from "zod";
|
|
20
23
|
import { defineTool } from "../define-tool.js";
|
|
21
|
-
import {
|
|
24
|
+
import { searchDiseaseAssociations, searchTargetAssociations } from "../lib/opentargets-client.js";
|
|
25
|
+
const DEFAULT_LIMIT = 10;
|
|
22
26
|
const inputSchema = z
|
|
23
27
|
.object({
|
|
24
28
|
action: z
|
|
25
|
-
.enum(["target", "disease"
|
|
29
|
+
.enum(["target", "disease"])
|
|
26
30
|
.describe("'target' (needs ensemblId) — the diseases associated with the gene: targetInfo (approvedSymbol, approvedName, " +
|
|
27
31
|
"tractability across small molecule / antibody / other modalities) plus associations[], each with an overall score and " +
|
|
28
32
|
"its per-datatype breakdown — genetic association, known drug, literature, animal model, somatic mutation. " +
|
|
29
33
|
"'disease' (needs efoId) — the targets ranked for that disease; each association carries targetId/targetSymbol/targetName " +
|
|
30
|
-
"and the same score breakdown.
|
|
31
|
-
|
|
32
|
-
"safetyLiabilities[] { event, biosamples (affected tissues), effects (direction of effect), source }. This is TARGET-level, " +
|
|
33
|
-
"mechanism-based safety — for the post-market adverse events of a specific marketed DRUG use search_faers instead."),
|
|
34
|
-
ensemblId: z.string().optional().describe("Ensembl gene ID, e.g. ENSG00000141510 (TP53). Required for action 'target' and 'safety'."),
|
|
34
|
+
"and the same score breakdown."),
|
|
35
|
+
ensemblId: z.string().optional().describe("Ensembl gene ID, e.g. ENSG00000141510 (TP53). Required for action 'target'."),
|
|
35
36
|
efoId: z.string().optional().describe("EFO disease ID, e.g. EFO_0000311. Required for action 'disease'."),
|
|
36
37
|
limit: z
|
|
37
38
|
.number()
|
|
38
39
|
.int()
|
|
39
40
|
.min(1)
|
|
40
41
|
.max(100)
|
|
41
|
-
.default(25)
|
|
42
42
|
.optional()
|
|
43
|
-
.describe(
|
|
43
|
+
.describe(`Max associations to return (default ${DEFAULT_LIMIT}, max 100), ordered by DESCENDING association score. The default is the top ` +
|
|
44
|
+
`${DEFAULT_LIMIT}, which is where the evidence is; raise it only when you are surveying the long tail rather than ranking.`),
|
|
44
45
|
})
|
|
45
|
-
.refine((d) => d.action
|
|
46
|
-
message: "ensemblId is required when action is 'target'
|
|
46
|
+
.refine((d) => d.action !== "target" || (d.ensemblId !== undefined && d.ensemblId.trim().length > 0), {
|
|
47
|
+
message: "ensemblId is required when action is 'target' — Open Targets accepts only Ensembl gene IDs (ENSG…), " +
|
|
47
48
|
"never gene symbols. Resolve the symbol to an ENSG id with search_gene, then retry.",
|
|
48
49
|
path: ["ensemblId"],
|
|
49
50
|
})
|
|
@@ -53,37 +54,25 @@ const inputSchema = z
|
|
|
53
54
|
});
|
|
54
55
|
export const openTargetsTool = defineTool({
|
|
55
56
|
id: "opentargets",
|
|
56
|
-
description: "Query the Open Targets Platform — the preferred FIRST call for target assessment, since one 'target' query yields the " +
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"id with search_gene first.\n" +
|
|
62
|
-
"NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence
|
|
63
|
-
"means no Open Targets record for that Ensembl id (usually a wrong id); safety found: true with an empty safetyLiabilities " +
|
|
64
|
-
"array means no curated liability.",
|
|
57
|
+
description: "Query the Open Targets Platform — the preferred FIRST call for target assessment, since one 'target' query yields the genetic evidence, " +
|
|
58
|
+
"tractability, and drug landscape together, already scored and ranked. See the action parameter for what each mode needs and returns.\n" +
|
|
59
|
+
"It is the integrated view; the underlying records live elsewhere. For the raw genetic evidence (individual SNPs, effect sizes, GDA scores, " +
|
|
60
|
+
"variant pathogenicity) use gene_disease_evidence. For this target's mechanism-based safety liabilities use target_safety.\n" +
|
|
61
|
+
"IDENTIFIERS ONLY: it accepts an Ensembl gene ID (action 'target') or an EFO disease ID (action 'disease'). A bare gene symbol or a free-text " +
|
|
62
|
+
"disease name silently returns an EMPTY result rather than an error — resolve a symbol to its ENSG id with search_gene first.\n" +
|
|
63
|
+
"NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an unresolvable id.",
|
|
65
64
|
inputSchema,
|
|
66
65
|
execute: async (input) => {
|
|
66
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
67
67
|
switch (input.action) {
|
|
68
68
|
case "target": {
|
|
69
|
-
const targetInfo = await searchTargetAssociations(input.ensemblId,
|
|
69
|
+
const targetInfo = await searchTargetAssociations(input.ensemblId, limit);
|
|
70
70
|
return ok({ targetInfo, associations: targetInfo?.associations ?? [] });
|
|
71
71
|
}
|
|
72
72
|
case "disease": {
|
|
73
|
-
const associations = await searchDiseaseAssociations(input.efoId,
|
|
73
|
+
const associations = await searchDiseaseAssociations(input.efoId, limit);
|
|
74
74
|
return ok({ associations });
|
|
75
75
|
}
|
|
76
|
-
case "safety": {
|
|
77
|
-
const result = await getTargetSafetyLiabilities(input.ensemblId);
|
|
78
|
-
// "Target not found" is an expected outcome — a data variant, not an error.
|
|
79
|
-
if (!result)
|
|
80
|
-
return ok({ found: false, ensemblId: input.ensemblId });
|
|
81
|
-
return ok({
|
|
82
|
-
found: true,
|
|
83
|
-
targetSymbol: result.targetSymbol,
|
|
84
|
-
safetyLiabilities: result.safetyLiabilities,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
76
|
}
|
|
88
77
|
},
|
|
89
78
|
});
|
|
@@ -44,14 +44,16 @@ const inputSchema = z
|
|
|
44
44
|
activeOnly: z
|
|
45
45
|
.boolean()
|
|
46
46
|
.optional()
|
|
47
|
-
.describe("Action 'assays' only. Default
|
|
47
|
+
.describe("Action 'assays' only. Default TRUE — only rows whose activityOutcome is 'Active'. A widely screened compound has thousands of " +
|
|
48
|
+
"inactive rows and they answer nothing on their own; set false only when a LOW hit rate is the finding you are after (selectivity, " +
|
|
49
|
+
"or confirming a compound was tested and came back clean)."),
|
|
48
50
|
limit: z
|
|
49
51
|
.number()
|
|
50
52
|
.int()
|
|
51
53
|
.min(1)
|
|
52
54
|
.max(500)
|
|
53
55
|
.optional()
|
|
54
|
-
.describe("Action 'assays' only. Max assay records to return (default
|
|
56
|
+
.describe("Action 'assays' only. Max assay records to return (default 25, max 500). Applied after the activeOnly filter."),
|
|
55
57
|
})
|
|
56
58
|
.refine((d) => d.action !== "compound" || (d.query !== undefined && d.query.trim().length > 0), {
|
|
57
59
|
message: "query is required when action is 'compound' — the identifier to resolve (a name, SMILES, InChI, InChIKey, or CID)",
|
|
@@ -83,8 +85,8 @@ export const pubchemTool = defineTool({
|
|
|
83
85
|
case "assays":
|
|
84
86
|
return ok({
|
|
85
87
|
assays: await fetchPubchemAssays(input.cid, {
|
|
86
|
-
activeOnly: input.activeOnly ??
|
|
87
|
-
limit: input.limit ??
|
|
88
|
+
activeOnly: input.activeOnly ?? true,
|
|
89
|
+
limit: input.limit ?? 25,
|
|
88
90
|
}),
|
|
89
91
|
});
|
|
90
92
|
}
|
|
@@ -16,24 +16,28 @@
|
|
|
16
16
|
* turns PMIDs into abstracts + the PMC id, fulltext turns a PMC id into the
|
|
17
17
|
* article body — which is why they share a tool: the identifier for each step
|
|
18
18
|
* is read off the previous step's result.
|
|
19
|
+
*
|
|
20
|
+
* Every step is bounded, because this is the tool most able to flood a context:
|
|
21
|
+
* an author list can run to hundreds of names, and one research article parses
|
|
22
|
+
* to 40–80k characters — more than the entire tool surface costs to attach.
|
|
23
|
+
* `details` trims the two unbounded list fields; `fulltext` admits whole
|
|
24
|
+
* sections up to a character budget and always returns the article's full
|
|
25
|
+
* section outline so a follow-up can name what it wants.
|
|
19
26
|
*/
|
|
20
|
-
import type
|
|
21
|
-
import { type ArticleDetail, type PubMedSummary } from "../lib/pubmed-client.js";
|
|
27
|
+
import { type BoundedArticleDetail, type BoundedFullText, type PubMedSummary } from "../lib/pubmed-client.js";
|
|
22
28
|
type PubMedOutput = {
|
|
23
29
|
totalFound: number;
|
|
24
30
|
results: PubMedSummary[];
|
|
25
31
|
} | {
|
|
26
|
-
articles:
|
|
32
|
+
articles: BoundedArticleDetail[];
|
|
27
33
|
notFound: string[];
|
|
28
34
|
} | {
|
|
29
35
|
pmcId: string;
|
|
30
36
|
available: false;
|
|
31
|
-
} | {
|
|
37
|
+
} | ({
|
|
32
38
|
pmcId: string;
|
|
33
39
|
available: true;
|
|
34
|
-
|
|
35
|
-
sections: ArticleSection[];
|
|
36
|
-
};
|
|
40
|
+
} & BoundedFullText);
|
|
37
41
|
export declare function createPubMedTool(deps: {
|
|
38
42
|
ncbiApiKey?: string;
|
|
39
43
|
}): import("../define-tool.js").Tool<{
|
|
@@ -46,6 +50,10 @@ export declare function createPubMedTool(deps: {
|
|
|
46
50
|
to: string;
|
|
47
51
|
} | undefined;
|
|
48
52
|
pmids?: string[] | undefined;
|
|
53
|
+
maxAuthors?: number | undefined;
|
|
54
|
+
maxMeshTerms?: number | undefined;
|
|
49
55
|
pmcId?: string | undefined;
|
|
56
|
+
sections?: string[] | undefined;
|
|
57
|
+
maxChars?: number | undefined;
|
|
50
58
|
}, PubMedOutput>;
|
|
51
59
|
export {};
|
package/dist/tools/bio/pubmed.js
CHANGED
|
@@ -16,27 +16,42 @@
|
|
|
16
16
|
* turns PMIDs into abstracts + the PMC id, fulltext turns a PMC id into the
|
|
17
17
|
* article body — which is why they share a tool: the identifier for each step
|
|
18
18
|
* is read off the previous step's result.
|
|
19
|
+
*
|
|
20
|
+
* Every step is bounded, because this is the tool most able to flood a context:
|
|
21
|
+
* an author list can run to hundreds of names, and one research article parses
|
|
22
|
+
* to 40–80k characters — more than the entire tool surface costs to attach.
|
|
23
|
+
* `details` trims the two unbounded list fields; `fulltext` admits whole
|
|
24
|
+
* sections up to a character budget and always returns the article's full
|
|
25
|
+
* section outline so a follow-up can name what it wants.
|
|
19
26
|
*/
|
|
20
27
|
import { ok } from "neverthrow";
|
|
21
28
|
import { z } from "zod";
|
|
22
29
|
import { defineTool } from "../define-tool.js";
|
|
23
|
-
import { getArticleDetails, getArticleFullText, searchPubmed } from "../lib/pubmed-client.js";
|
|
30
|
+
import { boundArticleDetails, getArticleDetails, getArticleFullText, searchPubmed, } from "../lib/pubmed-client.js";
|
|
31
|
+
const DEFAULTS = { searchResults: 10, maxAuthors: 5, maxMeshTerms: 10, maxChars: 12_000 };
|
|
24
32
|
const inputSchema = z
|
|
25
33
|
.object({
|
|
26
34
|
action: z
|
|
27
35
|
.enum(["search", "details", "fulltext"])
|
|
28
36
|
.describe("'search' (needs query) — returns totalFound plus, per hit, PMID, title, journal, year and an author summary. " +
|
|
29
|
-
"'details' (needs pmids, max 20 per call) — each article's full abstract,
|
|
30
|
-
"
|
|
31
|
-
"'fulltext' (needs pmcId, taken from a 'details' result) — the body of ONE open-access PMC article as plain text with " +
|
|
32
|
-
"
|
|
37
|
+
"'details' (needs pmids, max 20 per call) — each article's full abstract, journal, year, DOI, pmcId (non-null only when open-access " +
|
|
38
|
+
"full text exists in PubMed Central), plus the leading authors and MeSH terms with authorCount / meshTermCount giving the true totals. " +
|
|
39
|
+
"'fulltext' (needs pmcId, taken from a 'details' result) — the body of ONE open-access PMC article as plain text with section headers " +
|
|
40
|
+
"preserved, trimmed to maxChars; `outline` lists every section with its size and whether it came back, and truncated says whether " +
|
|
41
|
+
"anything was left out. available: false when the article is not open-access."),
|
|
33
42
|
query: z
|
|
34
43
|
.string()
|
|
35
44
|
.optional()
|
|
36
45
|
.describe("Required for action 'search'. PubMed query syntax — MeSH terms (\"breast neoplasms\"[MeSH]), field tags " +
|
|
37
46
|
'([Title/Abstract], [Author], [Gene]), Boolean AND/OR/NOT. Example: "BRCA1"[Gene] AND "drug resistance"[Title/Abstract].'),
|
|
38
|
-
maxResults: z
|
|
39
|
-
|
|
47
|
+
maxResults: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(50)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(`action 'search': max results, 1–50 (default ${DEFAULTS.searchResults}). totalFound reports the true match count regardless.`),
|
|
54
|
+
sort: z.enum(["relevance", "date"]).optional().describe("action 'search': sort order (default \"relevance\")."),
|
|
40
55
|
dateRange: z
|
|
41
56
|
.object({
|
|
42
57
|
from: z.string().describe("Start date in YYYY/MM/DD format"),
|
|
@@ -49,8 +64,36 @@ const inputSchema = z
|
|
|
49
64
|
.min(1)
|
|
50
65
|
.max(20)
|
|
51
66
|
.optional()
|
|
52
|
-
.describe("Required for action 'details'. Non-empty array of PMID strings, max 20 — batch them, do not call once per PMID."
|
|
67
|
+
.describe("Required for action 'details'. Non-empty array of PMID strings, max 20 — batch them, do not call once per PMID. Each article returns a " +
|
|
68
|
+
"FULL abstract, so 20 is a large result; pass the handful you actually intend to read."),
|
|
69
|
+
maxAuthors: z
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.min(0)
|
|
73
|
+
.max(100)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(`action 'details': leading authors kept per article (default ${DEFAULTS.maxAuthors}, 0 for none); authorCount gives the true total.`),
|
|
76
|
+
maxMeshTerms: z
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.min(0)
|
|
80
|
+
.max(100)
|
|
81
|
+
.optional()
|
|
82
|
+
.describe(`action 'details': MeSH terms kept per article (default ${DEFAULTS.maxMeshTerms}, 0 for none); meshTermCount gives the true total.`),
|
|
53
83
|
pmcId: z.string().optional().describe("Required for action 'fulltext'. A PMC ID (e.g. \"PMC1234567\") copied from a 'details' result — not a PMID."),
|
|
84
|
+
sections: z
|
|
85
|
+
.array(z.string())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("action 'fulltext': case-insensitive substring filter on section headings, e.g. ['methods','results']. Omit first to get the outline, " +
|
|
88
|
+
"then name what you need — far cheaper than raising maxChars."),
|
|
89
|
+
maxChars: z
|
|
90
|
+
.number()
|
|
91
|
+
.int()
|
|
92
|
+
.min(500)
|
|
93
|
+
.max(200_000)
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(`action 'fulltext': character budget across returned section bodies (default ${DEFAULTS.maxChars}). Sections are admitted whole, in ` +
|
|
96
|
+
"document order, until the budget would be exceeded."),
|
|
54
97
|
})
|
|
55
98
|
.refine((d) => d.action !== "search" || (d.query !== undefined && d.query.trim().length > 0), {
|
|
56
99
|
message: "query is required when action is 'search'",
|
|
@@ -63,6 +106,14 @@ const inputSchema = z
|
|
|
63
106
|
.refine((d) => d.action !== "fulltext" || (d.pmcId !== undefined && d.pmcId.trim().length > 0), {
|
|
64
107
|
message: "pmcId is required when action is 'fulltext' — copy it from the pmcId field of a 'details' result; a PMID is not a PMC ID",
|
|
65
108
|
path: ["pmcId"],
|
|
109
|
+
})
|
|
110
|
+
.refine((d) => d.action === "fulltext" || (d.sections === undefined && d.maxChars === undefined), {
|
|
111
|
+
message: "sections and maxChars apply to action 'fulltext' only",
|
|
112
|
+
path: ["action"],
|
|
113
|
+
})
|
|
114
|
+
.refine((d) => d.action === "details" || (d.maxAuthors === undefined && d.maxMeshTerms === undefined), {
|
|
115
|
+
message: "maxAuthors and maxMeshTerms apply to action 'details' only",
|
|
116
|
+
path: ["action"],
|
|
66
117
|
});
|
|
67
118
|
export function createPubMedTool(deps) {
|
|
68
119
|
return defineTool({
|
|
@@ -70,29 +121,37 @@ export function createPubMedTool(deps) {
|
|
|
70
121
|
description: "Search and read the biomedical literature via PubMed / PubMed Central (NCBI E-utilities). The three actions form a chain " +
|
|
71
122
|
"— search, then details on the relevant hits, then fulltext; see the action parameter for what each needs and returns. Read " +
|
|
72
123
|
"fulltext sparingly — only where the 'details' abstract is not enough, and only for articles that have a pmcId (open-access). " +
|
|
124
|
+
"A full article is larger than everything else this tool returns combined, so it comes back trimmed to a character budget: take the " +
|
|
125
|
+
"`outline` from the first call and re-request the specific `sections` you need rather than raising maxChars. " +
|
|
73
126
|
"available: false is an expected outcome, not an error — do not retry it.",
|
|
74
127
|
inputSchema,
|
|
75
128
|
execute: async (input) => {
|
|
76
129
|
switch (input.action) {
|
|
77
130
|
case "search":
|
|
78
131
|
return ok(await searchPubmed(deps.ncbiApiKey, input.query, {
|
|
79
|
-
maxResults: input.maxResults,
|
|
80
|
-
sort: input.sort,
|
|
81
|
-
dateRange: input.dateRange,
|
|
132
|
+
maxResults: input.maxResults ?? DEFAULTS.searchResults,
|
|
133
|
+
...(input.sort ? { sort: input.sort } : {}),
|
|
134
|
+
...(input.dateRange ? { dateRange: input.dateRange } : {}),
|
|
82
135
|
}));
|
|
83
|
-
case "details":
|
|
84
|
-
|
|
136
|
+
case "details": {
|
|
137
|
+
const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids);
|
|
138
|
+
return ok({
|
|
139
|
+
articles: boundArticleDetails(articles, {
|
|
140
|
+
maxAuthors: input.maxAuthors ?? DEFAULTS.maxAuthors,
|
|
141
|
+
maxMeshTerms: input.maxMeshTerms ?? DEFAULTS.maxMeshTerms,
|
|
142
|
+
}),
|
|
143
|
+
notFound,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
85
146
|
case "fulltext": {
|
|
86
|
-
const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId
|
|
147
|
+
const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId, {
|
|
148
|
+
...(input.sections ? { sections: input.sections } : {}),
|
|
149
|
+
maxChars: input.maxChars ?? DEFAULTS.maxChars,
|
|
150
|
+
});
|
|
87
151
|
// "Not open-access" is an expected outcome — a data variant, not an error.
|
|
88
152
|
if (!parsed)
|
|
89
153
|
return ok({ pmcId: input.pmcId, available: false });
|
|
90
|
-
return ok({
|
|
91
|
-
pmcId: input.pmcId,
|
|
92
|
-
available: true,
|
|
93
|
-
fullText: parsed.fullText,
|
|
94
|
-
sections: parsed.sections,
|
|
95
|
-
});
|
|
154
|
+
return ok({ pmcId: input.pmcId, available: true, ...parsed });
|
|
96
155
|
}
|
|
97
156
|
}
|
|
98
157
|
},
|
|
@@ -4,9 +4,28 @@
|
|
|
4
4
|
export declare const searchClinicalTrialsTool: import("../define-tool.js").Tool<{
|
|
5
5
|
query: string;
|
|
6
6
|
limit: number;
|
|
7
|
+
includeSummaries: boolean;
|
|
7
8
|
phase?: "EARLY_PHASE1" | "PHASE1" | "PHASE2" | "PHASE3" | "PHASE4" | undefined;
|
|
8
9
|
status?: "RECRUITING" | "ACTIVE_NOT_RECRUITING" | "COMPLETED" | "NOT_YET_RECRUITING" | undefined;
|
|
9
10
|
}, {
|
|
10
11
|
totalFound: number;
|
|
11
|
-
trials: import("../lib/clinical-trials-client.js").ClinicalTrial[]
|
|
12
|
+
trials: import("../lib/clinical-trials-client.js").ClinicalTrial[] | {
|
|
13
|
+
briefSummary: string | null;
|
|
14
|
+
summaryTruncated: boolean;
|
|
15
|
+
nctId: string;
|
|
16
|
+
title: string;
|
|
17
|
+
officialTitle: string | null;
|
|
18
|
+
status: string;
|
|
19
|
+
phase: string | null;
|
|
20
|
+
studyType: string | null;
|
|
21
|
+
primaryPurpose: string | null;
|
|
22
|
+
conditions: string[];
|
|
23
|
+
interventions: string[];
|
|
24
|
+
interventionDetails: import("../lib/clinical-trials-client.js").ClinicalTrialIntervention[];
|
|
25
|
+
enrollmentCount: number | null;
|
|
26
|
+
startDate: string | null;
|
|
27
|
+
primaryCompletionDate: string | null;
|
|
28
|
+
whyStopped: string | null;
|
|
29
|
+
sponsor: string | null;
|
|
30
|
+
}[];
|
|
12
31
|
}>;
|
|
@@ -5,11 +5,13 @@ import { ok } from "neverthrow";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineTool } from "../define-tool.js";
|
|
7
7
|
import { searchTrials } from "../lib/clinical-trials-client.js";
|
|
8
|
+
/** briefSummary preview length when `includeSummaries` is off. */
|
|
9
|
+
const SUMMARY_PREVIEW_CHARS = 300;
|
|
8
10
|
export const searchClinicalTrialsTool = defineTool({
|
|
9
11
|
id: "search_clinical_trials",
|
|
10
12
|
description: "Search ClinicalTrials.gov to map the clinical development landscape for a target, drug, or indication — what is being tried, at what phase, by whom, and what stopped. " +
|
|
11
|
-
"Returns totalFound plus per trial: NCT ID, title, status, phase, conditions, interventions, enrollment, start and completion dates, sponsor,
|
|
12
|
-
"(the termination reason, when there is one). " +
|
|
13
|
+
"Returns totalFound plus per trial: NCT ID, title, status, phase, conditions, interventions, enrollment, start and completion dates, sponsor, whyStopped " +
|
|
14
|
+
"(the termination reason, when there is one) and a briefSummary preview — set includeSummaries for the full protocol prose. " +
|
|
13
15
|
"`query` is free text matched across the whole study record, so a gene symbol, a drug name, a condition, or an NCT ID all work. " +
|
|
14
16
|
"`phase` and `status` are optional server-side filters — omit them to see the full landscape; totalFound reports the true match count even when it exceeds the page returned. " +
|
|
15
17
|
"An empty trials array is a valid 'nothing matched' (often an over-narrow filter) — do not retry the identical query.",
|
|
@@ -23,10 +25,29 @@ export const searchClinicalTrialsTool = defineTool({
|
|
|
23
25
|
.enum(["RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "NOT_YET_RECRUITING"])
|
|
24
26
|
.optional()
|
|
25
27
|
.describe("Optional recruitment-status filter. Omit for all statuses — note TERMINATED/WITHDRAWN trials are not selectable here and appear only in an unfiltered search."),
|
|
26
|
-
limit: z
|
|
28
|
+
limit: z
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.min(1)
|
|
32
|
+
.max(50)
|
|
33
|
+
.default(10)
|
|
34
|
+
.describe("Max trials to return (default 10, max 50). totalFound reports the true match count regardless."),
|
|
35
|
+
includeSummaries: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.default(false)
|
|
38
|
+
.describe("Default FALSE — briefSummary comes back as a 300-character preview and detailedDescription is omitted, which is enough to map a " +
|
|
39
|
+
"landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand characters " +
|
|
40
|
+
"per trial."),
|
|
27
41
|
}),
|
|
28
|
-
execute: async ({ query, phase, status, limit =
|
|
42
|
+
execute: async ({ query, phase, status, limit = 10, includeSummaries = false }) => {
|
|
29
43
|
const result = await searchTrials(query, { phase, status, limit });
|
|
30
|
-
|
|
44
|
+
const trials = includeSummaries
|
|
45
|
+
? result.trials
|
|
46
|
+
: result.trials.map(({ detailedDescription: _detailedDescription, ...trial }) => ({
|
|
47
|
+
...trial,
|
|
48
|
+
briefSummary: trial.briefSummary === null ? null : trial.briefSummary.slice(0, SUMMARY_PREVIEW_CHARS),
|
|
49
|
+
summaryTruncated: (trial.briefSummary?.length ?? 0) > SUMMARY_PREVIEW_CHARS,
|
|
50
|
+
}));
|
|
51
|
+
return ok({ totalFound: result.totalFound, trials });
|
|
31
52
|
},
|
|
32
53
|
});
|