@inflexa-ai/harness 0.28.1 → 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/app/data-profile-orientation.d.ts +52 -27
- package/dist/app/data-profile-orientation.js +331 -92
- package/dist/app/data-profile-view.d.ts +70 -0
- package/dist/app/data-profile-view.js +123 -0
- package/dist/app/synthesize-run.js +1 -1
- package/dist/config/resource-limits.d.ts +1 -1
- package/dist/config/resource-limits.js +1 -1
- package/dist/contracts/data-profile.d.ts +276 -25
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/profile-vocabulary.d.ts +103 -0
- package/dist/contracts/profile-vocabulary.js +449 -0
- 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 +91 -91
- package/dist/data/safety-panel-schema.d.ts +4 -4
- package/dist/execution/input-signature.d.ts +16 -3
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/post-step-pipeline.js +1 -1
- package/dist/execution/staged-input.d.ts +4 -4
- package/dist/index.d.ts +7 -2
- package/dist/index.js +14 -0
- package/dist/input-scan/clustering.d.ts +63 -0
- package/dist/input-scan/clustering.js +208 -0
- package/dist/input-scan/companions.d.ts +30 -0
- package/dist/input-scan/companions.js +138 -0
- package/dist/input-scan/decoder/container_readout.py +171 -0
- package/dist/input-scan/detect-sets.d.ts +24 -0
- package/dist/input-scan/detect-sets.js +279 -0
- package/dist/input-scan/enrich.d.ts +62 -19
- package/dist/input-scan/enrich.js +132 -187
- package/dist/input-scan/formats.d.ts +13 -5
- package/dist/input-scan/formats.js +13 -5
- package/dist/input-scan/index.d.ts +10 -4
- package/dist/input-scan/index.js +7 -4
- package/dist/input-scan/markers.d.ts +21 -0
- package/dist/input-scan/markers.js +89 -0
- package/dist/input-scan/menu.d.ts +127 -0
- package/dist/input-scan/menu.js +0 -0
- package/dist/input-scan/quarantine.d.ts +17 -0
- package/dist/input-scan/quarantine.js +69 -0
- package/dist/input-scan/readout-budget.d.ts +32 -0
- package/dist/input-scan/readout-budget.js +61 -0
- package/dist/input-scan/readout.d.ts +33 -0
- package/dist/input-scan/readout.js +336 -0
- package/dist/input-scan/scan.d.ts +5 -4
- package/dist/input-scan/scan.js +5 -5
- package/dist/input-scan/set-types.d.ts +209 -0
- package/dist/input-scan/set-types.js +15 -0
- package/dist/input-scan/templates.d.ts +93 -0
- package/dist/input-scan/templates.js +369 -0
- package/dist/input-scan/tokens.d.ts +94 -0
- package/dist/input-scan/tokens.js +267 -0
- package/dist/input-scan/tuning.d.ts +55 -0
- package/dist/input-scan/tuning.js +55 -0
- package/dist/input-scan/types.d.ts +2 -2
- 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 +45 -71
- package/dist/prompts/planner.js +57 -10
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +3 -2
- 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/data-profiler.d.ts +10 -1
- package/dist/prompts/sandbox/data-profiler.js +197 -53
- 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/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +10 -7
- 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 +947 -84
- package/dist/schemas/data-profile-schemas.js +300 -115
- package/dist/schemas/run-synthesis.d.ts +4 -4
- package/dist/state/data-profile.d.ts +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/vector-store.d.ts +13 -0
- package/dist/state/vector-store.js +8 -0
- package/dist/tasks/data-profile-absorb.d.ts +78 -0
- package/dist/tasks/data-profile-absorb.js +285 -0
- package/dist/tasks/data-profile-index.d.ts +34 -25
- package/dist/tasks/data-profile-index.js +160 -59
- package/dist/tasks/data-profile-resolve.d.ts +72 -0
- package/dist/tasks/data-profile-resolve.js +639 -0
- package/dist/tasks/data-profile.d.ts +16 -5
- package/dist/tasks/data-profile.js +270 -131
- 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/inspect-data-profile.d.ts +48 -22
- package/dist/tools/research/inspect-data-profile.js +73 -54
- 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/tools/workspace/scan-inputs.d.ts +4 -4
- package/dist/tools/workspace/scan-inputs.js +13 -14
- package/dist/tools/workspace/workspace-search.d.ts +11 -2
- package/dist/tools/workspace/workspace-search.js +44 -16
- 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 +38 -38
- 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 +5 -3
- package/dist/input-scan/coverage.d.ts +0 -22
- package/dist/input-scan/coverage.js +0 -85
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data-profile DBOS workflow — scans the already-staged input tree, runs the
|
|
3
3
|
* data-profiler sandbox agent over the resulting manifest, registers each staged
|
|
4
|
-
* file in cortex_artifacts, and indexes the profile's
|
|
5
|
-
* pgvector store. Inputs are staged under `data/inputs/` by the embedder before the
|
|
4
|
+
* file in cortex_artifacts, and indexes the profile's groups, dimensions, and annotated
|
|
5
|
+
* members into the pgvector store. Inputs are staged under `data/inputs/` by the embedder before the
|
|
6
6
|
* run (see the data-profile-init spec); the body assumes a populated tree and never
|
|
7
7
|
* downloads.
|
|
8
8
|
*
|
|
@@ -34,12 +34,16 @@ import { estimateDataProfileResources } from "../sandbox/estimate-data-profile-r
|
|
|
34
34
|
import { generateExecutionId } from "../sandbox/execution-id.js";
|
|
35
35
|
import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
36
36
|
import { createVectorStore } from "../state/vector-store.js";
|
|
37
|
-
import {
|
|
37
|
+
import { ProfileSubmissionSchema } from "../schemas/data-profile-schemas.js";
|
|
38
38
|
import { computeInputSignature } from "../execution/input-signature.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
39
|
+
import { readHeaders } from "../input-scan/enrich.js";
|
|
40
|
+
import { detectSets } from "../input-scan/detect-sets.js";
|
|
41
|
+
import { buildSetMenu, renderSetMenu } from "../input-scan/menu.js";
|
|
42
|
+
import { readoutTargets } from "../input-scan/readout-budget.js";
|
|
43
|
+
import { scanInputTree } from "../input-scan/scan.js";
|
|
44
|
+
import { PROFILE_INDEX_TYPES, buildProfileIndexEntries } from "./data-profile-index.js";
|
|
45
|
+
import { absorbRecipe, renderAbsorbDelta } from "./data-profile-absorb.js";
|
|
46
|
+
import { formatResolutionErrors, resolveProfileSubmission } from "./data-profile-resolve.js";
|
|
43
47
|
import { completeDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
44
48
|
import { createProfileActivityEmitter } from "./data-profile-activity.js";
|
|
45
49
|
import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.js";
|
|
@@ -56,6 +60,15 @@ const DATA_PROFILE_AGENT_ID = "data-profiler";
|
|
|
56
60
|
const DEFAULT_DEADLINE_MS = 1_200_000;
|
|
57
61
|
/** Index entries embedded and upserted per round trip. */
|
|
58
62
|
const INDEX_BATCH_SIZE = 256;
|
|
63
|
+
/**
|
|
64
|
+
* Submissions the tool accepts: the first, plus one repair.
|
|
65
|
+
*
|
|
66
|
+
* The repair is a FULL resubmit — the agent replaces the whole operation list and nothing
|
|
67
|
+
* is merged into the prior attempt. Past it the resolution stands as it resolved: an
|
|
68
|
+
* operation that still does not resolve produces no group, and its members land in the
|
|
69
|
+
* visible `unclassified` group rather than blocking a profile that gates planning.
|
|
70
|
+
*/
|
|
71
|
+
const MAX_SUBMIT_ROUNDS = 2;
|
|
59
72
|
/** Where the embedder stages this analysis's inputs (see the data-profile-init spec). */
|
|
60
73
|
const STAGED_INPUT_ROOT = "data/inputs";
|
|
61
74
|
/** Build the canonical artifact path for a staged input file. */
|
|
@@ -78,66 +91,44 @@ function inputArtifactPath(f) {
|
|
|
78
91
|
* indistinguishable from a legacy snapshot that predates the field, which is exactly
|
|
79
92
|
* the reading a consumer must already tolerate.
|
|
80
93
|
*/
|
|
81
|
-
export function buildDataProfileResult(
|
|
94
|
+
export function buildDataProfileResult(submission, resolution, stagedInputs, profiledAt) {
|
|
82
95
|
return {
|
|
83
|
-
summary:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
cols: f.cols,
|
|
91
|
-
tags: f.tags,
|
|
92
|
-
warnings: f.warnings,
|
|
93
|
-
metrics: f.metrics,
|
|
94
|
-
})),
|
|
95
|
-
kinds: profile.kinds.map((k) => ({
|
|
96
|
-
name: k.name,
|
|
97
|
-
memberRepresents: k.memberRepresents,
|
|
98
|
-
description: k.description,
|
|
99
|
-
count: k.count,
|
|
100
|
-
pathPattern: k.pathPattern,
|
|
101
|
-
format: k.format,
|
|
102
|
-
axisLabels: k.axisLabels,
|
|
103
|
-
})),
|
|
104
|
-
axes: profile.axes?.map((a) => ({
|
|
105
|
-
label: a.label,
|
|
106
|
-
cardinality: a.cardinality,
|
|
107
|
-
exampleValues: a.exampleValues,
|
|
108
|
-
description: a.description,
|
|
109
|
-
})),
|
|
96
|
+
summary: submission.analysisSummary,
|
|
97
|
+
groups: withoutMembers(resolution.groups),
|
|
98
|
+
dimensions: [...resolution.dimensions],
|
|
99
|
+
probes: [...resolution.probes],
|
|
100
|
+
partition: resolution.partition,
|
|
101
|
+
recipe: [...resolution.recipe],
|
|
102
|
+
caveats: submission.caveats,
|
|
110
103
|
inputSignature: computeInputSignature(stagedInputs),
|
|
111
|
-
coverage,
|
|
112
104
|
profiledAt,
|
|
113
|
-
domain:
|
|
114
|
-
subtype:
|
|
115
|
-
organism:
|
|
116
|
-
tissue:
|
|
117
|
-
cellType:
|
|
118
|
-
condition:
|
|
119
|
-
accessions:
|
|
120
|
-
experimentalDesign:
|
|
121
|
-
qualityAssessment: profile.qualityAssessment,
|
|
105
|
+
domain: submission.domain,
|
|
106
|
+
subtype: submission.subtype,
|
|
107
|
+
organism: submission.organism,
|
|
108
|
+
tissue: submission.tissue,
|
|
109
|
+
cellType: submission.cellType,
|
|
110
|
+
condition: submission.condition,
|
|
111
|
+
accessions: submission.accessions,
|
|
112
|
+
experimentalDesign: submission.experimentalDesign,
|
|
122
113
|
};
|
|
123
114
|
}
|
|
124
115
|
/**
|
|
125
|
-
*
|
|
116
|
+
* Read one header per set, once a container exists.
|
|
126
117
|
*
|
|
127
|
-
* The walk and the
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
118
|
+
* The walk and the set detection run before any sandbox — a profile that absorbs a prior
|
|
119
|
+
* recipe never provisions one. Every prefix-sufficient readout also runs in this process
|
|
120
|
+
* over the workspace read seam; only a footer-indexed container reaches into the sandbox,
|
|
121
|
+
* and only once per set. A readout that fails is logged and dropped: it is enrichment,
|
|
122
|
+
* and a menu without it still carries every structural observation the agent's grouping
|
|
123
|
+
* rests on, so failing the profile over it would trade the whole capability for a nicety.
|
|
132
124
|
*/
|
|
133
|
-
async function
|
|
134
|
-
const { session, deps, analysisId, sandbox, execId, deadlineMs, logger } = args;
|
|
135
|
-
const scan = await scanInputTree({ session, fs: deps.workspaceFs, root: STAGED_INPUT_ROOT });
|
|
136
|
-
if (scan.manifest.shapes.length === 0)
|
|
137
|
-
return scan;
|
|
125
|
+
async function readSetHeaders(args) {
|
|
126
|
+
const { session, deps, analysisId, detected, sandbox, execId, deadlineMs, logger } = args;
|
|
138
127
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
return await readHeaders({
|
|
129
|
+
targets: readoutTargets(detected),
|
|
130
|
+
session,
|
|
131
|
+
fs: deps.workspaceFs,
|
|
141
132
|
sandboxClient: deps.sandboxClient,
|
|
142
133
|
sandbox,
|
|
143
134
|
mountRoot: `/${analysisId}`,
|
|
@@ -145,12 +136,112 @@ async function runInputScan(args) {
|
|
|
145
136
|
deadlineMs,
|
|
146
137
|
emit: async () => { },
|
|
147
138
|
});
|
|
148
|
-
return { ...scan, manifest: { ...scan.manifest, shapes } };
|
|
149
139
|
}
|
|
150
140
|
catch (err) {
|
|
151
141
|
logger.warn("input-scan header readout failed (non-fatal)", logger.errorFields(err));
|
|
152
|
-
return
|
|
142
|
+
return new Map();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The submission a tree with nothing to group would have produced.
|
|
147
|
+
*
|
|
148
|
+
* Authored here rather than by a model: with no kept file there is no judgement to make,
|
|
149
|
+
* and the fields below say exactly that. Resolution turns it into the census — zero
|
|
150
|
+
* groups against a full quarantine accounting — which is the honest record of such a tree
|
|
151
|
+
* and is what keeps `inspect_data_profile` from reporting the analysis unprofiled.
|
|
152
|
+
*/
|
|
153
|
+
function emptySubmission() {
|
|
154
|
+
return {
|
|
155
|
+
operations: [],
|
|
156
|
+
analysisSummary: "The input scan kept no files: every staged file was quarantined as OS junk, an editor temp, or a partial download. " +
|
|
157
|
+
"There is no dataset structure to describe. See the quarantine accounting for what was set aside and why.",
|
|
158
|
+
domain: "",
|
|
159
|
+
organism: null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** The membership the persisted record deliberately does not carry. */
|
|
163
|
+
function withoutMembers(groups) {
|
|
164
|
+
return groups.map(({ memberPaths, ...group }) => {
|
|
165
|
+
void memberPaths;
|
|
166
|
+
return group;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The prior profile re-stamped over a deterministic absorb.
|
|
171
|
+
*
|
|
172
|
+
* Membership, counts, the partition, the recipe, and the signature come from the replay;
|
|
173
|
+
* everything the agent authored — the summary, the identity fields, the dimensions, the
|
|
174
|
+
* probes, the caveats — is the profile's existing finding and is carried verbatim. An
|
|
175
|
+
* absorb re-derives arithmetic, and re-deciding judgement without a model is not something
|
|
176
|
+
* this path is entitled to do.
|
|
177
|
+
*/
|
|
178
|
+
export function buildAbsorbedResult(prior, resolution, stagedInputs, profiledAt) {
|
|
179
|
+
return {
|
|
180
|
+
...prior,
|
|
181
|
+
groups: withoutMembers(resolution.groups),
|
|
182
|
+
// Re-resolved, not carried: a slot observation's cardinality and values are computed
|
|
183
|
+
// from the scan, and the scan just changed. The labels and the reasoning are the
|
|
184
|
+
// profile's existing finding; the numbers under them are this replay's.
|
|
185
|
+
dimensions: [...resolution.dimensions],
|
|
186
|
+
partition: resolution.partition,
|
|
187
|
+
recipe: [...resolution.recipe],
|
|
188
|
+
inputSignature: computeInputSignature(stagedInputs),
|
|
189
|
+
profiledAt,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Build and upsert the profile's vector index — one entry per group, one per dimension,
|
|
194
|
+
* one per annotated member, none per unannotated file.
|
|
195
|
+
*
|
|
196
|
+
* A pure projection of the persisted profile crossed with the scan, so an absorb rebuilds
|
|
197
|
+
* it from the same two things the agent path does. Embedding is not a model judgement:
|
|
198
|
+
* leaving the index behind after an absorb would make the newly absorbed members
|
|
199
|
+
* unsearchable while the record said they were profiled.
|
|
200
|
+
*/
|
|
201
|
+
async function indexProfile(args) {
|
|
202
|
+
const { deps, analysisId, session, record, scan, logger } = args;
|
|
203
|
+
await ensureSearchIndex(deps.pool, analysisId, deps.embedding.dimensions);
|
|
204
|
+
const vectorStore = createVectorStore(deps.pool);
|
|
205
|
+
const indexName = searchIndexName(analysisId);
|
|
206
|
+
const entries = buildProfileIndexEntries({ analysisId, result: record, scan });
|
|
207
|
+
// Replace, don't merge. The tiers are a projection of THIS profile, and an upsert keyed
|
|
208
|
+
// by entry id leaves a renamed group, a dropped dimension, and a de-annotated member
|
|
209
|
+
// behind — searchable, and describing a profile that no longer exists.
|
|
210
|
+
unwrapOrThrow(await vectorStore.deleteByType({ indexName, types: [...PROFILE_INDEX_TYPES] }));
|
|
211
|
+
// Batched: both interfaces already take arrays, and one round trip per entry is
|
|
212
|
+
// what made indexing 12 of the 39 minutes the motivating profile took.
|
|
213
|
+
for (let start = 0; start < entries.length; start += INDEX_BATCH_SIZE) {
|
|
214
|
+
const batch = entries.slice(start, start + INDEX_BATCH_SIZE);
|
|
215
|
+
const vectors = unwrapOrThrow(await deps.embedding.embed(batch.map((entry) => entry.text), session));
|
|
216
|
+
if (vectors.length !== batch.length)
|
|
217
|
+
throw new Error(`data-profile: embedding returned ${vectors.length} vectors for ${batch.length} entries`);
|
|
218
|
+
unwrapOrThrow(await vectorStore.upsert({
|
|
219
|
+
indexName,
|
|
220
|
+
vectors,
|
|
221
|
+
metadata: batch.map((entry) => ({ ...entry.metadata, text: entry.text })),
|
|
222
|
+
ids: batch.map((entry) => entry.id),
|
|
223
|
+
}));
|
|
153
224
|
}
|
|
225
|
+
logger.info("indexed profile", { entries: entries.length, groups: record.groups?.length ?? 0 });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The counters the catalogue's fit against real use is measured from — one structured
|
|
229
|
+
* event per completed profile, emitted whether or not anything went wrong.
|
|
230
|
+
*/
|
|
231
|
+
function logProfileMonitoring(logger, args) {
|
|
232
|
+
const { resolution, dimensions, probes, repairRounds, absorb } = args;
|
|
233
|
+
logger.info("profile monitoring", {
|
|
234
|
+
absorb,
|
|
235
|
+
repairRounds,
|
|
236
|
+
unresolvedOperations: resolution?.errors.length ?? 0,
|
|
237
|
+
contestedFiles: resolution?.contested.length ?? 0,
|
|
238
|
+
unclassifiedFiles: resolution?.partition.unclassifiedFiles ?? 0,
|
|
239
|
+
keptFiles: resolution?.partition.keptFiles ?? 0,
|
|
240
|
+
scanTruncated: resolution?.partition.scanTruncated ?? false,
|
|
241
|
+
otherGroupCategories: (resolution?.groups ?? []).filter((group) => group.category === "other" && !group.unclassified).length,
|
|
242
|
+
otherDimensionCategories: dimensions.filter((dimension) => dimension.category === "other").length,
|
|
243
|
+
probeNotFound: probes.filter((probe) => probe.outcome === "not-found").length,
|
|
244
|
+
});
|
|
154
245
|
}
|
|
155
246
|
/**
|
|
156
247
|
* Register the data-profile workflow with DBOS. Returns the registered
|
|
@@ -199,6 +290,10 @@ export async function runDataProfileBody(input, deps) {
|
|
|
199
290
|
// input. The body never downloads.
|
|
200
291
|
if (stagedInputs.length === 0) {
|
|
201
292
|
logger.warn("no input files staged");
|
|
293
|
+
// One event per completed profile, this one included: the counters are how the
|
|
294
|
+
// catalogue's fit is measured, and a completion that emitted none would make the
|
|
295
|
+
// empty-manifest path invisible to the same monitoring that watches every other.
|
|
296
|
+
logProfileMonitoring(logger, { dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
202
297
|
if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)))
|
|
203
298
|
logTerminalNoop(logger, analysisId, "completion");
|
|
204
299
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
@@ -216,7 +311,6 @@ export async function runDataProfileBody(input, deps) {
|
|
|
216
311
|
role: "input",
|
|
217
312
|
fileId: f.fileId,
|
|
218
313
|
})));
|
|
219
|
-
// 3. Run data-profiler sandbox agent
|
|
220
314
|
const executionId = generateExecutionId(DATA_PROFILE_AGENT_ID);
|
|
221
315
|
const workflowId = DBOS.workflowID ?? `${DATA_PROFILE_RUN_LITERAL}:${executionId}`;
|
|
222
316
|
const childSession = forSubAgent(runSession, DATA_PROFILE_AGENT_ID);
|
|
@@ -225,6 +319,69 @@ export async function runDataProfileBody(input, deps) {
|
|
|
225
319
|
// meaningless, so no softer handling is warranted.
|
|
226
320
|
const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
|
|
227
321
|
const profileWritePrefix = `${workspaceRoot}/runs/${DATA_PROFILE_RUN_LITERAL}/${DATA_PROFILE_STEP_LITERAL}`;
|
|
322
|
+
// 3. The deterministic scan, before any container and before the agent's first turn.
|
|
323
|
+
// It is always needed and its result does not depend on agent judgement, so spending
|
|
324
|
+
// an agent turn to request it would be waste — and a briefing carrying one line per
|
|
325
|
+
// input file consumes context that carries no structure. It runs BEFORE the sandbox
|
|
326
|
+
// because the absorb below may complete the profile without ever needing one.
|
|
327
|
+
await activity.scanning();
|
|
328
|
+
const scan = await scanInputTree({ session: childSession, fs: deps.workspaceFs, root: STAGED_INPUT_ROOT });
|
|
329
|
+
const detected = detectSets(scan.files);
|
|
330
|
+
const menu = buildSetMenu(detected, { truncated: scan.manifest.truncated });
|
|
331
|
+
logger.info("input scan complete", {
|
|
332
|
+
files: detected.fileCount,
|
|
333
|
+
kept: detected.keptFileCount,
|
|
334
|
+
sets: detected.sets.length,
|
|
335
|
+
listedSets: menu.sets.length,
|
|
336
|
+
leftovers: detected.leftovers.memberCount,
|
|
337
|
+
quarantined: detected.quarantine.count,
|
|
338
|
+
truncated: scan.manifest.truncated,
|
|
339
|
+
});
|
|
340
|
+
// 3a. A tree the scan kept nothing of. There is no menu to author against and no file
|
|
341
|
+
// to describe, so a sandbox and a model pass would produce an empty submission at the
|
|
342
|
+
// cost of a container. The quarantine accounting is the whole finding, and it is a
|
|
343
|
+
// real one: it names every file and why each was set aside.
|
|
344
|
+
if (detected.keptFileCount === 0) {
|
|
345
|
+
logger.warn("every scanned file was quarantined", { scanned: detected.fileCount, quarantined: detected.quarantine.count });
|
|
346
|
+
const resolution = resolveProfileSubmission(emptySubmission(), detected, menu, { finalRound: true });
|
|
347
|
+
const record = buildDataProfileResult(emptySubmission(), resolution, stagedInputs, new Date().toISOString());
|
|
348
|
+
await activity.indexing();
|
|
349
|
+
await indexProfile({ deps, analysisId, session: runSession, record, scan, logger });
|
|
350
|
+
logProfileMonitoring(logger, { resolution, dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
351
|
+
if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)))
|
|
352
|
+
logTerminalNoop(logger, analysisId, "completion");
|
|
353
|
+
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
354
|
+
await activity.complete();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// 3b. Absorption — replay a prior profile's recipe against the fresh scan. A recipe
|
|
358
|
+
// covering every kept file completes the profile here: an added directory of files
|
|
359
|
+
// that instantiate templates the profile already describes is arithmetic, and paying
|
|
360
|
+
// a container and a model pass for it is what this pre-step exists to remove.
|
|
361
|
+
const prior = unwrapOrThrow(await loadDataProfileStatus(deps.pool, analysisId))?.result ?? null;
|
|
362
|
+
const absorb = absorbRecipe(prior, detected, menu);
|
|
363
|
+
if (absorb.kind === "stranded") {
|
|
364
|
+
logger.warn("prior recipe stranded — re-profiling from scratch", { reason: absorb.reason });
|
|
365
|
+
}
|
|
366
|
+
if (absorb.kind === "full" && prior) {
|
|
367
|
+
logger.info("absorbed the prior recipe", { groups: absorb.resolution.groups.length, keptFiles: absorb.resolution.partition.keptFiles });
|
|
368
|
+
await activity.indexing();
|
|
369
|
+
const absorbed = buildAbsorbedResult(prior, absorb.resolution, stagedInputs, new Date().toISOString());
|
|
370
|
+
await indexProfile({ deps, analysisId, session: runSession, record: absorbed, scan, logger });
|
|
371
|
+
logProfileMonitoring(logger, {
|
|
372
|
+
resolution: absorb.resolution,
|
|
373
|
+
dimensions: absorbed.dimensions ?? [],
|
|
374
|
+
probes: absorbed.probes ?? [],
|
|
375
|
+
repairRounds: 0,
|
|
376
|
+
absorb: absorb.kind,
|
|
377
|
+
});
|
|
378
|
+
if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)))
|
|
379
|
+
logTerminalNoop(logger, analysisId, "completion");
|
|
380
|
+
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
381
|
+
await activity.complete();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// 4. Run the data-profiler sandbox agent.
|
|
228
385
|
logger.info("starting sandbox", { executionId });
|
|
229
386
|
// Reported BEFORE the container exists, deliberately: provisioning is the longest single
|
|
230
387
|
// operation in a profile and it precedes the agent entirely, so a body that reported only
|
|
@@ -260,27 +417,17 @@ export async function runDataProfileBody(input, deps) {
|
|
|
260
417
|
// which captures its step deadline the same way).
|
|
261
418
|
const deadlineAbs = (await DBOS.now()) + DEFAULT_DEADLINE_MS;
|
|
262
419
|
const nextFunctionId = makeNextFunctionId();
|
|
263
|
-
|
|
264
|
-
// and its result does not depend on agent judgement, so spending an agent turn
|
|
265
|
-
// to request it would be waste — and a briefing carrying one line per input
|
|
266
|
-
// file consumes context that carries no structure (~270 KB of bare paths for
|
|
267
|
-
// the tree that motivated this, against ~2 KB of shapes).
|
|
268
|
-
await activity.scanning();
|
|
269
|
-
const scan = await runInputScan({
|
|
420
|
+
const headers = await readSetHeaders({
|
|
270
421
|
session: childSession,
|
|
271
422
|
deps,
|
|
272
423
|
analysisId,
|
|
424
|
+
detected,
|
|
273
425
|
sandbox,
|
|
274
426
|
execId: `${workflowId}:${DATA_PROFILE_STEP_LITERAL}:${nextFunctionId()}`,
|
|
275
427
|
deadlineMs: deadlineAbs,
|
|
276
428
|
logger,
|
|
277
429
|
});
|
|
278
|
-
|
|
279
|
-
files: scan.manifest.fileCount,
|
|
280
|
-
shapes: scan.manifest.shapes.length,
|
|
281
|
-
unstructured: scan.manifest.unstructured.count,
|
|
282
|
-
truncated: scan.manifest.truncated,
|
|
283
|
-
});
|
|
430
|
+
const briefingMenu = { ...menu, headers };
|
|
284
431
|
// The profiler runs outside `executeAnalysis`, so no scheduler composes a
|
|
285
432
|
// briefing for it — this prompt IS its briefing, and the sandbox system
|
|
286
433
|
// prompt names no paths (it is static per agent type, for the prompt cache).
|
|
@@ -296,11 +443,16 @@ export async function runDataProfileBody(input, deps) {
|
|
|
296
443
|
``,
|
|
297
444
|
`CRITICAL: File and directory paths may contain spaces. You MUST always double-quote paths in shell commands (e.g. head "/${analysisId}/data/inputs/My Folder/file.csv"). Unquoted paths with spaces will silently break commands.`,
|
|
298
445
|
``,
|
|
299
|
-
|
|
446
|
+
renderSetMenu(briefingMenu, STAGED_INPUT_ROOT),
|
|
300
447
|
``,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
448
|
+
...(absorb.kind === "partial"
|
|
449
|
+
? [renderAbsorbDelta(absorb), ``]
|
|
450
|
+
: [
|
|
451
|
+
`Author the dataset's groups as operations on this menu: \`use\` a set, \`split\` one by a slot or by a`,
|
|
452
|
+
`value mapping, \`merge\` several, or \`group\` explicit paths the scan left over. Membership and counts`,
|
|
453
|
+
`are computed from your operations — you do not state them. Inspect ONE example file per group where a`,
|
|
454
|
+
`description needs content the scan did not capture, then call \`submit_profile\` once.`,
|
|
455
|
+
]),
|
|
304
456
|
].join("\n");
|
|
305
457
|
const sandboxAgentDeps = {
|
|
306
458
|
provider: deps.provider,
|
|
@@ -328,15 +480,34 @@ export async function runDataProfileBody(input, deps) {
|
|
|
328
480
|
deadlineMs: () => deadlineAbs,
|
|
329
481
|
},
|
|
330
482
|
};
|
|
331
|
-
let
|
|
483
|
+
let accepted = null;
|
|
484
|
+
let rounds = 0;
|
|
332
485
|
const submitProfileTool = defineTool({
|
|
333
486
|
id: "submit_profile",
|
|
334
|
-
description: "Submit the profiling results. Call this tool once after completing
|
|
335
|
-
|
|
487
|
+
description: "Submit the profiling results. Call this tool once after completing all profiling work — it resolves your " +
|
|
488
|
+
"operations against the scan, computes every group's membership and counts, and records the profile. " +
|
|
489
|
+
"A submission whose operations do not resolve, or that leaves kept files unclaimed, comes back once with " +
|
|
490
|
+
"the errors; the repair is a FULL resubmit of the whole operation list.",
|
|
491
|
+
inputSchema: ProfileSubmissionSchema,
|
|
336
492
|
describeCall: "none",
|
|
337
493
|
execute: async (input) => {
|
|
338
|
-
|
|
339
|
-
|
|
494
|
+
rounds++;
|
|
495
|
+
// The last round resolves as final: nothing that follows can repair an
|
|
496
|
+
// overlap, so a contested file sweeps rather than being awarded to whichever
|
|
497
|
+
// operation the agent happened to write first.
|
|
498
|
+
const finalRound = rounds >= MAX_SUBMIT_ROUNDS;
|
|
499
|
+
const resolution = resolveProfileSubmission(input, detected, briefingMenu, { finalRound });
|
|
500
|
+
if (!finalRound && (resolution.errors.length > 0 || resolution.unclaimed.length > 0)) {
|
|
501
|
+
return ok({ status: "needs_repair", detail: formatResolutionErrors(resolution) });
|
|
502
|
+
}
|
|
503
|
+
accepted = { submission: input, resolution };
|
|
504
|
+
return ok({
|
|
505
|
+
status: "accepted",
|
|
506
|
+
groups: resolution.groups.length,
|
|
507
|
+
keptFiles: resolution.partition.keptFiles,
|
|
508
|
+
unclassifiedFiles: resolution.partition.unclassifiedFiles,
|
|
509
|
+
unresolvedOperations: resolution.errors.length,
|
|
510
|
+
});
|
|
340
511
|
},
|
|
341
512
|
});
|
|
342
513
|
const baseAgent = createDataProfilerAgent(sandboxAgentDeps);
|
|
@@ -364,67 +535,35 @@ export async function runDataProfileBody(input, deps) {
|
|
|
364
535
|
await activity.forTool(event.name, event.input, resolveDetail);
|
|
365
536
|
},
|
|
366
537
|
runStep: durableStep,
|
|
367
|
-
resolved: () =>
|
|
538
|
+
resolved: () => accepted !== null,
|
|
368
539
|
usageRecorder: deps.usageRecorder,
|
|
369
540
|
}, {
|
|
370
541
|
tools: [submitProfileTool],
|
|
371
542
|
nudge: "You stopped without calling submit_profile, so no profile was " +
|
|
372
|
-
"recorded. Call submit_profile now with
|
|
373
|
-
"
|
|
374
|
-
"
|
|
543
|
+
"recorded. Call submit_profile now with your operations on the menu, " +
|
|
544
|
+
"what each resulting group means, and the dimensions you saw with " +
|
|
545
|
+
"their observations — base it on the scan and the work you already did.",
|
|
375
546
|
});
|
|
376
|
-
if (!
|
|
547
|
+
if (!accepted) {
|
|
377
548
|
throw new Error("Data profiling failed: agent did not call submit_profile");
|
|
378
549
|
}
|
|
379
|
-
const
|
|
380
|
-
//
|
|
550
|
+
const { submission, resolution } = accepted;
|
|
551
|
+
// 5. Index into vector store — one entry per group, one per dimension, one per annotated member.
|
|
381
552
|
//
|
|
382
553
|
// Reported as `indexing`, not `persisting`: the contract defines `persisting` as step
|
|
383
554
|
// bytes uploading to an artifact store, and a profile uploads nothing — its durable
|
|
384
555
|
// products are this vector index and the ledger row.
|
|
385
556
|
await activity.indexing();
|
|
386
|
-
const profileRecord = buildDataProfileResult(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const entries = buildProfileIndexEntries({
|
|
395
|
-
analysisId,
|
|
396
|
-
kinds: profileRecord.kinds ?? [],
|
|
397
|
-
...(profileRecord.axes ? { axes: profileRecord.axes } : {}),
|
|
398
|
-
scan,
|
|
399
|
-
files: profileRecord.files,
|
|
557
|
+
const profileRecord = buildDataProfileResult(submission, resolution, stagedInputs, new Date().toISOString());
|
|
558
|
+
await indexProfile({ deps, analysisId, session: runSession, record: profileRecord, scan, logger });
|
|
559
|
+
logProfileMonitoring(logger, {
|
|
560
|
+
resolution,
|
|
561
|
+
dimensions: resolution.dimensions,
|
|
562
|
+
probes: resolution.probes,
|
|
563
|
+
repairRounds: rounds - 1,
|
|
564
|
+
absorb: absorb.kind,
|
|
400
565
|
});
|
|
401
|
-
//
|
|
402
|
-
// what made indexing 12 of the 39 minutes the motivating profile took.
|
|
403
|
-
for (let start = 0; start < entries.length; start += INDEX_BATCH_SIZE) {
|
|
404
|
-
const batch = entries.slice(start, start + INDEX_BATCH_SIZE);
|
|
405
|
-
const vectors = unwrapOrThrow(await deps.embedding.embed(batch.map((entry) => entry.text), runSession));
|
|
406
|
-
if (vectors.length !== batch.length)
|
|
407
|
-
throw new Error(`data-profile: embedding returned ${vectors.length} vectors for ${batch.length} entries`);
|
|
408
|
-
unwrapOrThrow(await vectorStore.upsert({
|
|
409
|
-
indexName,
|
|
410
|
-
vectors,
|
|
411
|
-
metadata: batch.map((entry) => ({ ...entry.metadata, text: entry.text })),
|
|
412
|
-
ids: batch.map((entry) => entry.id),
|
|
413
|
-
}));
|
|
414
|
-
}
|
|
415
|
-
logger.info("indexed profile", {
|
|
416
|
-
entries: entries.length,
|
|
417
|
-
kinds: profileRecord.kinds?.length ?? 0,
|
|
418
|
-
stagedCount: stagedInputs.length,
|
|
419
|
-
});
|
|
420
|
-
if (profileRecord.coverage && profileRecord.coverage.unmatched > 0) {
|
|
421
|
-
logger.warn("profile kinds do not cover the scanned tree", {
|
|
422
|
-
matched: profileRecord.coverage.matched,
|
|
423
|
-
unmatched: profileRecord.coverage.unmatched,
|
|
424
|
-
total: profileRecord.coverage.total,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
// 5. Complete — store the FULL profiler finding plus the input signature for
|
|
566
|
+
// 6. Complete — store the FULL profiler finding plus the input signature for
|
|
428
567
|
// staleness detection. The scratch tree is gone; this row is all that survives.
|
|
429
568
|
if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord))) {
|
|
430
569
|
logTerminalNoop(logger, analysisId, "completion");
|
|
@@ -32,7 +32,7 @@ export declare const chemblTool: import("../define-tool.js").Tool<{
|
|
|
32
32
|
query?: string | undefined;
|
|
33
33
|
searchType?: "target" | "smiles" | "compound" | undefined;
|
|
34
34
|
chemblId?: string | undefined;
|
|
35
|
-
idType?: "target" | "compound" | undefined;
|
|
35
|
+
idType?: "target" | "compound" | "assay" | undefined;
|
|
36
36
|
activityType?: string | undefined;
|
|
37
37
|
limit?: number | undefined;
|
|
38
38
|
}, ChemblOutput>;
|
package/dist/tools/bio/chembl.js
CHANGED
|
@@ -45,7 +45,9 @@ const inputSchema = z
|
|
|
45
45
|
"(INHIBITOR, AGONIST, …), targetChemblId + targetName, moleculeChemblId. Curated mainly for clinical/approved molecules, so tool " +
|
|
46
46
|
"compounds often have none.\n" +
|
|
47
47
|
"'bioactivity' (chemblId + idType) — measured activity rows: the curated, QUOTABLE potency. → standardType, standardValue + " +
|
|
48
|
-
"standardUnits, pchemblValue (normalized -log10), assayChemblId, assayType, compoundChemblId, targetChemblId
|
|
48
|
+
"standardUnits, pchemblValue (normalized -log10), assayChemblId, assayType, compoundChemblId, targetChemblId. With idType='assay' " +
|
|
49
|
+
"it reads ONE assay back by the assayChemblId that an earlier bioactivity row carried — every measurement made under the same " +
|
|
50
|
+
"protocol, which is how you compare compounds on like terms.\n" +
|
|
49
51
|
"'targets' (query) — resolve a gene symbol or protein name to a ChEMBL target, producing the ID that bioactivity (idType='target') " +
|
|
50
52
|
"and compounds (searchType='target') need. → targetChemblId, preferredName, targetType, organism, geneNames. Check `organism`: the " +
|
|
51
53
|
"top hit for a human symbol may be a non-human ortholog."),
|
|
@@ -65,13 +67,13 @@ const inputSchema = z
|
|
|
65
67
|
.string()
|
|
66
68
|
.min(1)
|
|
67
69
|
.optional()
|
|
68
|
-
.describe("Required for 'mechanism' and 'bioactivity'. A molecule ID ('CHEMBL25' = aspirin), or — for bioactivity
|
|
69
|
-
"ID ('
|
|
70
|
+
.describe("Required for 'mechanism' and 'bioactivity'. A molecule ID ('CHEMBL25' = aspirin), or — for bioactivity — a target ID ('CHEMBL203' = " +
|
|
71
|
+
"EGFR) with idType='target', or an assay ID ('CHEMBL1217643') with idType='assay'. 'mechanism' takes a molecule ID only."),
|
|
70
72
|
idType: z
|
|
71
|
-
.enum(["compound", "target"])
|
|
73
|
+
.enum(["compound", "target", "assay"])
|
|
72
74
|
.optional()
|
|
73
|
-
.describe("Required for 'bioactivity' — which
|
|
74
|
-
"against. 'target': every compound assayed against that target."),
|
|
75
|
+
.describe("Required for 'bioactivity' — which column of the activity table `chemblId` indexes. 'compound': everything that molecule was assayed " +
|
|
76
|
+
"against. 'target': every compound assayed against that target. 'assay': every measurement made under that one assay protocol."),
|
|
75
77
|
activityType: z
|
|
76
78
|
.string()
|
|
77
79
|
.optional()
|
|
@@ -110,10 +112,13 @@ const inputSchema = z
|
|
|
110
112
|
});
|
|
111
113
|
export const chemblTool = defineTool({
|
|
112
114
|
id: "chembl",
|
|
113
|
-
description: "ChEMBL — the manually curated database of drug-like bioactives (~2.4M compounds), the targets they were measured against, their
|
|
114
|
-
"their approval status. Five lookups; pick with `action`, which gives each one's params and return fields.\n" +
|
|
115
|
-
"
|
|
116
|
-
"
|
|
115
|
+
description: "ChEMBL — the manually curated database of drug-like bioactives of EMBL-EBI (~2.4M compounds), the targets they were measured against, their " +
|
|
116
|
+
"mechanisms and their approval status. Five lookups; pick with `action`, which gives each one's params and return fields.\n" +
|
|
117
|
+
"ACCEPTED IDENTIFIERS: free text for 'compounds', 'drug' and 'targets' — a compound name ('imatinib'), an indication ('melanoma'), a gene symbol " +
|
|
118
|
+
"('EGFR') or a SMILES string. A ChEMBL ID for 'mechanism' and 'bioactivity' — a molecule ID ('CHEMBL25'), a target ID ('CHEMBL203') or an assay " +
|
|
119
|
+
"ID ('CHEMBL1217643'), named by `idType`.\n" +
|
|
120
|
+
"IDs are resolved, never guessed: a `chemblId` comes from a prior action='compounds'/'drug' (molecules), action='targets' (targets), the " +
|
|
121
|
+
"assayChemblId of a bioactivity row (assays), or pubchem action='crossrefs'. A compound or gene name is not a ChEMBL ID.\n" +
|
|
117
122
|
"Being curated, ChEMBL is what you quote from: prefer action='bioactivity' over pubchem action='assays' for any number you will cite, and resolve " +
|
|
118
123
|
"compounds here first. If ChEMBL misses the compound, resolve it via pubchem action='compound' and bridge back with pubchem action='crossrefs'.\n" +
|
|
119
124
|
"An empty array is valid no-data, not an error — do not retry the same call.",
|
|
@@ -98,9 +98,13 @@ const inputSchema = z
|
|
|
98
98
|
export function createComptoxTool(deps) {
|
|
99
99
|
return defineTool({
|
|
100
100
|
id: "comptox",
|
|
101
|
-
description: "
|
|
101
|
+
description: "The CompTox Chemicals Dashboard (CTX) of the U.S. Environmental Protection Agency — the chemical-safety corpus that holds ToxCast, ToxValDB, " +
|
|
102
|
+
"the genotoxicity and cancer summaries, and the SEEM exposure predictions. Four datasets behind `dataset`: 'toxcast' (in-vitro HTS " +
|
|
103
|
+
"bioactivity), 'hazard' (in-vivo: " +
|
|
102
104
|
"NOAEL/LOAEL/LD50, genotoxicity, cancer), 'chemical' (identity + physicochemical/ADMET), 'exposure' (SEEM predictions, toxicokinetics, uses, " +
|
|
103
105
|
"products). See `dataset` for what each returns; all four resolve `query` to a DTXSID first.\n" +
|
|
106
|
+
"ACCEPTED IDENTIFIERS: a DTXSID ('DTXSID7020182') used directly, or a CASRN ('80-05-7'), a chemical name ('bisphenol A') or — for dataset " +
|
|
107
|
+
"'chemical' — an InChIKey, each resolved by EXACT match.\n" +
|
|
104
108
|
"Requires EPA_CCTE_API_KEY — a missing key fails terminally: do NOT retry, tell the user the key needs configuring and proceed without EPA data.\n" +
|
|
105
109
|
"found: false means the query did not resolve to a chemical; a present-but-empty section is likewise valid no-data. Do not retry either.\n" +
|
|
106
110
|
"This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
|
|
@@ -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
|
|
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 " +
|