@inflexa-ai/harness 0.12.0 → 0.14.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.d.ts +9 -15
- package/dist/agents/conversation-agent.js +32 -31
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/index.js +0 -3
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.d.ts +2 -2
- package/dist/agents/sandbox/shared.js +8 -14
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/config/resource-limits.d.ts +0 -7
- package/dist/config/resource-limits.js +0 -1
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/dbos-run-launcher.js +1 -26
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-launcher.d.ts +2 -18
- package/dist/execution/run-launcher.js +2 -4
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +23 -6
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +51 -5
- package/dist/loop/run-to-terminal.d.ts +0 -2
- package/dist/loop/run-to-terminal.js +8 -1
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/reconstruct-cards.d.ts +1 -1
- package/dist/memory/reconstruct-cards.js +7 -5
- package/dist/memory/thread-history.js +72 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +41 -15
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.d.ts +3 -0
- package/dist/prompts/planner.js +16 -21
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/sandbox-standards.d.ts +4 -4
- package/dist/prompts/sandbox-standards.js +5 -5
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/assemble.d.ts +5 -10
- package/dist/runtime/assemble.js +0 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/dbos.d.ts +4 -5
- package/dist/runtime/dbos.js +6 -7
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/mount-plan.d.ts +1 -1
- package/dist/sandbox/sandbox-error.d.ts +1 -1
- package/dist/sandbox/sandbox-error.js +1 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/sandbox/types.d.ts +1 -1
- package/dist/schemas/validate-plan.d.ts +1 -1
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +6 -6
- package/dist/state/index.js +4 -4
- package/dist/state/init.js +37 -2
- package/dist/state/plans.js +1 -1
- package/dist/state/runs.d.ts +49 -0
- package/dist/state/runs.js +150 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/__fixtures__/tool-context.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +31 -0
- package/dist/tools/ad-hoc-router.js +132 -0
- package/dist/tools/analysis-invocation.d.ts +4 -0
- package/dist/tools/analysis-invocation.js +17 -0
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/define-tool.d.ts +6 -2
- package/dist/tools/define-tool.js +1 -1
- package/dist/tools/execute-analysis.d.ts +44 -0
- package/dist/tools/execute-analysis.js +252 -0
- package/dist/tools/iterate-report.js +7 -6
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +116 -70
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +68 -5
- package/dist/workflows/execute-analysis.js +76 -7
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
- package/dist/agents/sandbox/ephemeral-executor.js +0 -20
- package/dist/execution/ephemeral-runner.d.ts +0 -98
- package/dist/execution/ephemeral-runner.js +0 -189
- package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
- package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
- package/dist/tools/execute-plan.d.ts +0 -64
- package/dist/tools/execute-plan.js +0 -211
- package/dist/tools/run-ephemeral.d.ts +0 -33
- package/dist/tools/run-ephemeral.js +0 -65
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
|
|
3
|
+
*
|
|
4
|
+
* Public, no key required. Three search paths: by gene (SNP-by-gene lookup then
|
|
5
|
+
* per-SNP associations), by trait (EFO trait search then its associations), and
|
|
6
|
+
* by variant (direct rsID associations).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
10
|
+
import { GWAS_BASE, GWAS_HEADERS } from "./gwas-catalog-config.js";
|
|
11
|
+
// GWAS Catalog raw wire shapes (HAL+JSON), validated at the fetch boundary.
|
|
12
|
+
// Every field is optional because the API omits absent values; leaf strings
|
|
13
|
+
// that the API can return as an explicit `null` are `.nullable()`.
|
|
14
|
+
const RawGwasStudySchema = z.object({
|
|
15
|
+
accessionId: z.string().optional(),
|
|
16
|
+
pubmedId: z.string().optional(),
|
|
17
|
+
initialSampleSize: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
// `_snpRsId` is not on the wire — the 'gene' search path assigns it after
|
|
20
|
+
// parsing (`a._snpRsId = snp.rsId`), so it stays a writable optional field.
|
|
21
|
+
const RawGwasAssociationSchema = z.object({
|
|
22
|
+
_snpRsId: z.string().optional(),
|
|
23
|
+
loci: z
|
|
24
|
+
.array(z.object({
|
|
25
|
+
strongestRiskAlleles: z.array(z.object({ riskAlleleName: z.string().nullable().optional() })).optional(),
|
|
26
|
+
authorReportedGenes: z.array(z.object({ geneName: z.string().nullable().optional() })).optional(),
|
|
27
|
+
}))
|
|
28
|
+
.optional(),
|
|
29
|
+
efoTraits: z.array(z.object({ trait: z.string().nullable().optional() })).optional(),
|
|
30
|
+
study: RawGwasStudySchema.optional(),
|
|
31
|
+
pvalueMantissa: z.number().optional(),
|
|
32
|
+
pvalueExponent: z.number().optional(),
|
|
33
|
+
riskFrequency: z.string().optional(),
|
|
34
|
+
orPerCopyNum: z.number().nullable().optional(),
|
|
35
|
+
betaNum: z.number().nullable().optional(),
|
|
36
|
+
range: z.string().optional(),
|
|
37
|
+
});
|
|
38
|
+
const GwasEmbeddedSchema = z.object({
|
|
39
|
+
_embedded: z.object({ associations: z.array(RawGwasAssociationSchema).optional() }).optional(),
|
|
40
|
+
page: z.object({ totalElements: z.number().optional() }).optional(),
|
|
41
|
+
});
|
|
42
|
+
const GwasTraitSearchResponseSchema = z.object({
|
|
43
|
+
_embedded: z
|
|
44
|
+
.object({
|
|
45
|
+
efoTraits: z.array(z.object({ _links: z.object({ self: z.object({ href: z.string().optional() }).optional() }).optional() })).optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
});
|
|
49
|
+
const GwasSnpSearchResponseSchema = z.object({
|
|
50
|
+
_embedded: z
|
|
51
|
+
.object({
|
|
52
|
+
singleNucleotidePolymorphisms: z
|
|
53
|
+
.array(z.object({
|
|
54
|
+
rsId: z.string().optional(),
|
|
55
|
+
_links: z.object({ associations: z.object({ href: z.string().optional() }).optional() }).optional(),
|
|
56
|
+
}))
|
|
57
|
+
.optional(),
|
|
58
|
+
})
|
|
59
|
+
.optional(),
|
|
60
|
+
});
|
|
61
|
+
export function mapAssociation(a) {
|
|
62
|
+
const loci = a.loci ?? [];
|
|
63
|
+
const riskAlleles = loci.flatMap((l) => l.strongestRiskAlleles?.map((ra) => String(ra.riskAlleleName ?? "")) ?? []);
|
|
64
|
+
const genes = loci.flatMap((l) => l.authorReportedGenes?.map((g) => String(g.geneName ?? "")) ?? []);
|
|
65
|
+
const traits = (a.efoTraits ?? []).map((t) => String(t.trait ?? ""));
|
|
66
|
+
const study = a.study ?? {};
|
|
67
|
+
const pMantissa = a.pvalueMantissa ?? 0;
|
|
68
|
+
const pExponent = a.pvalueExponent ?? 0;
|
|
69
|
+
return {
|
|
70
|
+
rsId: String(a._snpRsId ?? riskAlleles[0]?.split("-")[0] ?? ""),
|
|
71
|
+
pValue: pMantissa * Math.pow(10, pExponent),
|
|
72
|
+
pValueMantissa: pMantissa,
|
|
73
|
+
pValueExponent: pExponent,
|
|
74
|
+
riskAllele: riskAlleles.join(", "),
|
|
75
|
+
riskFrequency: String(a.riskFrequency ?? ""),
|
|
76
|
+
orBeta: (a.orPerCopyNum ?? a.betaNum ?? null),
|
|
77
|
+
ci: String(a.range ?? ""),
|
|
78
|
+
trait: traits.join("; "),
|
|
79
|
+
mappedGenes: [...new Set(genes)],
|
|
80
|
+
studyAccession: String(study.accessionId ?? ""),
|
|
81
|
+
pubmedId: String(study.pubmedId ?? ""),
|
|
82
|
+
sampleSize: String(study.initialSampleSize ?? ""),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Max SNPs whose associations the 'gene' path walks; each is one request. */
|
|
86
|
+
const GENE_PATH_MAX_SNPS = 10;
|
|
87
|
+
export async function searchGwasCatalog(query, searchType, options = {}) {
|
|
88
|
+
const pValueThreshold = options.pValueThreshold ?? 5e-8;
|
|
89
|
+
const limit = options.limit ?? 25;
|
|
90
|
+
if (searchType === "gene") {
|
|
91
|
+
const url = `${GWAS_BASE}/singleNucleotidePolymorphisms/search/findByGene?geneName=${encodeURIComponent(query)}&projection=snpByGene`;
|
|
92
|
+
const snpRes = await apiFetchValidated(url, GwasSnpSearchResponseSchema, { headers: GWAS_HEADERS });
|
|
93
|
+
if (snpRes.isErr())
|
|
94
|
+
throw new Error(describeApiError(snpRes.error));
|
|
95
|
+
const snps = snpRes.value?._embedded?.singleNucleotidePolymorphisms ?? [];
|
|
96
|
+
const allAssocs = [];
|
|
97
|
+
for (const snp of snps.slice(0, Math.min(limit, GENE_PATH_MAX_SNPS))) {
|
|
98
|
+
const assocLink = snp?._links?.associations?.href;
|
|
99
|
+
if (!assocLink)
|
|
100
|
+
continue;
|
|
101
|
+
const aRes = await apiFetchValidated(assocLink, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
|
|
102
|
+
if (aRes.isOk()) {
|
|
103
|
+
for (const a of aRes.value?._embedded?.associations ?? []) {
|
|
104
|
+
a._snpRsId = snp.rsId;
|
|
105
|
+
allAssocs.push(a);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (allAssocs.length >= limit)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
return { totalFound: allAssocs.length, associations: allAssocs.slice(0, limit).map(mapAssociation) };
|
|
112
|
+
}
|
|
113
|
+
let associationsUrl;
|
|
114
|
+
if (searchType === "variant") {
|
|
115
|
+
const rsId = query.startsWith("rs") ? query : `rs${query}`;
|
|
116
|
+
associationsUrl = `${GWAS_BASE}/singleNucleotidePolymorphisms/${rsId}/associations?projection=associationBySnp`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findBySearchQuery?query=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
|
|
120
|
+
headers: GWAS_HEADERS,
|
|
121
|
+
});
|
|
122
|
+
if (traitRes.isErr())
|
|
123
|
+
throw new Error(describeApiError(traitRes.error));
|
|
124
|
+
const traitUri = (traitRes.value?._embedded?.efoTraits ?? [])[0]?._links?.self?.href;
|
|
125
|
+
if (!traitUri)
|
|
126
|
+
return { totalFound: 0, associations: [] };
|
|
127
|
+
associationsUrl = `${GWAS_BASE}/efoTraits/${traitUri.split("/").pop()}/associations?size=${limit}`;
|
|
128
|
+
}
|
|
129
|
+
const res = await apiFetchValidated(associationsUrl, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
|
|
130
|
+
if (res.isErr())
|
|
131
|
+
throw new Error(describeApiError(res.error));
|
|
132
|
+
const rawAssocs = res.value?._embedded?.associations ?? [];
|
|
133
|
+
const totalFound = res.value?.page?.totalElements ?? rawAssocs.length;
|
|
134
|
+
const filtered = rawAssocs.filter((a) => (a.pvalueMantissa ?? 1) * Math.pow(10, a.pvalueExponent ?? 0) <= pValueThreshold).slice(0, limit);
|
|
135
|
+
return { totalFound, associations: filtered.map(mapAssociation) };
|
|
136
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for PharmGKB clinical annotations.
|
|
3
|
+
*
|
|
4
|
+
* Public API, no key required. Matching is an exact field filter, not a search:
|
|
5
|
+
* `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* One PharmGKB clinical annotation.
|
|
9
|
+
*
|
|
10
|
+
* `levelOfEvidence` is the only strength signal this endpoint carries — the
|
|
11
|
+
* phenotype, guideline source and summary live on other PharmGKB resources, so
|
|
12
|
+
* they are absent here rather than present-and-null.
|
|
13
|
+
*/
|
|
14
|
+
export interface PharmgkbAnnotation {
|
|
15
|
+
gene: string;
|
|
16
|
+
drug: string;
|
|
17
|
+
levelOfEvidence: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function searchPharmgkb(query: string, searchType: "gene" | "drug", limit?: number): Promise<{
|
|
20
|
+
annotations: PharmgkbAnnotation[];
|
|
21
|
+
totalFound: number;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for PharmGKB clinical annotations.
|
|
3
|
+
*
|
|
4
|
+
* Public API, no key required. Matching is an exact field filter, not a search:
|
|
5
|
+
* `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
9
|
+
import { PHARMGKB_BASE, PHARMGKB_HEADERS } from "./pharmgkb-config.js";
|
|
10
|
+
// Raw PharmGKB clinicalAnnotation wire shape, validated at the fetch boundary.
|
|
11
|
+
// Every field is optional because the API omits absent values.
|
|
12
|
+
const PharmgkbAnnotationSchema = z.object({
|
|
13
|
+
location: z.object({ genes: z.array(z.object({ symbol: z.string().optional() })).optional() }).optional(),
|
|
14
|
+
relatedChemicals: z.array(z.object({ name: z.string().optional() })).optional(),
|
|
15
|
+
levelOfEvidence: z.object({ term: z.string().optional() }).optional(),
|
|
16
|
+
});
|
|
17
|
+
const PharmgkbResponseSchema = z.object({
|
|
18
|
+
data: z.array(PharmgkbAnnotationSchema).optional(),
|
|
19
|
+
});
|
|
20
|
+
export async function searchPharmgkb(query, searchType, limit = 20) {
|
|
21
|
+
const filter = searchType === "gene" ? `location.genes.symbol=${encodeURIComponent(query)}` : `relatedChemicals.name=${encodeURIComponent(query)}`;
|
|
22
|
+
const res = await apiFetchValidated(`${PHARMGKB_BASE}/clinicalAnnotation?${filter}`, PharmgkbResponseSchema, { headers: PHARMGKB_HEADERS });
|
|
23
|
+
if (res.isErr())
|
|
24
|
+
throw new Error(describeApiError(res.error));
|
|
25
|
+
const data = res.value?.data ?? [];
|
|
26
|
+
const annotations = data.slice(0, limit).map((ann) => ({
|
|
27
|
+
gene: ann.location?.genes?.map((g) => g.symbol).join(", ") ?? query,
|
|
28
|
+
drug: ann.relatedChemicals?.map((c) => c.name).join(", ") ?? "",
|
|
29
|
+
levelOfEvidence: ann.levelOfEvidence?.term ?? "Unknown",
|
|
30
|
+
}));
|
|
31
|
+
return { annotations, totalFound: data.length };
|
|
32
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
|
|
5
5
|
* pubmed-index Phase-1 collector.
|
|
6
6
|
*/
|
|
7
|
-
import { type ArticleDetail, type PubMedSummary, type FullTextResult } from "./ncbi-utils.js";
|
|
8
|
-
export type { PubMedSummary, ArticleDetail, FullTextResult };
|
|
7
|
+
import { type ArticleDetail, type ArticleSection, type PubMedSummary, type FullTextResult } from "./ncbi-utils.js";
|
|
8
|
+
export type { PubMedSummary, ArticleDetail, ArticleSection, FullTextResult };
|
|
9
9
|
export interface SearchOptions {
|
|
10
10
|
maxResults?: number;
|
|
11
11
|
sort?: "relevance" | "date";
|
|
@@ -19,10 +19,63 @@ export declare function searchPubmed(ncbiApiKey: string | undefined, query: stri
|
|
|
19
19
|
totalFound: number;
|
|
20
20
|
results: PubMedSummary[];
|
|
21
21
|
}>;
|
|
22
|
+
export interface DetailOptions {
|
|
23
|
+
/** Authors kept per article before collapsing the rest into `authorCount`. */
|
|
24
|
+
maxAuthors?: number;
|
|
25
|
+
/** MeSH terms kept per article; 0 drops the list entirely. */
|
|
26
|
+
maxMeshTerms?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One article as the tool returns it: `ArticleDetail` with the two unbounded
|
|
30
|
+
* list fields trimmed. A consortium paper can carry hundreds of authors and
|
|
31
|
+
* dozens of MeSH headings — neither is worth the context, so the counts travel
|
|
32
|
+
* instead of the tails.
|
|
33
|
+
*/
|
|
34
|
+
export interface BoundedArticleDetail extends ArticleDetail {
|
|
35
|
+
authorCount: number;
|
|
36
|
+
meshTermCount: number;
|
|
37
|
+
}
|
|
22
38
|
/** Fetch full PubMed article details (efetch + ID Converter for PMC). */
|
|
23
39
|
export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[]): Promise<{
|
|
24
40
|
articles: ArticleDetail[];
|
|
25
41
|
notFound: string[];
|
|
26
42
|
}>;
|
|
27
|
-
/**
|
|
28
|
-
export declare function
|
|
43
|
+
/** Trim the two unbounded list fields on each article, recording their true sizes. */
|
|
44
|
+
export declare function boundArticleDetails(articles: ArticleDetail[], options?: DetailOptions): BoundedArticleDetail[];
|
|
45
|
+
export interface FullTextOptions {
|
|
46
|
+
/** Case-insensitive substring match against section headings. */
|
|
47
|
+
sections?: string[];
|
|
48
|
+
/** Character budget across the returned section bodies. */
|
|
49
|
+
maxChars?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A full text trimmed to a character budget. `outline` always lists EVERY
|
|
53
|
+
* section in the article with its size and whether the body came back, so one
|
|
54
|
+
* call is enough to decide which sections a follow-up should request — the
|
|
55
|
+
* alternative is the agent guessing headings it has not seen.
|
|
56
|
+
*/
|
|
57
|
+
export interface BoundedFullText {
|
|
58
|
+
fullText: string;
|
|
59
|
+
sections: ArticleSection[];
|
|
60
|
+
outline: {
|
|
61
|
+
heading: string;
|
|
62
|
+
chars: number;
|
|
63
|
+
included: boolean;
|
|
64
|
+
}[];
|
|
65
|
+
totalChars: number;
|
|
66
|
+
returnedChars: number;
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fetch open-access full text from PMC, trimmed to a section filter and a
|
|
71
|
+
* character budget. Returns null when the article is not open-access.
|
|
72
|
+
*
|
|
73
|
+
* A research article routinely parses to 40–80k characters, so returning the
|
|
74
|
+
* whole body by default would spend more context on one paper than the entire
|
|
75
|
+
* tool surface costs to attach. Sections are kept whole (a half-sentence of
|
|
76
|
+
* Methods is worse than none) and admitted in document order until the budget
|
|
77
|
+
* would be exceeded.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions): Promise<BoundedFullText | null>;
|
|
80
|
+
/** Apply the section filter and character budget to a parsed full text. */
|
|
81
|
+
export declare function boundFullText(parsed: FullTextResult, options?: FullTextOptions): BoundedFullText;
|
|
@@ -75,8 +75,29 @@ export async function getArticleDetails(ncbiApiKey, pmids) {
|
|
|
75
75
|
const notFound = pmids.filter((id) => !foundPmids.has(id));
|
|
76
76
|
return { articles, notFound };
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
export
|
|
78
|
+
/** Trim the two unbounded list fields on each article, recording their true sizes. */
|
|
79
|
+
export function boundArticleDetails(articles, options = {}) {
|
|
80
|
+
const maxAuthors = options.maxAuthors ?? 5;
|
|
81
|
+
const maxMeshTerms = options.maxMeshTerms ?? 10;
|
|
82
|
+
return articles.map((a) => ({
|
|
83
|
+
...a,
|
|
84
|
+
authors: a.authors.slice(0, maxAuthors),
|
|
85
|
+
authorCount: a.authors.length,
|
|
86
|
+
meshTerms: a.meshTerms.slice(0, maxMeshTerms),
|
|
87
|
+
meshTermCount: a.meshTerms.length,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fetch open-access full text from PMC, trimmed to a section filter and a
|
|
92
|
+
* character budget. Returns null when the article is not open-access.
|
|
93
|
+
*
|
|
94
|
+
* A research article routinely parses to 40–80k characters, so returning the
|
|
95
|
+
* whole body by default would spend more context on one paper than the entire
|
|
96
|
+
* tool surface costs to attach. Sections are kept whole (a half-sentence of
|
|
97
|
+
* Methods is worse than none) and admitted in document order until the budget
|
|
98
|
+
* would be exceeded.
|
|
99
|
+
*/
|
|
100
|
+
export async function getArticleFullText(ncbiApiKey, pmcId, options = {}) {
|
|
80
101
|
const numericId = pmcId.replace(/^PMC/i, "");
|
|
81
102
|
const url = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
|
|
82
103
|
db: "pmc",
|
|
@@ -87,5 +108,38 @@ export async function getArticleFullText(ncbiApiKey, pmcId) {
|
|
|
87
108
|
const result = await apiFetch(url, { parseAs: "text" });
|
|
88
109
|
if (result.isErr())
|
|
89
110
|
throw new Error(describeApiError(result.error));
|
|
90
|
-
|
|
111
|
+
const parsed = parsePmcFullText(result.value);
|
|
112
|
+
if (!parsed)
|
|
113
|
+
return null;
|
|
114
|
+
return boundFullText(parsed, options);
|
|
115
|
+
}
|
|
116
|
+
/** Apply the section filter and character budget to a parsed full text. */
|
|
117
|
+
export function boundFullText(parsed, options = {}) {
|
|
118
|
+
const maxChars = options.maxChars ?? 12_000;
|
|
119
|
+
const needles = (options.sections ?? []).map((s) => s.toLowerCase()).filter(Boolean);
|
|
120
|
+
const totalChars = parsed.sections.reduce((n, s) => n + s.text.length, 0);
|
|
121
|
+
const outline = [];
|
|
122
|
+
const kept = [];
|
|
123
|
+
let returnedChars = 0;
|
|
124
|
+
for (const section of parsed.sections) {
|
|
125
|
+
const matchesFilter = needles.length === 0 || needles.some((n) => section.heading.toLowerCase().includes(n));
|
|
126
|
+
// A section is admitted whole or not at all. The first section is always
|
|
127
|
+
// admitted even if it alone busts the budget — a single-section article
|
|
128
|
+
// would otherwise come back empty with no way to ask for less.
|
|
129
|
+
const fits = returnedChars + section.text.length <= maxChars || kept.length === 0;
|
|
130
|
+
const included = matchesFilter && fits;
|
|
131
|
+
if (included) {
|
|
132
|
+
kept.push(section);
|
|
133
|
+
returnedChars += section.text.length;
|
|
134
|
+
}
|
|
135
|
+
outline.push({ heading: section.heading, chars: section.text.length, included });
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
fullText: kept.map((s) => (s.heading ? `## ${s.heading}\n\n${s.text}` : s.text)).join("\n\n"),
|
|
139
|
+
sections: kept,
|
|
140
|
+
outline,
|
|
141
|
+
totalChars,
|
|
142
|
+
returnedChars,
|
|
143
|
+
truncated: returnedChars < totalChars,
|
|
144
|
+
};
|
|
91
145
|
}
|
|
@@ -27,7 +27,28 @@ export interface InteractionOptions {
|
|
|
27
27
|
}
|
|
28
28
|
/** Fetch interaction partners (one-hop) for the given identifiers. */
|
|
29
29
|
export declare function getInteractionPartners(identifiers: string[], options?: InteractionOptions): Promise<StringInteraction[]>;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Fetch the network among the given identifiers.
|
|
32
|
+
*
|
|
33
|
+
* STRING has no server-side row cap on `/network` — edge count grows with the
|
|
34
|
+
* square of the input set, so `limit` truncates the score-sorted rows here.
|
|
35
|
+
* `totalEdges` reports the true count so a caller can tell a trimmed network
|
|
36
|
+
* from a sparse one.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getInteractionNetwork(identifiers: string[], options?: InteractionOptions): Promise<{
|
|
39
|
+
interactions: StringInteraction[];
|
|
40
|
+
totalEdges: number;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Functional enrichment for a gene set.
|
|
44
|
+
*
|
|
45
|
+
* STRING returns every enriched term across all categories (GO, KEGG,
|
|
46
|
+
* Reactome, Pfam, …) with the full member-gene list per term, so `limit`
|
|
47
|
+
* truncates the FDR-sorted rows and `totalTerms` reports what was there.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getEnrichment(identifiers: string[], species?: number, options?: {
|
|
50
|
+
limit?: number;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
enrichment: StringEnrichment[];
|
|
53
|
+
totalTerms: number;
|
|
54
|
+
}>;
|
|
@@ -73,7 +73,14 @@ export async function getInteractionPartners(identifiers, options = {}) {
|
|
|
73
73
|
throw new Error(describeApiError(res.error));
|
|
74
74
|
return res.value.sort((a, b) => b.score - a.score);
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Fetch the network among the given identifiers.
|
|
78
|
+
*
|
|
79
|
+
* STRING has no server-side row cap on `/network` — edge count grows with the
|
|
80
|
+
* square of the input set, so `limit` truncates the score-sorted rows here.
|
|
81
|
+
* `totalEdges` reports the true count so a caller can tell a trimmed network
|
|
82
|
+
* from a sparse one.
|
|
83
|
+
*/
|
|
77
84
|
export async function getInteractionNetwork(identifiers, options = {}) {
|
|
78
85
|
const params = buildParams(identifiers, options.species ?? 9606, {
|
|
79
86
|
required_score: options.minScore ?? 400,
|
|
@@ -81,13 +88,23 @@ export async function getInteractionNetwork(identifiers, options = {}) {
|
|
|
81
88
|
const res = await apiFetchValidated(`${STRING_BASE}/network?${params}`, z.array(StringInteractionSchema));
|
|
82
89
|
if (res.isErr())
|
|
83
90
|
throw new Error(describeApiError(res.error));
|
|
84
|
-
|
|
91
|
+
const sorted = res.value.sort((a, b) => b.score - a.score);
|
|
92
|
+
const limit = options.limit ?? sorted.length;
|
|
93
|
+
return { interactions: sorted.slice(0, limit), totalEdges: sorted.length };
|
|
85
94
|
}
|
|
86
|
-
/**
|
|
87
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Functional enrichment for a gene set.
|
|
97
|
+
*
|
|
98
|
+
* STRING returns every enriched term across all categories (GO, KEGG,
|
|
99
|
+
* Reactome, Pfam, …) with the full member-gene list per term, so `limit`
|
|
100
|
+
* truncates the FDR-sorted rows and `totalTerms` reports what was there.
|
|
101
|
+
*/
|
|
102
|
+
export async function getEnrichment(identifiers, species = 9606, options = {}) {
|
|
88
103
|
const params = buildParams(identifiers, species);
|
|
89
104
|
const res = await apiFetchValidated(`${STRING_BASE}/enrichment?${params}`, z.array(StringEnrichmentSchema));
|
|
90
105
|
if (res.isErr())
|
|
91
106
|
throw new Error(describeApiError(res.error));
|
|
92
|
-
|
|
107
|
+
const sorted = res.value.sort((a, b) => a.fdr - b.fdr);
|
|
108
|
+
const limit = options.limit ?? sorted.length;
|
|
109
|
+
return { enrichment: sorted.slice(0, limit), totalTerms: sorted.length };
|
|
93
110
|
}
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* database already holds, and it would lose fidelity at every hop.
|
|
12
12
|
*
|
|
13
13
|
* The tool drives an internal "planner" agent that communicates outcomes
|
|
14
|
-
* EXCLUSIVELY via
|
|
14
|
+
* EXCLUSIVELY via three terminal tool calls:
|
|
15
15
|
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* - submit_plan(plan) → terminal success: re-validates + persists
|
|
16
|
+
* - submit_plan(plan) → terminal success: re-validates + persists;
|
|
17
|
+
* rejected candidates return structured issues
|
|
19
18
|
* - request_clarification(…) → terminal: planner needs more context
|
|
20
19
|
* - report_blocker(reason) → terminal: no viable plan
|
|
21
20
|
*
|
|
@@ -29,19 +28,27 @@ import { type ChatProvider } from "../../providers/types.js";
|
|
|
29
28
|
import { type Tool } from "../define-tool.js";
|
|
30
29
|
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
31
30
|
import { type ResourcePolicy } from "../../config/resource-limits.js";
|
|
31
|
+
import type { Logger } from "../../lib/logger.js";
|
|
32
32
|
export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The conversation-role backend the planner is pinned to. Keeping the
|
|
35
|
+
* provider and its model label in one value prevents composition roots from
|
|
36
|
+
* accidentally pairing the planner with the sandbox or utility role.
|
|
37
|
+
*/
|
|
38
|
+
readonly conversation: {
|
|
39
|
+
readonly provider: ChatProvider;
|
|
40
|
+
readonly model: string;
|
|
41
|
+
};
|
|
35
42
|
/** Database pool — plan persistence and prior-plan loading. */
|
|
36
43
|
readonly pool: Pool;
|
|
37
|
-
/** Model id — provenance / metric label; the provider owns the wire model. */
|
|
38
|
-
readonly model: string;
|
|
39
44
|
/**
|
|
40
45
|
* Host resource policy — stated to the planner as concrete per-step
|
|
41
|
-
* ceilings and enforced by `
|
|
46
|
+
* ceilings and enforced by `submit_plan`. Absent, the prompt keeps its
|
|
42
47
|
* default guidance and validation skips the ceiling check.
|
|
43
48
|
*/
|
|
44
49
|
readonly resourcePolicy?: ResourcePolicy;
|
|
50
|
+
/** Diagnostic sink. Absent, the tool runs silently — see `RunAgentOptions.logger`. */
|
|
51
|
+
readonly logger?: Logger;
|
|
45
52
|
}
|
|
46
53
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
47
54
|
export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
|