@inflexa-ai/harness 0.13.0 → 0.14.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.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 +50 -5
- package/dist/loop/run-to-terminal.d.ts +0 -2
- package/dist/loop/run-to-terminal.js +8 -1
- 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 +115 -69
- 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
|
@@ -31,48 +31,50 @@ const inputSchema = z
|
|
|
31
31
|
.object({
|
|
32
32
|
dataset: z
|
|
33
33
|
.enum(["toxcast", "hazard", "chemical", "exposure"])
|
|
34
|
-
.describe("Which CompTox dataset to query
|
|
35
|
-
"'toxcast' — ToxCast/Tox21 in-vitro
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"'hazard' — ToxValDB / ToxRefDB IN-VIVO hazard: the points of departure a safety evaluation rests on.
|
|
39
|
-
"with value, units, study type and duration, species,
|
|
40
|
-
"'chemical' — identity and physicochemical
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"'exposure' — the exposure half of a risk assessment (e.g.
|
|
44
|
-
"(production volume and per-pathway
|
|
45
|
-
"reported functional
|
|
34
|
+
.describe("Which CompTox dataset to query; each names its return fields.\n" +
|
|
35
|
+
"'toxcast' — ToxCast/Tox21 in-vitro HTS across hundreds of endpoints (nuclear receptors, stress response, mitochondrial toxicity): 'is " +
|
|
36
|
+
"this an endocrine disruptor?'. → the resolved chemical plus assay rows (endpoint, AC50, hit call; AC50 ascending), and totalAssays / " +
|
|
37
|
+
"activeAssays / activeHitRate, which always describe the FULL tested panel, not the returned slice.\n" +
|
|
38
|
+
"'hazard' — ToxValDB / ToxRefDB IN-VIVO hazard: the points of departure a safety evaluation rests on. → dose-response rows (toxvalType " +
|
|
39
|
+
"NOAEL/LOAEL/LD50/BMD with value, units, study type and duration, species, route, effect), genotoxicity summaries, cancer classifications.\n" +
|
|
40
|
+
"'chemical' — identity and physicochemical profile, and the way to get the DTXSID the other datasets key off. → DTXSID, CASRN, SMILES, " +
|
|
41
|
+
"InChIKey, formula, mass, plus (unless includeProperties is false) experimental and predicted properties — logP, water solubility, vapor " +
|
|
42
|
+
"pressure, melting/boiling point, bioconcentration factor, Henry's law constant.\n" +
|
|
43
|
+
"'exposure' — the exposure half of a risk assessment (e.g. an AC50 against predicted exposure for a bioactivity-exposure ratio). → SEEM " +
|
|
44
|
+
"predictions (production volume and per-pathway probabilities: dietary, residential, pesticide, industrial — NOT a daily-intake dose), " +
|
|
45
|
+
"HTTK toxicokinetic parameters, reported functional uses, consumer-product composition."),
|
|
46
46
|
query: z
|
|
47
47
|
.string()
|
|
48
|
-
.describe("Chemical identifier (all datasets). A DTXSID (
|
|
49
|
-
"
|
|
48
|
+
.describe("Chemical identifier (all datasets). A DTXSID ('DTXSID7020182') is used directly; a CASRN ('80-05-7'), a name ('bisphenol A'), or — for " +
|
|
49
|
+
"dataset 'chemical' — an InChIKey is resolved by EXACT match, so a non-canonical or misspelled name yields found: false."),
|
|
50
50
|
dataType: z
|
|
51
51
|
.enum(["toxval", "genetox", "cancer", "seem", "httk", "functional-use", "product-data", "all"])
|
|
52
52
|
.optional()
|
|
53
|
-
.describe("Which sections to fetch — datasets 'hazard' and 'exposure' only
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"'
|
|
53
|
+
.describe("Which sections to fetch — datasets 'hazard' and 'exposure' only, ignored by the single-dataset 'toxcast' and 'chemical'. Default 'all', " +
|
|
54
|
+
"which fetches every section of that dataset concurrently. " +
|
|
55
|
+
"'hazard': 'toxval' (dose-response — NOAELs, LOAELs, LD50s, BMDs), 'genetox' (genotoxicity summaries), 'cancer' (IARC/EPA/NTP-style " +
|
|
56
|
+
"classifications). " +
|
|
57
|
+
"'exposure': 'seem' (exposure-pathway predictions + production volume), 'httk' (toxicokinetics — clearance, Css, protein binding), " +
|
|
58
|
+
"'functional-use' (reported use categories), 'product-data' (consumer-product composition)."),
|
|
59
59
|
activeOnly: z
|
|
60
60
|
.boolean()
|
|
61
61
|
.optional()
|
|
62
|
-
.describe("
|
|
62
|
+
.describe("'toxcast' only. Default true — hit (active) assays only. False for the whole tested panel including inactives, needed to judge selectivity " +
|
|
63
|
+
"or a low hit rate."),
|
|
63
64
|
includeProperties: z
|
|
64
65
|
.boolean()
|
|
65
66
|
.optional()
|
|
66
|
-
.describe("
|
|
67
|
+
.describe("'chemical' only. Default true — also fetch experimental/predicted properties. False skips that second call when only identity and mass are needed."),
|
|
67
68
|
limit: z
|
|
68
69
|
.number()
|
|
69
70
|
.int()
|
|
70
71
|
.min(1)
|
|
71
72
|
.max(200)
|
|
72
73
|
.optional()
|
|
73
|
-
.describe("Max rows
|
|
74
|
-
"'hazard' default 30
|
|
75
|
-
"product-data only — seem and httk
|
|
74
|
+
.describe("Max rows; ignored by 'chemical'. Per dataset: 'toxcast' default 25 / max 200, applied after activeOnly and AC50-ascending so the default " +
|
|
75
|
+
"keeps the most potent hits; 'hazard' default 30 / max 100, applied to toxval and genetox only — cancer always returns every row (only " +
|
|
76
|
+
"ever a handful); 'exposure' default 25 / max 100, applied to functional-use and product-data only — seem is one record and httk a short " +
|
|
77
|
+
"parameter list, both returned whole."),
|
|
76
78
|
})
|
|
77
79
|
.refine((d) => (d.dataset !== "toxcast" && d.dataset !== "chemical") || d.dataType === undefined || d.dataType === "all", {
|
|
78
80
|
message: "dataType applies only to dataset 'hazard' (toxval | genetox | cancer) and dataset 'exposure' (seem | httk | functional-use | product-data) — " +
|
|
@@ -96,13 +98,13 @@ const inputSchema = z
|
|
|
96
98
|
export function createComptoxTool(deps) {
|
|
97
99
|
return defineTool({
|
|
98
100
|
id: "comptox",
|
|
99
|
-
description: "
|
|
100
|
-
"genotoxicity, cancer
|
|
101
|
-
"See `dataset` for what each returns
|
|
102
|
-
"Requires EPA_CCTE_API_KEY — a missing key fails
|
|
103
|
-
"found: false means the query did not resolve to a chemical
|
|
104
|
-
"This is environmental/industrial-chemical
|
|
105
|
-
"
|
|
101
|
+
description: "EPA's CompTox (CTX) chemical-safety databases — four datasets behind `dataset`: 'toxcast' (in-vitro HTS bioactivity), 'hazard' (in-vivo: " +
|
|
102
|
+
"NOAEL/LOAEL/LD50, genotoxicity, cancer), 'chemical' (identity + physicochemical/ADMET), 'exposure' (SEEM predictions, toxicokinetics, uses, " +
|
|
103
|
+
"products). See `dataset` for what each returns; all four resolve `query` to a DTXSID first.\n" +
|
|
104
|
+
"Requires EPA_CCTE_API_KEY — a missing key fails terminally: do NOT retry, tell the user the key needs configuring and proceed without EPA data.\n" +
|
|
105
|
+
"found: false means the query did not resolve to a chemical; a present-but-empty section is likewise valid no-data. Do not retry either.\n" +
|
|
106
|
+
"This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
|
|
107
|
+
"`detail.pubchemCid` to bridge back.",
|
|
106
108
|
inputSchema,
|
|
107
109
|
execute: async (input) => {
|
|
108
110
|
const headers = getEpaCcteHeaders(deps.apiKey);
|
|
@@ -115,7 +117,7 @@ export function createComptoxTool(deps) {
|
|
|
115
117
|
case "toxcast": {
|
|
116
118
|
const bioactivity = await fetchToxcastBioactivity(dtxsid, headers, {
|
|
117
119
|
activeOnly: input.activeOnly ?? true,
|
|
118
|
-
limit: input.limit ??
|
|
120
|
+
limit: input.limit ?? 25,
|
|
119
121
|
});
|
|
120
122
|
return ok({ found: true, chemical: { dtxsid, preferredName, casrn, ...bioactivity } });
|
|
121
123
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drugGeneInteractions — one tool over the three drug↔gene corpora: DGIdb,
|
|
3
|
+
* DrugBank, and PharmGKB.
|
|
4
|
+
*
|
|
5
|
+
* These were three tools with the same input skeleton (`query` + a gene/drug
|
|
6
|
+
* discriminator + `limit`) answering one question in two directions: what drugs
|
|
7
|
+
* hit this gene, and what genes does this drug act on. DGIdb *aggregates* the
|
|
8
|
+
* other two (it names ChEMBL, DrugBank, PharmGKB and GuideToPharmacology among
|
|
9
|
+
* its 30+ sources), so the choice between them was never a routing decision —
|
|
10
|
+
* it is coverage, and it rides in `sources`.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `gene_disease_evidence`, the default here is the AGGREGATOR ALONE:
|
|
13
|
+
* DGIdb already covers what the other two hold, so fanning out by default would
|
|
14
|
+
* mostly return the same interactions three times.
|
|
15
|
+
*
|
|
16
|
+
* Each source runs independently and reports its own outcome in `perSource`, so
|
|
17
|
+
* a missing DrugBank key degrades that corpus rather than the whole call.
|
|
18
|
+
*/
|
|
19
|
+
import type { Logger } from "../../lib/logger.js";
|
|
20
|
+
import { type DgidbResult } from "../lib/dgidb-client.js";
|
|
21
|
+
import { type DrugResult } from "../lib/drugbank-client.js";
|
|
22
|
+
import { type PharmgkbAnnotation } from "../lib/pharmgkb-client.js";
|
|
23
|
+
declare const SOURCES: readonly ["dgidb", "drugbank", "pharmgkb"];
|
|
24
|
+
type Source = (typeof SOURCES)[number];
|
|
25
|
+
interface SourceOutcome {
|
|
26
|
+
source: Source;
|
|
27
|
+
status: "ok" | "no_data" | "unavailable";
|
|
28
|
+
returned: number;
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A DrugBank record projected for context. `includeDrugRecord` decides whether
|
|
33
|
+
* the five prose fields arrive whole or as previews — five 500-character fields
|
|
34
|
+
* across ten records is 25k characters of narrative that a drug-vs-gene
|
|
35
|
+
* question rarely needs.
|
|
36
|
+
*/
|
|
37
|
+
interface ProjectedDrug {
|
|
38
|
+
drugbankId: string;
|
|
39
|
+
name: string;
|
|
40
|
+
type: string;
|
|
41
|
+
groups: string[];
|
|
42
|
+
categories: string[];
|
|
43
|
+
halfLife: string;
|
|
44
|
+
targets: DrugResult["targets"];
|
|
45
|
+
targetCount: number;
|
|
46
|
+
interactions: DrugResult["interactions"];
|
|
47
|
+
interactionCount: number;
|
|
48
|
+
description: string;
|
|
49
|
+
indication: string;
|
|
50
|
+
pharmacodynamics: string;
|
|
51
|
+
mechanismOfAction: string;
|
|
52
|
+
toxicity: string;
|
|
53
|
+
proseTruncated: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface DrugGeneOutput {
|
|
56
|
+
direction: "gene_to_drugs" | "drug_to_genes";
|
|
57
|
+
perSource: SourceOutcome[];
|
|
58
|
+
dgidb?: DgidbResult[];
|
|
59
|
+
drugbank?: ProjectedDrug[];
|
|
60
|
+
pharmgkb?: PharmgkbAnnotation[];
|
|
61
|
+
}
|
|
62
|
+
export declare function createDrugGeneInteractionsTool(deps: {
|
|
63
|
+
drugbankApiKey: string;
|
|
64
|
+
logger?: Logger;
|
|
65
|
+
}): import("../define-tool.js").Tool<{
|
|
66
|
+
query: string | string[];
|
|
67
|
+
direction: "gene_to_drugs" | "drug_to_genes";
|
|
68
|
+
sources?: ("dgidb" | "drugbank" | "pharmgkb")[] | undefined;
|
|
69
|
+
limit?: number | undefined;
|
|
70
|
+
interactionTypes?: string[] | undefined;
|
|
71
|
+
sourceDbs?: string[] | undefined;
|
|
72
|
+
minSources?: number | undefined;
|
|
73
|
+
includeAttributes?: boolean | undefined;
|
|
74
|
+
includeDrugRecord?: boolean | undefined;
|
|
75
|
+
}, DrugGeneOutput>;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drugGeneInteractions — one tool over the three drug↔gene corpora: DGIdb,
|
|
3
|
+
* DrugBank, and PharmGKB.
|
|
4
|
+
*
|
|
5
|
+
* These were three tools with the same input skeleton (`query` + a gene/drug
|
|
6
|
+
* discriminator + `limit`) answering one question in two directions: what drugs
|
|
7
|
+
* hit this gene, and what genes does this drug act on. DGIdb *aggregates* the
|
|
8
|
+
* other two (it names ChEMBL, DrugBank, PharmGKB and GuideToPharmacology among
|
|
9
|
+
* its 30+ sources), so the choice between them was never a routing decision —
|
|
10
|
+
* it is coverage, and it rides in `sources`.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `gene_disease_evidence`, the default here is the AGGREGATOR ALONE:
|
|
13
|
+
* DGIdb already covers what the other two hold, so fanning out by default would
|
|
14
|
+
* mostly return the same interactions three times.
|
|
15
|
+
*
|
|
16
|
+
* Each source runs independently and reports its own outcome in `perSource`, so
|
|
17
|
+
* a missing DrugBank key degrades that corpus rather than the whole call.
|
|
18
|
+
*/
|
|
19
|
+
import { ok } from "neverthrow";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
22
|
+
import { defineTool } from "../define-tool.js";
|
|
23
|
+
import { searchDgidb } from "../lib/dgidb-client.js";
|
|
24
|
+
import { searchDrugbank } from "../lib/drugbank-client.js";
|
|
25
|
+
import { searchPharmgkb } from "../lib/pharmgkb-client.js";
|
|
26
|
+
const SOURCES = ["dgidb", "drugbank", "pharmgkb"];
|
|
27
|
+
const DEFAULT_LIMIT = 20;
|
|
28
|
+
const MAX_LIMIT = 200;
|
|
29
|
+
const MAX_INPUTS = 25;
|
|
30
|
+
/** Prose kept per DrugBank field when the full record is not requested. */
|
|
31
|
+
const PROSE_PREVIEW_CHARS = 200;
|
|
32
|
+
/** Targets and interactions kept per DrugBank record. */
|
|
33
|
+
const MAX_DRUG_TARGETS = 10;
|
|
34
|
+
const MAX_DRUG_INTERACTIONS = 10;
|
|
35
|
+
const inputSchema = z.object({
|
|
36
|
+
query: z
|
|
37
|
+
.union([z.string().min(1), z.array(z.string().min(1)).min(1).max(MAX_INPUTS)])
|
|
38
|
+
.describe(`One identifier or up to ${MAX_INPUTS} — batch rather than calling per identifier. 'gene_to_drugs': HUGO symbols. 'drug_to_genes': drug ` +
|
|
39
|
+
"names, DrugBank IDs (DB00619), or DGIdb concept IDs. Only 'dgidb' batches; the others read the FIRST and report the rest as skipped."),
|
|
40
|
+
direction: z
|
|
41
|
+
.enum(["gene_to_drugs", "drug_to_genes"])
|
|
42
|
+
.describe("'gene_to_drugs' — what drugs act on these genes ('what is druggable in my gene set?'). 'drug_to_genes' — what genes these drugs act on " +
|
|
43
|
+
"(target deconvolution, polypharmacology)."),
|
|
44
|
+
sources: z
|
|
45
|
+
.array(z.enum(SOURCES))
|
|
46
|
+
.min(1)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Corpora to query in parallel. Default ['dgidb'] ALONE — it aggregates the other two, so add them only for what they uniquely hold. " +
|
|
49
|
+
"'dgidb': 30+ aggregated sources, per-interaction source count as confidence; the only one that batches. " +
|
|
50
|
+
"'drugbank': the curated drug RECORD (indication, pharmacodynamics, mechanism, toxicity, half-life, DDIs); needs DRUGBANK_API_KEY. " +
|
|
51
|
+
"'pharmgkb': pharmacogenomic annotations graded 1A…4 (1A guideline-backed); exact match only — 'CYP2D6' resolves, 'cytochrome P450 2D6' does not."),
|
|
52
|
+
limit: z
|
|
53
|
+
.number()
|
|
54
|
+
.int()
|
|
55
|
+
.min(1)
|
|
56
|
+
.max(MAX_LIMIT)
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(`Max records per identifier per source (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}). DGIdb rows are ordered by source count descending; ` +
|
|
59
|
+
"`totalInteractions` gives the pre-trim count."),
|
|
60
|
+
interactionTypes: z.array(z.string()).optional().describe("'dgidb' only. Case-insensitive substring filter on interaction type, e.g. ['inhibitor']."),
|
|
61
|
+
sourceDbs: z.array(z.string()).optional().describe("'dgidb' only. Keep only interactions from these underlying databases (substring), e.g. ['chembl']."),
|
|
62
|
+
minSources: z.number().int().min(1).optional().describe("'dgidb' only. Drop interactions with fewer than this many supporting sources; default 1."),
|
|
63
|
+
includeAttributes: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("'dgidb' only. Default FALSE — attributes are verbose free-text pairs. True only when you need the assay detail, not the interaction's existence."),
|
|
67
|
+
includeDrugRecord: z
|
|
68
|
+
.boolean()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe(`'drugbank' only. Default FALSE — the five prose fields come back as ${PROSE_PREVIEW_CHARS}-char previews with proseTruncated: true. ` +
|
|
71
|
+
"True for full text; narrow `limit` when you do."),
|
|
72
|
+
});
|
|
73
|
+
function projectDrug(drug, includeFullProse) {
|
|
74
|
+
const prose = (value) => (includeFullProse ? value : value.slice(0, PROSE_PREVIEW_CHARS));
|
|
75
|
+
return {
|
|
76
|
+
drugbankId: drug.drugbankId,
|
|
77
|
+
name: drug.name,
|
|
78
|
+
type: drug.type,
|
|
79
|
+
groups: drug.groups,
|
|
80
|
+
categories: drug.categories,
|
|
81
|
+
halfLife: drug.halfLife,
|
|
82
|
+
targets: drug.targets.slice(0, MAX_DRUG_TARGETS),
|
|
83
|
+
targetCount: drug.targets.length,
|
|
84
|
+
interactions: drug.interactions.slice(0, MAX_DRUG_INTERACTIONS),
|
|
85
|
+
interactionCount: drug.interactions.length,
|
|
86
|
+
description: prose(drug.description),
|
|
87
|
+
indication: prose(drug.indication),
|
|
88
|
+
pharmacodynamics: prose(drug.pharmacodynamics),
|
|
89
|
+
mechanismOfAction: prose(drug.mechanismOfAction),
|
|
90
|
+
toxicity: prose(drug.toxicity),
|
|
91
|
+
proseTruncated: !includeFullProse,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run one corpus, folding its failure into an `unavailable` outcome so a dead
|
|
96
|
+
* or unkeyed source cannot take down a call the others could answer.
|
|
97
|
+
*/
|
|
98
|
+
async function runSource(source, fetch) {
|
|
99
|
+
try {
|
|
100
|
+
const records = await fetch();
|
|
101
|
+
return { outcome: { source, status: records.length > 0 ? "ok" : "no_data", returned: records.length }, records };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
outcome: { source, status: "unavailable", returned: 0, detail: error instanceof Error ? error.message : String(error) },
|
|
106
|
+
records: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function createDrugGeneInteractionsTool(deps) {
|
|
111
|
+
const logger = (deps.logger ?? createNoopLogger()).named("drug_gene_interactions");
|
|
112
|
+
return defineTool({
|
|
113
|
+
id: "drug_gene_interactions",
|
|
114
|
+
description: "Drug↔gene interactions across DGIdb, DrugBank and PharmGKB, in one call — 'what drugs hit these genes?' and 'what genes does this drug act " +
|
|
115
|
+
"on?'. See `direction` and `sources` for the modes.\n" +
|
|
116
|
+
"It answers whether an interaction is KNOWN, by whom, and of what type. For quotable POTENCY (IC50/Ki) use chembl({action:'bioactivity'}) instead.\n" +
|
|
117
|
+
"Verbose fields (`includeAttributes`, `includeDrugRecord`) are off by default — both cost a lot of context and are rarely what the question needs.\n" +
|
|
118
|
+
"ALWAYS read `perSource` before concluding an interaction is unknown: 'no_data' means that corpus has nothing; 'unavailable' means it could " +
|
|
119
|
+
"not be reached (a missing DRUGBANK_API_KEY lands here — tell the user and proceed with DGIdb). A `found: false` entry means the identifier " +
|
|
120
|
+
"is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
|
|
121
|
+
inputSchema,
|
|
122
|
+
execute: async (input) => {
|
|
123
|
+
const inputs = typeof input.query === "string" ? [input.query] : input.query;
|
|
124
|
+
const selected = input.sources ?? ["dgidb"];
|
|
125
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
126
|
+
const isGeneSide = input.direction === "gene_to_drugs";
|
|
127
|
+
const output = { direction: input.direction, perSource: [] };
|
|
128
|
+
const [dgidb, drugbank, pharmgkb] = await Promise.all([
|
|
129
|
+
selected.includes("dgidb")
|
|
130
|
+
? runSource("dgidb", () => searchDgidb(inputs, isGeneSide ? "gene" : "drug", {
|
|
131
|
+
limit,
|
|
132
|
+
...(input.interactionTypes ? { interactionTypes: input.interactionTypes } : {}),
|
|
133
|
+
...(input.sourceDbs ? { sourceDbs: input.sourceDbs } : {}),
|
|
134
|
+
...(input.minSources !== undefined ? { minSources: input.minSources } : {}),
|
|
135
|
+
...(input.includeAttributes !== undefined ? { includeAttributes: input.includeAttributes } : {}),
|
|
136
|
+
}, (errors) => logger.warn("dgidb partial errors alongside data", { errors })))
|
|
137
|
+
: null,
|
|
138
|
+
selected.includes("drugbank")
|
|
139
|
+
? runSource("drugbank", async () => {
|
|
140
|
+
if (!deps.drugbankApiKey)
|
|
141
|
+
throw new Error("DRUGBANK_API_KEY is not configured");
|
|
142
|
+
const drugs = await searchDrugbank(deps.drugbankApiKey, inputs[0], isGeneSide ? "target" : "drug", limit);
|
|
143
|
+
return drugs.map((d) => projectDrug(d, input.includeDrugRecord ?? false));
|
|
144
|
+
})
|
|
145
|
+
: null,
|
|
146
|
+
selected.includes("pharmgkb")
|
|
147
|
+
? runSource("pharmgkb", async () => {
|
|
148
|
+
const res = await searchPharmgkb(inputs[0], isGeneSide ? "gene" : "drug", limit);
|
|
149
|
+
return res.annotations;
|
|
150
|
+
})
|
|
151
|
+
: null,
|
|
152
|
+
]);
|
|
153
|
+
// DrugBank and PharmGKB take one identifier per call; say so on the
|
|
154
|
+
// outcome rather than letting the extra inputs vanish silently.
|
|
155
|
+
const skipped = inputs.length > 1 ? `read only '${inputs[0]}' — this source takes one identifier per call` : "";
|
|
156
|
+
const noteSkipped = (outcome) => {
|
|
157
|
+
const detail = [outcome.detail, skipped].filter(Boolean).join("; ");
|
|
158
|
+
return detail ? { ...outcome, detail } : outcome;
|
|
159
|
+
};
|
|
160
|
+
if (dgidb) {
|
|
161
|
+
output.perSource.push(dgidb.outcome);
|
|
162
|
+
output.dgidb = dgidb.records;
|
|
163
|
+
}
|
|
164
|
+
if (drugbank) {
|
|
165
|
+
output.perSource.push(noteSkipped(drugbank.outcome));
|
|
166
|
+
output.drugbank = drugbank.records;
|
|
167
|
+
}
|
|
168
|
+
if (pharmgkb) {
|
|
169
|
+
output.perSource.push(noteSkipped(pharmgkb.outcome));
|
|
170
|
+
output.pharmgkb = pharmgkb.records;
|
|
171
|
+
}
|
|
172
|
+
return ok(output);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geneDiseaseEvidence — one tool over the three gene↔disease evidence corpora:
|
|
3
|
+
* the NHGRI-EBI GWAS Catalog, DisGeNET, and NCBI ClinVar.
|
|
4
|
+
*
|
|
5
|
+
* These were three tools with the same input skeleton (`query` + a
|
|
6
|
+
* gene/disease/variant discriminator + a strength threshold + `limit`)
|
|
7
|
+
* answering the same question — what genetic evidence links this gene to this
|
|
8
|
+
* disease or trait. Picking between them was never a routing decision an agent
|
|
9
|
+
* should spend a turn on; it is corpus coverage, so it rides in `sources` and
|
|
10
|
+
* defaults to all of them. `search_pathway`'s `source: kegg | reactome | both`
|
|
11
|
+
* is the same shape.
|
|
12
|
+
*
|
|
13
|
+
* Each source runs independently and reports its own outcome in `perSource`: a
|
|
14
|
+
* missing DisGeNET key or a GWAS Catalog outage degrades that one corpus to
|
|
15
|
+
* `unavailable` instead of failing a call the other two could have answered.
|
|
16
|
+
*
|
|
17
|
+
* The input is a flat object with a `queryType` discriminator — not a
|
|
18
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
19
|
+
* a top-level `"type":"object"`).
|
|
20
|
+
*/
|
|
21
|
+
import { type ClinvarVariant } from "../lib/clinvar-client.js";
|
|
22
|
+
import { type Gda } from "../lib/disgenet-client.js";
|
|
23
|
+
import { type GwasAssociation } from "../lib/gwas-catalog-client.js";
|
|
24
|
+
declare const SOURCES: readonly ["gwas", "disgenet", "clinvar"];
|
|
25
|
+
type Source = (typeof SOURCES)[number];
|
|
26
|
+
/**
|
|
27
|
+
* Per-source outcome. `unavailable` (key absent, corpus erroring) and
|
|
28
|
+
* `not_applicable` (the corpus has no lookup for this queryType) are both
|
|
29
|
+
* normal states the caller must be able to tell apart from genuine no-data —
|
|
30
|
+
* otherwise an unkeyed DisGeNET reads as "no genetic support".
|
|
31
|
+
*/
|
|
32
|
+
interface SourceOutcome {
|
|
33
|
+
source: Source;
|
|
34
|
+
status: "ok" | "no_data" | "unavailable" | "not_applicable";
|
|
35
|
+
returned: number;
|
|
36
|
+
totalFound?: number;
|
|
37
|
+
detail?: string;
|
|
38
|
+
}
|
|
39
|
+
interface GeneDiseaseEvidenceOutput {
|
|
40
|
+
query: string;
|
|
41
|
+
queryType: "gene" | "disease" | "variant";
|
|
42
|
+
perSource: SourceOutcome[];
|
|
43
|
+
gwas?: GwasAssociation[];
|
|
44
|
+
disgenet?: Gda[];
|
|
45
|
+
clinvar?: ClinvarVariant[];
|
|
46
|
+
}
|
|
47
|
+
export declare function createGeneDiseaseEvidenceTool(deps: {
|
|
48
|
+
ncbiApiKey?: string;
|
|
49
|
+
disgenetApiKey: string;
|
|
50
|
+
}): import("../define-tool.js").Tool<{
|
|
51
|
+
query: string;
|
|
52
|
+
queryType: "gene" | "disease" | "variant";
|
|
53
|
+
sources?: ("clinvar" | "gwas" | "disgenet")[] | undefined;
|
|
54
|
+
limit?: number | undefined;
|
|
55
|
+
pValueThreshold?: number | undefined;
|
|
56
|
+
minScore?: number | undefined;
|
|
57
|
+
disgenetSource?: "ALL" | "CURATED" | "ANIMAL_MODELS" | "BEFREE" | undefined;
|
|
58
|
+
clinicalSignificance?: "pathogenic" | "likely-pathogenic" | "benign" | "likely-benign" | "uncertain" | undefined;
|
|
59
|
+
informativeOnly?: boolean | undefined;
|
|
60
|
+
}, GeneDiseaseEvidenceOutput>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geneDiseaseEvidence — one tool over the three gene↔disease evidence corpora:
|
|
3
|
+
* the NHGRI-EBI GWAS Catalog, DisGeNET, and NCBI ClinVar.
|
|
4
|
+
*
|
|
5
|
+
* These were three tools with the same input skeleton (`query` + a
|
|
6
|
+
* gene/disease/variant discriminator + a strength threshold + `limit`)
|
|
7
|
+
* answering the same question — what genetic evidence links this gene to this
|
|
8
|
+
* disease or trait. Picking between them was never a routing decision an agent
|
|
9
|
+
* should spend a turn on; it is corpus coverage, so it rides in `sources` and
|
|
10
|
+
* defaults to all of them. `search_pathway`'s `source: kegg | reactome | both`
|
|
11
|
+
* is the same shape.
|
|
12
|
+
*
|
|
13
|
+
* Each source runs independently and reports its own outcome in `perSource`: a
|
|
14
|
+
* missing DisGeNET key or a GWAS Catalog outage degrades that one corpus to
|
|
15
|
+
* `unavailable` instead of failing a call the other two could have answered.
|
|
16
|
+
*
|
|
17
|
+
* The input is a flat object with a `queryType` discriminator — not a
|
|
18
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
19
|
+
* a top-level `"type":"object"`).
|
|
20
|
+
*/
|
|
21
|
+
import { ok } from "neverthrow";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { defineTool } from "../define-tool.js";
|
|
24
|
+
import { filterInformative, searchClinvar } from "../lib/clinvar-client.js";
|
|
25
|
+
import { searchDisgenet } from "../lib/disgenet-client.js";
|
|
26
|
+
import { searchGwasCatalog } from "../lib/gwas-catalog-client.js";
|
|
27
|
+
const SOURCES = ["gwas", "disgenet", "clinvar"];
|
|
28
|
+
const DEFAULT_LIMIT = 10;
|
|
29
|
+
const MAX_LIMIT = 100;
|
|
30
|
+
/** DisGeNET has no variant-level lookup; the other two do. */
|
|
31
|
+
function applicableSources(queryType) {
|
|
32
|
+
return queryType === "variant" ? ["gwas", "clinvar"] : [...SOURCES];
|
|
33
|
+
}
|
|
34
|
+
const inputSchema = z.object({
|
|
35
|
+
query: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.describe("The entity, matching `queryType`. gene: a HUGO symbol ('PCSK9'); DisGeNET also takes an Entrez ID. disease: a disease or trait name " +
|
|
39
|
+
"('LDL cholesterol'); DisGeNET also takes a UMLS CUI ('C0006142'). variant: a dbSNP rsID ('rs11591147')."),
|
|
40
|
+
queryType: z
|
|
41
|
+
.enum(["gene", "disease", "variant"])
|
|
42
|
+
.describe("'gene' — the diseases/traits associated with a gene. 'disease' — the genes associated with a disease or trait (the GWAS Catalog calls it a " +
|
|
43
|
+
"trait; this value covers both). 'variant' — one rsID; DisGeNET is skipped, having no variant-level lookup."),
|
|
44
|
+
sources: z
|
|
45
|
+
.array(z.enum(SOURCES))
|
|
46
|
+
.min(1)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Corpora to query in parallel. Default ALL applicable — they carry different evidence types, and disagreement between them is itself informative. " +
|
|
49
|
+
"'gwas': population SNP-trait associations with effect sizes and p-values (common-variant / MR support). " +
|
|
50
|
+
"'disgenet': curated + text-mined gene-disease associations scored 0–1, broadest coverage; needs DISGENET_API_KEY. " +
|
|
51
|
+
"'clinvar': clinically classified germline variants — pathogenicity and review status; rare/Mendelian rather than common-variant."),
|
|
52
|
+
limit: z
|
|
53
|
+
.number()
|
|
54
|
+
.int()
|
|
55
|
+
.min(1)
|
|
56
|
+
.max(MAX_LIMIT)
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(`Max records PER SOURCE (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}); all three sources at the default return at most ` +
|
|
59
|
+
`${DEFAULT_LIMIT * 3} records. Check each source's \`totalFound\` before raising it.`),
|
|
60
|
+
pValueThreshold: z
|
|
61
|
+
.number()
|
|
62
|
+
.positive()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("'gwas' only. P-value ceiling; default 5e-8 (genome-wide significance). Raise for suggestive hits."),
|
|
65
|
+
minScore: z.number().min(0).max(1).optional().describe("'disgenet' only. Minimum GDA score 0–1; default 0.1. Higher = stronger evidence."),
|
|
66
|
+
disgenetSource: z
|
|
67
|
+
.enum(["ALL", "CURATED", "ANIMAL_MODELS", "BEFREE"])
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("'disgenet' only. Provenance filter; default 'ALL'. Prefer 'CURATED' for anything you will quote — 'BEFREE' is text-mined and noisy."),
|
|
70
|
+
clinicalSignificance: z
|
|
71
|
+
.enum(["pathogenic", "likely-pathogenic", "benign", "likely-benign", "uncertain"])
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("'clinvar' only. Restrict to one pathogenicity classification; omit for all."),
|
|
74
|
+
informativeOnly: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("'clinvar' only. Default TRUE — drop variants classified 'not provided', 'not specified' or 'reclassified', which carry no clinical signal."),
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Run one corpus, folding its failure into an `unavailable` outcome. A source
|
|
81
|
+
* that throws must not take the others down — that is the whole reason three
|
|
82
|
+
* tools could become one without losing the ability to say "this corpus was
|
|
83
|
+
* not reachable" as distinct from "this corpus knows nothing".
|
|
84
|
+
*/
|
|
85
|
+
async function runSource(source, fetch) {
|
|
86
|
+
try {
|
|
87
|
+
const { records, totalFound } = await fetch();
|
|
88
|
+
return {
|
|
89
|
+
outcome: {
|
|
90
|
+
source,
|
|
91
|
+
status: records.length > 0 ? "ok" : "no_data",
|
|
92
|
+
returned: records.length,
|
|
93
|
+
...(totalFound !== undefined ? { totalFound } : {}),
|
|
94
|
+
},
|
|
95
|
+
records,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
outcome: {
|
|
101
|
+
source,
|
|
102
|
+
status: "unavailable",
|
|
103
|
+
returned: 0,
|
|
104
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
105
|
+
},
|
|
106
|
+
records: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function createGeneDiseaseEvidenceTool(deps) {
|
|
111
|
+
return defineTool({
|
|
112
|
+
id: "gene_disease_evidence",
|
|
113
|
+
description: "Genetic evidence linking a gene to a disease or trait, across the GWAS Catalog, DisGeNET and ClinVar in one call — 'is there human genetic " +
|
|
114
|
+
"support for this target in this indication?'. See `sources` for what each corpus carries.\n" +
|
|
115
|
+
"For the integrated, pre-scored view prefer opentargets({action:'target'}) FIRST — it folds genetic association together with tractability " +
|
|
116
|
+
"and the drug landscape. Reach here for the underlying records: actual SNPs, effect sizes, scores and pathogenicity calls with their studies and PMIDs.\n" +
|
|
117
|
+
"ALWAYS read `perSource` before concluding anything is absent. 'no_data' means that corpus genuinely has nothing; 'unavailable' means it " +
|
|
118
|
+
"could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the other two); 'not_applicable' means that " +
|
|
119
|
+
"corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
|
|
120
|
+
inputSchema,
|
|
121
|
+
execute: async (input) => {
|
|
122
|
+
const applicable = applicableSources(input.queryType);
|
|
123
|
+
const requested = input.sources ?? applicable;
|
|
124
|
+
const selected = requested.filter((s) => applicable.includes(s));
|
|
125
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
126
|
+
const output = {
|
|
127
|
+
query: input.query,
|
|
128
|
+
queryType: input.queryType,
|
|
129
|
+
perSource: requested
|
|
130
|
+
.filter((s) => !applicable.includes(s))
|
|
131
|
+
.map((source) => ({
|
|
132
|
+
source,
|
|
133
|
+
status: "not_applicable",
|
|
134
|
+
returned: 0,
|
|
135
|
+
detail: `${source} has no lookup for queryType '${input.queryType}'`,
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
const [gwas, disgenet, clinvar] = await Promise.all([
|
|
139
|
+
selected.includes("gwas")
|
|
140
|
+
? runSource("gwas", async () => {
|
|
141
|
+
const searchType = input.queryType === "gene" ? "gene" : input.queryType === "disease" ? "trait" : "variant";
|
|
142
|
+
const res = await searchGwasCatalog(input.query, searchType, {
|
|
143
|
+
limit,
|
|
144
|
+
...(input.pValueThreshold !== undefined ? { pValueThreshold: input.pValueThreshold } : {}),
|
|
145
|
+
});
|
|
146
|
+
return { records: res.associations, totalFound: res.totalFound };
|
|
147
|
+
})
|
|
148
|
+
: null,
|
|
149
|
+
selected.includes("disgenet")
|
|
150
|
+
? runSource("disgenet", async () => {
|
|
151
|
+
if (!deps.disgenetApiKey)
|
|
152
|
+
throw new Error("DISGENET_API_KEY is not configured");
|
|
153
|
+
const records = await searchDisgenet(deps.disgenetApiKey, input.query, input.queryType === "gene" ? "gene" : "disease", {
|
|
154
|
+
limit,
|
|
155
|
+
...(input.minScore !== undefined ? { minScore: input.minScore } : {}),
|
|
156
|
+
...(input.disgenetSource !== undefined ? { source: input.disgenetSource } : {}),
|
|
157
|
+
});
|
|
158
|
+
return { records };
|
|
159
|
+
})
|
|
160
|
+
: null,
|
|
161
|
+
selected.includes("clinvar")
|
|
162
|
+
? runSource("clinvar", async () => {
|
|
163
|
+
const res = await searchClinvar(deps.ncbiApiKey, input.query, {
|
|
164
|
+
limit,
|
|
165
|
+
...(input.clinicalSignificance !== undefined ? { clinicalSignificance: input.clinicalSignificance } : {}),
|
|
166
|
+
});
|
|
167
|
+
const variants = (input.informativeOnly ?? true) ? filterInformative(res.variants) : res.variants;
|
|
168
|
+
return { records: variants, totalFound: res.totalFound };
|
|
169
|
+
})
|
|
170
|
+
: null,
|
|
171
|
+
]);
|
|
172
|
+
if (gwas) {
|
|
173
|
+
output.perSource.push(gwas.outcome);
|
|
174
|
+
output.gwas = gwas.records;
|
|
175
|
+
}
|
|
176
|
+
if (disgenet) {
|
|
177
|
+
output.perSource.push(disgenet.outcome);
|
|
178
|
+
output.disgenet = disgenet.records;
|
|
179
|
+
}
|
|
180
|
+
if (clinvar) {
|
|
181
|
+
output.perSource.push(clinvar.outcome);
|
|
182
|
+
output.clinvar = clinvar.records;
|
|
183
|
+
}
|
|
184
|
+
return ok(output);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|