@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* targetSafety — target-level, mechanism-based safety liability from the two
|
|
3
|
+
* sources that carry it: the curated secondary-pharmacology panel that ships
|
|
4
|
+
* with the harness, and Open Targets' curated liabilities.
|
|
5
|
+
*
|
|
6
|
+
* These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
|
|
7
|
+
* panel tool's own description instructed calling them together — a pairing
|
|
8
|
+
* every caller had to remember, expressed in prose that had already gone stale
|
|
9
|
+
* (it named two tools that no longer exist). Both are keyed on the same target
|
|
10
|
+
* and answer the same question, so the pairing belongs in the tool.
|
|
11
|
+
*
|
|
12
|
+
* Identifier resolution is the tool's job, not the caller's: Open Targets
|
|
13
|
+
* accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
|
|
14
|
+
* there is one and resolved from the gene symbol when there is not.
|
|
15
|
+
*/
|
|
16
|
+
import { type SafetyTarget } from "../../data/safety-panel-schema.js";
|
|
17
|
+
import { type SafetyLiability } from "../lib/opentargets-client.js";
|
|
18
|
+
declare const SOURCES: readonly ["panel", "opentargets"];
|
|
19
|
+
type Source = (typeof SOURCES)[number];
|
|
20
|
+
type ConcreteType = "chembl_id" | "gene_symbol" | "uniprot" | "ensembl_id";
|
|
21
|
+
interface TargetRow {
|
|
22
|
+
input: string;
|
|
23
|
+
identifierType: ConcreteType;
|
|
24
|
+
/** The Ensembl id Open Targets was queried with, when one could be obtained. */
|
|
25
|
+
ensemblId?: string;
|
|
26
|
+
panelEntry?: SafetyTarget | null;
|
|
27
|
+
liabilities?: SafetyLiability[];
|
|
28
|
+
/** Liabilities Open Targets held before `liabilityLimit` trimmed them. */
|
|
29
|
+
liabilityCount?: number;
|
|
30
|
+
/** Why the Open Targets half is absent for this identifier, when it is. */
|
|
31
|
+
opentargetsNote?: string;
|
|
32
|
+
}
|
|
33
|
+
interface SourceOutcome {
|
|
34
|
+
source: Source;
|
|
35
|
+
status: "ok" | "no_data" | "unavailable";
|
|
36
|
+
/** Identifiers this source returned something for. */
|
|
37
|
+
matched: number;
|
|
38
|
+
detail?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const targetSafetyTool: import("../define-tool.js").Tool<{
|
|
41
|
+
identifiers: string[];
|
|
42
|
+
identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
|
|
43
|
+
sources?: ("opentargets" | "panel")[] | undefined;
|
|
44
|
+
filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi" | undefined;
|
|
45
|
+
minSeverity?: "high" | "medium" | "low" | undefined;
|
|
46
|
+
liabilityLimit?: number | undefined;
|
|
47
|
+
}, {
|
|
48
|
+
perSource: SourceOutcome[];
|
|
49
|
+
summary: {
|
|
50
|
+
totalInput: number;
|
|
51
|
+
panelMatched: number;
|
|
52
|
+
bySeverity: Record<"high" | "medium" | "low", number>;
|
|
53
|
+
byOrgan: Record<string, number>;
|
|
54
|
+
openTargetsWithLiabilities: number;
|
|
55
|
+
};
|
|
56
|
+
targets: TargetRow[];
|
|
57
|
+
panelVersion: string;
|
|
58
|
+
}>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* targetSafety — target-level, mechanism-based safety liability from the two
|
|
3
|
+
* sources that carry it: the curated secondary-pharmacology panel that ships
|
|
4
|
+
* with the harness, and Open Targets' curated liabilities.
|
|
5
|
+
*
|
|
6
|
+
* These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
|
|
7
|
+
* panel tool's own description instructed calling them together — a pairing
|
|
8
|
+
* every caller had to remember, expressed in prose that had already gone stale
|
|
9
|
+
* (it named two tools that no longer exist). Both are keyed on the same target
|
|
10
|
+
* and answer the same question, so the pairing belongs in the tool.
|
|
11
|
+
*
|
|
12
|
+
* Identifier resolution is the tool's job, not the caller's: Open Targets
|
|
13
|
+
* accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
|
|
14
|
+
* there is one and resolved from the gene symbol when there is not.
|
|
15
|
+
*/
|
|
16
|
+
import { ok } from "neverthrow";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { CHEMBL_RE, ORGAN_SYSTEMS, SEVERITIES, SafetyPanelFileSchema, UNIPROT_RE, } from "../../data/safety-panel-schema.js";
|
|
19
|
+
import panelData from "../../data/safety-panel.json" with { type: "json" };
|
|
20
|
+
import { defineTool } from "../define-tool.js";
|
|
21
|
+
import { resolveSymbolToEnsemblId } from "../lib/ensembl-client.js";
|
|
22
|
+
import { getTargetSafetyLiabilities } from "../lib/opentargets-client.js";
|
|
23
|
+
const PANEL = SafetyPanelFileSchema.parse(panelData);
|
|
24
|
+
const BY_CHEMBL = new Map(PANEL.targets.map((t) => [t.chembl_id, t]));
|
|
25
|
+
const BY_GENE = new Map(PANEL.targets.map((t) => [t.gene_symbol, t]));
|
|
26
|
+
const BY_UNIPROT = new Map(PANEL.targets.map((t) => [t.uniprot, t]));
|
|
27
|
+
const ENSG_RE = /^ENSG\d{11}$/;
|
|
28
|
+
/** Ordered weakest → strongest so `minSeverity` can be a numeric floor. */
|
|
29
|
+
const SEVERITY_ORDER = { low: 0, medium: 1, high: 2 };
|
|
30
|
+
const SOURCES = ["panel", "opentargets"];
|
|
31
|
+
/** Identifiers the panel can be swept for free; Open Targets costs a request each. */
|
|
32
|
+
const MAX_PANEL_IDENTIFIERS = 200;
|
|
33
|
+
const MAX_OPENTARGETS_IDENTIFIERS = 25;
|
|
34
|
+
const DEFAULT_LIABILITY_LIMIT = 10;
|
|
35
|
+
function detectType(id) {
|
|
36
|
+
if (CHEMBL_RE.test(id))
|
|
37
|
+
return "chembl_id";
|
|
38
|
+
if (ENSG_RE.test(id))
|
|
39
|
+
return "ensembl_id";
|
|
40
|
+
if (UNIPROT_RE.test(id))
|
|
41
|
+
return "uniprot";
|
|
42
|
+
return "gene_symbol";
|
|
43
|
+
}
|
|
44
|
+
function panelLookup(id, type) {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case "chembl_id":
|
|
47
|
+
return BY_CHEMBL.get(id) ?? null;
|
|
48
|
+
case "gene_symbol":
|
|
49
|
+
return BY_GENE.get(id.toUpperCase()) ?? null;
|
|
50
|
+
case "uniprot":
|
|
51
|
+
return BY_UNIPROT.get(id) ?? null;
|
|
52
|
+
case "ensembl_id":
|
|
53
|
+
return PANEL.targets.find((t) => t.ensembl_gene_id === id) ?? null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const inputSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
identifiers: z
|
|
59
|
+
.array(z.string().min(1))
|
|
60
|
+
.min(1)
|
|
61
|
+
.max(MAX_PANEL_IDENTIFIERS)
|
|
62
|
+
.describe(`Target identifiers, 1–${MAX_PANEL_IDENTIFIERS} — gene symbols, ChEMBL target IDs, UniProt accessions, or Ensembl gene IDs, mixed ` +
|
|
63
|
+
`freely. Capped at ${MAX_OPENTARGETS_IDENTIFIERS} when source 'opentargets' is included, since that source costs a request per target.`),
|
|
64
|
+
identifierType: z
|
|
65
|
+
.enum(["chembl_id", "gene_symbol", "uniprot", "ensembl_id", "auto"])
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Default 'auto', detecting per input by shape (CHEMBL…, ENSG…, UniProt accession, else a gene symbol). Set it only to force one reading."),
|
|
68
|
+
sources: z
|
|
69
|
+
.array(z.enum(SOURCES))
|
|
70
|
+
.min(1)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Default BOTH — the panel is a local lookup with no request cost, and they cover different ground. " +
|
|
73
|
+
"'panel': the curated secondary-pharmacology panel — a fast first-pass off-target screen giving organ system, severity and clinical " +
|
|
74
|
+
"consequence with references. Finite and hand-curated, so absence is NOT evidence of safety. " +
|
|
75
|
+
"'opentargets': curated target liabilities — event, affected biosamples, direction of effect, source. Needs an Ensembl gene id, taken " +
|
|
76
|
+
"from the panel entry or resolved from the symbol for you."),
|
|
77
|
+
filterOrgan: z.enum(ORGAN_SYSTEMS).optional().describe("'panel' only. Keep only matches in this organ system, when screening one liability class."),
|
|
78
|
+
minSeverity: z.enum(SEVERITIES).optional().describe("'panel' only. Drop matches below this severity; default 'low'. Use 'high' to triage a large set."),
|
|
79
|
+
liabilityLimit: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.min(1)
|
|
83
|
+
.max(100)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(`'opentargets' only. Max liabilities per target (default ${DEFAULT_LIABILITY_LIMIT}, max 100); \`liabilityCount\` gives the true total.`),
|
|
86
|
+
})
|
|
87
|
+
.refine((d) => (d.sources ?? [...SOURCES]).includes("opentargets") === false || d.identifiers.length <= MAX_OPENTARGETS_IDENTIFIERS, {
|
|
88
|
+
message: `identifiers is capped at ${MAX_OPENTARGETS_IDENTIFIERS} when source 'opentargets' is included (one request per target). ` +
|
|
89
|
+
`Either narrow the list, or pass sources: ['panel'] to sweep up to ${MAX_PANEL_IDENTIFIERS} against the local panel first and follow up on the hits.`,
|
|
90
|
+
path: ["identifiers"],
|
|
91
|
+
});
|
|
92
|
+
export const targetSafetyTool = defineTool({
|
|
93
|
+
id: "target_safety",
|
|
94
|
+
description: "TARGET-level, mechanism-based safety liability — the curated secondary-pharmacology panel and Open Targets' curated liabilities in one call. " +
|
|
95
|
+
"Judges what engaging a target is likely to do to an organ system, before any specific molecule exists. Identifiers may be gene symbols, ChEMBL " +
|
|
96
|
+
"target IDs, UniProt accessions or Ensembl gene IDs, mixed.\n" +
|
|
97
|
+
"This is the safety of the TARGET, not of a drug. For a marketed molecule's post-market adverse events use search_faers; for a chemical's " +
|
|
98
|
+
"toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
|
|
99
|
+
"ABSENCE IS NOT SAFETY. The panel is finite and hand-curated, so no match means only 'not on the panel'; an empty Open Targets liability list " +
|
|
100
|
+
"means no CURATED liability, not that none exists. Both are valid no-data — do not retry. Read `perSource` and each row's `opentargetsNote`: a " +
|
|
101
|
+
"ChEMBL or UniProt identifier absent from the panel cannot be resolved to an Ensembl id, so its Open Targets half is skipped rather than empty.",
|
|
102
|
+
inputSchema,
|
|
103
|
+
execute: async ({ identifiers, identifierType, sources, filterOrgan, minSeverity, liabilityLimit }) => {
|
|
104
|
+
const selected = sources ?? [...SOURCES];
|
|
105
|
+
const wantPanel = selected.includes("panel");
|
|
106
|
+
const wantOpenTargets = selected.includes("opentargets");
|
|
107
|
+
const severityFloor = SEVERITY_ORDER[minSeverity ?? "low"];
|
|
108
|
+
const limit = liabilityLimit ?? DEFAULT_LIABILITY_LIMIT;
|
|
109
|
+
const rows = identifiers.map((input) => {
|
|
110
|
+
const type = identifierType && identifierType !== "auto" ? identifierType : detectType(input);
|
|
111
|
+
const row = { input, identifierType: type };
|
|
112
|
+
if (wantPanel) {
|
|
113
|
+
const entry = panelLookup(input, type);
|
|
114
|
+
const organMatches = !entry || !filterOrgan || entry.organ_system === filterOrgan;
|
|
115
|
+
const severityMatches = !entry || SEVERITY_ORDER[entry.severity] >= severityFloor;
|
|
116
|
+
row.panelEntry = entry && organMatches && severityMatches ? entry : null;
|
|
117
|
+
}
|
|
118
|
+
return row;
|
|
119
|
+
});
|
|
120
|
+
let opentargetsError;
|
|
121
|
+
if (wantOpenTargets) {
|
|
122
|
+
await Promise.all(rows.map(async (row) => {
|
|
123
|
+
// Open Targets takes an ENSG only. The panel entry carries one for
|
|
124
|
+
// free; a gene symbol can be resolved; a ChEMBL or UniProt id that
|
|
125
|
+
// is not on the panel cannot, and is skipped explicitly.
|
|
126
|
+
const panelEnsembl = panelLookup(row.input, row.identifierType)?.ensembl_gene_id;
|
|
127
|
+
let ensemblId = row.identifierType === "ensembl_id" ? row.input : (panelEnsembl ?? null);
|
|
128
|
+
if (!ensemblId && row.identifierType === "gene_symbol") {
|
|
129
|
+
ensemblId = await resolveSymbolToEnsemblId(row.input);
|
|
130
|
+
if (!ensemblId) {
|
|
131
|
+
row.opentargetsNote = "gene symbol did not resolve to an Ensembl gene id";
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!ensemblId) {
|
|
136
|
+
row.opentargetsNote = `no Ensembl gene id available for a ${row.identifierType} that is not on the panel`;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
row.ensemblId = ensemblId;
|
|
140
|
+
try {
|
|
141
|
+
const result = await getTargetSafetyLiabilities(ensemblId);
|
|
142
|
+
if (!result) {
|
|
143
|
+
row.opentargetsNote = "no Open Targets record for this Ensembl gene id";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
row.liabilityCount = result.safetyLiabilities.length;
|
|
147
|
+
row.liabilities = result.safetyLiabilities.slice(0, limit);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
opentargetsError = error instanceof Error ? error.message : String(error);
|
|
151
|
+
row.opentargetsNote = "Open Targets could not be reached";
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
const bySeverity = { high: 0, medium: 0, low: 0 };
|
|
156
|
+
const byOrgan = {};
|
|
157
|
+
let panelMatched = 0;
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
if (!row.panelEntry)
|
|
160
|
+
continue;
|
|
161
|
+
panelMatched++;
|
|
162
|
+
bySeverity[row.panelEntry.severity]++;
|
|
163
|
+
byOrgan[row.panelEntry.organ_system] = (byOrgan[row.panelEntry.organ_system] ?? 0) + 1;
|
|
164
|
+
}
|
|
165
|
+
const withLiabilities = rows.filter((r) => (r.liabilities?.length ?? 0) > 0).length;
|
|
166
|
+
const perSource = [];
|
|
167
|
+
if (wantPanel) {
|
|
168
|
+
perSource.push({ source: "panel", status: panelMatched > 0 ? "ok" : "no_data", matched: panelMatched });
|
|
169
|
+
}
|
|
170
|
+
if (wantOpenTargets) {
|
|
171
|
+
perSource.push({
|
|
172
|
+
source: "opentargets",
|
|
173
|
+
status: opentargetsError ? "unavailable" : withLiabilities > 0 ? "ok" : "no_data",
|
|
174
|
+
matched: withLiabilities,
|
|
175
|
+
...(opentargetsError ? { detail: opentargetsError } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return ok({
|
|
179
|
+
perSource,
|
|
180
|
+
summary: {
|
|
181
|
+
totalInput: identifiers.length,
|
|
182
|
+
panelMatched,
|
|
183
|
+
bySeverity,
|
|
184
|
+
byOrgan: byOrgan,
|
|
185
|
+
openTargetsWithLiabilities: withLiabilities,
|
|
186
|
+
},
|
|
187
|
+
targets: rows,
|
|
188
|
+
panelVersion: PANEL.panel_version,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
});
|
|
@@ -150,7 +150,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
150
150
|
description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
|
|
151
151
|
"Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
|
|
152
152
|
"that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
|
|
153
|
-
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId; inspect results on a later turn.",
|
|
153
|
+
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn.",
|
|
154
154
|
inputSchema,
|
|
155
155
|
execute: async (input, ctx) => {
|
|
156
156
|
if (ctx.session.scope.kind !== "analysis")
|
|
@@ -174,7 +174,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
174
174
|
const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
|
|
175
175
|
if (active) {
|
|
176
176
|
await emitRunCard(active.runId);
|
|
177
|
-
return ok({ runId: active.runId });
|
|
177
|
+
return ok({ runId: active.runId, status: "in_progress" });
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
else {
|
|
@@ -196,7 +196,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
196
196
|
const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
|
|
197
197
|
if (active) {
|
|
198
198
|
await emitRunCard(active.runId);
|
|
199
|
-
return ok({ runId: active.runId });
|
|
199
|
+
return ok({ runId: active.runId, status: "in_progress" });
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
throw error;
|
|
@@ -212,7 +212,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
212
212
|
}));
|
|
213
213
|
if (!reservation.inserted) {
|
|
214
214
|
await emitRunCard(reservation.row.runId);
|
|
215
|
-
return ok({ runId: reservation.row.runId });
|
|
215
|
+
return ok({ runId: reservation.row.runId, status: "in_progress" });
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
let authorization;
|
|
@@ -246,7 +246,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
246
246
|
}
|
|
247
247
|
logger.info("analysis launched", { analysisId, runId, planId, mode: input.mode });
|
|
248
248
|
await emitRunCard(runId);
|
|
249
|
-
return ok({ runId });
|
|
249
|
+
return ok({ runId, status: "in_progress" });
|
|
250
250
|
},
|
|
251
251
|
});
|
|
252
252
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for the Drug-Gene Interaction Database (DGIdb)
|
|
3
|
+
* GraphQL API. Free, no key required.
|
|
4
|
+
*
|
|
5
|
+
* DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
|
|
6
|
+
* GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
|
|
7
|
+
* which the caller uses as a confidence signal.
|
|
8
|
+
*/
|
|
9
|
+
export interface DgidbInteraction {
|
|
10
|
+
geneName: string;
|
|
11
|
+
drugName: string;
|
|
12
|
+
drugConceptId?: string;
|
|
13
|
+
interactionTypes: {
|
|
14
|
+
type: string;
|
|
15
|
+
directionality?: string;
|
|
16
|
+
}[];
|
|
17
|
+
interactionScore?: number;
|
|
18
|
+
sourceCount: number;
|
|
19
|
+
sources: string[];
|
|
20
|
+
publicationCount: number;
|
|
21
|
+
pmids: string[];
|
|
22
|
+
attributes?: {
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
export interface DgidbResult {
|
|
28
|
+
input: string;
|
|
29
|
+
found: boolean;
|
|
30
|
+
interactions: DgidbInteraction[];
|
|
31
|
+
/** Interactions this input had before `limit` trimmed them. */
|
|
32
|
+
totalInteractions: number;
|
|
33
|
+
}
|
|
34
|
+
export interface DgidbFilters {
|
|
35
|
+
interactionTypes?: string[];
|
|
36
|
+
sourceDbs?: string[];
|
|
37
|
+
minSources?: number;
|
|
38
|
+
includeAttributes?: boolean;
|
|
39
|
+
limit?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Query DGIdb for interactions of the given genes or drugs.
|
|
43
|
+
*
|
|
44
|
+
* `onPartialErrors` receives GraphQL partial errors that arrive alongside
|
|
45
|
+
* usable data — the client itself never logs, so the caller decides.
|
|
46
|
+
*/
|
|
47
|
+
export declare function searchDgidb(inputs: string[], searchType: "gene" | "drug", filters?: DgidbFilters, onPartialErrors?: (errors: {
|
|
48
|
+
message?: string;
|
|
49
|
+
}[]) => void): Promise<DgidbResult[]>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for the Drug-Gene Interaction Database (DGIdb)
|
|
3
|
+
* GraphQL API. Free, no key required.
|
|
4
|
+
*
|
|
5
|
+
* DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
|
|
6
|
+
* GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
|
|
7
|
+
* which the caller uses as a confidence signal.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
11
|
+
const DGIDB_GRAPHQL_URL = "https://dgidb.org/api/graphql";
|
|
12
|
+
const GENE_INTERACTIONS_QUERY = `query($genes: [String!]!) {
|
|
13
|
+
genes(names: $genes) {
|
|
14
|
+
nodes {
|
|
15
|
+
name
|
|
16
|
+
interactions {
|
|
17
|
+
interactionScore
|
|
18
|
+
interactionTypes { type directionality }
|
|
19
|
+
drug { name conceptId }
|
|
20
|
+
interactionAttributes { name value }
|
|
21
|
+
publications { pmid }
|
|
22
|
+
sources { sourceDbName }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}`;
|
|
27
|
+
const DRUG_INTERACTIONS_QUERY = `query($drugs: [String!]!) {
|
|
28
|
+
drugs(names: $drugs) {
|
|
29
|
+
nodes {
|
|
30
|
+
name
|
|
31
|
+
conceptId
|
|
32
|
+
interactions {
|
|
33
|
+
interactionScore
|
|
34
|
+
interactionTypes { type directionality }
|
|
35
|
+
gene { name }
|
|
36
|
+
interactionAttributes { name value }
|
|
37
|
+
publications { pmid }
|
|
38
|
+
sources { sourceDbName }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}`;
|
|
43
|
+
// Shape of the DGIdb GraphQL payload as it arrives on the wire. Validated at
|
|
44
|
+
// the fetch boundary; `mapInteraction` normalizes each record. Most fields are
|
|
45
|
+
// optional/nullable to match GraphQL's per-field nullability — but
|
|
46
|
+
// `RawNode.name` is required because we key the node map on
|
|
47
|
+
// `name.toLowerCase()`; a node missing it is a contract break we surface as an
|
|
48
|
+
// `invalid_response` error rather than a runtime TypeError.
|
|
49
|
+
const RawInteractionSchema = z.object({
|
|
50
|
+
interactionScore: z.number().nullable().optional(),
|
|
51
|
+
interactionTypes: z.array(z.object({ type: z.string().optional(), directionality: z.string().nullable().optional() })).optional(),
|
|
52
|
+
drug: z.object({ name: z.string().optional(), conceptId: z.string().nullable().optional() }).nullable().optional(),
|
|
53
|
+
gene: z.object({ name: z.string().optional() }).nullable().optional(),
|
|
54
|
+
interactionAttributes: z.array(z.object({ name: z.string().optional(), value: z.string().optional() })).optional(),
|
|
55
|
+
publications: z.array(z.object({ pmid: z.union([z.string(), z.number()]).optional() })).optional(),
|
|
56
|
+
sources: z.array(z.object({ sourceDbName: z.string().optional() })).optional(),
|
|
57
|
+
});
|
|
58
|
+
const RawNodeSchema = z.object({
|
|
59
|
+
name: z.string(),
|
|
60
|
+
conceptId: z.string().nullable().optional(),
|
|
61
|
+
interactions: z.array(RawInteractionSchema).optional(),
|
|
62
|
+
});
|
|
63
|
+
const DgidbResponseSchema = z.object({
|
|
64
|
+
data: z
|
|
65
|
+
.object({
|
|
66
|
+
genes: z.object({ nodes: z.array(RawNodeSchema).optional() }).optional(),
|
|
67
|
+
drugs: z.object({ nodes: z.array(RawNodeSchema).optional() }).optional(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
errors: z.array(z.object({ message: z.string().optional() })).optional(),
|
|
71
|
+
});
|
|
72
|
+
/** PMIDs kept per interaction — the count travels for the rest. */
|
|
73
|
+
const MAX_PMIDS = 5;
|
|
74
|
+
/** Attributes kept per interaction, and the per-value character ceiling. */
|
|
75
|
+
const MAX_ATTRIBUTES = 10;
|
|
76
|
+
const MAX_ATTRIBUTE_CHARS = 160;
|
|
77
|
+
function mapInteraction(raw, perspective, inputSide, includeAttributes) {
|
|
78
|
+
const sources = (raw.sources ?? []).map((s) => s.sourceDbName ?? "").filter(Boolean);
|
|
79
|
+
const pmids = (raw.publications ?? []).map((p) => (p.pmid != null ? String(p.pmid) : "")).filter(Boolean);
|
|
80
|
+
const types = (raw.interactionTypes ?? []).map((t) => ({
|
|
81
|
+
type: t.type ?? "",
|
|
82
|
+
...(t.directionality ? { directionality: t.directionality } : {}),
|
|
83
|
+
}));
|
|
84
|
+
const geneName = perspective === "gene" ? (inputSide.geneName ?? "") : (raw.gene?.name ?? "");
|
|
85
|
+
const drugName = perspective === "drug" ? (inputSide.drugName ?? "") : (raw.drug?.name ?? "");
|
|
86
|
+
const drugConceptId = perspective === "drug" ? inputSide.drugConceptId : (raw.drug?.conceptId ?? undefined);
|
|
87
|
+
return {
|
|
88
|
+
geneName,
|
|
89
|
+
drugName,
|
|
90
|
+
...(drugConceptId ? { drugConceptId } : {}),
|
|
91
|
+
interactionTypes: types,
|
|
92
|
+
...(raw.interactionScore != null ? { interactionScore: raw.interactionScore } : {}),
|
|
93
|
+
sourceCount: sources.length,
|
|
94
|
+
sources,
|
|
95
|
+
publicationCount: pmids.length,
|
|
96
|
+
pmids: pmids.slice(0, MAX_PMIDS),
|
|
97
|
+
...(includeAttributes
|
|
98
|
+
? {
|
|
99
|
+
attributes: (raw.interactionAttributes ?? []).slice(0, MAX_ATTRIBUTES).map((a) => ({
|
|
100
|
+
name: a.name ?? "",
|
|
101
|
+
value: (a.value ?? "").slice(0, MAX_ATTRIBUTE_CHARS),
|
|
102
|
+
})),
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function applyFilters(interactions, filters) {
|
|
108
|
+
let out = interactions;
|
|
109
|
+
if (filters.interactionTypes?.length) {
|
|
110
|
+
const needles = filters.interactionTypes.map((s) => s.toLowerCase());
|
|
111
|
+
out = out.filter((i) => i.interactionTypes.some((t) => needles.some((n) => t.type.toLowerCase().includes(n))));
|
|
112
|
+
}
|
|
113
|
+
if (filters.sourceDbs?.length) {
|
|
114
|
+
const needles = filters.sourceDbs.map((s) => s.toLowerCase());
|
|
115
|
+
out = out.filter((i) => i.sources.some((s) => needles.some((n) => s.toLowerCase().includes(n))));
|
|
116
|
+
}
|
|
117
|
+
const minSources = filters.minSources ?? 1;
|
|
118
|
+
if (minSources > 0) {
|
|
119
|
+
out = out.filter((i) => i.sourceCount >= minSources);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Query DGIdb for interactions of the given genes or drugs.
|
|
125
|
+
*
|
|
126
|
+
* `onPartialErrors` receives GraphQL partial errors that arrive alongside
|
|
127
|
+
* usable data — the client itself never logs, so the caller decides.
|
|
128
|
+
*/
|
|
129
|
+
export async function searchDgidb(inputs, searchType, filters = {}, onPartialErrors) {
|
|
130
|
+
const isGene = searchType === "gene";
|
|
131
|
+
const res = await apiFetchValidated(DGIDB_GRAPHQL_URL, DgidbResponseSchema, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
query: isGene ? GENE_INTERACTIONS_QUERY : DRUG_INTERACTIONS_QUERY,
|
|
136
|
+
variables: { [isGene ? "genes" : "drugs"]: inputs },
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
if (res.isErr())
|
|
140
|
+
throw new Error(describeApiError(res.error));
|
|
141
|
+
const body = res.value;
|
|
142
|
+
if (!body || !body.data)
|
|
143
|
+
throw new Error("DGIdb returned no data");
|
|
144
|
+
if (body.errors?.length)
|
|
145
|
+
onPartialErrors?.(body.errors.slice(0, 3));
|
|
146
|
+
const nodes = (isGene ? body.data.genes?.nodes : body.data.drugs?.nodes) ?? [];
|
|
147
|
+
const nodeMap = new Map();
|
|
148
|
+
for (const node of nodes) {
|
|
149
|
+
nodeMap.set(node.name.toLowerCase(), node);
|
|
150
|
+
if (!isGene && node.conceptId)
|
|
151
|
+
nodeMap.set(node.conceptId.toLowerCase(), node);
|
|
152
|
+
}
|
|
153
|
+
const limit = filters.limit ?? 20;
|
|
154
|
+
return inputs.map((input) => {
|
|
155
|
+
const node = nodeMap.get(input.toLowerCase());
|
|
156
|
+
if (!node)
|
|
157
|
+
return { input, found: false, interactions: [], totalInteractions: 0 };
|
|
158
|
+
const inputSide = isGene ? { geneName: node.name } : { drugName: node.name, drugConceptId: node.conceptId ?? undefined };
|
|
159
|
+
const mapped = (node.interactions ?? []).map((raw) => mapInteraction(raw, searchType, inputSide, filters.includeAttributes ?? false));
|
|
160
|
+
const filtered = applyFilters(mapped, filters);
|
|
161
|
+
const sorted = filtered.sort((a, b) => {
|
|
162
|
+
if (b.sourceCount !== a.sourceCount)
|
|
163
|
+
return b.sourceCount - a.sourceCount;
|
|
164
|
+
return (b.interactionScore ?? 0) - (a.interactionScore ?? 0);
|
|
165
|
+
});
|
|
166
|
+
return { input, found: true, interactions: sorted.slice(0, limit), totalInteractions: sorted.length };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for DisGeNET gene-disease associations (GDAs).
|
|
3
|
+
*
|
|
4
|
+
* Requires a DisGeNET API key; `getDisgenetHeaders` throws when it is absent.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
declare const GdaSchema: z.ZodPipe<z.ZodObject<{
|
|
8
|
+
gene_symbol: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
9
|
+
gene_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
|
+
geneid: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
11
|
+
disease_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
12
|
+
diseaseid: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
13
|
+
disease_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
14
|
+
score: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
15
|
+
ei: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
16
|
+
year_initial: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
17
|
+
year_final: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
18
|
+
pmid_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
19
|
+
source: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
20
|
+
}, z.core.$strip>, z.ZodTransform<{
|
|
21
|
+
geneSymbol: string;
|
|
22
|
+
geneName: string;
|
|
23
|
+
geneId: number;
|
|
24
|
+
diseaseName: string;
|
|
25
|
+
diseaseId: string;
|
|
26
|
+
diseaseType: string;
|
|
27
|
+
score: number;
|
|
28
|
+
evidenceIndex: number;
|
|
29
|
+
yearInitial: number | null;
|
|
30
|
+
yearFinal: number | null;
|
|
31
|
+
nPmids: number;
|
|
32
|
+
source: string;
|
|
33
|
+
}, {
|
|
34
|
+
gene_symbol?: string | null | undefined;
|
|
35
|
+
gene_name?: string | null | undefined;
|
|
36
|
+
geneid?: number | null | undefined;
|
|
37
|
+
disease_name?: string | null | undefined;
|
|
38
|
+
diseaseid?: string | null | undefined;
|
|
39
|
+
disease_type?: string | null | undefined;
|
|
40
|
+
score?: number | null | undefined;
|
|
41
|
+
ei?: number | null | undefined;
|
|
42
|
+
year_initial?: number | null | undefined;
|
|
43
|
+
year_final?: number | null | undefined;
|
|
44
|
+
pmid_count?: number | null | undefined;
|
|
45
|
+
source?: string | null | undefined;
|
|
46
|
+
}>>;
|
|
47
|
+
export type Gda = z.infer<typeof GdaSchema>;
|
|
48
|
+
export type DisgenetSearchType = "gene" | "disease";
|
|
49
|
+
export type DisgenetSource = "ALL" | "CURATED" | "ANIMAL_MODELS" | "BEFREE";
|
|
50
|
+
export interface DisgenetSearchOptions {
|
|
51
|
+
minScore?: number;
|
|
52
|
+
source?: DisgenetSource;
|
|
53
|
+
limit?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare function searchDisgenet(apiKey: string, query: string, searchType: DisgenetSearchType, options?: DisgenetSearchOptions): Promise<Gda[]>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for DisGeNET gene-disease associations (GDAs).
|
|
3
|
+
*
|
|
4
|
+
* Requires a DisGeNET API key; `getDisgenetHeaders` throws when it is absent.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
8
|
+
import { DISGENET_BASE, getDisgenetHeaders } from "./disgenet-config.js";
|
|
9
|
+
// A single schema that both validates and normalizes one DisGeNET GDA record:
|
|
10
|
+
// the `.object(...)` half is the snake_case wire shape (every field optional —
|
|
11
|
+
// the API omits absent values), the `.transform(...)` half maps it to the
|
|
12
|
+
// camelCase `Gda` we return. Parsing IS the validation (`apiFetchValidated`
|
|
13
|
+
// runs it over the JSON), and because the transform rides on the schema,
|
|
14
|
+
// `z.infer` below is the OUTPUT type — no separate raw interface or mapper.
|
|
15
|
+
const GdaSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
gene_symbol: z.string().nullable().optional(),
|
|
18
|
+
gene_name: z.string().nullable().optional(),
|
|
19
|
+
geneid: z.number().nullable().optional(),
|
|
20
|
+
disease_name: z.string().nullable().optional(),
|
|
21
|
+
diseaseid: z.string().nullable().optional(),
|
|
22
|
+
disease_type: z.string().nullable().optional(),
|
|
23
|
+
score: z.number().nullable().optional(),
|
|
24
|
+
ei: z.number().nullable().optional(),
|
|
25
|
+
year_initial: z.number().nullable().optional(),
|
|
26
|
+
year_final: z.number().nullable().optional(),
|
|
27
|
+
pmid_count: z.number().nullable().optional(),
|
|
28
|
+
source: z.string().nullable().optional(),
|
|
29
|
+
})
|
|
30
|
+
.transform((gda) => ({
|
|
31
|
+
geneSymbol: gda.gene_symbol ?? "",
|
|
32
|
+
geneName: gda.gene_name ?? "",
|
|
33
|
+
geneId: gda.geneid ?? 0,
|
|
34
|
+
diseaseName: gda.disease_name ?? "",
|
|
35
|
+
diseaseId: gda.diseaseid ?? "",
|
|
36
|
+
diseaseType: gda.disease_type ?? "",
|
|
37
|
+
score: gda.score ?? 0,
|
|
38
|
+
evidenceIndex: gda.ei ?? 0,
|
|
39
|
+
yearInitial: gda.year_initial ?? null,
|
|
40
|
+
yearFinal: gda.year_final ?? null,
|
|
41
|
+
nPmids: gda.pmid_count ?? 0,
|
|
42
|
+
source: gda.source ?? "",
|
|
43
|
+
}));
|
|
44
|
+
const GdaListSchema = z.array(GdaSchema);
|
|
45
|
+
export async function searchDisgenet(apiKey, query, searchType, options = {}) {
|
|
46
|
+
const headers = getDisgenetHeaders(apiKey);
|
|
47
|
+
const minScore = options.minScore ?? 0.1;
|
|
48
|
+
const limit = options.limit ?? 25;
|
|
49
|
+
const source = options.source ?? "ALL";
|
|
50
|
+
const path = searchType === "gene" ? `gene/${encodeURIComponent(query)}` : `disease/${encodeURIComponent(query)}`;
|
|
51
|
+
let url = `${DISGENET_BASE}/gda/${path}?min_score=${minScore}&limit=${limit}`;
|
|
52
|
+
if (source !== "ALL")
|
|
53
|
+
url += `&source=${source}`;
|
|
54
|
+
const res = await apiFetchValidated(url, GdaListSchema, { headers });
|
|
55
|
+
if (res.isErr())
|
|
56
|
+
throw new Error(describeApiError(res.error));
|
|
57
|
+
// Already validated + normalized to camelCase by GdaSchema's transform.
|
|
58
|
+
return res.value;
|
|
59
|
+
}
|