@inflexa-ai/harness 0.29.0 → 0.30.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 +7 -4
- package/dist/agents/report-session-agent.js +1 -1
- package/dist/agents/sandbox/data-profiler.js +0 -1
- package/dist/agents/sandbox/immune-profiling-agent.js +3 -1
- package/dist/agents/sandbox/network-agent.js +0 -1
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/sandbox/types.js +1 -1
- package/dist/config/resource-limits.d.ts +1 -1
- package/dist/config/resource-limits.js +1 -1
- package/dist/contracts/schemas/chat-parts.d.ts +12 -12
- package/dist/contracts/severity.d.ts +2 -2
- package/dist/contracts/target-dossier.d.ts +70 -70
- package/dist/data/safety-panel-schema.d.ts +4 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -0
- package/dist/loop/run-agent.d.ts +13 -1
- package/dist/loop/run-agent.js +2 -1
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +17 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +43 -71
- package/dist/prompts/planner.js +57 -10
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -1
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +18 -12
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +6 -2
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +8 -4
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +12 -6
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +9 -6
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +4 -2
- package/dist/providers/ai-sdk.js +34 -1
- package/dist/providers/reasoning.d.ts +65 -0
- package/dist/providers/reasoning.js +81 -0
- package/dist/providers/types.d.ts +14 -0
- package/dist/schemas/data-profile-schemas.d.ts +4 -4
- package/dist/schemas/run-synthesis.d.ts +4 -4
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +15 -10
- package/dist/tools/bio/comptox.js +5 -1
- package/dist/tools/bio/drug-gene-interactions.js +5 -2
- package/dist/tools/bio/gene-disease-evidence.d.ts +19 -9
- package/dist/tools/bio/gene-disease-evidence.js +78 -22
- package/dist/tools/bio/gene-preclinical-profile.d.ts +33 -13
- package/dist/tools/bio/gene-preclinical-profile.js +63 -22
- package/dist/tools/bio/lookup-annotation.d.ts +1 -0
- package/dist/tools/bio/lookup-annotation.js +28 -10
- package/dist/tools/bio/opentargets.d.ts +10 -2
- package/dist/tools/bio/opentargets.js +42 -13
- package/dist/tools/bio/pubchem.js +5 -2
- package/dist/tools/bio/pubmed.js +9 -3
- package/dist/tools/bio/search-clinical-trials.d.ts +25 -25
- package/dist/tools/bio/search-clinical-trials.js +56 -18
- package/dist/tools/bio/search-faers.d.ts +26 -6
- package/dist/tools/bio/search-faers.js +70 -18
- package/dist/tools/bio/search-gene.d.ts +19 -3
- package/dist/tools/bio/search-gene.js +69 -12
- package/dist/tools/bio/search-geo-datasets.js +4 -1
- package/dist/tools/bio/search-interactions.js +6 -2
- package/dist/tools/bio/target-safety.d.ts +2 -2
- package/dist/tools/bio/target-safety.js +3 -2
- package/dist/tools/lib/cbioportal-client.js +25 -6
- package/dist/tools/lib/chembl-client.d.ts +3 -1
- package/dist/tools/lib/chembl-client.js +6 -1
- package/dist/tools/lib/gwas-catalog-client.d.ts +7 -6
- package/dist/tools/lib/gwas-catalog-client.js +18 -6
- package/dist/tools/lib/openfda-client.d.ts +7 -5
- package/dist/tools/lib/openfda-client.js +17 -9
- package/dist/tools/lib/opentargets-client.d.ts +20 -0
- package/dist/tools/lib/opentargets-client.js +45 -0
- package/dist/tools/lib/pathway-client.d.ts +7 -0
- package/dist/tools/lib/pathway-client.js +68 -1
- package/dist/tools/research/context7-docs.js +3 -3
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +64 -5
- package/dist/tools/research/resolve-citation.js +8 -3
- package/dist/tools/research/search-arxiv.js +8 -3
- package/dist/tools/research/search-semantic-scholar.js +10 -4
- package/dist/workflows/sandbox-step.d.ts +6 -0
- package/dist/workflows/sandbox-step.js +31 -14
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +17 -17
- package/dist/workflows/target-assessment/schemas.d.ts +12 -12
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +6 -6
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +6 -6
- package/dist/workflows/target-assessment/synthesis/index.d.ts +12 -12
- package/package.json +1 -1
|
@@ -8,13 +8,17 @@ import { getEnrichment, getInteractionNetwork, getInteractionPartners } from "..
|
|
|
8
8
|
const DEFAULT_LIMIT = 20;
|
|
9
9
|
export const searchInteractionsTool = defineTool({
|
|
10
10
|
id: "search_interactions",
|
|
11
|
-
description: "Query STRING
|
|
11
|
+
description: "Query the STRING database — the protein-protein association network of EMBL, which folds experimental, database, co-expression and text-mined " +
|
|
12
|
+
"evidence into one confidence score — for interactions and gene-set functional enrichment. " +
|
|
12
13
|
"'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
|
|
13
14
|
"'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
|
|
14
15
|
"'enrichment' — statistical over-representation of GO, KEGG, Reactome and Pfam terms in the input set, FDR-sorted: the enrichment TEST, as " +
|
|
15
16
|
"opposed to lookup_annotation, which just reads a vocabulary. " +
|
|
17
|
+
"ACCEPTED IDENTIFIERS: STRING resolves each one itself, so a HUGO gene symbol ('TP53'), a UniProt accession ('P04637'), an Ensembl protein ID " +
|
|
18
|
+
"('ENSP00000269305') and a full protein name all work, mixed in one call. `species` takes an NCBI Taxonomy ID (9606 = human).\n" +
|
|
16
19
|
"`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
|
|
20
|
+
"pre-trim counts. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — report it and continue, do " +
|
|
21
|
+
"not retry.",
|
|
18
22
|
inputSchema: z.object({
|
|
19
23
|
identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
|
|
20
24
|
species: z.number().int().default(9606).describe("NCBI Taxonomy ID (9606 = human, 10090 = mouse)"),
|
|
@@ -40,14 +40,14 @@ export declare const targetSafetyTool: import("../define-tool.js").Tool<{
|
|
|
40
40
|
identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
|
|
41
41
|
sources?: ("opentargets" | "panel")[] | undefined;
|
|
42
42
|
filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "musculoskeletal" | "cns" | "renal" | "reproductive" | "respiratory" | "cardiac" | "vascular" | "pns" | "gi" | "pancreas" | "endocrine_thyroid" | "dermatologic" | "ocular" | "oncology" | undefined;
|
|
43
|
-
minSeverity?: "
|
|
43
|
+
minSeverity?: "low" | "medium" | "high" | undefined;
|
|
44
44
|
liabilityLimit?: number | undefined;
|
|
45
45
|
}, {
|
|
46
46
|
perSource: SourceOutcome[];
|
|
47
47
|
summary: {
|
|
48
48
|
totalInput: number;
|
|
49
49
|
panelMatched: number;
|
|
50
|
-
bySeverity: Record<"
|
|
50
|
+
bySeverity: Record<"low" | "medium" | "high", number>;
|
|
51
51
|
byOrgan: Record<string, number>;
|
|
52
52
|
openTargetsWithLiabilities: number;
|
|
53
53
|
};
|
|
@@ -92,8 +92,9 @@ const inputSchema = z
|
|
|
92
92
|
export const targetSafetyTool = defineTool({
|
|
93
93
|
id: "target_safety",
|
|
94
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
|
|
96
|
-
"
|
|
95
|
+
"Judges what engaging a target is likely to do to an organ system, before any specific molecule exists.\n" +
|
|
96
|
+
"ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('HERG', 'KCNH2'), a ChEMBL target ID ('CHEMBL240'), a UniProt accession " +
|
|
97
|
+
"('Q12809') and an Ensembl gene ID ('ENSG00000055118').\n" +
|
|
97
98
|
"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
99
|
"toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
|
|
99
100
|
"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 " +
|
|
@@ -36,6 +36,8 @@ const RawCancerStudySchema = z.object({
|
|
|
36
36
|
cancerType: z.object({ name: z.string().optional(), cancerTypeId: z.string().optional() }).optional(),
|
|
37
37
|
name: z.string().optional(),
|
|
38
38
|
description: z.string().optional(),
|
|
39
|
+
/** The denominator of a study. The sample-list SUMMARY projection carries no count. */
|
|
40
|
+
allSampleCount: z.number().optional(),
|
|
39
41
|
});
|
|
40
42
|
async function listStudies() {
|
|
41
43
|
const url = `${CBIOPORTAL_BASE}/studies?projection=SUMMARY&pageSize=10000`;
|
|
@@ -44,6 +46,23 @@ async function listStudies() {
|
|
|
44
46
|
throw new Error(describeApiError(res.error));
|
|
45
47
|
return res.value ?? [];
|
|
46
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The cancer-type vocabulary, keyed by its id. The study SUMMARY projection
|
|
51
|
+
* carries the id alone, thus a row would otherwise report `esca` where it must
|
|
52
|
+
* report `Esophageal Adenocarcinoma`.
|
|
53
|
+
*/
|
|
54
|
+
async function cancerTypeNames() {
|
|
55
|
+
const url = `${CBIOPORTAL_BASE}/cancer-types?pageSize=10000`;
|
|
56
|
+
const res = await apiFetchValidated(url, z.array(z.object({ cancerTypeId: z.string().optional(), name: z.string().optional() })), { headers: HEADERS });
|
|
57
|
+
const names = new Map();
|
|
58
|
+
if (res.isErr())
|
|
59
|
+
return names;
|
|
60
|
+
for (const row of res.value ?? []) {
|
|
61
|
+
if (row.cancerTypeId && row.name)
|
|
62
|
+
names.set(row.cancerTypeId, row.name);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
47
66
|
async function listAllSampleLists() {
|
|
48
67
|
const url = `${CBIOPORTAL_BASE}/sample-lists?projection=SUMMARY&pageSize=10000`;
|
|
49
68
|
const res = await apiFetchValidated(url, z.array(RawSampleListSchema), { headers: HEADERS });
|
|
@@ -98,7 +117,7 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
|
|
|
98
117
|
const entrezGeneId = await resolveEntrezId(geneSymbol);
|
|
99
118
|
if (entrezGeneId == null)
|
|
100
119
|
return { entrezGeneId: null, rows: [] };
|
|
101
|
-
const [studies, profiles, sampleLists] = await Promise.all([listStudies(), listMutationProfiles(), listAllSampleLists()]);
|
|
120
|
+
const [studies, profiles, sampleLists, typeNames] = await Promise.all([listStudies(), listMutationProfiles(), listAllSampleLists(), cancerTypeNames()]);
|
|
102
121
|
const allSampleListByStudy = new Map();
|
|
103
122
|
for (const sl of sampleLists) {
|
|
104
123
|
if (sl.category === "all_cases_in_study") {
|
|
@@ -122,12 +141,12 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
|
|
|
122
141
|
}
|
|
123
142
|
const byCancerType = new Map();
|
|
124
143
|
for (const studyId of studiesWithMutations) {
|
|
125
|
-
const sl = allSampleListByStudy.get(studyId);
|
|
126
|
-
if (!sl?.sampleCount)
|
|
127
|
-
continue;
|
|
128
144
|
const study = studyById.get(studyId);
|
|
145
|
+
const sampleCount = study?.allSampleCount ?? 0;
|
|
146
|
+
if (sampleCount === 0)
|
|
147
|
+
continue;
|
|
129
148
|
const cancerTypeId = study?.cancerType?.cancerTypeId ?? study?.cancerTypeId ?? "unknown";
|
|
130
|
-
const cancerTypeName = study?.cancerType?.name ?? cancerTypeId;
|
|
149
|
+
const cancerTypeName = study?.cancerType?.name ?? typeNames.get(cancerTypeId) ?? cancerTypeId;
|
|
131
150
|
if (!byCancerType.has(cancerTypeId)) {
|
|
132
151
|
byCancerType.set(cancerTypeId, {
|
|
133
152
|
name: cancerTypeName,
|
|
@@ -137,7 +156,7 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
|
|
|
137
156
|
});
|
|
138
157
|
}
|
|
139
158
|
const bucket = byCancerType.get(cancerTypeId);
|
|
140
|
-
bucket.total +=
|
|
159
|
+
bucket.total += sampleCount;
|
|
141
160
|
bucket.mutated += mutatedStudySamples.get(studyId)?.size ?? 0;
|
|
142
161
|
bucket.studies.add(studyId);
|
|
143
162
|
}
|
|
@@ -68,7 +68,9 @@ export interface ChemblPolypharmHit {
|
|
|
68
68
|
/** Search ChEMBL for compounds by target, compound name, or SMILES. */
|
|
69
69
|
export declare function searchCompounds(query: string, searchType: CompoundSearchType, limit?: number): Promise<ChemblCompound[]>;
|
|
70
70
|
export declare function searchTargets(query: string, limit?: number): Promise<ChemblTarget[]>;
|
|
71
|
-
|
|
71
|
+
/** Which column of the ChEMBL activity table the supplied id indexes. */
|
|
72
|
+
export type BioactivityIdType = "compound" | "target" | "assay";
|
|
73
|
+
export declare function getBioactivity(chemblId: string, type: BioactivityIdType, options?: {
|
|
72
74
|
activityType?: string;
|
|
73
75
|
limit?: number;
|
|
74
76
|
}): Promise<ChemblActivity[]>;
|
|
@@ -241,9 +241,14 @@ export async function searchTargets(query, limit = 25) {
|
|
|
241
241
|
}
|
|
242
242
|
return (res.value.targets ?? []).map(mapTarget);
|
|
243
243
|
}
|
|
244
|
+
const ACTIVITY_ID_FIELD = {
|
|
245
|
+
compound: "molecule_chembl_id",
|
|
246
|
+
target: "target_chembl_id",
|
|
247
|
+
assay: "assay_chembl_id",
|
|
248
|
+
};
|
|
244
249
|
export async function getBioactivity(chemblId, type, options = {}) {
|
|
245
250
|
const limit = options.limit ?? 500;
|
|
246
|
-
const idParam = type
|
|
251
|
+
const idParam = `${ACTIVITY_ID_FIELD[type]}=${encodeURIComponent(chemblId)}`;
|
|
247
252
|
let url = `${CHEMBL_BASE}/activity.json?${idParam}&limit=${limit}`;
|
|
248
253
|
if (options.activityType) {
|
|
249
254
|
url += `&standard_type=${encodeURIComponent(options.activityType)}`;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
|
|
3
3
|
*
|
|
4
|
-
* Public, no key required.
|
|
5
|
-
* per-SNP associations), by trait (EFO trait search then its associations),
|
|
6
|
-
*
|
|
4
|
+
* Public, no key required. Four search paths: by gene (SNP-by-gene lookup then
|
|
5
|
+
* per-SNP associations), by trait (EFO trait search then its associations), by
|
|
6
|
+
* variant (direct rsID associations), and by study (the associations of one
|
|
7
|
+
* GCST study accession).
|
|
7
8
|
*/
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
export interface GwasAssociation {
|
|
@@ -21,7 +22,7 @@ export interface GwasAssociation {
|
|
|
21
22
|
pubmedId: string;
|
|
22
23
|
sampleSize: string;
|
|
23
24
|
}
|
|
24
|
-
export type GwasSearchType = "gene" | "trait" | "variant";
|
|
25
|
+
export type GwasSearchType = "gene" | "trait" | "variant" | "study";
|
|
25
26
|
declare const RawGwasAssociationSchema: z.ZodObject<{
|
|
26
27
|
_snpRsId: z.ZodOptional<z.ZodString>;
|
|
27
28
|
loci: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -42,10 +43,10 @@ declare const RawGwasAssociationSchema: z.ZodObject<{
|
|
|
42
43
|
}, z.core.$strip>>;
|
|
43
44
|
pvalueMantissa: z.ZodOptional<z.ZodNumber>;
|
|
44
45
|
pvalueExponent: z.ZodOptional<z.ZodNumber>;
|
|
45
|
-
riskFrequency: z.ZodOptional<z.ZodString
|
|
46
|
+
riskFrequency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
47
|
orPerCopyNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
47
48
|
betaNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
48
|
-
range: z.ZodOptional<z.ZodString
|
|
49
|
+
range: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
49
50
|
}, z.core.$strip>;
|
|
50
51
|
type RawGwasAssociation = z.infer<typeof RawGwasAssociationSchema>;
|
|
51
52
|
export declare function mapAssociation(a: RawGwasAssociation): GwasAssociation;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
|
|
3
3
|
*
|
|
4
|
-
* Public, no key required.
|
|
5
|
-
* per-SNP associations), by trait (EFO trait search then its associations),
|
|
6
|
-
*
|
|
4
|
+
* Public, no key required. Four search paths: by gene (SNP-by-gene lookup then
|
|
5
|
+
* per-SNP associations), by trait (EFO trait search then its associations), by
|
|
6
|
+
* variant (direct rsID associations), and by study (the associations of one
|
|
7
|
+
* GCST study accession).
|
|
7
8
|
*/
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
@@ -30,10 +31,10 @@ const RawGwasAssociationSchema = z.object({
|
|
|
30
31
|
study: RawGwasStudySchema.optional(),
|
|
31
32
|
pvalueMantissa: z.number().optional(),
|
|
32
33
|
pvalueExponent: z.number().optional(),
|
|
33
|
-
riskFrequency: z.string().optional(),
|
|
34
|
+
riskFrequency: z.string().nullable().optional(),
|
|
34
35
|
orPerCopyNum: z.number().nullable().optional(),
|
|
35
36
|
betaNum: z.number().nullable().optional(),
|
|
36
|
-
range: z.string().optional(),
|
|
37
|
+
range: z.string().nullable().optional(),
|
|
37
38
|
});
|
|
38
39
|
const GwasEmbeddedSchema = z.object({
|
|
39
40
|
_embedded: z.object({ associations: z.array(RawGwasAssociationSchema).optional() }).optional(),
|
|
@@ -115,6 +116,12 @@ export async function searchGwasCatalog(query, searchType, options = {}) {
|
|
|
115
116
|
const rsId = query.startsWith("rs") ? query : `rs${query}`;
|
|
116
117
|
associationsUrl = `${GWAS_BASE}/singleNucleotidePolymorphisms/${rsId}/associations?projection=associationBySnp`;
|
|
117
118
|
}
|
|
119
|
+
else if (searchType === "study") {
|
|
120
|
+
// The study endpoint keys on the accession alone. The projection nests
|
|
121
|
+
// the study record on each association, thus `mapAssociation` reads the
|
|
122
|
+
// accession, the PubMed id and the sample size without a second request.
|
|
123
|
+
associationsUrl = `${GWAS_BASE}/studies/${encodeURIComponent(query)}/associations?projection=associationByStudy&size=${limit}`;
|
|
124
|
+
}
|
|
118
125
|
else {
|
|
119
126
|
const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findBySearchQuery?query=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
|
|
120
127
|
headers: GWAS_HEADERS,
|
|
@@ -127,8 +134,13 @@ export async function searchGwasCatalog(query, searchType, options = {}) {
|
|
|
127
134
|
associationsUrl = `${GWAS_BASE}/efoTraits/${traitUri.split("/").pop()}/associations?size=${limit}`;
|
|
128
135
|
}
|
|
129
136
|
const res = await apiFetchValidated(associationsUrl, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
|
|
130
|
-
if (res.isErr())
|
|
137
|
+
if (res.isErr()) {
|
|
138
|
+
// An accession or an rsID that the catalog does not hold is absence, not
|
|
139
|
+
// a failure, thus it returns an empty record set.
|
|
140
|
+
if (res.error.type === "http_status" && res.error.status === 404)
|
|
141
|
+
return { totalFound: 0, associations: [] };
|
|
131
142
|
throw new Error(describeApiError(res.error));
|
|
143
|
+
}
|
|
132
144
|
const rawAssocs = res.value?._embedded?.associations ?? [];
|
|
133
145
|
const totalFound = res.value?.page?.totalElements ?? rawAssocs.length;
|
|
134
146
|
const filtered = rawAssocs.filter((a) => (a.pvalueMantissa ?? 1) * Math.pow(10, a.pvalueExponent ?? 0) <= pValueThreshold).slice(0, limit);
|
|
@@ -43,11 +43,13 @@ export declare function getFaersByDrug(drugName: string, options?: {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function getFaersSeriousness(drugName: string): Promise<SeriousnessProfile | null>;
|
|
45
45
|
/**
|
|
46
|
-
* Fetch FDA Structured Product Label entries by
|
|
47
|
-
* the
|
|
48
|
-
* the
|
|
49
|
-
*
|
|
46
|
+
* Fetch FDA Structured Product Label entries by drug name. The name matches
|
|
47
|
+
* the generic name or the brand name, thus a caller that holds only a brand
|
|
48
|
+
* name reads the generic name back off each row. Returns the most recent N
|
|
49
|
+
* labels (sorted by effective_time desc). Each row carries the boxed-warning
|
|
50
|
+
* text, the Section 5 warnings_and_cautions excerpt, and a REMS indicator.
|
|
51
|
+
* Returns [] on 404 / no match.
|
|
50
52
|
*/
|
|
51
|
-
export declare function getDrugLabelActions(
|
|
53
|
+
export declare function getDrugLabelActions(drugName: string, options?: {
|
|
52
54
|
limit?: number;
|
|
53
55
|
}): Promise<DrugLabelAction[]>;
|
|
@@ -42,11 +42,17 @@ const OpenFdaLabelSchema = z.object({
|
|
|
42
42
|
function escapeQueryPhrase(value) {
|
|
43
43
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A boolean operator is separated by a space, never by a literal `+`. The whole
|
|
47
|
+
* search string goes through `encodeURIComponent`, which turns a `+` into
|
|
48
|
+
* `%2B`. openFDA then reads that as a character of the term and matches
|
|
49
|
+
* nothing, but it reads `%20` as the separator that it is.
|
|
50
|
+
*/
|
|
45
51
|
function buildSearch(drugName, serious) {
|
|
46
52
|
const escaped = escapeQueryPhrase(drugName);
|
|
47
53
|
let search = `patient.drug.openfda.generic_name:"${escaped}"`;
|
|
48
54
|
if (serious)
|
|
49
|
-
search += "
|
|
55
|
+
search += " AND serious:1";
|
|
50
56
|
return search;
|
|
51
57
|
}
|
|
52
58
|
/** Top adverse-reaction terms by count for a generic drug name. */
|
|
@@ -98,7 +104,7 @@ export async function getFaersSeriousness(drugName) {
|
|
|
98
104
|
};
|
|
99
105
|
}
|
|
100
106
|
async function countWith(field) {
|
|
101
|
-
const q = `${search}
|
|
107
|
+
const q = `${search} AND ${field}:1`;
|
|
102
108
|
const url = `${OPENFDA_BASE}?search=${encodeURIComponent(q)}&limit=1`;
|
|
103
109
|
const res = await apiFetchValidated(url, FaersMetaResponseSchema);
|
|
104
110
|
if (res.isErr())
|
|
@@ -180,15 +186,17 @@ const OpenFdaLabelResponseSchema = z.object({
|
|
|
180
186
|
results: z.array(OpenFdaLabelSchema.transform(mapLabel)).optional(),
|
|
181
187
|
});
|
|
182
188
|
/**
|
|
183
|
-
* Fetch FDA Structured Product Label entries by
|
|
184
|
-
* the
|
|
185
|
-
* the
|
|
186
|
-
*
|
|
189
|
+
* Fetch FDA Structured Product Label entries by drug name. The name matches
|
|
190
|
+
* the generic name or the brand name, thus a caller that holds only a brand
|
|
191
|
+
* name reads the generic name back off each row. Returns the most recent N
|
|
192
|
+
* labels (sorted by effective_time desc). Each row carries the boxed-warning
|
|
193
|
+
* text, the Section 5 warnings_and_cautions excerpt, and a REMS indicator.
|
|
194
|
+
* Returns [] on 404 / no match.
|
|
187
195
|
*/
|
|
188
|
-
export async function getDrugLabelActions(
|
|
196
|
+
export async function getDrugLabelActions(drugName, options = {}) {
|
|
189
197
|
const limit = options.limit ?? 5;
|
|
190
|
-
const escaped = escapeQueryPhrase(
|
|
191
|
-
const search = `openfda.generic_name:"${escaped}"`;
|
|
198
|
+
const escaped = escapeQueryPhrase(drugName);
|
|
199
|
+
const search = `(openfda.generic_name:"${escaped}" OR openfda.brand_name:"${escaped}")`;
|
|
192
200
|
const url = `${OPENFDA_LABEL_BASE}?search=${encodeURIComponent(search)}` + `&sort=effective_time:desc&limit=${limit}`;
|
|
193
201
|
const res = await apiFetchValidated(url, OpenFdaLabelResponseSchema);
|
|
194
202
|
if (res.isErr()) {
|
|
@@ -35,6 +35,17 @@ export interface SafetyLiability {
|
|
|
35
35
|
effects: string | null;
|
|
36
36
|
source: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* One disease or trait that Open Targets holds, as the free-text resolver
|
|
40
|
+
* returns it. The `id` is the identifier that `searchDiseaseAssociations`
|
|
41
|
+
* accepts. Open Targets keys a disease on the EFO ontology, which imports
|
|
42
|
+
* MONDO, Orphanet and HP, thus an id can read `EFO_…`, `MONDO_…` or `HP_…`.
|
|
43
|
+
*/
|
|
44
|
+
export interface DiseaseCandidate {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
description: string | null;
|
|
48
|
+
}
|
|
38
49
|
export interface BaselineExpressionEntry {
|
|
39
50
|
tissueId: string;
|
|
40
51
|
tissueLabel: string;
|
|
@@ -51,6 +62,15 @@ export interface BaselineExpressionEntry {
|
|
|
51
62
|
export declare function searchTargetAssociations(ensemblId: string, limit?: number): Promise<TargetInfo | null>;
|
|
52
63
|
/** Fetch target associations for a disease (EFO id). */
|
|
53
64
|
export declare function searchDiseaseAssociations(efoId: string, limit?: number): Promise<Association[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a free-text disease or trait name to the disease ids that
|
|
67
|
+
* `searchDiseaseAssociations` accepts. A name that matches nothing gives an
|
|
68
|
+
* empty candidate list, which is a normal outcome and not an error.
|
|
69
|
+
*/
|
|
70
|
+
export declare function searchDiseases(query: string, limit?: number): Promise<{
|
|
71
|
+
total: number;
|
|
72
|
+
candidates: DiseaseCandidate[];
|
|
73
|
+
}>;
|
|
54
74
|
/** Fetch known safety liabilities for a target. */
|
|
55
75
|
export declare function getTargetSafetyLiabilities(ensemblId: string): Promise<{
|
|
56
76
|
targetSymbol: string;
|
|
@@ -50,6 +50,18 @@ const DISEASE_QUERY = `
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
`;
|
|
53
|
+
const DISEASE_SEARCH_QUERY = `
|
|
54
|
+
query DiseaseSearch($queryString: String!, $size: Int!) {
|
|
55
|
+
search(queryString: $queryString, entityNames: ["disease"], page: { size: $size, index: 0 }) {
|
|
56
|
+
total
|
|
57
|
+
hits {
|
|
58
|
+
id
|
|
59
|
+
name
|
|
60
|
+
description
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
53
65
|
const SAFETY_QUERY = `
|
|
54
66
|
query TargetSafety($ensemblId: String!) {
|
|
55
67
|
target(ensemblId: $ensemblId) {
|
|
@@ -147,6 +159,22 @@ const DiseaseAssociationsDataSchema = z.object({
|
|
|
147
159
|
.nullable()
|
|
148
160
|
.optional(),
|
|
149
161
|
});
|
|
162
|
+
const DiseaseSearchDataSchema = z.object({
|
|
163
|
+
search: z
|
|
164
|
+
.object({
|
|
165
|
+
total: z.number().nullable().optional(),
|
|
166
|
+
hits: z
|
|
167
|
+
.array(z.object({
|
|
168
|
+
id: z.string(),
|
|
169
|
+
name: z.string().nullable().optional(),
|
|
170
|
+
description: z.string().nullable().optional(),
|
|
171
|
+
}))
|
|
172
|
+
.nullable()
|
|
173
|
+
.optional(),
|
|
174
|
+
})
|
|
175
|
+
.nullable()
|
|
176
|
+
.optional(),
|
|
177
|
+
});
|
|
150
178
|
const TargetSafetyDataSchema = z.object({
|
|
151
179
|
target: z
|
|
152
180
|
.object({
|
|
@@ -253,6 +281,23 @@ export async function searchDiseaseAssociations(efoId, limit = 25) {
|
|
|
253
281
|
literaturePmids: [],
|
|
254
282
|
}));
|
|
255
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve a free-text disease or trait name to the disease ids that
|
|
286
|
+
* `searchDiseaseAssociations` accepts. A name that matches nothing gives an
|
|
287
|
+
* empty candidate list, which is a normal outcome and not an error.
|
|
288
|
+
*/
|
|
289
|
+
export async function searchDiseases(query, limit = 10) {
|
|
290
|
+
const data = await gqlFetch(DISEASE_SEARCH_QUERY, { queryString: query, size: limit }, DiseaseSearchDataSchema);
|
|
291
|
+
const hits = data.search?.hits ?? [];
|
|
292
|
+
return {
|
|
293
|
+
total: data.search?.total ?? hits.length,
|
|
294
|
+
candidates: hits.map((hit) => ({
|
|
295
|
+
id: hit.id,
|
|
296
|
+
name: hit.name ?? "",
|
|
297
|
+
description: hit.description ?? null,
|
|
298
|
+
})),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
256
301
|
/** Fetch known safety liabilities for a target. */
|
|
257
302
|
export async function getTargetSafetyLiabilities(ensemblId) {
|
|
258
303
|
const data = await gqlFetch(SAFETY_QUERY, { ensemblId }, TargetSafetyDataSchema);
|
|
@@ -23,5 +23,12 @@ export interface PathwaySearchOptions {
|
|
|
23
23
|
export declare function searchKegg(query: string, organism: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
|
|
24
24
|
export declare function searchReactome(query: string, species: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
|
|
25
25
|
export declare function searchPathways(query: string, options?: PathwaySearchOptions): Promise<Pathway[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Read one pathway by its own identifier. The prefix of the identifier picks
|
|
28
|
+
* the database, thus the caller passes the id back exactly as a search
|
|
29
|
+
* returned it. An identifier that neither database holds gives null, which is
|
|
30
|
+
* absence and not a failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getPathwayById(id: string): Promise<Pathway | null>;
|
|
26
33
|
/** Pathway memberships for a gene symbol — used directly by §3.7 collector. */
|
|
27
34
|
export declare function getPathwayMemberships(geneSymbol: string): Promise<Pathway[]>;
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { apiFetch, apiFetchValidated, parseTSV } from "./api-utils.js";
|
|
8
8
|
const KEGG_BASE = "https://rest.kegg.jp";
|
|
9
|
-
|
|
9
|
+
// The ContentService of the primary host answers 404 on each path. The release
|
|
10
|
+
// host serves the same API, and a probe of `search/query`, `data/query/{stId}`
|
|
11
|
+
// and `data/participants/{stId}` returned 200 on it. The web pages that a result
|
|
12
|
+
// links to stay on the primary host, thus only the API base moves.
|
|
13
|
+
const REACTOME_BASE = "https://release.reactome.org/ContentService";
|
|
10
14
|
export function stripHtmlAndCollapseWs(s) {
|
|
11
15
|
// Strip tags to a fixpoint: a single pass can leave a `<…>` behind when
|
|
12
16
|
// removing an inner tag lets the surrounding angle brackets form a new one,
|
|
@@ -40,6 +44,12 @@ const ReactomeSearchResponseSchema = z.object({
|
|
|
40
44
|
});
|
|
41
45
|
/** Participant list — `searchReactome` reads `displayName` per participant. */
|
|
42
46
|
const ReactomeParticipantsSchema = z.array(z.object({ displayName: z.string().optional() }));
|
|
47
|
+
/** One Reactome event record, as `/data/query/{stId}` serves it. */
|
|
48
|
+
const ReactomeEntrySchema = z.object({
|
|
49
|
+
stId: z.string().optional(),
|
|
50
|
+
displayName: z.string().optional(),
|
|
51
|
+
summation: z.array(z.object({ text: z.string().optional() })).optional(),
|
|
52
|
+
});
|
|
43
53
|
/** Participant list — `getPathwayMemberships` reads `refEntities` per participant. */
|
|
44
54
|
const ReactomeParticipantEntitiesSchema = z.array(z.object({
|
|
45
55
|
refEntities: z.array(z.object({ schemaClass: z.string().optional(), identifier: z.string().optional() })).optional(),
|
|
@@ -126,6 +136,63 @@ export async function searchPathways(query, options = {}) {
|
|
|
126
136
|
const results = await Promise.all(tasks);
|
|
127
137
|
return results.flat();
|
|
128
138
|
}
|
|
139
|
+
/** A Reactome stable identifier, for example `R-HSA-109581`. */
|
|
140
|
+
const REACTOME_ID_RE = /^R-[A-Z]{3}-\d+(\.\d+)?$/;
|
|
141
|
+
/** A KEGG pathway identifier, for example `hsa04010` or `map04010`. */
|
|
142
|
+
const KEGG_ID_RE = /^[a-z]{3,4}\d{5}$/;
|
|
143
|
+
/** Read one field out of a KEGG flat-file record. */
|
|
144
|
+
function keggField(record, field) {
|
|
145
|
+
const match = new RegExp(`^${field}\\s+(.*)$`, "m").exec(record);
|
|
146
|
+
return match?.[1]?.trim() || null;
|
|
147
|
+
}
|
|
148
|
+
async function getKeggPathway(id) {
|
|
149
|
+
const res = await apiFetch(`${KEGG_BASE}/get/${encodeURIComponent(id)}`, { parseAs: "text" });
|
|
150
|
+
if (res.isErr())
|
|
151
|
+
return null;
|
|
152
|
+
const record = res.value;
|
|
153
|
+
const name = keggField(record, "NAME");
|
|
154
|
+
if (!name)
|
|
155
|
+
return null;
|
|
156
|
+
const description = keggField(record, "DESCRIPTION");
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
name,
|
|
160
|
+
source: "kegg",
|
|
161
|
+
...(description ? { description } : {}),
|
|
162
|
+
url: `https://www.kegg.jp/pathway/${id}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function getReactomePathway(id) {
|
|
166
|
+
const res = await apiFetchValidated(`${REACTOME_BASE}/data/query/${encodeURIComponent(id)}`, ReactomeEntrySchema, {
|
|
167
|
+
headers: { Accept: "application/json" },
|
|
168
|
+
});
|
|
169
|
+
if (res.isErr())
|
|
170
|
+
return null;
|
|
171
|
+
const entry = res.value;
|
|
172
|
+
const stId = entry.stId ?? id;
|
|
173
|
+
const description = entry.summation?.map((s) => s.text ?? "").find((text) => text.length > 0);
|
|
174
|
+
return {
|
|
175
|
+
id: stId,
|
|
176
|
+
name: stripHtmlAndCollapseWs(entry.displayName ?? ""),
|
|
177
|
+
source: "reactome",
|
|
178
|
+
...(description ? { description: stripHtmlAndCollapseWs(description) } : {}),
|
|
179
|
+
url: `https://reactome.org/content/detail/${stId}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read one pathway by its own identifier. The prefix of the identifier picks
|
|
184
|
+
* the database, thus the caller passes the id back exactly as a search
|
|
185
|
+
* returned it. An identifier that neither database holds gives null, which is
|
|
186
|
+
* absence and not a failure.
|
|
187
|
+
*/
|
|
188
|
+
export async function getPathwayById(id) {
|
|
189
|
+
const trimmed = id.trim();
|
|
190
|
+
if (REACTOME_ID_RE.test(trimmed))
|
|
191
|
+
return getReactomePathway(trimmed);
|
|
192
|
+
if (KEGG_ID_RE.test(trimmed))
|
|
193
|
+
return getKeggPathway(trimmed);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
129
196
|
/** Pathway memberships for a gene symbol — used directly by §3.7 collector. */
|
|
130
197
|
export async function getPathwayMemberships(geneSymbol) {
|
|
131
198
|
const [kegg, reactome] = await Promise.all([searchKegg(geneSymbol, "hsa", 25, false), searchReactome(geneSymbol, "Homo sapiens", 25, false)]);
|
|
@@ -20,7 +20,7 @@ const Context7SearchResponseSchema = z.object({
|
|
|
20
20
|
const Context7DocsResponseSchema = z.object({ content: z.string().optional() });
|
|
21
21
|
export const resolveLibraryIdTool = defineTool({
|
|
22
22
|
id: "resolve_library_id",
|
|
23
|
-
description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE
|
|
23
|
+
description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE `query_docs` to get the library ID needed for documentation lookup. Returns matching libraries ranked by relevance.",
|
|
24
24
|
inputSchema: z.object({
|
|
25
25
|
libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
|
|
26
26
|
query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
|
|
@@ -44,9 +44,9 @@ export const resolveLibraryIdTool = defineTool({
|
|
|
44
44
|
});
|
|
45
45
|
export const queryDocsTool = defineTool({
|
|
46
46
|
id: "query_docs",
|
|
47
|
-
description: "Query up-to-date documentation and code examples for a library. You must call
|
|
47
|
+
description: "Query up-to-date documentation and code examples for a library. You must call `resolve_library_id` first to get the libraryId. Use this to verify function signatures, parameters, and usage patterns before writing code.",
|
|
48
48
|
inputSchema: z.object({
|
|
49
|
-
libraryId: z.string().describe("Context7 library ID from
|
|
49
|
+
libraryId: z.string().describe("Context7 library ID from `resolve_library_id` (for example '/scverse/scanpy')."),
|
|
50
50
|
query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
|
|
51
51
|
}),
|
|
52
52
|
describeCall: "none",
|
|
@@ -178,8 +178,8 @@ export function createGenerateAnalogyReportTool(deps) {
|
|
|
178
178
|
"stuck', 'brainstorm ideas', 'are there precedents outside biology'. The " +
|
|
179
179
|
"trigger is exploratory INTENT, not the words 'analogy' or 'cross-domain' " +
|
|
180
180
|
"— users rarely name it, so infer it and reach for the tool proactively.\n" +
|
|
181
|
-
"NOT for: in-domain literature review (use `
|
|
182
|
-
"
|
|
181
|
+
"NOT for: in-domain literature review (use `pubmed` and the other " +
|
|
182
|
+
"bio-lookup tools), single-gene factual lookup (use `search_gene`), or " +
|
|
183
183
|
"execution-mode turns where the user just wants the next step done. It " +
|
|
184
184
|
"drives a multi-step research sub-agent over live literature search, so " +
|
|
185
185
|
"it is slow — never spend it on a fact you could look up.\n" +
|
|
@@ -28,6 +28,7 @@ import { type ChatProvider } from "../../providers/types.js";
|
|
|
28
28
|
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
29
29
|
import { type Tool } from "../define-tool.js";
|
|
30
30
|
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
31
|
+
import { type BioToolKeys } from "../bio/keys.js";
|
|
31
32
|
import { type ResourcePolicy } from "../../config/resource-limits.js";
|
|
32
33
|
import type { Logger } from "../../lib/logger.js";
|
|
33
34
|
export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
@@ -52,6 +53,8 @@ export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
|
52
53
|
readonly logger?: Logger;
|
|
53
54
|
/** LLM usage-accounting seam for the planner loop; omitted falls back to the no-op recorder. */
|
|
54
55
|
readonly usageRecorder?: UsageRecorder;
|
|
56
|
+
/** API keys for the search tools the planner uses to ground a plan. */
|
|
57
|
+
readonly bioKeys: BioToolKeys;
|
|
55
58
|
}
|
|
56
59
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
57
60
|
export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
|