@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
|
@@ -10,14 +10,20 @@ export const searchFaersTool = defineTool({
|
|
|
10
10
|
description: "Search FDA FAERS for post-market adverse-event reports on one marketed drug — the real-world safety signal for a specific molecule. " +
|
|
11
11
|
"Returns totalReports and adverseEvents[]: { reaction (MedDRA preferred term), count }, most-reported first. " +
|
|
12
12
|
"These are spontaneous report counts, NOT incidence rates: there is no denominator and reporting is heavily biased, so use them to rank signals, never to state a rate. " +
|
|
13
|
-
"Matching is on the openFDA GENERIC name only — 'imatinib' works, the brand name 'Gleevec' does not. For mechanism-based liabilities of a
|
|
13
|
+
"Matching is on the openFDA GENERIC name only — 'imatinib' works, the brand name 'Gleevec' does not. For mechanism-based liabilities of a TARGET (rather than a marketed drug), use target_safety. " +
|
|
14
14
|
"An empty adverseEvents array is valid no-data (drug absent from FAERS under that generic name) — do not retry.",
|
|
15
15
|
inputSchema: z.object({
|
|
16
16
|
drugName: z.string().describe("Generic (INN) drug name, e.g. 'imatinib', 'pembrolizumab'. Brand names do not match."),
|
|
17
|
-
limit: z
|
|
17
|
+
limit: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(100)
|
|
22
|
+
.default(15)
|
|
23
|
+
.describe("Max distinct adverse-reaction terms to return, most-reported first (default 15, max 100). totalReports covers the whole drug regardless."),
|
|
18
24
|
serious: z.boolean().default(false).describe("When true, count only reports flagged serious (death, hospitalization, life-threatening, disabling)."),
|
|
19
25
|
}),
|
|
20
|
-
execute: async ({ drugName, limit =
|
|
26
|
+
execute: async ({ drugName, limit = 15, serious = false }) => {
|
|
21
27
|
const result = await getFaersByDrug(drugName, { limit, serious });
|
|
22
28
|
return ok({
|
|
23
29
|
totalReports: result.totalReports,
|
|
@@ -7,13 +7,12 @@ import { defineTool } from "../define-tool.js";
|
|
|
7
7
|
import { lookupGenes } from "../lib/ensembl-client.js";
|
|
8
8
|
export const searchGeneTool = defineTool({
|
|
9
9
|
id: "search_gene",
|
|
10
|
-
description: "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
description: "Resolve gene symbols against Ensembl — the identifier step other tools depend on. Per symbol: Ensembl gene ID (ENSG…), coordinates, strand, " +
|
|
11
|
+
"biotype, assembly, description; plus notFound[]. Gene-level only — no transcripts or exons. Batch up to 200 symbols per call. " +
|
|
12
|
+
"notFound is valid no-data (deprecated, aliased or mistyped symbol, or wrong species) — do not retry unchanged.",
|
|
13
13
|
inputSchema: z.object({
|
|
14
14
|
symbols: z.array(z.string()).min(1).max(200).describe("Gene symbols to look up (e.g. ['BRCA1', 'TP53'])"),
|
|
15
15
|
species: z.string().default("homo_sapiens").describe("Species name (e.g. 'homo_sapiens', 'mus_musculus')"),
|
|
16
|
-
expand: z.boolean().default(false).describe("Include transcript/exon details in the response"),
|
|
17
16
|
}),
|
|
18
|
-
execute: async ({ symbols, species
|
|
17
|
+
execute: async ({ symbols, species }) => ok(await lookupGenes(symbols, { species })),
|
|
19
18
|
});
|
|
@@ -39,7 +39,7 @@ const GeoEsummaryResponseSchema = z.object({
|
|
|
39
39
|
export const searchGeoDatasetsTool = defineTool({
|
|
40
40
|
id: "search_geo_datasets",
|
|
41
41
|
description: "Search NCBI GEO for public gene-expression datasets by disease, tissue, or experimental condition — use it to find external validation cohorts or to cite published data. " +
|
|
42
|
-
"Returns totalFound plus datasets[]: { accession (GSE…/GDS…), title, summary (truncated to
|
|
42
|
+
"Returns totalFound plus datasets[]: { accession (GSE…/GDS…), title, summary (truncated to 300 chars), platform, sampleCount, organism, pubmedIds }. " +
|
|
43
43
|
"HARD CAVEAT: this tool returns metadata only, and sandbox containers have no network — never plan an in-sandbox step that downloads a GEO dataset. " +
|
|
44
44
|
"Bringing the data itself in is a capability of the surrounding environment, not of this tool: consult the tools you actually have rather than assuming it is possible or that it is not. " +
|
|
45
45
|
"An empty datasets array is a valid 'nothing matched' — do not retry the identical query.",
|
|
@@ -55,9 +55,15 @@ export const searchGeoDatasetsTool = defineTool({
|
|
|
55
55
|
.enum(["gds", "gse"])
|
|
56
56
|
.default("gse")
|
|
57
57
|
.describe("'gse' (default) — GEO Series: raw submitter-deposited studies, far more numerous. 'gds' — curated DataSets: fewer, but normalized and value-added."),
|
|
58
|
-
limit: z
|
|
58
|
+
limit: z
|
|
59
|
+
.number()
|
|
60
|
+
.int()
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(50)
|
|
63
|
+
.default(10)
|
|
64
|
+
.describe("Max datasets to return (default 10, max 50). totalFound reports the true match count regardless."),
|
|
59
65
|
}),
|
|
60
|
-
execute: async ({ query, organism, datasetType = "gse", limit =
|
|
66
|
+
execute: async ({ query, organism, datasetType = "gse", limit = 10 }) => {
|
|
61
67
|
let searchQuery = query;
|
|
62
68
|
// Escape backslashes first, then quotes, so a `\` in the organism value
|
|
63
69
|
// cannot smuggle an unescaped `"` past the escaping and break out of the
|
|
@@ -87,7 +93,7 @@ export const searchGeoDatasetsTool = defineTool({
|
|
|
87
93
|
return {
|
|
88
94
|
accession: r.accession ?? r.gse ?? `GDS${id}`,
|
|
89
95
|
title: r.title ?? "",
|
|
90
|
-
summary: (r.summary ?? "").slice(0,
|
|
96
|
+
summary: (r.summary ?? "").slice(0, 300),
|
|
91
97
|
platform: r.gpl ?? r.platform ?? null,
|
|
92
98
|
sampleCount: r.n_samples ? Number(r.n_samples) : null,
|
|
93
99
|
organism: r.taxon ?? null,
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* searchInteractions — query protein-protein interactions via STRING DB.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { type StringEnrichment, type StringInteraction } from "../lib/string-client.js";
|
|
5
5
|
type InteractionsOutput = {
|
|
6
|
-
enrichment:
|
|
6
|
+
enrichment: StringEnrichment[];
|
|
7
|
+
totalTerms: number;
|
|
7
8
|
} | {
|
|
8
|
-
interactions:
|
|
9
|
+
interactions: StringInteraction[];
|
|
10
|
+
} | {
|
|
11
|
+
interactions: StringInteraction[];
|
|
12
|
+
totalEdges: number;
|
|
9
13
|
};
|
|
10
14
|
export declare const searchInteractionsTool: import("../define-tool.js").Tool<{
|
|
11
15
|
identifiers: string[];
|
|
12
16
|
species: number;
|
|
13
|
-
action: "
|
|
17
|
+
action: "enrichment" | "partners" | "network";
|
|
14
18
|
minScore: number;
|
|
15
19
|
limit: number;
|
|
16
20
|
}, InteractionsOutput>;
|
|
@@ -5,27 +5,43 @@ import { ok } from "neverthrow";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineTool } from "../define-tool.js";
|
|
7
7
|
import { getEnrichment, getInteractionNetwork, getInteractionPartners } from "../lib/string-client.js";
|
|
8
|
+
const DEFAULT_LIMIT = 20;
|
|
8
9
|
export const searchInteractionsTool = defineTool({
|
|
9
10
|
id: "search_interactions",
|
|
10
|
-
description: "Query protein-protein interactions
|
|
11
|
-
"
|
|
12
|
-
"'network'
|
|
13
|
-
"'enrichment'
|
|
11
|
+
description: "Query STRING DB for protein-protein interactions and gene-set functional enrichment. " +
|
|
12
|
+
"'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
|
|
13
|
+
"'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
|
|
14
|
+
"'enrichment' — statistical over-representation of GO, KEGG, Reactome and Pfam terms in the input set, FDR-sorted: the enrichment TEST, as " +
|
|
15
|
+
"opposed to lookup_annotation, which just reads a vocabulary. " +
|
|
16
|
+
"`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges. `totalEdges` / `totalTerms` give the " +
|
|
17
|
+
"pre-trim counts. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — do not retry.",
|
|
14
18
|
inputSchema: z.object({
|
|
15
19
|
identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
|
|
16
20
|
species: z.number().int().default(9606).describe("NCBI Taxonomy ID (9606 = human, 10090 = mouse)"),
|
|
17
21
|
action: z.enum(["partners", "network", "enrichment"]).default("partners").describe("Query type"),
|
|
18
|
-
minScore: z
|
|
19
|
-
|
|
22
|
+
minScore: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(0)
|
|
26
|
+
.max(1000)
|
|
27
|
+
.default(400)
|
|
28
|
+
.describe("Minimum confidence score, 'partners' and 'network' only (400=medium, 700=high, 900=highest). Raise it to cut a large network down."),
|
|
29
|
+
limit: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(500)
|
|
34
|
+
.default(DEFAULT_LIMIT)
|
|
35
|
+
.describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
|
|
36
|
+
"Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
|
|
20
37
|
}),
|
|
21
38
|
execute: async ({ identifiers, species, action, minScore, limit }) => {
|
|
22
39
|
if (action === "enrichment") {
|
|
23
|
-
|
|
24
|
-
return ok({ enrichment });
|
|
40
|
+
return ok(await getEnrichment(identifiers, species, { limit }));
|
|
25
41
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return ok({ interactions });
|
|
42
|
+
if (action === "network") {
|
|
43
|
+
return ok(await getInteractionNetwork(identifiers, { species, minScore, limit }));
|
|
44
|
+
}
|
|
45
|
+
return ok({ interactions: await getInteractionPartners(identifiers, { species, minScore, limit }) });
|
|
30
46
|
},
|
|
31
47
|
});
|
|
@@ -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[]>;
|