@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.
Files changed (93) hide show
  1. package/dist/agents/conversation-agent.js +7 -4
  2. package/dist/agents/report-session-agent.js +1 -1
  3. package/dist/agents/sandbox/data-profiler.js +0 -1
  4. package/dist/agents/sandbox/immune-profiling-agent.js +3 -1
  5. package/dist/agents/sandbox/network-agent.js +0 -1
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/agents/sandbox/types.js +1 -1
  8. package/dist/config/resource-limits.d.ts +1 -1
  9. package/dist/config/resource-limits.js +1 -1
  10. package/dist/contracts/schemas/chat-parts.d.ts +12 -12
  11. package/dist/contracts/severity.d.ts +2 -2
  12. package/dist/contracts/target-dossier.d.ts +70 -70
  13. package/dist/data/safety-panel-schema.d.ts +4 -4
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +7 -0
  16. package/dist/loop/run-agent.d.ts +13 -1
  17. package/dist/loop/run-agent.js +2 -1
  18. package/dist/prompts/SOUL.d.ts +2 -2
  19. package/dist/prompts/SOUL.js +17 -1
  20. package/dist/prompts/conversation.d.ts +1 -1
  21. package/dist/prompts/conversation.js +43 -71
  22. package/dist/prompts/planner.js +57 -10
  23. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  24. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -1
  25. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  26. package/dist/prompts/sandbox/dna-methylation-agent.js +18 -12
  27. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  28. package/dist/prompts/sandbox/immune-profiling-agent.js +6 -2
  29. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  30. package/dist/prompts/sandbox/metabolomics-agent.js +8 -4
  31. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  32. package/dist/prompts/sandbox/microbiome-agent.js +12 -6
  33. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  34. package/dist/prompts/sandbox/network-agent.js +9 -6
  35. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  36. package/dist/prompts/sandbox/spatial-omics-agent.js +4 -2
  37. package/dist/providers/ai-sdk.js +34 -1
  38. package/dist/providers/reasoning.d.ts +65 -0
  39. package/dist/providers/reasoning.js +81 -0
  40. package/dist/providers/types.d.ts +14 -0
  41. package/dist/schemas/data-profile-schemas.d.ts +4 -4
  42. package/dist/schemas/run-synthesis.d.ts +4 -4
  43. package/dist/tools/bio/chembl.d.ts +1 -1
  44. package/dist/tools/bio/chembl.js +15 -10
  45. package/dist/tools/bio/comptox.js +5 -1
  46. package/dist/tools/bio/drug-gene-interactions.js +5 -2
  47. package/dist/tools/bio/gene-disease-evidence.d.ts +19 -9
  48. package/dist/tools/bio/gene-disease-evidence.js +78 -22
  49. package/dist/tools/bio/gene-preclinical-profile.d.ts +33 -13
  50. package/dist/tools/bio/gene-preclinical-profile.js +63 -22
  51. package/dist/tools/bio/lookup-annotation.d.ts +1 -0
  52. package/dist/tools/bio/lookup-annotation.js +28 -10
  53. package/dist/tools/bio/opentargets.d.ts +10 -2
  54. package/dist/tools/bio/opentargets.js +42 -13
  55. package/dist/tools/bio/pubchem.js +5 -2
  56. package/dist/tools/bio/pubmed.js +9 -3
  57. package/dist/tools/bio/search-clinical-trials.d.ts +25 -25
  58. package/dist/tools/bio/search-clinical-trials.js +56 -18
  59. package/dist/tools/bio/search-faers.d.ts +26 -6
  60. package/dist/tools/bio/search-faers.js +70 -18
  61. package/dist/tools/bio/search-gene.d.ts +19 -3
  62. package/dist/tools/bio/search-gene.js +69 -12
  63. package/dist/tools/bio/search-geo-datasets.js +4 -1
  64. package/dist/tools/bio/search-interactions.js +6 -2
  65. package/dist/tools/bio/target-safety.d.ts +2 -2
  66. package/dist/tools/bio/target-safety.js +3 -2
  67. package/dist/tools/lib/cbioportal-client.js +25 -6
  68. package/dist/tools/lib/chembl-client.d.ts +3 -1
  69. package/dist/tools/lib/chembl-client.js +6 -1
  70. package/dist/tools/lib/gwas-catalog-client.d.ts +7 -6
  71. package/dist/tools/lib/gwas-catalog-client.js +18 -6
  72. package/dist/tools/lib/openfda-client.d.ts +7 -5
  73. package/dist/tools/lib/openfda-client.js +17 -9
  74. package/dist/tools/lib/opentargets-client.d.ts +20 -0
  75. package/dist/tools/lib/opentargets-client.js +45 -0
  76. package/dist/tools/lib/pathway-client.d.ts +7 -0
  77. package/dist/tools/lib/pathway-client.js +68 -1
  78. package/dist/tools/research/context7-docs.js +3 -3
  79. package/dist/tools/research/generate-analogy-report.js +2 -2
  80. package/dist/tools/research/generate-plan.d.ts +3 -0
  81. package/dist/tools/research/generate-plan.js +64 -5
  82. package/dist/tools/research/resolve-citation.js +8 -3
  83. package/dist/tools/research/search-arxiv.js +8 -3
  84. package/dist/tools/research/search-semantic-scholar.js +10 -4
  85. package/dist/workflows/sandbox-step.d.ts +6 -0
  86. package/dist/workflows/sandbox-step.js +31 -14
  87. package/dist/workflows/target-assessment/assemblers/safety.d.ts +1 -1
  88. package/dist/workflows/target-assessment/phase4-assemble.d.ts +17 -17
  89. package/dist/workflows/target-assessment/schemas.d.ts +12 -12
  90. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +6 -6
  91. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +6 -6
  92. package/dist/workflows/target-assessment/synthesis/index.d.ts +12 -12
  93. package/package.json +1 -1
@@ -111,8 +111,11 @@ export function createDrugGeneInteractionsTool(deps) {
111
111
  const logger = (deps.logger ?? createNoopLogger()).named("drug_gene_interactions");
112
112
  return defineTool({
113
113
  id: "drug_gene_interactions",
114
- description: "Drug↔gene interactions across DGIdb, DrugBank and PharmGKB, in one call — 'what drugs hit these genes?' and 'what genes does this drug act " +
115
- "on?'. See `direction` and `sources` for the modes.\n" +
114
+ description: "Drug↔gene interactions across DGIdb (the Drug Gene Interaction Database), DrugBank and PharmGKB, in one call — 'what drugs hit these " +
115
+ "genes?' and 'what genes does this drug act on?'. See `direction` and `sources` for the modes.\n" +
116
+ "ACCEPTED IDENTIFIERS: a HUGO gene symbol ('EGFR') for direction 'gene_to_drugs'; a drug name ('imatinib'), a DrugBank ID ('DB00619') or a " +
117
+ "DGIdb concept ID for 'drug_to_genes'. A brand name and a non-HUGO symbol match nothing — resolve the symbol with search_gene and the brand " +
118
+ "name with search_faers({action:'label'}) first.\n" +
116
119
  "It answers whether an interaction is KNOWN, by whom, and of what type. For quotable POTENCY (IC50/Ki) use chembl({action:'bioactivity'}) instead.\n" +
117
120
  "Verbose fields (`includeAttributes`, `includeDrugRecord`) are off by default — both cost a lot of context and are rarely what the question needs.\n" +
118
121
  "ALWAYS read `perSource` before concluding an interaction is unknown: 'no_data' means that corpus has nothing; 'unavailable' means it could " +
@@ -1,8 +1,8 @@
1
1
  /**
2
- * geneDiseaseEvidence — one tool over the three gene↔disease evidence corpora:
3
- * the NHGRI-EBI GWAS Catalog, DisGeNET, and NCBI ClinVar.
2
+ * geneDiseaseEvidence — one tool over the four gene↔disease evidence corpora:
3
+ * the NHGRI-EBI GWAS Catalog, DisGeNET, NCBI ClinVar, and cBioPortal.
4
4
  *
5
- * These were three tools with the same input skeleton (`query` + a
5
+ * The first three were three tools with the same input skeleton (`query` + a
6
6
  * gene/disease/variant discriminator + a strength threshold + `limit`)
7
7
  * answering the same question — what genetic evidence links this gene to this
8
8
  * disease or trait. Picking between them was never a routing decision an agent
@@ -10,19 +10,28 @@
10
10
  * defaults to all of them. `search_pathway`'s `source: kegg | reactome | both`
11
11
  * is the same shape.
12
12
  *
13
+ * cBioPortal joins them as the somatic half of the same question, and it is the
14
+ * one source that is NOT in the default set: it scans every curated study, thus
15
+ * it costs several large requests that a routine gene query must not pay.
16
+ *
13
17
  * Each source runs independently and reports its own outcome in `perSource`: a
14
18
  * missing DisGeNET key or a GWAS Catalog outage degrades that one corpus to
15
- * `unavailable` instead of failing a call the other two could have answered.
19
+ * `unavailable` instead of failing a call the others could have answered.
16
20
  *
17
21
  * The input is a flat object with a `queryType` discriminator — not a
18
22
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
19
- * a top-level `"type":"object"`).
23
+ * a top-level `"type":"object"`). A `queryType` that only one corpus can serve
24
+ * — an accession of that corpus — marks the others `not_applicable` rather
25
+ * than sending them a query they cannot read.
20
26
  */
27
+ import { type MutationFrequency } from "../lib/cbioportal-client.js";
21
28
  import { type ClinvarVariant } from "../lib/clinvar-client.js";
22
29
  import { type Gda } from "../lib/disgenet-client.js";
23
30
  import { type GwasAssociation } from "../lib/gwas-catalog-client.js";
24
- declare const SOURCES: readonly ["gwas", "disgenet", "clinvar"];
31
+ declare const SOURCES: readonly ["gwas", "disgenet", "clinvar", "cbioportal"];
25
32
  type Source = (typeof SOURCES)[number];
33
+ declare const QUERY_TYPES: readonly ["gene", "disease", "variant", "gwas_study", "clinvar_accession"];
34
+ type QueryType = (typeof QUERY_TYPES)[number];
26
35
  /**
27
36
  * Per-source outcome. `unavailable` (key absent, corpus erroring) and
28
37
  * `not_applicable` (the corpus has no lookup for this queryType) are both
@@ -38,19 +47,20 @@ interface SourceOutcome {
38
47
  }
39
48
  interface GeneDiseaseEvidenceOutput {
40
49
  query: string;
41
- queryType: "gene" | "disease" | "variant";
50
+ queryType: QueryType;
42
51
  perSource: SourceOutcome[];
43
52
  gwas?: GwasAssociation[];
44
53
  disgenet?: Gda[];
45
54
  clinvar?: ClinvarVariant[];
55
+ cbioportal?: MutationFrequency[];
46
56
  }
47
57
  export declare function createGeneDiseaseEvidenceTool(deps: {
48
58
  ncbiApiKey?: string;
49
59
  disgenetApiKey: string;
50
60
  }): import("../define-tool.js").Tool<{
51
61
  query: string;
52
- queryType: "gene" | "disease" | "variant";
53
- sources?: ("clinvar" | "gwas" | "disgenet")[] | undefined;
62
+ queryType: "gene" | "disease" | "variant" | "gwas_study" | "clinvar_accession";
63
+ sources?: ("clinvar" | "gwas" | "disgenet" | "cbioportal")[] | undefined;
54
64
  limit?: number | undefined;
55
65
  pValueThreshold?: number | undefined;
56
66
  minScore?: number | undefined;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * geneDiseaseEvidence — one tool over the three gene↔disease evidence corpora:
3
- * the NHGRI-EBI GWAS Catalog, DisGeNET, and NCBI ClinVar.
2
+ * geneDiseaseEvidence — one tool over the four gene↔disease evidence corpora:
3
+ * the NHGRI-EBI GWAS Catalog, DisGeNET, NCBI ClinVar, and cBioPortal.
4
4
  *
5
- * These were three tools with the same input skeleton (`query` + a
5
+ * The first three were three tools with the same input skeleton (`query` + a
6
6
  * gene/disease/variant discriminator + a strength threshold + `limit`)
7
7
  * answering the same question — what genetic evidence links this gene to this
8
8
  * disease or trait. Picking between them was never a routing decision an agent
@@ -10,45 +10,87 @@
10
10
  * defaults to all of them. `search_pathway`'s `source: kegg | reactome | both`
11
11
  * is the same shape.
12
12
  *
13
+ * cBioPortal joins them as the somatic half of the same question, and it is the
14
+ * one source that is NOT in the default set: it scans every curated study, thus
15
+ * it costs several large requests that a routine gene query must not pay.
16
+ *
13
17
  * Each source runs independently and reports its own outcome in `perSource`: a
14
18
  * missing DisGeNET key or a GWAS Catalog outage degrades that one corpus to
15
- * `unavailable` instead of failing a call the other two could have answered.
19
+ * `unavailable` instead of failing a call the others could have answered.
16
20
  *
17
21
  * The input is a flat object with a `queryType` discriminator — not a
18
22
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
19
- * a top-level `"type":"object"`).
23
+ * a top-level `"type":"object"`). A `queryType` that only one corpus can serve
24
+ * — an accession of that corpus — marks the others `not_applicable` rather
25
+ * than sending them a query they cannot read.
20
26
  */
21
27
  import { ok } from "neverthrow";
22
28
  import { z } from "zod";
23
29
  import { defineTool } from "../define-tool.js";
30
+ import { getSomaticMutationFrequencies } from "../lib/cbioportal-client.js";
24
31
  import { filterInformative, searchClinvar } from "../lib/clinvar-client.js";
25
32
  import { searchDisgenet } from "../lib/disgenet-client.js";
26
33
  import { searchGwasCatalog } from "../lib/gwas-catalog-client.js";
27
- const SOURCES = ["gwas", "disgenet", "clinvar"];
34
+ const SOURCES = ["gwas", "disgenet", "clinvar", "cbioportal"];
35
+ const QUERY_TYPES = ["gene", "disease", "variant", "gwas_study", "clinvar_accession"];
28
36
  const DEFAULT_LIMIT = 10;
29
37
  const MAX_LIMIT = 100;
30
- /** DisGeNET has no variant-level lookup; the other two do. */
38
+ /** Which query types each corpus can read. An accession is readable by one corpus only. */
39
+ const SOURCE_QUERY_TYPES = {
40
+ gwas: ["gene", "disease", "variant", "gwas_study"],
41
+ disgenet: ["gene", "disease"],
42
+ clinvar: ["gene", "disease", "variant", "clinvar_accession"],
43
+ cbioportal: ["gene"],
44
+ };
45
+ /**
46
+ * cBioPortal stays out of the default set. It scans every curated study, thus
47
+ * a caller opts into it by naming it in `sources`.
48
+ */
49
+ const OPT_IN_SOURCES = ["cbioportal"];
31
50
  function applicableSources(queryType) {
32
- return queryType === "variant" ? ["gwas", "clinvar"] : [...SOURCES];
51
+ return SOURCES.filter((source) => SOURCE_QUERY_TYPES[source].includes(queryType));
33
52
  }
53
+ /** The corpora a call queries when it names none. */
54
+ function defaultSources(queryType) {
55
+ return applicableSources(queryType).filter((source) => !OPT_IN_SOURCES.includes(source));
56
+ }
57
+ /**
58
+ * The GWAS Catalog name for each query type it serves. `clinvar_accession` is
59
+ * absent because `applicableSources` never selects the GWAS Catalog for it.
60
+ */
61
+ const GWAS_SEARCH_TYPES = {
62
+ gene: "gene",
63
+ disease: "trait",
64
+ variant: "variant",
65
+ gwas_study: "study",
66
+ };
34
67
  const inputSchema = z.object({
35
68
  query: z
36
69
  .string()
37
70
  .min(1)
38
- .describe("The entity, matching `queryType`. gene: a HUGO symbol ('PCSK9'); DisGeNET also takes an Entrez ID. disease: a disease or trait name " +
39
- "('LDL cholesterol'); DisGeNET also takes a UMLS CUI ('C0006142'). variant: a dbSNP rsID ('rs11591147')."),
71
+ .describe("The entity, matching `queryType`. gene: a HUGO symbol ('PCSK9'); DisGeNET also takes an Entrez ID, and cBioPortal takes the symbol " +
72
+ "directly. disease: a disease or trait name ('LDL cholesterol'); DisGeNET also takes a UMLS CUI ('C0006142'). variant: a dbSNP rsID " +
73
+ "('rs11591147'). gwas_study: a GWAS Catalog study accession ('GCST000392'). clinvar_accession: a ClinVar accession ('VCV000012345', " +
74
+ "'RCV000009910') or a bare variation ID."),
40
75
  queryType: z
41
- .enum(["gene", "disease", "variant"])
76
+ .enum(QUERY_TYPES)
42
77
  .describe("'gene' — the diseases/traits associated with a gene. 'disease' — the genes associated with a disease or trait (the GWAS Catalog calls it a " +
43
- "trait; this value covers both). 'variant' — one rsID; DisGeNET is skipped, having no variant-level lookup."),
78
+ "trait; this value covers both). 'variant' — one rsID; DisGeNET is skipped, having no variant-level lookup. 'gwas_study' — every " +
79
+ "association reported by ONE GWAS Catalog study, read back from the studyAccession that a gwas record carries; GWAS Catalog only. " +
80
+ "'clinvar_accession' — one ClinVar record, read back from the accession that a clinvar record carries; ClinVar only."),
44
81
  sources: z
45
82
  .array(z.enum(SOURCES))
46
83
  .min(1)
47
84
  .optional()
48
- .describe("Corpora to query in parallel. Default ALL applicable — they carry different evidence types, and disagreement between them is itself informative. " +
49
- "'gwas': population SNP-trait associations with effect sizes and p-values (common-variant / MR support). " +
85
+ .describe("Corpora to query in parallel. Default ALL applicable EXCEPT cbioportal — they carry different evidence types, and disagreement between them " +
86
+ "is itself informative. " +
87
+ "'gwas': the NHGRI-EBI GWAS Catalog — population SNP-trait associations with effect sizes and p-values (common-variant / MR support). " +
50
88
  "'disgenet': curated + text-mined gene-disease associations scored 0–1, broadest coverage; needs DISGENET_API_KEY. " +
51
- "'clinvar': clinically classified germline variants — pathogenicity and review status; rare/Mendelian rather than common-variant."),
89
+ "'clinvar': NCBI ClinVar — clinically classified germline variants, pathogenicity and review status; rare/Mendelian rather than " +
90
+ "common-variant. " +
91
+ "'cbioportal': the somatic half — how often the gene is mutated in each cancer type across every curated cBioPortal study, as " +
92
+ "cancerTypeName, mutatedSamples/totalSamples, frequency and the contributing studies. OPT IN by naming it: it scans every study, so it is " +
93
+ "several seconds slower than the rest, and it applies to queryType 'gene' only."),
52
94
  limit: z
53
95
  .number()
54
96
  .int()
@@ -110,18 +152,22 @@ async function runSource(source, fetch) {
110
152
  export function createGeneDiseaseEvidenceTool(deps) {
111
153
  return defineTool({
112
154
  id: "gene_disease_evidence",
113
- description: "Genetic evidence linking a gene to a disease or trait, across the GWAS Catalog, DisGeNET and ClinVar in one call — 'is there human genetic " +
114
- "support for this target in this indication?'. See `sources` for what each corpus carries.\n" +
155
+ description: "Genetic evidence linking a gene to a disease or trait, across the NHGRI-EBI GWAS Catalog, DisGeNET, NCBI ClinVar and cBioPortal in one call " +
156
+ "— 'is there human genetic support for this target in this indication?'. See `sources` for what each corpus carries.\n" +
115
157
  "For the integrated, pre-scored view prefer opentargets({action:'target'}) FIRST — it folds genetic association together with tractability " +
116
- "and the drug landscape. Reach here for the underlying records: actual SNPs, effect sizes, scores and pathogenicity calls with their studies and PMIDs.\n" +
158
+ "and the drug landscape. Reach here for the underlying records: actual SNPs, effect sizes, scores, pathogenicity calls and somatic mutation " +
159
+ "frequencies with their studies and PMIDs.\n" +
160
+ "ACCEPTED IDENTIFIERS, each named by its `queryType`: a HUGO gene symbol or an Entrez ID ('PCSK9', '5008'); a disease or trait name or a UMLS " +
161
+ "CUI ('LDL cholesterol', 'C0006142'); a dbSNP rsID ('rs11591147'); a GWAS Catalog study accession ('GCST000392'); and a ClinVar accession " +
162
+ "('VCV000012345'). The last two read one record back by the accession that an earlier result of this same tool reported.\n" +
117
163
  "ALWAYS read `perSource` before concluding anything is absent. 'no_data' means that corpus genuinely has nothing; 'unavailable' means it " +
118
- "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the other two); 'not_applicable' means that " +
164
+ "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the others); 'not_applicable' means that " +
119
165
  "corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
120
166
  inputSchema,
121
167
  describeCall: "none",
122
168
  execute: async (input) => {
123
169
  const applicable = applicableSources(input.queryType);
124
- const requested = input.sources ?? applicable;
170
+ const requested = input.sources ?? defaultSources(input.queryType);
125
171
  const selected = requested.filter((s) => applicable.includes(s));
126
172
  const limit = input.limit ?? DEFAULT_LIMIT;
127
173
  const output = {
@@ -136,10 +182,10 @@ export function createGeneDiseaseEvidenceTool(deps) {
136
182
  detail: `${source} has no lookup for queryType '${input.queryType}'`,
137
183
  })),
138
184
  };
139
- const [gwas, disgenet, clinvar] = await Promise.all([
185
+ const [gwas, disgenet, clinvar, cbioportal] = await Promise.all([
140
186
  selected.includes("gwas")
141
187
  ? runSource("gwas", async () => {
142
- const searchType = input.queryType === "gene" ? "gene" : input.queryType === "disease" ? "trait" : "variant";
188
+ const searchType = GWAS_SEARCH_TYPES[input.queryType];
143
189
  const res = await searchGwasCatalog(input.query, searchType, {
144
190
  limit,
145
191
  ...(input.pValueThreshold !== undefined ? { pValueThreshold: input.pValueThreshold } : {}),
@@ -169,6 +215,12 @@ export function createGeneDiseaseEvidenceTool(deps) {
169
215
  return { records: variants, totalFound: res.totalFound };
170
216
  })
171
217
  : null,
218
+ selected.includes("cbioportal")
219
+ ? runSource("cbioportal", async () => {
220
+ const res = await getSomaticMutationFrequencies(input.query);
221
+ return { records: res.rows.slice(0, limit), totalFound: res.rows.length };
222
+ })
223
+ : null,
172
224
  ]);
173
225
  if (gwas) {
174
226
  output.perSource.push(gwas.outcome);
@@ -182,6 +234,10 @@ export function createGeneDiseaseEvidenceTool(deps) {
182
234
  output.perSource.push(clinvar.outcome);
183
235
  output.clinvar = clinvar.records;
184
236
  }
237
+ if (cbioportal) {
238
+ output.perSource.push(cbioportal.outcome);
239
+ output.cbioportal = cbioportal.records;
240
+ }
185
241
  return ok(output);
186
242
  },
187
243
  });
@@ -1,20 +1,24 @@
1
1
  /**
2
- * genePreclinicalProfile — the in-vivo preclinical picture of one gene: where
3
- * it is expressed at baseline (Bgee) and what removing it does to a mouse
4
- * (IMPC), behind one `include`.
2
+ * genePreclinicalProfile — the in-vivo picture of one gene: where it is
3
+ * expressed at baseline (Bgee), what removing it does to a mouse (IMPC), and
4
+ * what people who carry variation in it present with (Monarch, HPO), behind
5
+ * one `include`.
5
6
  *
6
- * The two halves share an input exactly — ONE human gene symbol, with the
7
- * per-species orthologs resolved internally — and answer one question between
8
- * them: is this target expressed where it needs to act, and is a model organism
9
- * a fair surrogate for it. They were two tools that were called as a pair.
7
+ * The three halves share an input exactly — ONE human gene symbol, with the
8
+ * per-species orthologs and the gene curie resolved internally — and answer one
9
+ * question between them: is this target expressed where it needs to act, is a
10
+ * model organism a fair surrogate for it, and what does losing it do to a
11
+ * person. The first two were two tools that were called as a pair.
10
12
  *
11
- * Neither source caps its own output: Bgee returns every annotated tissue per
12
- * species and IMPC every significant phenotype term, so both halves are trimmed
13
- * here rather than in the clients — the target-assessment collectors read the
14
- * same clients and legitimately want full fidelity for the dossier.
13
+ * No source caps its own output: Bgee returns every annotated tissue per
14
+ * species, IMPC every significant phenotype term, and Monarch every curated
15
+ * HPO annotation, so each half is trimmed here rather than in the clients — the
16
+ * target-assessment collectors read the same clients and legitimately want full
17
+ * fidelity for the dossier.
15
18
  */
16
19
  import { type SupportedSpecies, type TissueRow } from "../lib/bgee-client.js";
17
20
  import { type MpTerm, type ViabilityCall, type ViabilityCategory } from "../lib/impc-client.js";
21
+ import { type MonarchPhenotypeAssociation } from "../lib/monarch-client.js";
18
22
  interface BoundedSpeciesEntry {
19
23
  species: SupportedSpecies;
20
24
  taxonId: number;
@@ -30,6 +34,21 @@ interface ExpressionHalf {
30
34
  bySpecies: BoundedSpeciesEntry[];
31
35
  notFound: string[];
32
36
  }
37
+ /**
38
+ * One curated human phenotype. The HPO ancestor closure of the source record
39
+ * is dropped here: it exists so a dossier collector can resolve an organ system
40
+ * by identifier, and it runs to hundreds of ids that a reader never uses.
41
+ */
42
+ type HumanPhenotypeTerm = Omit<MonarchPhenotypeAssociation, "ancestorIds">;
43
+ interface HumanPhenotypeHalf {
44
+ /** The curie Monarch was queried by, e.g. `HGNC:1100`; null when the symbol did not anchor on HGNC. */
45
+ geneCurie: string | null;
46
+ hpoTerms: HumanPhenotypeTerm[];
47
+ /** Curated HPO annotations before `phenotypeLimit`. */
48
+ phenotypeCount: number;
49
+ /** True when `phenotypeLimit` trimmed `hpoTerms`; `phenotypeCount` is the real total. */
50
+ phenotypesTrimmed: boolean;
51
+ }
33
52
  interface KnockoutHalf {
34
53
  mouseMarkerSymbol: string | null;
35
54
  mgiAccessionId: string | null;
@@ -46,12 +65,13 @@ type PreclinicalOutput = {
46
65
  geneSymbol: string;
47
66
  expression?: ExpressionHalf;
48
67
  knockout?: KnockoutHalf;
68
+ phenotypes?: HumanPhenotypeHalf;
49
69
  };
50
70
  export declare const genePreclinicalProfileTool: import("../define-tool.js").Tool<{
51
71
  geneSymbol: string;
52
- include?: ("expression" | "knockout")[] | undefined;
72
+ include?: ("phenotypes" | "expression" | "knockout")[] | undefined;
53
73
  species?: ("homo_sapiens" | "mus_musculus" | "rattus_norvegicus" | "canis_lupus_familiaris" | "macaca_mulatta")[] | undefined;
54
- minRank?: "absent" | "high" | "medium" | "low" | undefined;
74
+ minRank?: "low" | "medium" | "high" | "absent" | undefined;
55
75
  tissueLimit?: number | undefined;
56
76
  phenotypeLimit?: number | undefined;
57
77
  }, PreclinicalOutput>;
@@ -1,23 +1,28 @@
1
1
  /**
2
- * genePreclinicalProfile — the in-vivo preclinical picture of one gene: where
3
- * it is expressed at baseline (Bgee) and what removing it does to a mouse
4
- * (IMPC), behind one `include`.
2
+ * genePreclinicalProfile — the in-vivo picture of one gene: where it is
3
+ * expressed at baseline (Bgee), what removing it does to a mouse (IMPC), and
4
+ * what people who carry variation in it present with (Monarch, HPO), behind
5
+ * one `include`.
5
6
  *
6
- * The two halves share an input exactly — ONE human gene symbol, with the
7
- * per-species orthologs resolved internally — and answer one question between
8
- * them: is this target expressed where it needs to act, and is a model organism
9
- * a fair surrogate for it. They were two tools that were called as a pair.
7
+ * The three halves share an input exactly — ONE human gene symbol, with the
8
+ * per-species orthologs and the gene curie resolved internally — and answer one
9
+ * question between them: is this target expressed where it needs to act, is a
10
+ * model organism a fair surrogate for it, and what does losing it do to a
11
+ * person. The first two were two tools that were called as a pair.
10
12
  *
11
- * Neither source caps its own output: Bgee returns every annotated tissue per
12
- * species and IMPC every significant phenotype term, so both halves are trimmed
13
- * here rather than in the clients — the target-assessment collectors read the
14
- * same clients and legitimately want full fidelity for the dossier.
13
+ * No source caps its own output: Bgee returns every annotated tissue per
14
+ * species, IMPC every significant phenotype term, and Monarch every curated
15
+ * HPO annotation, so each half is trimmed here rather than in the clients — the
16
+ * target-assessment collectors read the same clients and legitimately want full
17
+ * fidelity for the dossier.
15
18
  */
16
19
  import { ok } from "neverthrow";
17
20
  import { z } from "zod";
18
21
  import { defineTool } from "../define-tool.js";
19
22
  import { SUPPORTED_SPECIES, getMultiSpeciesExpression } from "../lib/bgee-client.js";
23
+ import { resolveTarget } from "../lib/identifier-resolver.js";
20
24
  import { getKoPhenotypeProfile } from "../lib/impc-client.js";
25
+ import { getGenePhenotypeProfile } from "../lib/monarch-client.js";
21
26
  /** Ordered weakest → strongest so `minRank` can be a numeric floor. */
22
27
  const RANK_ORDER = { absent: 0, low: 1, medium: 2, high: 3 };
23
28
  /** Human + mouse: the pairing that answers "is the mouse a fair surrogate?". */
@@ -30,11 +35,13 @@ const inputSchema = z.object({
30
35
  .describe("HUMAN gene symbol, e.g. 'BRCA1' — one gene per call. Ensembl IDs, mouse symbols and MGI IDs are NOT accepted; the ENSG and the per-species " +
31
36
  "orthologs are resolved for you."),
32
37
  include: z
33
- .array(z.enum(["expression", "knockout"]))
38
+ .array(z.enum(["expression", "knockout", "phenotypes"]))
34
39
  .min(1)
35
40
  .optional()
36
- .describe("Default BOTH — two sides of one judgement, one request each. 'expression': Bgee baseline (healthy, untreated) expression per species and " +
37
- "tissue. 'knockout': IMPC mouse loss-of-function phenotype."),
41
+ .describe("Default ['expression','knockout'] — two sides of one judgement, one request each. 'expression': Bgee baseline (healthy, untreated) " +
42
+ "expression per species and tissue. 'knockout': IMPC mouse loss-of-function phenotype. 'phenotypes': the HUMAN counterpart — the curated " +
43
+ "HPO phenotypes that Monarch attributes to variation in this gene, from HPOA, OMIM and Orphanet, each with its PMIDs and the disease it " +
44
+ "was annotated under. Ask for it explicitly, and pair it with 'knockout' when the question is whether the mouse recapitulates the person."),
38
45
  species: z
39
46
  .array(z.enum(SUPPORTED_SPECIES))
40
47
  .min(1)
@@ -60,8 +67,9 @@ const inputSchema = z.object({
60
67
  .min(1)
61
68
  .max(200)
62
69
  .optional()
63
- .describe(`'knockout' only. Max MP phenotype terms, best p-value first (default ${DEFAULTS.phenotypeLimit}, max 200). \`phenotypeCount\` gives the ` +
64
- "true total; `organSystems` is never trimmed, so the top-line picture survives."),
70
+ .describe(`'knockout' and 'phenotypes'. Max phenotype terms per half — MP terms best p-value first, HPO terms in curation order (default ` +
71
+ `${DEFAULTS.phenotypeLimit}, max 200). Each half reports its own \`phenotypeCount\` for the true total; \`organSystems\` is never ` +
72
+ "trimmed, so the top-line knockout picture survives."),
65
73
  });
66
74
  function boundExpression(raw, minRank, tissueLimit) {
67
75
  const floor = RANK_ORDER[minRank];
@@ -95,31 +103,64 @@ function boundKnockout(raw, phenotypeLimit) {
95
103
  phenotypesTrimmed: raw.mpTerms.length > phenotypeLimit,
96
104
  };
97
105
  }
106
+ /**
107
+ * Anchor the symbol on HGNC and read the curated human phenotypes. A symbol
108
+ * that anchors on no HGNC id gives a null curie and an empty term list, which
109
+ * is absence and not a failure.
110
+ */
111
+ async function fetchHumanPhenotypes(geneSymbol, phenotypeLimit) {
112
+ let geneCurie;
113
+ try {
114
+ geneCurie = (await resolveTarget(geneSymbol)).ids.hgnc;
115
+ }
116
+ catch {
117
+ geneCurie = null;
118
+ }
119
+ if (!geneCurie)
120
+ return { geneCurie: null, hpoTerms: [], phenotypeCount: 0, phenotypesTrimmed: false };
121
+ const profile = await getGenePhenotypeProfile(geneCurie);
122
+ return {
123
+ geneCurie: profile.geneCurie,
124
+ hpoTerms: profile.phenotypes.slice(0, phenotypeLimit).map(({ ancestorIds: _ancestorIds, ...term }) => term),
125
+ phenotypeCount: profile.phenotypes.length,
126
+ phenotypesTrimmed: profile.phenotypes.length > phenotypeLimit,
127
+ };
128
+ }
98
129
  export const genePreclinicalProfileTool = defineTool({
99
130
  id: "gene_preclinical_profile",
100
- description: "The in-vivo preclinical profile of ONE human gene — baseline expression (Bgee) and mouse-knockout phenotype (IMPC) in one call. Answers " +
101
- "tissue-of-action ('where does this target act?'), model-organism suitability ('is the mouse a fair surrogate?') and essentiality ('what happens " +
102
- "when this gene is lost?').\n" +
131
+ description: "The in-vivo profile of ONE human gene across three corpora in one call — baseline expression from Bgee, the mouse-knockout phenotype from IMPC " +
132
+ "(the International Mouse Phenotyping Consortium), and the curated human phenotypes from the Monarch Initiative (HPO terms sourced from HPOA, " +
133
+ "OMIM and Orphanet). Answers tissue-of-action ('where does this target act?'), model-organism suitability ('is the mouse a fair surrogate?'), " +
134
+ "essentiality ('what happens when this gene is lost?') and the human read-out ('what do people carrying variation in it present with?').\n" +
103
135
  "Expression is BASELINE (healthy, untreated) only — differential expression between conditions is the analysis pipeline's job, never this tool.\n" +
136
+ "ACCEPTED IDENTIFIERS: one HUMAN gene symbol, e.g. 'BRCA1'. The ENSG, the per-species orthologs and the HGNC curie are resolved for you; an " +
137
+ "Ensembl ID, a mouse symbol and an MGI ID are not accepted here — put those through search_gene first.\n" +
104
138
  "Output is trimmed by default (see `minRank`, `tissueLimit`, `phenotypeLimit`); the accompanying counts distinguish a trimmed list from a sparse one.\n" +
105
139
  "SPARSE OUTPUT IS VALID NO-DATA, not an error — do not retry. A null humanEnsemblId means the symbol did not resolve; a null mouseMarkerSymbol " +
106
- "with no phenotype terms means the gene has not been IMPC-phenotyped, which is common.",
140
+ "with no phenotype terms means the gene has not been IMPC-phenotyped, which is common; an empty hpoTerms list means Monarch curates no human " +
141
+ "phenotype for the gene, and a null geneCurie means the symbol anchored on no HGNC id.",
107
142
  inputSchema,
108
143
  describeCall: "none",
109
144
  execute: async ({ geneSymbol, include, species, minRank, tissueLimit, phenotypeLimit }) => {
110
145
  const halves = include ?? ["expression", "knockout"];
111
146
  const wantExpression = halves.includes("expression");
112
147
  const wantKnockout = halves.includes("knockout");
113
- const [expressionRaw, knockoutRaw] = await Promise.all([
148
+ const wantPhenotypes = halves.includes("phenotypes");
149
+ const termLimit = phenotypeLimit ?? DEFAULTS.phenotypeLimit;
150
+ const [expressionRaw, knockoutRaw, phenotypesHalf] = await Promise.all([
114
151
  wantExpression ? getMultiSpeciesExpression(geneSymbol, [...(species ?? DEFAULT_SPECIES)]) : Promise.resolve(null),
115
152
  wantKnockout ? getKoPhenotypeProfile(geneSymbol) : Promise.resolve(null),
153
+ wantPhenotypes ? fetchHumanPhenotypes(geneSymbol, termLimit) : Promise.resolve(null),
116
154
  ]);
117
155
  const output = { geneSymbol };
118
156
  if (expressionRaw) {
119
157
  output.expression = boundExpression(expressionRaw, minRank ?? DEFAULTS.minRank, tissueLimit ?? DEFAULTS.tissueLimit);
120
158
  }
121
159
  if (knockoutRaw) {
122
- output.knockout = boundKnockout(knockoutRaw, phenotypeLimit ?? DEFAULTS.phenotypeLimit);
160
+ output.knockout = boundKnockout(knockoutRaw, termLimit);
161
+ }
162
+ if (phenotypesHalf) {
163
+ output.phenotypes = phenotypesHalf;
123
164
  }
124
165
  return ok(output);
125
166
  },
@@ -38,6 +38,7 @@ type AnnotationOutput = {
38
38
  export declare const lookupAnnotationTool: import("../define-tool.js").Tool<{
39
39
  vocabulary: "kegg" | "reactome" | "go" | "pathways";
40
40
  query?: string | undefined;
41
+ pathwayId?: string | undefined;
41
42
  goId?: string | undefined;
42
43
  geneProductId?: string | undefined;
43
44
  taxonId?: number | undefined;
@@ -18,7 +18,7 @@ import { ok } from "neverthrow";
18
18
  import { z } from "zod";
19
19
  import { defineTool } from "../define-tool.js";
20
20
  import { apiFetchValidated, describeApiError } from "../lib/api-utils.js";
21
- import { searchPathways } from "../lib/pathway-client.js";
21
+ import { getPathwayById, searchPathways } from "../lib/pathway-client.js";
22
22
  const QUICKGO_BASE = "https://www.ebi.ac.uk/QuickGO/services";
23
23
  const HEADERS = { Accept: "application/json" };
24
24
  const DEFAULT_LIMIT = 10;
@@ -70,13 +70,20 @@ const inputSchema = z
70
70
  .enum(["go", "kegg", "reactome", "pathways"])
71
71
  .describe("'go' — Gene Ontology via QuickGO (EBI); needs ONE of goId, query, or geneProductId. Returns terms[] { id, name, definition, aspect } " +
72
72
  "and/or annotations[] { geneProductId, goId, goName, aspect, evidenceCode, qualifier }. " +
73
- "'kegg' / 'reactome' — one pathway database; needs query. 'pathways' — both, in parallel; needs query. " +
74
- "Returns pathways[] { id, name, source, url, genes? }."),
73
+ "'kegg' / 'reactome' — one pathway database; needs query or pathwayId. 'pathways' — both, in parallel; needs query or pathwayId. " +
74
+ "Returns pathways[] { id, name, source, url, description?, genes? }."),
75
75
  query: z
76
76
  .string()
77
77
  .min(1)
78
78
  .optional()
79
- .describe("Free-text term. Required for the pathway vocabularies ('apoptosis', 'MAPK signaling'); for 'go' it is one of the three accepted inputs."),
79
+ .describe("Free-text term. Required for the pathway vocabularies ('apoptosis', 'MAPK signaling') unless pathwayId is given; for 'go' it is one of " +
80
+ "the three accepted inputs."),
81
+ pathwayId: z
82
+ .string()
83
+ .min(1)
84
+ .optional()
85
+ .describe("Pathway vocabularies only. ONE pathway identifier, read back exactly as a search returned it — a Reactome stable ID ('R-HSA-109581') " +
86
+ "or a KEGG pathway ID ('hsa04010'). The prefix picks the database, so `vocabulary` and `organism` are ignored for it."),
80
87
  goId: z
81
88
  .string()
82
89
  .regex(/^GO:\d{7}$/)
@@ -98,9 +105,13 @@ const inputSchema = z
98
105
  .optional()
99
106
  .describe(`Max records per vocabulary queried (default ${DEFAULT_LIMIT}, max 100); with 'pathways' it applies to KEGG and Reactome separately.`),
100
107
  })
101
- .refine((d) => d.vocabulary === "go" || (d.query !== undefined && d.query.trim().length > 0), {
102
- message: "query is required for vocabulary 'kegg', 'reactome' and 'pathways' — the pathway term to search for",
108
+ .refine((d) => d.vocabulary === "go" || (d.query !== undefined && d.query.trim().length > 0) || (d.pathwayId !== undefined && d.pathwayId.length > 0), {
109
+ message: "vocabulary 'kegg', 'reactome' and 'pathways' need query (the pathway term to search for) or pathwayId (one 'R-HSA-…' or 'hsa…' identifier)",
103
110
  path: ["query"],
111
+ })
112
+ .refine((d) => d.vocabulary !== "go" || d.pathwayId === undefined, {
113
+ message: "pathwayId belongs to the pathway vocabularies — vocabulary 'go' takes goId instead",
114
+ path: ["pathwayId"],
104
115
  })
105
116
  .refine((d) => d.vocabulary !== "go" || d.goId !== undefined || d.query !== undefined || d.geneProductId !== undefined, {
106
117
  message: "vocabulary 'go' needs at least one of goId (term by ID), query (free-text term search), or geneProductId (a protein's annotations)",
@@ -116,16 +127,23 @@ const inputSchema = z
116
127
  });
117
128
  export const lookupAnnotationTool = defineTool({
118
129
  id: "lookup_annotation",
119
- description: "Functional annotation for a gene, protein or term — Gene Ontology (QuickGO) and the pathway databases (KEGG, Reactome) behind one `vocabulary`. " +
120
- "Names what a gene is known to do, resolves a GO or pathway identifier, or finds the pathways a term belongs to.\n" +
130
+ description: "Functional annotation for a gene, protein or term — the Gene Ontology (through EBI QuickGO) and the pathway databases KEGG and Reactome, behind " +
131
+ "one `vocabulary`. Names what a gene is known to do, resolves a GO or pathway identifier, or finds the pathways a term belongs to.\n" +
132
+ "ACCEPTED IDENTIFIERS: a GO term ID ('GO:0008150'), a UniProt accession for a protein's annotations ('P04637'), an NCBI Taxon ID ('9606'), a " +
133
+ "Reactome stable ID ('R-HSA-109581'), a KEGG pathway ID ('hsa04010'), and free text for any of the vocabularies ('apoptosis').\n" +
121
134
  "This is annotation LOOKUP, not enrichment: to test whether a gene SET is over-represented in GO/KEGG/Reactome terms use " +
122
135
  "search_interactions({action:'enrichment'}), which does the statistics.\n" +
123
- "An empty terms / annotations / pathways array is valid no-data — do not retry the same input.",
136
+ "An empty terms / annotations / pathways array is valid no-data — report it and continue, do not retry the same input. An empty pathways array " +
137
+ "for a pathwayId means neither database holds that identifier.",
124
138
  inputSchema,
125
139
  describeCall: "none",
126
- execute: async ({ vocabulary, query, goId, geneProductId, taxonId, organism, includeGenes, limit }) => {
140
+ execute: async ({ vocabulary, query, pathwayId, goId, geneProductId, taxonId, organism, includeGenes, limit, }) => {
127
141
  const cap = limit ?? DEFAULT_LIMIT;
128
142
  if (vocabulary !== "go") {
143
+ if (pathwayId) {
144
+ const pathway = await getPathwayById(pathwayId);
145
+ return ok({ pathways: pathway ? [pathway] : [] });
146
+ }
129
147
  const pathways = await searchPathways(query, {
130
148
  source: vocabulary === "pathways" ? "both" : vocabulary,
131
149
  organism: organism ?? "hsa",
@@ -17,18 +17,26 @@
17
17
  * gene ID or an EFO disease ID, and a gene symbol silently returns an empty
18
18
  * result rather than an error. Naming the two identifiers apart is what makes
19
19
  * that contract visible at the call site.
20
+ *
21
+ * `resolve_disease` closes that contract. It takes the plain disease name a
22
+ * user supplies and returns the disease ids of the same corpus, thus the
23
+ * caller never has to know an ontology id before it can ask a question.
20
24
  */
21
- import { type Association, type TargetInfo } from "../lib/opentargets-client.js";
25
+ import { type Association, type DiseaseCandidate, type TargetInfo } from "../lib/opentargets-client.js";
22
26
  type OpenTargetsOutput = {
23
27
  targetInfo: TargetInfo | null;
24
28
  associations: Association[];
25
29
  } | {
26
30
  associations: Association[];
31
+ } | {
32
+ totalFound: number;
33
+ candidates: DiseaseCandidate[];
27
34
  };
28
35
  export declare const openTargetsTool: import("../define-tool.js").Tool<{
29
- action: "target" | "disease";
36
+ action: "target" | "disease" | "resolve_disease";
30
37
  ensemblId?: string | undefined;
31
38
  efoId?: string | undefined;
39
+ diseaseName?: string | undefined;
32
40
  limit?: number | undefined;
33
41
  }, OpenTargetsOutput>;
34
42
  export {};