@inflexa-ai/harness 0.48.3 → 0.48.5
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 +5 -1
- package/dist/agents/sandbox/index.js +3 -0
- package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
- package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/shared.js +3 -1
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn-workflow.js +1 -0
- package/dist/app/chat-turn.d.ts +4 -2
- package/dist/app/chat-turn.js +46 -46
- package/dist/app/work.d.ts +7 -3
- package/dist/app/work.js +17 -16
- package/dist/contracts/chat-parts.d.ts +46 -5
- package/dist/contracts/chat-parts.js +2 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +168 -4
- package/dist/contracts/schemas/chat-parts.js +21 -1
- package/dist/execution/run-event-stream.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
- package/dist/lib/slug.d.ts +18 -0
- package/dist/lib/slug.js +29 -0
- package/dist/lib/test/pdf-fixture.d.ts +5 -0
- package/dist/lib/test/pdf-fixture.js +14 -0
- package/dist/loop/chat-stream.d.ts +4 -1
- package/dist/loop/chat-stream.js +30 -13
- package/dist/loop/run-loop.js +23 -35
- package/dist/loop/sub-agent.d.ts +2 -2
- package/dist/loop/sub-agent.js +1 -1
- package/dist/loop/test/metric-reader.d.ts +9 -0
- package/dist/loop/test/metric-reader.js +33 -0
- package/dist/loop/test/unnamed-model.d.ts +6 -0
- package/dist/loop/test/unnamed-model.js +24 -0
- package/dist/memory/conversation-display-recorder.d.ts +2 -0
- package/dist/memory/conversation-display-recorder.js +6 -2
- package/dist/memory/conversation-display-storage.d.ts +4 -2
- package/dist/memory/conversation-display-storage.js +19 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +6 -4
- package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
- package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +4 -0
- package/dist/providers/errors.js +2 -1
- package/dist/report-model/fixture-resolver.js +9 -135
- package/dist/report-model/production-resolver.d.ts +3 -3
- package/dist/report-model/production-resolver.js +12 -127
- package/dist/report-model/reference-read.d.ts +38 -0
- package/dist/report-model/reference-read.js +149 -0
- package/dist/report-model/reference-resolver.d.ts +7 -0
- package/dist/report-model/reference-resolver.js +1 -1
- package/dist/report-render/chart.js +48 -46
- package/dist/report-render/figures/common.d.ts +34 -0
- package/dist/report-render/figures/common.js +4 -1
- package/dist/report-render/figures/equal-units.js +2 -4
- package/dist/report-render/figures/sankey.js +1 -3
- package/dist/report-render/figures/upset.js +2 -5
- package/dist/runtime/assemble.js +2 -1
- package/dist/sandbox/execution-id.js +2 -14
- package/dist/sandbox/leases.js +46 -9
- package/dist/schemas/validate-plan.js +2 -14
- package/dist/state/active-turns.d.ts +6 -2
- package/dist/state/active-turns.js +11 -7
- package/dist/state/data-profile.d.ts +8 -0
- package/dist/state/data-profile.js +18 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/paused-work.d.ts +5 -3
- package/dist/state/paused-work.js +11 -7
- package/dist/state/runs.d.ts +3 -2
- package/dist/state/runs.js +4 -3
- package/dist/tasks/data-profile-index.js +3 -17
- package/dist/tasks/data-profile-resolve.js +3 -17
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/bio/humanbase.d.ts +32 -0
- package/dist/tools/bio/humanbase.js +180 -0
- package/dist/tools/bio/index.d.ts +3 -1
- package/dist/tools/bio/index.js +3 -1
- package/dist/tools/bio/resolve-gene-ids.d.ts +13 -0
- package/dist/tools/bio/resolve-gene-ids.js +67 -0
- package/dist/tools/lib/humanbase-client.d.ts +215 -0
- package/dist/tools/lib/humanbase-client.js +187 -0
- package/dist/tools/lib/mygene-client.d.ts +150 -0
- package/dist/tools/lib/mygene-client.js +109 -0
- package/dist/tools/lib/opentargets-client.js +15 -25
- package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
- package/dist/tools/report-authoring/authoring-tools.js +7 -6
- package/dist/tools/research/generate-analogy-report.js +10 -2
- package/dist/tools/script/run-script.js +48 -51
- package/dist/workflows/execute-analysis.js +4 -3
- package/dist/workflows/pause.d.ts +3 -3
- package/dist/workflows/pause.js +15 -9
- package/dist/workflows/sandbox-step.js +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for HumanBase (Flatiron Institute), which serves
|
|
3
|
+
* the GIANT functional networks: one network for each tissue, cell type, or
|
|
4
|
+
* biological-process context (Greene et al., Nature Genetics 2015).
|
|
5
|
+
*
|
|
6
|
+
* Absence policy: HumanBase keeps the key of an absent value and sends an
|
|
7
|
+
* explicit `null` — `mincut` of a v3 network, and `term` and `version` of a
|
|
8
|
+
* context. Thus a maybe-absent field carries `.nullable()`, not `.optional()`.
|
|
9
|
+
*
|
|
10
|
+
* HumanBase asks for no key and no caller identity. The shared
|
|
11
|
+
* `HARNESS_USER_AGENT` of `apiFetch` names the caller.
|
|
12
|
+
*
|
|
13
|
+
* The GIANT version is a property of the slug: each v3 slug ends in `-v3`, and
|
|
14
|
+
* no v1 slug does. The network endpoint refuses a `giant_version` that disagrees
|
|
15
|
+
* with the slug (a 400 for a v3 slug under v1, a 404 for a v1 slug under v3),
|
|
16
|
+
* thus the client derives the version from the slug and no caller states it.
|
|
17
|
+
*
|
|
18
|
+
* Only an HTTP 404 is an expected absence (an unknown slug, or no queried gene in
|
|
19
|
+
* the network). A 400 carries the message of a refused request, thus it is not
|
|
20
|
+
* read as "nothing there" — `isUnexpectedApiError` would read every 4xx that way.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
24
|
+
const HUMANBASE_BASE = "https://hb.flatironinstitute.org/api/integrations";
|
|
25
|
+
/** The network endpoint refuses a larger query gene set with a 400. */
|
|
26
|
+
export const HUMANBASE_MAX_NETWORK_GENES = 30;
|
|
27
|
+
const V3_SLUG_SUFFIX = "-v3";
|
|
28
|
+
/** The GIANT version of a context slug. */
|
|
29
|
+
export function giantVersionOf(slug) {
|
|
30
|
+
return slug.endsWith(V3_SLUG_SUFFIX) ? "v3" : "v1";
|
|
31
|
+
}
|
|
32
|
+
const ContextSchema = z.object({
|
|
33
|
+
title: z.string(),
|
|
34
|
+
term: z
|
|
35
|
+
.object({
|
|
36
|
+
identifier: z.string(),
|
|
37
|
+
database: z.object({ name: z.string() }),
|
|
38
|
+
})
|
|
39
|
+
.nullable(),
|
|
40
|
+
version: z.object({ identifier: z.string() }).nullable(),
|
|
41
|
+
});
|
|
42
|
+
function toContext(slug, context) {
|
|
43
|
+
return {
|
|
44
|
+
slug,
|
|
45
|
+
title: context.title,
|
|
46
|
+
giantVersion: giantVersionOf(slug),
|
|
47
|
+
term: context.term === null ? null : { id: context.term.identifier, ontology: context.term.database.name },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** One row of `GET /api/integrations/`. */
|
|
51
|
+
export const HumanBaseIntegrationSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
slug: z.string(),
|
|
54
|
+
context: ContextSchema,
|
|
55
|
+
})
|
|
56
|
+
.transform((d) => toContext(d.slug, d.context));
|
|
57
|
+
/** One row of `POST /api/integrations/relevant/`. */
|
|
58
|
+
export const HumanBaseRelevantSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
slug: z.string(),
|
|
61
|
+
context: ContextSchema,
|
|
62
|
+
score: z.number(),
|
|
63
|
+
})
|
|
64
|
+
.transform((d) => ({ ...toContext(d.slug, d.context), score: d.score }));
|
|
65
|
+
const XrefSchema = z.object({
|
|
66
|
+
xrid: z.string(),
|
|
67
|
+
crossrefdb: z.object({ name: z.string() }),
|
|
68
|
+
});
|
|
69
|
+
const NetworkGeneSchema = z.object({
|
|
70
|
+
entrez: z.number().int(),
|
|
71
|
+
standard_name: z.string(),
|
|
72
|
+
description: z.string(),
|
|
73
|
+
xrefs: z.array(XrefSchema),
|
|
74
|
+
query: z.boolean(),
|
|
75
|
+
});
|
|
76
|
+
/** An edge names its two genes by their index in `genes`, not by an Entrez ID. */
|
|
77
|
+
const NetworkEdgeSchema = z.object({
|
|
78
|
+
source: z.number().int().nonnegative(),
|
|
79
|
+
target: z.number().int().nonnegative(),
|
|
80
|
+
weight: z.number(),
|
|
81
|
+
});
|
|
82
|
+
function xrefOf(xrefs, database) {
|
|
83
|
+
return xrefs.find((xref) => xref.crossrefdb.name === database)?.xrid ?? null;
|
|
84
|
+
}
|
|
85
|
+
/** The body of `GET /api/integrations/{slug}/network/`, with each edge resolved to the symbols of its genes. */
|
|
86
|
+
export const HumanBaseNetworkSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
mincut: z.number().nullable(),
|
|
89
|
+
genes: z.array(NetworkGeneSchema),
|
|
90
|
+
edges: z.array(NetworkEdgeSchema),
|
|
91
|
+
})
|
|
92
|
+
.superRefine((d, ctx) => {
|
|
93
|
+
for (const [index, edge] of d.edges.entries()) {
|
|
94
|
+
if (edge.source >= d.genes.length || edge.target >= d.genes.length) {
|
|
95
|
+
ctx.addIssue({ code: "custom", path: ["edges", index], message: "an edge names a gene index outside genes" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.transform((d) => {
|
|
100
|
+
const genes = d.genes.map((gene) => ({
|
|
101
|
+
entrez: gene.entrez,
|
|
102
|
+
symbol: gene.standard_name,
|
|
103
|
+
description: gene.description,
|
|
104
|
+
ensembl: xrefOf(gene.xrefs, "Ensembl"),
|
|
105
|
+
hgnc: xrefOf(gene.xrefs, "HGNC"),
|
|
106
|
+
query: gene.query,
|
|
107
|
+
}));
|
|
108
|
+
const symbolAt = (index) => genes[index]?.symbol ?? "";
|
|
109
|
+
const edges = d.edges.map((edge) => ({ geneA: symbolAt(edge.source), geneB: symbolAt(edge.target), weight: edge.weight }));
|
|
110
|
+
return { mincut: d.mincut, genes, edges };
|
|
111
|
+
});
|
|
112
|
+
function isNotFound(error) {
|
|
113
|
+
return error.type === "http_status" && error.status === 404;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Each context that HumanBase serves, filtered by a case-insensitive substring of
|
|
117
|
+
* the slug or the title. The list holds about 600 rows, thus `limit` trims it and
|
|
118
|
+
* `totalFound` reports the count of the filtered list.
|
|
119
|
+
*/
|
|
120
|
+
export async function listContexts(options = {}) {
|
|
121
|
+
const res = await apiFetchValidated(`${HUMANBASE_BASE}/`, z.array(HumanBaseIntegrationSchema));
|
|
122
|
+
if (res.isErr())
|
|
123
|
+
throw new Error(describeApiError(res.error));
|
|
124
|
+
const needle = options.query?.trim().toLowerCase() ?? "";
|
|
125
|
+
const matched = needle === "" ? res.value : res.value.filter((c) => c.slug.toLowerCase().includes(needle) || c.title.toLowerCase().includes(needle));
|
|
126
|
+
const limit = options.limit ?? matched.length;
|
|
127
|
+
return { contexts: matched.slice(0, limit), totalFound: matched.length };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The contexts ranked by their relevance to a gene set, best first.
|
|
131
|
+
*
|
|
132
|
+
* HumanBase ranks the v1 contexts only, and it gives an empty list when it knows
|
|
133
|
+
* none of the genes. The endpoint takes each Entrez ID as a JSON string and
|
|
134
|
+
* answers a JSON number with a 500.
|
|
135
|
+
*/
|
|
136
|
+
export async function getRelevantContexts(entrez, options = {}) {
|
|
137
|
+
const res = await apiFetchValidated(`${HUMANBASE_BASE}/relevant/`, z.array(HumanBaseRelevantSchema), {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "content-type": "application/json" },
|
|
140
|
+
body: JSON.stringify({ entrez: entrez.map(String) }),
|
|
141
|
+
});
|
|
142
|
+
if (res.isErr()) {
|
|
143
|
+
// The published schema of the endpoint documents a 404 for an unknown gene.
|
|
144
|
+
if (isNotFound(res.error))
|
|
145
|
+
return { contexts: [], totalFound: 0 };
|
|
146
|
+
throw new Error(describeApiError(res.error));
|
|
147
|
+
}
|
|
148
|
+
const sorted = res.value.sort((a, b) => b.score - a.score);
|
|
149
|
+
const limit = options.limit ?? sorted.length;
|
|
150
|
+
return { contexts: sorted.slice(0, limit), totalFound: sorted.length };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The functional subnetwork of the query genes in one context, plus `nodeSize`
|
|
154
|
+
* neighbor genes that HumanBase adds. The answer holds an edge for each gene
|
|
155
|
+
* pair, thus the edge count grows with the square of the gene count: `limit`
|
|
156
|
+
* trims the weight-sorted edges, and `totalEdges` reports the true count.
|
|
157
|
+
*
|
|
158
|
+
* Gives `null` on an HTTP 404: the slug is unknown, or no query gene is in the
|
|
159
|
+
* network.
|
|
160
|
+
*/
|
|
161
|
+
export async function getNetwork(slug, entrez, options = {}) {
|
|
162
|
+
const giantVersion = giantVersionOf(slug);
|
|
163
|
+
const params = new URLSearchParams();
|
|
164
|
+
for (const id of entrez)
|
|
165
|
+
params.append("entrez", String(id));
|
|
166
|
+
if (options.nodeSize !== undefined)
|
|
167
|
+
params.set("node_size", String(options.nodeSize));
|
|
168
|
+
params.set("giant_version", giantVersion);
|
|
169
|
+
const res = await apiFetchValidated(`${HUMANBASE_BASE}/${encodeURIComponent(slug)}/network/?${params}`, HumanBaseNetworkSchema);
|
|
170
|
+
if (res.isErr()) {
|
|
171
|
+
if (isNotFound(res.error))
|
|
172
|
+
return null;
|
|
173
|
+
throw new Error(describeApiError(res.error));
|
|
174
|
+
}
|
|
175
|
+
const returned = new Set(res.value.genes.map((gene) => gene.entrez));
|
|
176
|
+
const edges = res.value.edges.sort((a, b) => b.weight - a.weight);
|
|
177
|
+
const limit = options.limit ?? edges.length;
|
|
178
|
+
return {
|
|
179
|
+
context: slug,
|
|
180
|
+
giantVersion,
|
|
181
|
+
mincut: res.value.mincut,
|
|
182
|
+
genes: res.value.genes,
|
|
183
|
+
edges: edges.slice(0, limit),
|
|
184
|
+
totalEdges: edges.length,
|
|
185
|
+
notFound: entrez.filter((id) => !returned.has(id)),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for MyGene.info (BioThings, Scripps), which maps a
|
|
3
|
+
* gene across the identifier spaces of its sources: the HGNC symbol, the NCBI
|
|
4
|
+
* Entrez Gene ID, the Ensembl gene ID, and the UniProt accession (Xin et al.,
|
|
5
|
+
* Genome Biology 2016).
|
|
6
|
+
*
|
|
7
|
+
* Absence policy: MyGene omits the key of an absent value and never sends an
|
|
8
|
+
* explicit `null`. Every field of a hit can be absent — an Ensembl-only gene has
|
|
9
|
+
* no `symbol`, `name`, or `entrezgene`, and a noncoding gene has no `uniprot` —
|
|
10
|
+
* thus each field carries `.optional()`.
|
|
11
|
+
*
|
|
12
|
+
* Two fields change their shape with their count: `ensembl` is one object or an
|
|
13
|
+
* array of objects (a gene on alternate haplotypes, or a mouse gene with strain
|
|
14
|
+
* copies), and `uniprot.Swiss-Prot` is one string or an array. The client
|
|
15
|
+
* normalizes both to an array, in the wire order, which carries no rank.
|
|
16
|
+
*
|
|
17
|
+
* A miss is no error: the batch answers HTTP 200, and it marks the miss with
|
|
18
|
+
* `notfound: true`.
|
|
19
|
+
*
|
|
20
|
+
* MyGene cuts the hits of each identifier at `size`, 10 by default, and the plain
|
|
21
|
+
* batch answer gives no sign of the cut. Thus the client sends `size` and
|
|
22
|
+
* `with_total`, and the answer is then an object: `max_total`, the largest hit
|
|
23
|
+
* count of one identifier before the cut, and `hits`, the rows. An unknown species answers HTTP 400 with a message, which the
|
|
24
|
+
* caller sees as an error.
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
/** The most hits that one identifier gives. A UniProt accession that a gene family shares gives a hit for each member. */
|
|
28
|
+
export declare const MYGENE_HITS_PER_ID = 100;
|
|
29
|
+
export type MyGeneHit = {
|
|
30
|
+
/** The identifier of the request that this hit answers. */
|
|
31
|
+
query: string;
|
|
32
|
+
symbol: string | null;
|
|
33
|
+
/** The full name of the gene. */
|
|
34
|
+
name: string | null;
|
|
35
|
+
entrez: number | null;
|
|
36
|
+
ensembl: string[];
|
|
37
|
+
/** The reviewed (Swiss-Prot) UniProt accessions. */
|
|
38
|
+
uniprot: string[];
|
|
39
|
+
};
|
|
40
|
+
export type MyGeneRow = {
|
|
41
|
+
kind: "hit";
|
|
42
|
+
hit: MyGeneHit;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "miss";
|
|
45
|
+
query: string;
|
|
46
|
+
};
|
|
47
|
+
export type GeneIdResolution = {
|
|
48
|
+
genes: MyGeneHit[];
|
|
49
|
+
/** Each identifier that matched no gene. */
|
|
50
|
+
notFound: string[];
|
|
51
|
+
/** Each identifier that matched more than one gene. Each of its genes is in `genes`. */
|
|
52
|
+
ambiguous: string[];
|
|
53
|
+
/** Each identifier whose hits were cut at `MYGENE_HITS_PER_ID`. `genes` holds the first of them only. */
|
|
54
|
+
truncated: string[];
|
|
55
|
+
};
|
|
56
|
+
/** One row of the batch answer of `POST /v3/query`: a hit, or the miss of one query. */
|
|
57
|
+
export declare const MyGeneBatchRowSchema: z.ZodPipe<z.ZodUnion<readonly [z.ZodObject<{
|
|
58
|
+
query: z.ZodString;
|
|
59
|
+
notfound: z.ZodLiteral<true>;
|
|
60
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
61
|
+
query: z.ZodString;
|
|
62
|
+
_id: z.ZodString;
|
|
63
|
+
symbol: z.ZodOptional<z.ZodString>;
|
|
64
|
+
name: z.ZodOptional<z.ZodString>;
|
|
65
|
+
entrezgene: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>>;
|
|
66
|
+
ensembl: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
67
|
+
gene: z.ZodString;
|
|
68
|
+
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
|
|
69
|
+
gene: z.ZodString;
|
|
70
|
+
}, z.core.$strip>>]>>;
|
|
71
|
+
uniprot: z.ZodOptional<z.ZodObject<{
|
|
72
|
+
"Swiss-Prot": z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>]>, z.ZodTransform<{
|
|
75
|
+
kind: "hit";
|
|
76
|
+
hit: MyGeneHit;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "miss";
|
|
79
|
+
query: string;
|
|
80
|
+
}, {
|
|
81
|
+
query: string;
|
|
82
|
+
notfound: true;
|
|
83
|
+
} | {
|
|
84
|
+
query: string;
|
|
85
|
+
_id: string;
|
|
86
|
+
symbol?: string | undefined;
|
|
87
|
+
name?: string | undefined;
|
|
88
|
+
entrezgene?: number | null | undefined;
|
|
89
|
+
ensembl?: {
|
|
90
|
+
gene: string;
|
|
91
|
+
}[] | {
|
|
92
|
+
gene: string;
|
|
93
|
+
} | undefined;
|
|
94
|
+
uniprot?: {
|
|
95
|
+
"Swiss-Prot"?: string | string[] | undefined;
|
|
96
|
+
} | undefined;
|
|
97
|
+
}>>;
|
|
98
|
+
/** The batch answer of `POST /v3/query` with `with_total`. */
|
|
99
|
+
export declare const MyGeneBatchAnswerSchema: z.ZodObject<{
|
|
100
|
+
max_total: z.ZodNumber;
|
|
101
|
+
hits: z.ZodArray<z.ZodPipe<z.ZodUnion<readonly [z.ZodObject<{
|
|
102
|
+
query: z.ZodString;
|
|
103
|
+
notfound: z.ZodLiteral<true>;
|
|
104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
105
|
+
query: z.ZodString;
|
|
106
|
+
_id: z.ZodString;
|
|
107
|
+
symbol: z.ZodOptional<z.ZodString>;
|
|
108
|
+
name: z.ZodOptional<z.ZodString>;
|
|
109
|
+
entrezgene: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>>;
|
|
110
|
+
ensembl: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
111
|
+
gene: z.ZodString;
|
|
112
|
+
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
|
|
113
|
+
gene: z.ZodString;
|
|
114
|
+
}, z.core.$strip>>]>>;
|
|
115
|
+
uniprot: z.ZodOptional<z.ZodObject<{
|
|
116
|
+
"Swiss-Prot": z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
117
|
+
}, z.core.$strip>>;
|
|
118
|
+
}, z.core.$strip>]>, z.ZodTransform<{
|
|
119
|
+
kind: "hit";
|
|
120
|
+
hit: MyGeneHit;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "miss";
|
|
123
|
+
query: string;
|
|
124
|
+
}, {
|
|
125
|
+
query: string;
|
|
126
|
+
notfound: true;
|
|
127
|
+
} | {
|
|
128
|
+
query: string;
|
|
129
|
+
_id: string;
|
|
130
|
+
symbol?: string | undefined;
|
|
131
|
+
name?: string | undefined;
|
|
132
|
+
entrezgene?: number | null | undefined;
|
|
133
|
+
ensembl?: {
|
|
134
|
+
gene: string;
|
|
135
|
+
}[] | {
|
|
136
|
+
gene: string;
|
|
137
|
+
} | undefined;
|
|
138
|
+
uniprot?: {
|
|
139
|
+
"Swiss-Prot"?: string | string[] | undefined;
|
|
140
|
+
} | undefined;
|
|
141
|
+
}>>>;
|
|
142
|
+
}, z.core.$strip>;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a batch of gene identifiers, mixed freely, in one request. A duplicate
|
|
145
|
+
* identifier is sent one time, and the order of the input is kept.
|
|
146
|
+
*
|
|
147
|
+
* `species` is a common name that MyGene knows ('human', 'mouse', 'rat',
|
|
148
|
+
* 'zebrafish', …) or an NCBI Taxonomy ID as a string ('9606').
|
|
149
|
+
*/
|
|
150
|
+
export declare function resolveGeneIds(identifiers: readonly string[], species: string): Promise<GeneIdResolution>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure async client functions for MyGene.info (BioThings, Scripps), which maps a
|
|
3
|
+
* gene across the identifier spaces of its sources: the HGNC symbol, the NCBI
|
|
4
|
+
* Entrez Gene ID, the Ensembl gene ID, and the UniProt accession (Xin et al.,
|
|
5
|
+
* Genome Biology 2016).
|
|
6
|
+
*
|
|
7
|
+
* Absence policy: MyGene omits the key of an absent value and never sends an
|
|
8
|
+
* explicit `null`. Every field of a hit can be absent — an Ensembl-only gene has
|
|
9
|
+
* no `symbol`, `name`, or `entrezgene`, and a noncoding gene has no `uniprot` —
|
|
10
|
+
* thus each field carries `.optional()`.
|
|
11
|
+
*
|
|
12
|
+
* Two fields change their shape with their count: `ensembl` is one object or an
|
|
13
|
+
* array of objects (a gene on alternate haplotypes, or a mouse gene with strain
|
|
14
|
+
* copies), and `uniprot.Swiss-Prot` is one string or an array. The client
|
|
15
|
+
* normalizes both to an array, in the wire order, which carries no rank.
|
|
16
|
+
*
|
|
17
|
+
* A miss is no error: the batch answers HTTP 200, and it marks the miss with
|
|
18
|
+
* `notfound: true`.
|
|
19
|
+
*
|
|
20
|
+
* MyGene cuts the hits of each identifier at `size`, 10 by default, and the plain
|
|
21
|
+
* batch answer gives no sign of the cut. Thus the client sends `size` and
|
|
22
|
+
* `with_total`, and the answer is then an object: `max_total`, the largest hit
|
|
23
|
+
* count of one identifier before the cut, and `hits`, the rows. An unknown species answers HTTP 400 with a message, which the
|
|
24
|
+
* caller sees as an error.
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { apiFetchValidated, describeApiError, zWireNumber } from "./api-utils.js";
|
|
28
|
+
const MYGENE_QUERY_URL = "https://mygene.info/v3/query";
|
|
29
|
+
/**
|
|
30
|
+
* The identifier spaces that a query matches. `alias` is not one of them: an old
|
|
31
|
+
* or informal symbol matches several current genes with an equal score, and no
|
|
32
|
+
* rule picks one.
|
|
33
|
+
*/
|
|
34
|
+
const SCOPES = "symbol,entrezgene,ensembl.gene,uniprot";
|
|
35
|
+
const FIELDS = "symbol,name,entrezgene,ensembl.gene,uniprot.Swiss-Prot";
|
|
36
|
+
/** The most hits that one identifier gives. A UniProt accession that a gene family shares gives a hit for each member. */
|
|
37
|
+
export const MYGENE_HITS_PER_ID = 100;
|
|
38
|
+
function asArray(value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
return Array.isArray(value) ? value : [value];
|
|
42
|
+
}
|
|
43
|
+
const EnsemblRefSchema = z.object({ gene: z.string() });
|
|
44
|
+
const MissRowSchema = z.object({ query: z.string(), notfound: z.literal(true) });
|
|
45
|
+
const HitRowSchema = z.object({
|
|
46
|
+
query: z.string(),
|
|
47
|
+
_id: z.string(),
|
|
48
|
+
symbol: z.string().optional(),
|
|
49
|
+
name: z.string().optional(),
|
|
50
|
+
entrezgene: zWireNumber.optional(),
|
|
51
|
+
ensembl: z.union([EnsemblRefSchema, z.array(EnsemblRefSchema)]).optional(),
|
|
52
|
+
uniprot: z.object({ "Swiss-Prot": z.union([z.string(), z.array(z.string())]).optional() }).optional(),
|
|
53
|
+
});
|
|
54
|
+
/** One row of the batch answer of `POST /v3/query`: a hit, or the miss of one query. */
|
|
55
|
+
export const MyGeneBatchRowSchema = z.union([MissRowSchema, HitRowSchema]).transform((row) => {
|
|
56
|
+
if ("notfound" in row)
|
|
57
|
+
return { kind: "miss", query: row.query };
|
|
58
|
+
return {
|
|
59
|
+
kind: "hit",
|
|
60
|
+
hit: {
|
|
61
|
+
query: row.query,
|
|
62
|
+
symbol: row.symbol ?? null,
|
|
63
|
+
name: row.name ?? null,
|
|
64
|
+
entrez: row.entrezgene ?? null,
|
|
65
|
+
ensembl: asArray(row.ensembl).map((ref) => ref.gene),
|
|
66
|
+
uniprot: asArray(row.uniprot?.["Swiss-Prot"]),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
/** The batch answer of `POST /v3/query` with `with_total`. */
|
|
71
|
+
export const MyGeneBatchAnswerSchema = z.object({
|
|
72
|
+
max_total: z.number(),
|
|
73
|
+
hits: z.array(MyGeneBatchRowSchema),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a batch of gene identifiers, mixed freely, in one request. A duplicate
|
|
77
|
+
* identifier is sent one time, and the order of the input is kept.
|
|
78
|
+
*
|
|
79
|
+
* `species` is a common name that MyGene knows ('human', 'mouse', 'rat',
|
|
80
|
+
* 'zebrafish', …) or an NCBI Taxonomy ID as a string ('9606').
|
|
81
|
+
*/
|
|
82
|
+
export async function resolveGeneIds(identifiers, species) {
|
|
83
|
+
const queries = [...new Set(identifiers.map((id) => id.trim()).filter((id) => id !== ""))];
|
|
84
|
+
if (queries.length === 0)
|
|
85
|
+
return { genes: [], notFound: [], ambiguous: [], truncated: [] };
|
|
86
|
+
const res = await apiFetchValidated(MYGENE_QUERY_URL, MyGeneBatchAnswerSchema, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ q: queries, scopes: SCOPES, fields: FIELDS, species, size: MYGENE_HITS_PER_ID, with_total: true }),
|
|
90
|
+
});
|
|
91
|
+
if (res.isErr())
|
|
92
|
+
throw new Error(describeApiError(res.error));
|
|
93
|
+
const genes = [];
|
|
94
|
+
const notFound = [];
|
|
95
|
+
const hitCount = new Map();
|
|
96
|
+
for (const row of res.value.hits) {
|
|
97
|
+
if (row.kind === "miss") {
|
|
98
|
+
notFound.push(row.query);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
genes.push(row.hit);
|
|
102
|
+
hitCount.set(row.hit.query, (hitCount.get(row.hit.query) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
const ambiguous = [...hitCount].filter(([, count]) => count > 1).map(([query]) => query);
|
|
105
|
+
// `max_total` is one number for the whole batch. Past the cap, each identifier that filled the cap can be the one
|
|
106
|
+
// that was cut, thus each such identifier counts as cut.
|
|
107
|
+
const truncated = res.value.max_total > MYGENE_HITS_PER_ID ? [...hitCount].filter(([, count]) => count >= MYGENE_HITS_PER_ID).map(([query]) => query) : [];
|
|
108
|
+
return { genes, notFound, ambiguous, truncated };
|
|
109
|
+
}
|
|
@@ -122,6 +122,10 @@ const BASELINE_EXPRESSION_PAGE_SIZE = 3000;
|
|
|
122
122
|
// that resolves empty — an unknown/retired id yields `{"target": null}`, which a
|
|
123
123
|
// bare `.optional()` rejects, turning a clean "not found" into a thrown error.
|
|
124
124
|
const DatatypeScoreSchema = z.object({ id: z.string().optional(), score: z.number().optional() });
|
|
125
|
+
/** The datatype-scores list follows the same nullable-optional reading rule as its row, and may be absent or null. */
|
|
126
|
+
const scoresField = z.array(DatatypeScoreSchema).nullable().optional();
|
|
127
|
+
/** The paged rows list of an association query: the API may omit or null the whole envelope. */
|
|
128
|
+
const rowsEnvelope = (item) => z.object({ rows: z.array(item).nullable().optional() });
|
|
125
129
|
export const TargetAssociationsDataSchema = z.object({
|
|
126
130
|
target: z
|
|
127
131
|
.object({
|
|
@@ -137,18 +141,11 @@ export const TargetAssociationsDataSchema = z.object({
|
|
|
137
141
|
.optional(),
|
|
138
142
|
associations: z
|
|
139
143
|
.object({
|
|
140
|
-
associatedDiseases: z
|
|
141
|
-
.object({
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
score: z.number(),
|
|
146
|
-
datatypeScores: z.array(DatatypeScoreSchema).nullable().optional(),
|
|
147
|
-
}))
|
|
148
|
-
.nullable()
|
|
149
|
-
.optional(),
|
|
150
|
-
})
|
|
151
|
-
.optional(),
|
|
144
|
+
associatedDiseases: rowsEnvelope(z.object({
|
|
145
|
+
disease: z.object({ id: z.string(), name: z.string() }),
|
|
146
|
+
score: z.number(),
|
|
147
|
+
datatypeScores: scoresField,
|
|
148
|
+
})).optional(),
|
|
152
149
|
})
|
|
153
150
|
.nullable()
|
|
154
151
|
.optional(),
|
|
@@ -158,21 +155,14 @@ const DiseaseAssociationsDataSchema = z.object({
|
|
|
158
155
|
.object({
|
|
159
156
|
id: z.string().optional(),
|
|
160
157
|
name: z.string().optional(),
|
|
161
|
-
associatedTargets: z
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.array(z.object({
|
|
165
|
-
target: z
|
|
166
|
-
.object({ id: z.string().optional(), approvedSymbol: z.string().optional(), approvedName: z.string().optional() })
|
|
167
|
-
.nullable()
|
|
168
|
-
.optional(),
|
|
169
|
-
score: z.number(),
|
|
170
|
-
datatypeScores: z.array(DatatypeScoreSchema).nullable().optional(),
|
|
171
|
-
}))
|
|
158
|
+
associatedTargets: rowsEnvelope(z.object({
|
|
159
|
+
target: z
|
|
160
|
+
.object({ id: z.string().optional(), approvedSymbol: z.string().optional(), approvedName: z.string().optional() })
|
|
172
161
|
.nullable()
|
|
173
162
|
.optional(),
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
score: z.number(),
|
|
164
|
+
datatypeScores: scoresField,
|
|
165
|
+
})).optional(),
|
|
176
166
|
})
|
|
177
167
|
.nullable()
|
|
178
168
|
.optional(),
|
|
@@ -34,9 +34,8 @@ import type { DraftDocument } from "../../report-model/draft.js";
|
|
|
34
34
|
import type { ReportSnapshot } from "../../report-model/reference-resolver.js";
|
|
35
35
|
import type { DerivationRecord } from "../../state/report-session-state.js";
|
|
36
36
|
import { type Tool } from "../define-tool.js";
|
|
37
|
-
/** The flat destination fields
|
|
37
|
+
/** The flat destination fields of the add, plus the payload of the new block. */
|
|
38
38
|
declare const addBlockInput: z.ZodObject<{
|
|
39
|
-
block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>;
|
|
40
39
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
40
|
place: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
42
41
|
end: "end";
|
|
@@ -44,6 +43,7 @@ declare const addBlockInput: z.ZodObject<{
|
|
|
44
43
|
}>>>;
|
|
45
44
|
before: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
45
|
after: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>;
|
|
47
47
|
}, z.core.$strip>;
|
|
48
48
|
/**
|
|
49
49
|
* The change payload is a title for a section target, or a block for an atom target, and exactly one of
|
|
@@ -59,7 +59,6 @@ declare const removeBlockInput: z.ZodObject<{
|
|
|
59
59
|
targetId: z.ZodString;
|
|
60
60
|
}, z.core.$strip>;
|
|
61
61
|
declare const moveBlockInput: z.ZodObject<{
|
|
62
|
-
targetId: z.ZodString;
|
|
63
62
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
64
63
|
place: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
65
64
|
end: "end";
|
|
@@ -67,6 +66,7 @@ declare const moveBlockInput: z.ZodObject<{
|
|
|
67
66
|
}>>>;
|
|
68
67
|
before: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
69
68
|
after: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
69
|
+
targetId: z.ZodString;
|
|
70
70
|
}, z.core.$strip>;
|
|
71
71
|
declare const readOutlineInput: z.ZodObject<{}, z.core.$strip>;
|
|
72
72
|
/** The read names its target with the same field name as each mutation, thus one id key serves them all. */
|
|
@@ -70,12 +70,16 @@ const DESTINATION_NOTES = {
|
|
|
70
70
|
after: "The id of a sibling block. The block lands directly after it.",
|
|
71
71
|
};
|
|
72
72
|
/** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
|
|
73
|
-
const
|
|
74
|
-
block: blockPayload,
|
|
73
|
+
const destinationFields = {
|
|
75
74
|
parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
|
|
76
75
|
place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
|
|
77
76
|
before: absentable(z.string()).describe(DESTINATION_NOTES.before),
|
|
78
77
|
after: absentable(z.string()).describe(DESTINATION_NOTES.after),
|
|
78
|
+
};
|
|
79
|
+
/** The flat destination fields of the add, plus the payload of the new block. */
|
|
80
|
+
const addBlockInput = z.object({
|
|
81
|
+
block: blockPayload,
|
|
82
|
+
...destinationFields,
|
|
79
83
|
});
|
|
80
84
|
/**
|
|
81
85
|
* The change payload is a title for a section target, or a block for an atom target, and exactly one of
|
|
@@ -92,10 +96,7 @@ const removeBlockInput = z.object({
|
|
|
92
96
|
});
|
|
93
97
|
const moveBlockInput = z.object({
|
|
94
98
|
targetId: z.string().describe("The id of the block to move, as the outline gives it."),
|
|
95
|
-
|
|
96
|
-
place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
|
|
97
|
-
before: absentable(z.string()).describe(DESTINATION_NOTES.before),
|
|
98
|
-
after: absentable(z.string()).describe(DESTINATION_NOTES.after),
|
|
99
|
+
...destinationFields,
|
|
99
100
|
});
|
|
100
101
|
const readOutlineInput = z.object({});
|
|
101
102
|
/** The read names its target with the same field name as each mutation, thus one id key serves them all. */
|
|
@@ -260,7 +260,9 @@ export function createGenerateAnalogyReportTool(deps, registerSubAgent) {
|
|
|
260
260
|
"drives a multi-step research sub-agent (up to 40 model turns) over live " +
|
|
261
261
|
"literature search, so it is slow; a fact that one lookup answers does " +
|
|
262
262
|
"not need it.\n" +
|
|
263
|
-
"RETURNS an AnalogyReport envelope
|
|
263
|
+
"RETURNS an AnalogyReport envelope. The analogy block shows the full " +
|
|
264
|
+
"report to the user, thus your reply names the analogies that matter by " +
|
|
265
|
+
"their `analogyTitle` and does not copy the report. An " +
|
|
264
266
|
"analogy the search could not fill carries a `coverage` tag " +
|
|
265
267
|
"(`queried_no_data` | `search_failed` | `not_loaded`) with an empty " +
|
|
266
268
|
"`solutions` array — a partial result, not an error. A top-level `error` " +
|
|
@@ -273,7 +275,13 @@ export function createGenerateAnalogyReportTool(deps, registerSubAgent) {
|
|
|
273
275
|
view: "none",
|
|
274
276
|
describeCall: "none",
|
|
275
277
|
startsWorkflow: true,
|
|
276
|
-
execute: async (input, ctx) =>
|
|
278
|
+
execute: async (input, ctx) => {
|
|
279
|
+
const report = await research(input, ctx);
|
|
280
|
+
// The execute runs in the body of the turn, where a stream write is checkpointed, thus a replay gives no second
|
|
281
|
+
// part. The root session keeps the part in the stored display, which drops each frame of a sub-agent.
|
|
282
|
+
await ctx.emit({ type: "data-analogy-report", source: ctx.session.provenance, data: { id: ctx.invocationId, report } });
|
|
283
|
+
return ok(report);
|
|
284
|
+
},
|
|
277
285
|
});
|
|
278
286
|
}
|
|
279
287
|
/** The envelope of a run that recorded no outcome, also after its salvage. */
|