@inflexa-ai/harness 0.29.0 → 0.30.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 +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 +9 -0
- package/dist/loop/run-agent.d.ts +14 -1
- package/dist/loop/run-agent.js +6 -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 +4 -0
- package/dist/providers/reasoning.d.ts +35 -0
- package/dist/providers/reasoning.js +34 -0
- package/dist/providers/types.d.ts +26 -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
|
@@ -17,23 +17,38 @@
|
|
|
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
25
|
import { ok } from "neverthrow";
|
|
22
26
|
import { z } from "zod";
|
|
23
27
|
import { defineTool } from "../define-tool.js";
|
|
24
|
-
import { searchDiseaseAssociations, searchTargetAssociations } from "../lib/opentargets-client.js";
|
|
28
|
+
import { searchDiseaseAssociations, searchDiseases, searchTargetAssociations, } from "../lib/opentargets-client.js";
|
|
25
29
|
const DEFAULT_LIMIT = 10;
|
|
26
30
|
const inputSchema = z
|
|
27
31
|
.object({
|
|
28
32
|
action: z
|
|
29
|
-
.enum(["target", "disease"])
|
|
33
|
+
.enum(["target", "disease", "resolve_disease"])
|
|
30
34
|
.describe("'target' (needs ensemblId) — the diseases associated with the gene: targetInfo (approvedSymbol, approvedName, " +
|
|
31
35
|
"tractability across small molecule / antibody / other modalities) plus associations[], each with an overall score and " +
|
|
32
36
|
"its per-datatype breakdown — genetic association, known drug, literature, animal model, somatic mutation. " +
|
|
33
37
|
"'disease' (needs efoId) — the targets ranked for that disease; each association carries targetId/targetSymbol/targetName " +
|
|
34
|
-
"and the same score breakdown."
|
|
38
|
+
"and the same score breakdown. " +
|
|
39
|
+
"'resolve_disease' (needs diseaseName) — the disease-name resolver: a plain name ('type 2 diabetes', 'asthma') comes back as " +
|
|
40
|
+
"candidates[] { id, name, description }, best match first. Take the id of the row whose name you meant and pass it as efoId."),
|
|
35
41
|
ensemblId: z.string().optional().describe("Ensembl gene ID, e.g. ENSG00000141510 (TP53). Required for action 'target'."),
|
|
36
|
-
efoId: z
|
|
42
|
+
efoId: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Open Targets disease ID, e.g. EFO_0000311, MONDO_0005148 or HP_0001250. Required for action 'disease'. Get one from " +
|
|
46
|
+
"action 'resolve_disease', or from the diseaseId of an action 'target' association."),
|
|
47
|
+
diseaseName: z
|
|
48
|
+
.string()
|
|
49
|
+
.min(1)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Required for action 'resolve_disease'. A plain disease or trait name, e.g. 'type 2 diabetes'. No ontology id, no synonym list."),
|
|
37
52
|
limit: z
|
|
38
53
|
.number()
|
|
39
54
|
.int()
|
|
@@ -41,7 +56,8 @@ const inputSchema = z
|
|
|
41
56
|
.max(100)
|
|
42
57
|
.optional()
|
|
43
58
|
.describe(`Max associations to return (default ${DEFAULT_LIMIT}, max 100), ordered by DESCENDING association score. The default is the top ` +
|
|
44
|
-
`${DEFAULT_LIMIT}, which is where the evidence is; raise it only when you are surveying the long tail rather than ranking
|
|
59
|
+
`${DEFAULT_LIMIT}, which is where the evidence is; raise it only when you are surveying the long tail rather than ranking. ` +
|
|
60
|
+
"For action 'resolve_disease' it caps the candidate rows instead."),
|
|
45
61
|
})
|
|
46
62
|
.refine((d) => d.action !== "target" || (d.ensemblId !== undefined && d.ensemblId.trim().length > 0), {
|
|
47
63
|
message: "ensemblId is required when action is 'target' — Open Targets accepts only Ensembl gene IDs (ENSG…), " +
|
|
@@ -49,18 +65,27 @@ const inputSchema = z
|
|
|
49
65
|
path: ["ensemblId"],
|
|
50
66
|
})
|
|
51
67
|
.refine((d) => d.action !== "disease" || (d.efoId !== undefined && d.efoId.trim().length > 0), {
|
|
52
|
-
message: "efoId is required when action is 'disease' — Open Targets accepts only
|
|
68
|
+
message: "efoId is required when action is 'disease' — Open Targets accepts only a disease ID (e.g. EFO_0000311), never a free-text disease name. " +
|
|
69
|
+
"Call action='resolve_disease' with the name to get the ID, then retry.",
|
|
53
70
|
path: ["efoId"],
|
|
71
|
+
})
|
|
72
|
+
.refine((d) => d.action !== "resolve_disease" || (d.diseaseName !== undefined && d.diseaseName.trim().length > 0), {
|
|
73
|
+
message: "diseaseName is required when action is 'resolve_disease' — the plain disease or trait name to resolve.",
|
|
74
|
+
path: ["diseaseName"],
|
|
54
75
|
});
|
|
55
76
|
export const openTargetsTool = defineTool({
|
|
56
77
|
id: "opentargets",
|
|
57
|
-
description: "Query the Open Targets Platform — the
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
78
|
+
description: "Query the Open Targets Platform — the integrated target-disease evidence corpus of EMBL-EBI, GSK and their partners — the preferred FIRST call " +
|
|
79
|
+
"for target assessment, since one 'target' query yields the genetic evidence, tractability, and drug landscape together, already scored and " +
|
|
80
|
+
"ranked. See the action parameter for what each mode needs and returns.\n" +
|
|
81
|
+
"It is the integrated view; the underlying records live elsewhere. For the raw genetic evidence (individual SNPs from the GWAS Catalog, DisGeNET " +
|
|
82
|
+
"GDA scores, ClinVar variant pathogenicity) use gene_disease_evidence. For this target's mechanism-based safety liabilities use target_safety.\n" +
|
|
83
|
+
"ACCEPTED IDENTIFIERS: an Ensembl gene ID for action 'target' (ENSG00000141510); an Open Targets disease ID for action 'disease' — EFO " +
|
|
84
|
+
"(EFO_0000311), MONDO (MONDO_0005148) or HP (HP_0001250), because Open Targets keys a disease on EFO and EFO imports the other two; and a plain " +
|
|
85
|
+
"disease NAME for action 'resolve_disease' ('type 2 diabetes'), which is how you obtain that disease ID. A bare gene symbol is NOT accepted by " +
|
|
86
|
+
"action 'target' and silently returns an empty result — resolve it to its ENSG id with search_gene first.\n" +
|
|
87
|
+
"NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an id the platform does not hold. An empty candidates " +
|
|
88
|
+
"list means the name matched no disease: rephrase it, do not invent an id.",
|
|
64
89
|
inputSchema,
|
|
65
90
|
describeCall: "none",
|
|
66
91
|
execute: async (input) => {
|
|
@@ -74,6 +99,10 @@ export const openTargetsTool = defineTool({
|
|
|
74
99
|
const associations = await searchDiseaseAssociations(input.efoId, limit);
|
|
75
100
|
return ok({ associations });
|
|
76
101
|
}
|
|
102
|
+
case "resolve_disease": {
|
|
103
|
+
const { total, candidates } = await searchDiseases(input.diseaseName, limit);
|
|
104
|
+
return ok({ totalFound: total, candidates });
|
|
105
|
+
}
|
|
77
106
|
}
|
|
78
107
|
},
|
|
79
108
|
});
|
|
@@ -69,8 +69,11 @@ const inputSchema = z
|
|
|
69
69
|
});
|
|
70
70
|
export const pubchemTool = defineTool({
|
|
71
71
|
id: "pubchem",
|
|
72
|
-
description: "Query PubChem
|
|
73
|
-
"
|
|
72
|
+
description: "Query PubChem — the open chemical database of the NCBI, with over 110M compounds and their BioAssay screening records. Pick the lookup with " +
|
|
73
|
+
"`action`: 'compound' (resolve an identifier), 'crossrefs' (external registry ids for a CID), 'assays' (bioassay screening summaries for a CID). " +
|
|
74
|
+
"See `action` for what each returns. " +
|
|
75
|
+
"ACCEPTED IDENTIFIERS: for 'compound', the form named by `searchBy` — a chemical name ('aspirin'), a SMILES string, an InChI, an InChIKey " +
|
|
76
|
+
"('BSYNRYMUTXBXSQ-UHFFFAOYSA-N') or a numeric CID ('2244'). 'crossrefs' and 'assays' take the numeric CID only. " +
|
|
74
77
|
"Reach for PubChem when ChEMBL misses the compound — it covers metabolites, vendor chemicals, food additives and environmental compounds that ChEMBL does not. " +
|
|
75
78
|
"PubChem carries no curated mechanism or potency data, so resolve broadly HERE and then bridge OUT: action 'compound' to get the cid, action 'crossrefs' to take its ChEMBL ID, " +
|
|
76
79
|
"then chembl action='mechanism' / 'bioactivity' there for curated activity data. " +
|
package/dist/tools/bio/pubmed.js
CHANGED
|
@@ -118,12 +118,18 @@ const inputSchema = z
|
|
|
118
118
|
export function createPubMedTool(deps) {
|
|
119
119
|
return defineTool({
|
|
120
120
|
id: "pubmed",
|
|
121
|
-
description: "Search and read the biomedical literature
|
|
122
|
-
"
|
|
121
|
+
description: "Search and read the biomedical literature in PubMed and PubMed Central — the MEDLINE citation index and the open-access full-text archive of " +
|
|
122
|
+
"the U.S. National Library of Medicine, through the NCBI E-utilities. The three actions form a chain " +
|
|
123
|
+
"— search, then details on the relevant hits, then fulltext; see the action parameter for what each needs and returns.\n" +
|
|
124
|
+
"ACCEPTED IDENTIFIERS: an Entrez free-text query with optional field tags for 'search' ('PCSK9[Title] AND 2020:2024[dp]'); PMIDs for " +
|
|
125
|
+
"'details' ('34567890'); and a PMID or a PMC ID for 'fulltext' ('PMC7096066'). For literature outside MEDLINE — a method, a model, a " +
|
|
126
|
+
"preprint — use search_semantic_scholar or search_arxiv instead.\n" +
|
|
127
|
+
"Read " +
|
|
123
128
|
"fulltext sparingly — only where the 'details' abstract is not enough, and only for articles that have a pmcId (open-access). " +
|
|
124
129
|
"A full article is larger than everything else this tool returns combined, so it comes back trimmed to a character budget: take the " +
|
|
125
130
|
"`outline` from the first call and re-request the specific `sections` you need rather than raising maxChars. " +
|
|
126
|
-
"available: false
|
|
131
|
+
"An empty result set and available: false are expected outcomes, not errors — an article with no PMC record simply has no open full text. " +
|
|
132
|
+
"Report it and continue, do not retry it.",
|
|
127
133
|
inputSchema,
|
|
128
134
|
// One tool, three jobs. The action alone would still render three
|
|
129
135
|
// different searches identically, so each action names its own subject:
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchClinicalTrials — search ClinicalTrials.gov for clinical trials
|
|
2
|
+
* searchClinicalTrials — search ClinicalTrials.gov for clinical trials, and
|
|
3
|
+
* read one study back by the NCT id that a search returns.
|
|
4
|
+
*
|
|
5
|
+
* The input is a flat object with an `action` discriminator — not a
|
|
6
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
7
|
+
* a top-level `"type":"object"`). `action` defaults to 'search', thus the
|
|
8
|
+
* common call stays a query and nothing more.
|
|
3
9
|
*/
|
|
10
|
+
import { type ClinicalTrial, type TrialDetails } from "../lib/clinical-trials-client.js";
|
|
11
|
+
/** A listed trial keeps the full record, or the preview shape that trims it. */
|
|
12
|
+
type TrialListRow = ClinicalTrial | (Omit<ClinicalTrial, "detailedDescription"> & {
|
|
13
|
+
summaryTruncated: boolean;
|
|
14
|
+
});
|
|
15
|
+
/** One result shape per action — the key names the records it carries. */
|
|
16
|
+
type ClinicalTrialsOutput = {
|
|
17
|
+
trial: TrialDetails | null;
|
|
18
|
+
} | {
|
|
19
|
+
totalFound: number;
|
|
20
|
+
trials: TrialListRow[];
|
|
21
|
+
};
|
|
4
22
|
export declare const searchClinicalTrialsTool: import("../define-tool.js").Tool<{
|
|
5
|
-
|
|
23
|
+
action: "search" | "details";
|
|
6
24
|
limit: number;
|
|
7
25
|
includeSummaries: boolean;
|
|
26
|
+
query?: string | undefined;
|
|
27
|
+
nctId?: string | undefined;
|
|
8
28
|
phase?: "EARLY_PHASE1" | "PHASE1" | "PHASE2" | "PHASE3" | "PHASE4" | undefined;
|
|
9
|
-
status?: "RECRUITING" | "ACTIVE_NOT_RECRUITING" | "COMPLETED" | "NOT_YET_RECRUITING" | undefined;
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
trials: import("../lib/clinical-trials-client.js").ClinicalTrial[] | {
|
|
13
|
-
briefSummary: string | null;
|
|
14
|
-
summaryTruncated: boolean;
|
|
15
|
-
nctId: string;
|
|
16
|
-
title: string;
|
|
17
|
-
officialTitle: string | null;
|
|
18
|
-
status: string;
|
|
19
|
-
phase: string | null;
|
|
20
|
-
studyType: string | null;
|
|
21
|
-
primaryPurpose: string | null;
|
|
22
|
-
conditions: string[];
|
|
23
|
-
interventions: string[];
|
|
24
|
-
interventionDetails: import("../lib/clinical-trials-client.js").ClinicalTrialIntervention[];
|
|
25
|
-
enrollmentCount: number | null;
|
|
26
|
-
startDate: string | null;
|
|
27
|
-
primaryCompletionDate: string | null;
|
|
28
|
-
whyStopped: string | null;
|
|
29
|
-
sponsor: string | null;
|
|
30
|
-
}[];
|
|
31
|
-
}>;
|
|
29
|
+
status?: "RECRUITING" | "ACTIVE_NOT_RECRUITING" | "COMPLETED" | "NOT_YET_RECRUITING" | "TERMINATED" | "WITHDRAWN" | "SUSPENDED" | undefined;
|
|
30
|
+
}, ClinicalTrialsOutput>;
|
|
31
|
+
export {};
|
|
@@ -1,46 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchClinicalTrials — search ClinicalTrials.gov for clinical trials
|
|
2
|
+
* searchClinicalTrials — search ClinicalTrials.gov for clinical trials, and
|
|
3
|
+
* read one study back by the NCT id that a search returns.
|
|
4
|
+
*
|
|
5
|
+
* The input is a flat object with an `action` discriminator — not a
|
|
6
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
7
|
+
* a top-level `"type":"object"`). `action` defaults to 'search', thus the
|
|
8
|
+
* common call stays a query and nothing more.
|
|
3
9
|
*/
|
|
4
10
|
import { ok } from "neverthrow";
|
|
5
11
|
import { z } from "zod";
|
|
6
12
|
import { defineTool } from "../define-tool.js";
|
|
7
|
-
import { searchTrials } from "../lib/clinical-trials-client.js";
|
|
13
|
+
import { getTrialDetails, searchTrials } from "../lib/clinical-trials-client.js";
|
|
8
14
|
/** briefSummary preview length when `includeSummaries` is off. */
|
|
9
15
|
const SUMMARY_PREVIEW_CHARS = 300;
|
|
10
16
|
export const searchClinicalTrialsTool = defineTool({
|
|
11
17
|
id: "search_clinical_trials",
|
|
12
|
-
description: "Search ClinicalTrials.gov
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
description: "Search ClinicalTrials.gov — the trial registry of the U.S. National Library of Medicine, which holds interventional and observational studies " +
|
|
19
|
+
"worldwide — to map the clinical development landscape for a target, drug, or indication: what is being tried, at what phase, by whom, and what " +
|
|
20
|
+
"stopped.\n" +
|
|
21
|
+
"action 'search' (the default) returns totalFound plus per trial: NCT ID, title, status, phase, conditions, interventions, enrollment, start and " +
|
|
22
|
+
"completion dates, sponsor, whyStopped (the termination reason, when there is one) and a briefSummary preview — set includeSummaries for the full " +
|
|
23
|
+
"protocol prose.\n" +
|
|
24
|
+
"action 'details' takes ONE nctId and returns that study in full: the same trial record plus outcomes[] (each primary/secondary/other outcome " +
|
|
25
|
+
"measure with its numeric effect where the sponsor posted results) and the reported adverse events with their per-arm counts. This is how you " +
|
|
26
|
+
"read a result, as opposed to finding a trial.\n" +
|
|
27
|
+
"ACCEPTED IDENTIFIERS: `query` is free text matched across the whole study record, so a gene symbol ('KRAS'), a drug or brand name ('imatinib', " +
|
|
28
|
+
"'Gleevec'), a condition ('melanoma') or an NCT ID ('NCT00000102') all work. `nctId` takes one NCT ID and nothing else.\n" +
|
|
29
|
+
"`phase` and `status` are optional server-side filters — omit them to see the full landscape; totalFound reports the true match count even when it " +
|
|
30
|
+
"exceeds the page returned.\n" +
|
|
31
|
+
"ABSENCE IS NORMAL. An empty trials array is a valid 'nothing matched' (often an over-narrow filter) and a null trial from 'details' means the " +
|
|
32
|
+
"registry holds no such NCT ID — report it and move on, do not retry the identical call. A trial with no posted results carries empty outcomes " +
|
|
33
|
+
"and adverse events, which means unreported, never zero.",
|
|
34
|
+
inputSchema: z
|
|
35
|
+
.object({
|
|
36
|
+
action: z
|
|
37
|
+
.enum(["search", "details"])
|
|
38
|
+
.default("search")
|
|
39
|
+
.describe("'search' (default) — find trials with `query`. 'details' — read one trial, with its outcomes and adverse events, by `nctId`."),
|
|
40
|
+
query: z.string().optional().describe("Required for 'search'. Free-text search term: condition name, drug/brand name, gene symbol, or an NCT ID."),
|
|
41
|
+
nctId: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Required for 'details'. One ClinicalTrials.gov registry ID, e.g. 'NCT00000102' — exactly as the nctId field of a search result."),
|
|
20
45
|
phase: z
|
|
21
46
|
.enum(["EARLY_PHASE1", "PHASE1", "PHASE2", "PHASE3", "PHASE4"])
|
|
22
47
|
.optional()
|
|
23
|
-
.describe("Optional exact phase filter. Omit for all phases (observational and expanded-access studies carry no phase and are
|
|
48
|
+
.describe("'search' only. Optional exact phase filter. Omit for all phases (observational and expanded-access studies carry no phase and are " +
|
|
49
|
+
"excluded when this is set)."),
|
|
24
50
|
status: z
|
|
25
|
-
.enum(["RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "NOT_YET_RECRUITING"])
|
|
51
|
+
.enum(["RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "NOT_YET_RECRUITING", "TERMINATED", "WITHDRAWN", "SUSPENDED"])
|
|
26
52
|
.optional()
|
|
27
|
-
.describe("Optional recruitment-status filter. Omit for all statuses
|
|
53
|
+
.describe("'search' only. Optional recruitment-status filter. Omit for all statuses. TERMINATED (stopped early), WITHDRAWN (stopped before " +
|
|
54
|
+
"enrollment) and SUSPENDED are the attrition record — filter on one of them to ask what failed, and read whyStopped for the reason."),
|
|
28
55
|
limit: z
|
|
29
56
|
.number()
|
|
30
57
|
.int()
|
|
31
58
|
.min(1)
|
|
32
59
|
.max(50)
|
|
33
60
|
.default(10)
|
|
34
|
-
.describe("Max trials to return (default 10, max 50). totalFound reports the true match count regardless."),
|
|
61
|
+
.describe("'search' only. Max trials to return (default 10, max 50). totalFound reports the true match count regardless."),
|
|
35
62
|
includeSummaries: z
|
|
36
63
|
.boolean()
|
|
37
64
|
.default(false)
|
|
38
|
-
.describe("Default FALSE — briefSummary comes back as a 300-character preview and detailedDescription is omitted, which is enough
|
|
39
|
-
"landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand
|
|
40
|
-
"per trial."),
|
|
65
|
+
.describe("'search' only. Default FALSE — briefSummary comes back as a 300-character preview and detailedDescription is omitted, which is enough " +
|
|
66
|
+
"to map a landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand " +
|
|
67
|
+
"characters per trial."),
|
|
68
|
+
})
|
|
69
|
+
.refine((d) => d.action === "details" || (d.query !== undefined && d.query.trim().length > 0), {
|
|
70
|
+
message: "query is required when action is 'search' — the free text to match across the study records.",
|
|
71
|
+
path: ["query"],
|
|
72
|
+
})
|
|
73
|
+
.refine((d) => d.action !== "details" || (d.nctId !== undefined && d.nctId.trim().length > 0), {
|
|
74
|
+
message: "nctId is required when action is 'details' — one NCT ID, e.g. 'NCT00000102'. Find one with action 'search'.",
|
|
75
|
+
path: ["nctId"],
|
|
41
76
|
}),
|
|
42
77
|
describeCall: "none",
|
|
43
|
-
execute: async ({ query, phase, status, limit = 10, includeSummaries = false }) => {
|
|
78
|
+
execute: async ({ action = "search", query, nctId, phase, status, limit = 10, includeSummaries = false, }) => {
|
|
79
|
+
if (action === "details") {
|
|
80
|
+
return ok({ trial: await getTrialDetails(nctId) });
|
|
81
|
+
}
|
|
44
82
|
const result = await searchTrials(query, { phase, status, limit });
|
|
45
83
|
const trials = includeSummaries
|
|
46
84
|
? result.trials
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchFaers —
|
|
2
|
+
* searchFaers — one tool over the two openFDA drug corpora: the FAERS
|
|
3
|
+
* spontaneous adverse-event reports and the Structured Product Labels.
|
|
4
|
+
*
|
|
5
|
+
* The three actions answer one question between them — what is known about
|
|
6
|
+
* the post-market safety of this molecule. `adverse_events` ranks the reported
|
|
7
|
+
* reactions, `seriousness` counts the outcome flags behind them, and `label`
|
|
8
|
+
* gives the regulatory answer: the boxed warning, the Section 5 warnings, and
|
|
9
|
+
* whether a REMS program constrains the drug.
|
|
10
|
+
*
|
|
11
|
+
* The input is a flat object with an `action` discriminator — not a
|
|
12
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
13
|
+
* a top-level `"type":"object"`).
|
|
3
14
|
*/
|
|
15
|
+
import { type AdverseEventCount, type DrugLabelAction, type SeriousnessProfile } from "../lib/openfda-client.js";
|
|
16
|
+
/** One result shape per action — the key names the records it carries. */
|
|
17
|
+
type FaersOutput = {
|
|
18
|
+
totalReports: number | undefined;
|
|
19
|
+
adverseEvents: AdverseEventCount[];
|
|
20
|
+
} | {
|
|
21
|
+
seriousness: SeriousnessProfile | null;
|
|
22
|
+
} | {
|
|
23
|
+
labels: DrugLabelAction[];
|
|
24
|
+
};
|
|
4
25
|
export declare const searchFaersTool: import("../define-tool.js").Tool<{
|
|
26
|
+
action: "label" | "adverse_events" | "seriousness";
|
|
5
27
|
drugName: string;
|
|
6
|
-
limit: number;
|
|
7
28
|
serious: boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}>;
|
|
29
|
+
limit?: number | undefined;
|
|
30
|
+
}, FaersOutput>;
|
|
31
|
+
export {};
|
|
@@ -1,34 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchFaers —
|
|
2
|
+
* searchFaers — one tool over the two openFDA drug corpora: the FAERS
|
|
3
|
+
* spontaneous adverse-event reports and the Structured Product Labels.
|
|
4
|
+
*
|
|
5
|
+
* The three actions answer one question between them — what is known about
|
|
6
|
+
* the post-market safety of this molecule. `adverse_events` ranks the reported
|
|
7
|
+
* reactions, `seriousness` counts the outcome flags behind them, and `label`
|
|
8
|
+
* gives the regulatory answer: the boxed warning, the Section 5 warnings, and
|
|
9
|
+
* whether a REMS program constrains the drug.
|
|
10
|
+
*
|
|
11
|
+
* The input is a flat object with an `action` discriminator — not a
|
|
12
|
+
* `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
|
|
13
|
+
* a top-level `"type":"object"`).
|
|
3
14
|
*/
|
|
4
15
|
import { ok } from "neverthrow";
|
|
5
16
|
import { z } from "zod";
|
|
6
17
|
import { defineTool } from "../define-tool.js";
|
|
7
|
-
import { getFaersByDrug } from "../lib/openfda-client.js";
|
|
18
|
+
import { getDrugLabelActions, getFaersByDrug, getFaersSeriousness, } from "../lib/openfda-client.js";
|
|
19
|
+
/** `label` returns whole prose sections, so its ceiling is far below the others. */
|
|
20
|
+
const LABEL_MAX_LIMIT = 10;
|
|
21
|
+
const LABEL_DEFAULT_LIMIT = 3;
|
|
8
22
|
export const searchFaersTool = defineTool({
|
|
9
23
|
id: "search_faers",
|
|
10
|
-
description: "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
description: "openFDA — the post-market drug record of the U.S. Food and Drug Administration, across two corpora: FAERS (the FDA Adverse Event Reporting " +
|
|
25
|
+
"System, spontaneous reports) and the Structured Product Labels (the approved prescribing information, served through DailyMed). Three lookups; " +
|
|
26
|
+
"pick with `action`.\n" +
|
|
27
|
+
"'adverse_events' (default) — totalReports and adverseEvents[] { reaction (MedDRA preferred term), count }, most-reported first. These are " +
|
|
28
|
+
"spontaneous report COUNTS, not incidence rates: there is no denominator and reporting is heavily biased, so use them to rank signals, never to " +
|
|
29
|
+
"state a rate.\n" +
|
|
30
|
+
"'seriousness' — the outcome breakdown behind those reports: totalReports plus fatalCount, hospitalizationCount, lifeThreateningCount, " +
|
|
31
|
+
"disablingCount, congenitalAnomalyCount and otherSeriousCount. This is how you tell a common nuisance reaction from a lethal one; the same " +
|
|
32
|
+
"no-denominator caveat holds.\n" +
|
|
33
|
+
"'label' — the regulatory position on the drug: boxedWarning text, the Section 5 warningsAndCautions excerpt, a REMS indicator, the brand and " +
|
|
34
|
+
"generic names, the application number, the label effective date and a citable DailyMed URL. This is the only action that carries an official " +
|
|
35
|
+
"warning rather than a report count.\n" +
|
|
36
|
+
"ACCEPTED IDENTIFIERS: a drug name in `drugName`. 'adverse_events' and 'seriousness' match the openFDA GENERIC name only — 'imatinib' works, the " +
|
|
37
|
+
"brand name 'Gleevec' does not. 'label' matches the generic OR the brand name, so a brand name goes in there and the genericName field of each " +
|
|
38
|
+
"row gives you the generic to use for the other two actions.\n" +
|
|
39
|
+
"For mechanism-based liabilities of a TARGET (rather than a marketed drug), use target_safety; for a chemical's toxicology use comptox.\n" +
|
|
40
|
+
"ABSENCE IS NORMAL: an empty adverseEvents array, a null seriousness, or an empty labels array means the drug is absent from that corpus under " +
|
|
41
|
+
"that name — say so and continue, do not retry the same name.",
|
|
42
|
+
inputSchema: z
|
|
43
|
+
.object({
|
|
44
|
+
action: z
|
|
45
|
+
.enum(["adverse_events", "seriousness", "label"])
|
|
46
|
+
.default("adverse_events")
|
|
47
|
+
.describe("'adverse_events' (default) — the ranked reaction terms. 'seriousness' — the outcome-flag breakdown for the same reports. 'label' — " +
|
|
48
|
+
"the FDA label: boxed warning, Section 5 warnings and REMS."),
|
|
49
|
+
drugName: z
|
|
50
|
+
.string()
|
|
51
|
+
.min(1)
|
|
52
|
+
.describe("The drug name. Generic (INN) for 'adverse_events' and 'seriousness', e.g. 'imatinib' or 'pembrolizumab' — a brand name matches " +
|
|
53
|
+
"nothing there. 'label' also accepts a brand name, e.g. 'Gleevec'."),
|
|
17
54
|
limit: z
|
|
18
55
|
.number()
|
|
19
56
|
.int()
|
|
20
57
|
.min(1)
|
|
21
58
|
.max(100)
|
|
22
|
-
.
|
|
23
|
-
.describe(
|
|
24
|
-
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(`'adverse_events': max distinct adverse-reaction terms to return, most-reported first (default 15, max 100); totalReports covers the ` +
|
|
61
|
+
`whole drug regardless. 'label': max label versions, newest first (default ${LABEL_DEFAULT_LIMIT}, max ${LABEL_MAX_LIMIT}) — a ` +
|
|
62
|
+
`boxed warning runs to thousands of characters, so raise it only to trace how a warning changed. Ignored by 'seriousness'.`),
|
|
63
|
+
serious: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.default(false)
|
|
66
|
+
.describe("'adverse_events' only. When true, count only reports flagged serious (death, hospitalization, life-threatening, disabling). For the " +
|
|
67
|
+
"breakdown of WHICH serious outcome, use action 'seriousness' instead."),
|
|
68
|
+
})
|
|
69
|
+
.refine((d) => d.action !== "label" || d.limit === undefined || d.limit <= LABEL_MAX_LIMIT, {
|
|
70
|
+
message: `limit is capped at ${LABEL_MAX_LIMIT} for action 'label' — each label version carries whole prose sections`,
|
|
71
|
+
path: ["limit"],
|
|
25
72
|
}),
|
|
26
73
|
describeCall: "none",
|
|
27
|
-
execute: async ({ drugName, limit
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
74
|
+
execute: async ({ action = "adverse_events", drugName, limit, serious = false }) => {
|
|
75
|
+
switch (action) {
|
|
76
|
+
case "seriousness":
|
|
77
|
+
return ok({ seriousness: await getFaersSeriousness(drugName) });
|
|
78
|
+
case "label":
|
|
79
|
+
return ok({ labels: await getDrugLabelActions(drugName, { limit: limit ?? LABEL_DEFAULT_LIMIT }) });
|
|
80
|
+
case "adverse_events": {
|
|
81
|
+
const result = await getFaersByDrug(drugName, { limit: limit ?? 15, serious });
|
|
82
|
+
return ok({ totalReports: result.totalReports, adverseEvents: result.adverseEvents });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
33
85
|
},
|
|
34
86
|
});
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchGene —
|
|
2
|
+
* searchGene — resolve gene identifiers against the Ensembl REST API.
|
|
3
|
+
*
|
|
4
|
+
* Ensembl itself keys only on a symbol (`/lookup/symbol`), which makes an
|
|
5
|
+
* ENSG, a UniProt accession, a ChEMBL target id or an HGNC id unusable as
|
|
6
|
+
* input. That is the identifier a user pastes, so this tool takes it: an
|
|
7
|
+
* input that is not a plain symbol goes through `resolveTarget`, which anchors
|
|
8
|
+
* it on HGNC and UniProt, and the approved symbol it returns is what reaches
|
|
9
|
+
* Ensembl. `resolvedFrom` reports each such translation, thus the caller can
|
|
10
|
+
* see which input became which symbol.
|
|
3
11
|
*/
|
|
12
|
+
import { type GeneInfo } from "../lib/ensembl-client.js";
|
|
13
|
+
/** One input that was not a symbol, beside the approved symbol it became. */
|
|
14
|
+
interface ResolvedIdentifier {
|
|
15
|
+
input: string;
|
|
16
|
+
symbol: string;
|
|
17
|
+
}
|
|
4
18
|
export declare const searchGeneTool: import("../define-tool.js").Tool<{
|
|
5
|
-
|
|
19
|
+
identifiers: string[];
|
|
6
20
|
species: string;
|
|
7
21
|
}, {
|
|
8
|
-
genes:
|
|
22
|
+
genes: GeneInfo[];
|
|
23
|
+
resolvedFrom: ResolvedIdentifier[];
|
|
9
24
|
notFound: string[];
|
|
10
25
|
}>;
|
|
26
|
+
export {};
|
|
@@ -1,30 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* searchGene —
|
|
2
|
+
* searchGene — resolve gene identifiers against the Ensembl REST API.
|
|
3
|
+
*
|
|
4
|
+
* Ensembl itself keys only on a symbol (`/lookup/symbol`), which makes an
|
|
5
|
+
* ENSG, a UniProt accession, a ChEMBL target id or an HGNC id unusable as
|
|
6
|
+
* input. That is the identifier a user pastes, so this tool takes it: an
|
|
7
|
+
* input that is not a plain symbol goes through `resolveTarget`, which anchors
|
|
8
|
+
* it on HGNC and UniProt, and the approved symbol it returns is what reaches
|
|
9
|
+
* Ensembl. `resolvedFrom` reports each such translation, thus the caller can
|
|
10
|
+
* see which input became which symbol.
|
|
3
11
|
*/
|
|
4
12
|
import { ok } from "neverthrow";
|
|
5
13
|
import { z } from "zod";
|
|
6
14
|
import { defineTool } from "../define-tool.js";
|
|
7
15
|
import { lookupGenes } from "../lib/ensembl-client.js";
|
|
16
|
+
import { resolveTarget } from "../lib/identifier-resolver.js";
|
|
8
17
|
/**
|
|
9
|
-
* How many
|
|
10
|
-
* Eight
|
|
11
|
-
* listed form is never the thing that trips it.
|
|
18
|
+
* How many identifiers a detail names before it switches to a count plus a
|
|
19
|
+
* sample. Eight identifiers of typical length stay well inside the emit-site
|
|
20
|
+
* cap, so the listed form is never the thing that trips it.
|
|
12
21
|
*/
|
|
13
|
-
const
|
|
22
|
+
const IDENTIFIERS_LISTED_IN_FULL = 8;
|
|
23
|
+
/**
|
|
24
|
+
* The identifier spaces that Ensembl cannot key on. Anything that matches none
|
|
25
|
+
* of them is treated as a symbol and goes straight to Ensembl.
|
|
26
|
+
*/
|
|
27
|
+
const NON_SYMBOL_PATTERNS = [/^ENSG\d{11}$/, /^HGNC:\d+$/, /^CHEMBL\d+$/, /^[A-Z][0-9][A-Z0-9]{3}[0-9](-\d+)?$/];
|
|
28
|
+
function isSymbol(identifier) {
|
|
29
|
+
return !NON_SYMBOL_PATTERNS.some((pattern) => pattern.test(identifier));
|
|
30
|
+
}
|
|
14
31
|
export const searchGeneTool = defineTool({
|
|
15
32
|
id: "search_gene",
|
|
16
|
-
description: "Resolve gene
|
|
17
|
-
"biotype, assembly, description; plus notFound[]. Gene-level only — no transcripts or exons. Batch up to 200
|
|
18
|
-
"
|
|
33
|
+
description: "Resolve gene identifiers against Ensembl — the identifier step other tools depend on. Per gene: Ensembl gene ID (ENSG…), coordinates, strand, " +
|
|
34
|
+
"biotype, assembly, description; plus resolvedFrom[] and notFound[]. Gene-level only — no transcripts or exons. Batch up to 200 identifiers per " +
|
|
35
|
+
"call.\n" +
|
|
36
|
+
"ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('BRCA1'), an Ensembl gene ID ('ENSG00000012048'), an HGNC ID ('HGNC:1100'), " +
|
|
37
|
+
"a UniProt accession ('P38398') and a ChEMBL target ID ('CHEMBL5619'). An input that is not a symbol is anchored on the HGNC and UniProt " +
|
|
38
|
+
"registries first, and the approved symbol it yields is reported in resolvedFrom[] — that anchoring is human, so a non-symbol input with a " +
|
|
39
|
+
"non-human `species` looks the ortholog up by the human approved symbol.\n" +
|
|
40
|
+
"notFound is valid no-data (a deprecated, aliased or mistyped identifier, or one this species has no gene for) — report it and continue, do not " +
|
|
41
|
+
"retry unchanged.",
|
|
19
42
|
inputSchema: z.object({
|
|
20
|
-
|
|
43
|
+
identifiers: z
|
|
44
|
+
.array(z.string())
|
|
45
|
+
.min(1)
|
|
46
|
+
.max(200)
|
|
47
|
+
.describe("Gene identifiers to look up, mixed freely (e.g. ['BRCA1', 'ENSG00000141510', 'P38398', 'HGNC:1100', 'CHEMBL5619'])"),
|
|
21
48
|
species: z.string().default("homo_sapiens").describe("Species name (e.g. 'homo_sapiens', 'mus_musculus')"),
|
|
22
49
|
}),
|
|
23
|
-
// The
|
|
50
|
+
// The identifiers are the call. A large batch is summarized here rather than
|
|
24
51
|
// left to the emit-site cap: that cap cuts at a character, so a 200-symbol
|
|
25
52
|
// join arrives ending in `TP5` — a fragment that reads as a real symbol and
|
|
26
53
|
// says nothing about how many were dropped. The count leads, so a batch is
|
|
27
54
|
// legible as a batch even when the sample after it is itself trimmed.
|
|
28
|
-
describeCall: ({
|
|
29
|
-
|
|
55
|
+
describeCall: ({ identifiers }) => identifiers.length <= IDENTIFIERS_LISTED_IN_FULL
|
|
56
|
+
? identifiers.join(", ")
|
|
57
|
+
: `${identifiers.length} genes: ${identifiers.slice(0, IDENTIFIERS_LISTED_IN_FULL).join(", ")}, …`,
|
|
58
|
+
execute: async ({ identifiers, species }) => {
|
|
59
|
+
const symbols = [];
|
|
60
|
+
const resolvedFrom = [];
|
|
61
|
+
const notFound = [];
|
|
62
|
+
for (const identifier of identifiers) {
|
|
63
|
+
const trimmed = identifier.trim();
|
|
64
|
+
if (isSymbol(trimmed)) {
|
|
65
|
+
symbols.push(trimmed);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// `resolveTarget` throws when it can anchor the input on no human
|
|
69
|
+
// gene. That is absence, so it becomes a notFound row.
|
|
70
|
+
try {
|
|
71
|
+
const resolved = await resolveTarget(trimmed);
|
|
72
|
+
resolvedFrom.push({ input: trimmed, symbol: resolved.geneSymbol });
|
|
73
|
+
symbols.push(resolved.geneSymbol);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
notFound.push(trimmed);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const genes = [];
|
|
80
|
+
if (symbols.length > 0) {
|
|
81
|
+
const lookup = await lookupGenes(symbols, { species });
|
|
82
|
+
genes.push(...lookup.genes);
|
|
83
|
+
notFound.push(...lookup.notFound);
|
|
84
|
+
}
|
|
85
|
+
return ok({ genes, resolvedFrom, notFound });
|
|
86
|
+
},
|
|
30
87
|
});
|
|
@@ -38,7 +38,10 @@ const GeoEsummaryResponseSchema = z.object({
|
|
|
38
38
|
});
|
|
39
39
|
export const searchGeoDatasetsTool = defineTool({
|
|
40
40
|
id: "search_geo_datasets",
|
|
41
|
-
description: "Search NCBI
|
|
41
|
+
description: "Search the NCBI Gene Expression Omnibus (GEO) — the public functional-genomics repository of the U.S. National Library of Medicine — for " +
|
|
42
|
+
"gene-expression datasets by disease, tissue, or experimental condition. Use it to find external validation cohorts or to cite published data. " +
|
|
43
|
+
"ACCEPTED IDENTIFIERS: an Entrez free-text query with optional field tags ('breast cancer RNA-seq'), a GEO accession ('GSE12345', 'GDS1234'), and " +
|
|
44
|
+
"an organism scientific name ('Homo sapiens') in `organism`. " +
|
|
42
45
|
"Returns totalFound plus datasets[]: { accession (GSE…/GDS…), title, summary (truncated to 300 chars), platform, sampleCount, organism, pubmedIds }. " +
|
|
43
46
|
"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
47
|
"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. " +
|