@inflexa-ai/harness 0.35.8 → 0.36.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/auth/types.d.ts +5 -14
- package/dist/auth/types.js +3 -10
- package/dist/billing/usage-recorder.d.ts +1 -1
- package/dist/contracts/analogy-report.d.ts +1 -1
- package/dist/contracts/analogy-report.js +1 -1
- package/dist/contracts/chat-parts.d.ts +2 -3
- package/dist/contracts/index.d.ts +0 -4
- package/dist/contracts/index.js +0 -2
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -21
- package/dist/lib/metrics.d.ts +1 -1
- package/dist/lib/otel-spans.d.ts +0 -2
- package/dist/lib/otel-spans.js +0 -2
- package/dist/loop/run-agent.d.ts +1 -1
- package/dist/runtime/assemble.d.ts +0 -3
- package/dist/runtime/assemble.js +0 -3
- package/dist/state/index.d.ts +0 -2
- package/dist/state/index.js +0 -1
- package/dist/state/init.js +1 -60
- package/dist/state/purge-analysis.d.ts +1 -2
- package/dist/state/purge-analysis.js +1 -2
- package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
- package/dist/tools/bio/gene-preclinical-profile.js +1 -3
- package/dist/tools/lib/chembl-client.d.ts +1 -4
- package/dist/tools/lib/chembl-client.js +1 -4
- package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
- package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
- package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
- package/dist/tools/lib/clinical-trials-client.js +0 -3
- package/dist/tools/lib/identifier-resolver.d.ts +5 -7
- package/dist/tools/lib/identifier-resolver.js +5 -7
- package/dist/tools/lib/iuphar-client.d.ts +1 -3
- package/dist/tools/lib/iuphar-client.js +1 -3
- package/dist/tools/lib/opentargets-client.d.ts +1 -1
- package/dist/tools/lib/opentargets-client.js +1 -1
- package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
- package/dist/tools/lib/protein-family-normalize.js +5 -7
- package/dist/tools/lib/uniprot-client.d.ts +0 -7
- package/dist/tools/lib/uniprot-client.js +2 -9
- package/package.json +1 -1
- package/dist/contracts/target-assessment-row.d.ts +0 -85
- package/dist/contracts/target-assessment-row.js +0 -37
- package/dist/contracts/target-dossier.d.ts +0 -12423
- package/dist/contracts/target-dossier.js +0 -1282
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
- package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
- package/dist/prompts/target-assessment/briefs/index.js +0 -4
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
- package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
- package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
- package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
- package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
- package/dist/prompts/target-assessment/investigation/index.js +0 -3
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
- package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
- package/dist/prompts/target-assessment/modulator-triage.js +0 -31
- package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
- package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
- package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
- package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
- package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
- package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
- package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
- package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
- package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
- package/dist/state/target-assessments.d.ts +0 -68
- package/dist/state/target-assessments.js +0 -222
- package/dist/workflows/execute-target-assessment.d.ts +0 -118
- package/dist/workflows/execute-target-assessment.js +0 -631
- package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
- package/dist/workflows/target-assessment/assemblers/index.js +0 -4
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
- package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
- package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
- package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
- package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
- package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
- package/dist/workflows/target-assessment/collectors/index.js +0 -538
- package/dist/workflows/target-assessment/coverage.d.ts +0 -97
- package/dist/workflows/target-assessment/coverage.js +0 -86
- package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
- package/dist/workflows/target-assessment/decisions/index.js +0 -127
- package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
- package/dist/workflows/target-assessment/fanout/index.js +0 -156
- package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
- package/dist/workflows/target-assessment/investigation/index.js +0 -419
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
- package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
- package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
- package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
- package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
- package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
- package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
- package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
- package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
- package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
- package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
- package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
- package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
- package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
- package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
- package/dist/workflows/target-assessment/metrics.d.ts +0 -16
- package/dist/workflows/target-assessment/metrics.js +0 -30
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
- package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
- package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
- package/dist/workflows/target-assessment/phase5-persist.js +0 -349
- package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
- package/dist/workflows/target-assessment/progress-stream.js +0 -110
- package/dist/workflows/target-assessment/progress.d.ts +0 -39
- package/dist/workflows/target-assessment/progress.js +0 -77
- package/dist/workflows/target-assessment/schemas.d.ts +0 -970
- package/dist/workflows/target-assessment/schemas.js +0 -318
- package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
- package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
- package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
- package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
- package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
- package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
- package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
- package/dist/workflows/target-assessment/synthesis/index.js +0 -354
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
- package/dist/workflows/target-assessment/synthesis-probes.js +0 -320
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export type RecommendationAuditEntry = {
|
|
2
|
-
surface: "rationale" | "key_strengths" | "key_risks" | "modality_choice";
|
|
3
|
-
path: string;
|
|
4
|
-
excerpt: string;
|
|
5
|
-
} | {
|
|
6
|
-
surface: "external_missing";
|
|
7
|
-
id: string;
|
|
8
|
-
excerpt: string;
|
|
9
|
-
} | {
|
|
10
|
-
surface: "organ_claim_without_probe_pass";
|
|
11
|
-
excerpt: string;
|
|
12
|
-
bullet_category: string;
|
|
13
|
-
} | {
|
|
14
|
-
surface: "nct_wrong_class";
|
|
15
|
-
id: string;
|
|
16
|
-
excerpt: string;
|
|
17
|
-
} | {
|
|
18
|
-
surface: "pmid_not_in_key_papers";
|
|
19
|
-
id: string;
|
|
20
|
-
excerpt: string;
|
|
21
|
-
};
|
|
22
|
-
export type RecommendationAudit = {
|
|
23
|
-
citations_total: number;
|
|
24
|
-
citations_unresolved: RecommendationAuditEntry[];
|
|
25
|
-
non_dossier_citations: Array<{
|
|
26
|
-
token: string;
|
|
27
|
-
source: string;
|
|
28
|
-
}>;
|
|
29
|
-
};
|
|
30
|
-
type KeyRiskLike = {
|
|
31
|
-
text: string;
|
|
32
|
-
category?: string;
|
|
33
|
-
} | string;
|
|
34
|
-
type RecLike = {
|
|
35
|
-
rationale: string;
|
|
36
|
-
key_strengths: KeyRiskLike[];
|
|
37
|
-
key_risks: KeyRiskLike[];
|
|
38
|
-
modality_choice: {
|
|
39
|
-
rationale: string;
|
|
40
|
-
};
|
|
41
|
-
external_citations?: Array<{
|
|
42
|
-
id: string;
|
|
43
|
-
}>;
|
|
44
|
-
};
|
|
45
|
-
export declare function validateRecommendationCitations(rec: RecLike, dossier: unknown): RecommendationAudit;
|
|
46
|
-
export {};
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
// The recommendation agent emits citations as `[dossier: path; path; …]`
|
|
2
|
-
// blocks (colon-space, semicolon-separated), sometimes with a trailing
|
|
3
|
-
// `external citation: id; id` segment inside the same bracket. The old
|
|
4
|
-
// `dossier.path` regex never matched this in production — citations_total
|
|
5
|
-
// was always 0. These patterns match the format the agent actually writes.
|
|
6
|
-
// The block content may include array index notation like `[0]`, so we
|
|
7
|
-
// use a pattern that allows `[digits]` inside the outer brackets.
|
|
8
|
-
const DOSSIER_CITE_BLOCK_RE = /\[dossier:\s*((?:[^[\]]|\[\d+\])*)\]/gi;
|
|
9
|
-
// External citation IDs are plain strings — no array-index notation like `[n]` — so the simple [^\]]+ class suffices here instead of the array-index-aware pattern used by DOSSIER_CITE_BLOCK_RE.
|
|
10
|
-
const EXTERNAL_CITE_BLOCK_RE = /\[external citation:\s*([^\]]+)\]/gi;
|
|
11
|
-
// Assumes at most one `external citation:` segment per dossier block (the recommendation prompt produces at most one); a hypothetical second segment in the same block would be parsed imperfectly.
|
|
12
|
-
const EXTERNAL_SEGMENT_RE = /external citation:\s*((?:[^[\]]|\[\d+\])*)/i;
|
|
13
|
-
const NCT_RE = /\bNCT\d{8}\b/g;
|
|
14
|
-
const PMID_RE = /\bPMID[:\s]?(\d{6,9})\b/g;
|
|
15
|
-
// Legacy inline format: `dossier.path.foo` written directly in prose, no
|
|
16
|
-
// brackets. Still emitted by some recommendation outputs and exercised by
|
|
17
|
-
// the regression fixtures, so BOTH this and the `[dossier: …]` block format
|
|
18
|
-
// must be supported. This pattern only matches a bare `dossier.x.y` token —
|
|
19
|
-
// inside a `[dossier: …]` block the word `dossier` is followed by `:`, not
|
|
20
|
-
// `.`/`[`, so the two are disjoint and never double-count.
|
|
21
|
-
const DOSSIER_PATH_RE = /\bdossier(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d+\])+/g;
|
|
22
|
-
// Legacy bare external reference: `[ANDA215864]` — an all-caps id in
|
|
23
|
-
// brackets. Disjoint from `[dossier: …]` / `[external citation: …]` blocks
|
|
24
|
-
// (those start with a lowercase word) and from `[0]` array indices (digits).
|
|
25
|
-
const EXTERNAL_ID_RE = /\[([A-Z]{2,}[A-Z0-9_-]+)\]/g;
|
|
26
|
-
// Path prefixes the agent uses that are NOT part of the persisted dossier
|
|
27
|
-
// body — they refer to the per-section synthesis inputs the agent was
|
|
28
|
-
// shown. Citing them is legitimate; resolving them against the dossier is
|
|
29
|
-
// not, so they go to `non_dossier_citations` instead of `unresolved`.
|
|
30
|
-
const NON_DOSSIER_PREFIXES = ["per_section_synthesis"];
|
|
31
|
-
const ORGAN_CATEGORIES = new Set(["high_safety_organ_expression", "broad_expression", "class_liability"]);
|
|
32
|
-
/**
|
|
33
|
-
* Keyword fallback for detecting organ-level claims when the bullet has
|
|
34
|
-
* no structural `category` field (the production
|
|
35
|
-
* DossierRecommendationOutputSchema types key_risks as z.array(z.string())
|
|
36
|
-
* with no category metadata, so a category check alone would be dead
|
|
37
|
-
* code). Matches the regression-test approach in
|
|
38
|
-
* [C4] of calcr-target-assessment.test.ts.
|
|
39
|
-
*/
|
|
40
|
-
const ORGAN_KEYWORDS_RE = /\b(kidney|kidneys|renal|nephro\w*|cns|central\s+nervous|brain|hypothalam\w*|cardiac|cardiovascular|hepatic|liver|hepato\w*|pulmonary|respiratory|gastrointestinal)\b/i;
|
|
41
|
-
/**
|
|
42
|
-
* Bullet-keyword → canonical organ class. The keys mirror the alternatives
|
|
43
|
-
* in `ORGAN_KEYWORDS_RE`; values match `target_organ_liabilities[i].organ`,
|
|
44
|
-
* which the assembler emits using the canonical safety-panel set
|
|
45
|
-
* (`cardiac` | `cns` | `hepatic` | `renal` | `gi` | `respiratory` | ...).
|
|
46
|
-
*
|
|
47
|
-
* Used to cross-match an organ-level key_risk bullet against the TOL probe
|
|
48
|
-
* pass without depending on the bullet text containing the literal JSON
|
|
49
|
-
* path `dossier.safety_profile.target_organ_liabilities` — LLM bullets are
|
|
50
|
-
* plain English so the path-based check from earlier never matched in
|
|
51
|
-
* production.
|
|
52
|
-
*/
|
|
53
|
-
const BULLET_KEYWORD_TO_ORGAN = [
|
|
54
|
-
{ rx: /\b(kidney|kidneys|renal|nephro\w*)\b/i, organ: "renal" },
|
|
55
|
-
{ rx: /\b(cns|central\s+nervous|brain|hypothalam\w*)\b/i, organ: "cns" },
|
|
56
|
-
{ rx: /\b(cardiac|cardiovascular)\b/i, organ: "cardiac" },
|
|
57
|
-
{ rx: /\b(hepatic|liver|hepato\w*)\b/i, organ: "hepatic" },
|
|
58
|
-
{ rx: /\b(pulmonary|respiratory)\b/i, organ: "respiratory" },
|
|
59
|
-
{ rx: /\b(gastrointestinal)\b/i, organ: "gi" },
|
|
60
|
-
];
|
|
61
|
-
function extractOrganClassesFromText(text) {
|
|
62
|
-
const result = new Set();
|
|
63
|
-
for (const { rx, organ } of BULLET_KEYWORD_TO_ORGAN) {
|
|
64
|
-
if (rx.test(text))
|
|
65
|
-
result.add(organ);
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
function resolvePath(root, path) {
|
|
70
|
-
if (!path.startsWith("dossier"))
|
|
71
|
-
return undefined;
|
|
72
|
-
const stripped = path.replace(/^dossier\.?/, "");
|
|
73
|
-
if (stripped === "")
|
|
74
|
-
return root;
|
|
75
|
-
const parts = stripped.split(/\.|\[(\d+)\]/).filter(Boolean);
|
|
76
|
-
return walkWithDataFallback(root, parts);
|
|
77
|
-
}
|
|
78
|
-
function walkWithDataFallback(root, parts) {
|
|
79
|
-
let cur = root;
|
|
80
|
-
for (const p of parts) {
|
|
81
|
-
if (cur == null)
|
|
82
|
-
return undefined;
|
|
83
|
-
const next = readPart(cur, p);
|
|
84
|
-
if (next !== undefined) {
|
|
85
|
-
cur = next;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
// The dossier wraps every section as { coverage, data: {...} }. When a
|
|
89
|
-
// lookup misses and the current node has a `data` child, descend through
|
|
90
|
-
// it transparently — the LLM consistently writes paths without the wrapper
|
|
91
|
-
// (e.g. "translational_chain.peak_evidence_tier" instead of
|
|
92
|
-
// "translational_chain.data.peak_evidence_tier"). This mirrors how a human
|
|
93
|
-
// reader refers to the field; flagging it as unresolved produces noise.
|
|
94
|
-
if (typeof cur === "object" && "data" in cur) {
|
|
95
|
-
const viaData = readPart(cur.data, p);
|
|
96
|
-
if (viaData !== undefined) {
|
|
97
|
-
cur = viaData;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
return cur;
|
|
104
|
-
}
|
|
105
|
-
function readPart(node, p) {
|
|
106
|
-
if (node == null)
|
|
107
|
-
return undefined;
|
|
108
|
-
if (/^\d+$/.test(p))
|
|
109
|
-
return Array.isArray(node) ? node[parseInt(p, 10)] : undefined;
|
|
110
|
-
return typeof node === "object" ? node[p] : undefined;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Extract dossier-path citations from `text`, supporting two formats:
|
|
114
|
-
* - New `[dossier: path; path]` blocks — `;`-separated bare paths, a
|
|
115
|
-
* trailing `/ID-list` suffix dropped, `external citation:` segment
|
|
116
|
-
* excluded (picked up by `findExternalIds`).
|
|
117
|
-
* - Legacy inline `dossier.path.foo` tokens written directly in prose.
|
|
118
|
-
*
|
|
119
|
-
* Returns `{ path, resolvable }` pairs: `path` is what surfaces in an
|
|
120
|
-
* unresolved-citation entry (bare for block tokens, `dossier.`-prefixed for
|
|
121
|
-
* legacy tokens — matching what each format's callers expect); `resolvable`
|
|
122
|
-
* is always `dossier.`-prefixed so `resolvePath` can walk it.
|
|
123
|
-
*/
|
|
124
|
-
function findPathTokens(text) {
|
|
125
|
-
const tokens = [];
|
|
126
|
-
// New `[dossier: …]` block format.
|
|
127
|
-
for (const block of text.matchAll(DOSSIER_CITE_BLOCK_RE)) {
|
|
128
|
-
const dossierPart = block[1].split(/external citation:/i)[0];
|
|
129
|
-
for (const raw of dossierPart.split(";")) {
|
|
130
|
-
const token = raw.trim().split("/")[0].trim();
|
|
131
|
-
if (token.length > 0)
|
|
132
|
-
tokens.push({ path: token, resolvable: `dossier.${token}` });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Legacy inline `dossier.path.foo` format. The match already carries the
|
|
136
|
-
// `dossier.` prefix — keep it in `path` (callers expect the prefixed form
|
|
137
|
-
// for this format) and use it directly as `resolvable`.
|
|
138
|
-
for (const m of text.matchAll(DOSSIER_PATH_RE)) {
|
|
139
|
-
tokens.push({ path: m[0], resolvable: m[0] });
|
|
140
|
-
}
|
|
141
|
-
return tokens;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Extract external-citation ids. The agent writes them either as a
|
|
145
|
-
* standalone `[external citation: id; id]` block or as a trailing segment
|
|
146
|
-
* inside a `[dossier: …; external citation: id]` block.
|
|
147
|
-
*/
|
|
148
|
-
function findExternalIds(text) {
|
|
149
|
-
const ids = [];
|
|
150
|
-
const push = (segment) => {
|
|
151
|
-
for (const raw of segment.split(";")) {
|
|
152
|
-
const id = raw.trim();
|
|
153
|
-
if (id.length > 0)
|
|
154
|
-
ids.push(id);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
for (const block of text.matchAll(EXTERNAL_CITE_BLOCK_RE))
|
|
158
|
-
push(block[1]);
|
|
159
|
-
for (const block of text.matchAll(DOSSIER_CITE_BLOCK_RE)) {
|
|
160
|
-
const m = block[1].match(EXTERNAL_SEGMENT_RE);
|
|
161
|
-
if (m)
|
|
162
|
-
push(m[1]);
|
|
163
|
-
}
|
|
164
|
-
// Legacy bare `[UPPERCASE_ID]` external reference (e.g. `[ANDA215864]`).
|
|
165
|
-
// NCT and PMID references in bracket form are already handled by NCT_RE /
|
|
166
|
-
// PMID_RE in the main loop — skip them here to avoid double-processing.
|
|
167
|
-
for (const m of text.matchAll(EXTERNAL_ID_RE)) {
|
|
168
|
-
const id = m[1];
|
|
169
|
-
if (/^NCT\d/.test(id) || /^PMID\d/.test(id))
|
|
170
|
-
continue;
|
|
171
|
-
ids.push(id);
|
|
172
|
-
}
|
|
173
|
-
return ids;
|
|
174
|
-
}
|
|
175
|
-
function normaliseBullet(b) {
|
|
176
|
-
return typeof b === "string" ? { text: b } : { text: b.text, category: b.category };
|
|
177
|
-
}
|
|
178
|
-
function collectClassDrugNames(dossier) {
|
|
179
|
-
const rows = dossier?.drug_interactions?.data?.rows ?? [];
|
|
180
|
-
return new Set(rows.map((r) => String(r.drug_name ?? "").toUpperCase()).filter(Boolean));
|
|
181
|
-
}
|
|
182
|
-
function collectKeyPaperPmids(dossier) {
|
|
183
|
-
const rows = dossier?.reference_biology?.key_papers?.data?.rows ?? [];
|
|
184
|
-
return new Set(rows.map((r) => String(r.pmid)).filter(Boolean));
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Extract intervention names from a trial row. Handles two shapes:
|
|
188
|
-
* - Flat `interventions: string[]` — the production dossier shape assembled by
|
|
189
|
-
* `partitionTrialsByAttribution`.
|
|
190
|
-
* - Nested `armsInterventionsModule.interventions: Array<{ name: string }>` — the
|
|
191
|
-
* raw CT.gov v2 JSON shape that may appear in test fixtures or future collector
|
|
192
|
-
* output before the assembler flattens it.
|
|
193
|
-
*/
|
|
194
|
-
function extractInterventionNames(row) {
|
|
195
|
-
// Production flat shape (priority)
|
|
196
|
-
if (Array.isArray(row.interventions)) {
|
|
197
|
-
return row.interventions.map((iv) => (typeof iv === "string" ? iv.toUpperCase() : String(iv?.name ?? "").toUpperCase()));
|
|
198
|
-
}
|
|
199
|
-
// Raw CT.gov nested shape fallback
|
|
200
|
-
const nested = row?.armsInterventionsModule?.interventions;
|
|
201
|
-
if (Array.isArray(nested)) {
|
|
202
|
-
return nested.map((iv) => String(iv?.name ?? "").toUpperCase());
|
|
203
|
-
}
|
|
204
|
-
return [];
|
|
205
|
-
}
|
|
206
|
-
function findTrialInterventions(dossier, nctId) {
|
|
207
|
-
const trialSets = [
|
|
208
|
-
dossier?.clinical_development?.trials?.data?.rows ?? [],
|
|
209
|
-
dossier?.clinical_development?.failed_trials?.data?.rows ?? [],
|
|
210
|
-
dossier?.analytics?.discovery_trials?.data?.rows ?? [],
|
|
211
|
-
];
|
|
212
|
-
for (const set of trialSets) {
|
|
213
|
-
const row = set.find((t) => t.nct_id === nctId);
|
|
214
|
-
if (!row)
|
|
215
|
-
continue;
|
|
216
|
-
return extractInterventionNames(row);
|
|
217
|
-
}
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
export function validateRecommendationCitations(rec, dossier) {
|
|
221
|
-
const dossierView = dossier;
|
|
222
|
-
const unresolved = [];
|
|
223
|
-
const nonDossier = [];
|
|
224
|
-
let total = 0;
|
|
225
|
-
const externalIds = new Set((rec.external_citations ?? []).map((c) => c.id));
|
|
226
|
-
const surfaces = [
|
|
227
|
-
{ surface: "rationale", text: rec.rationale },
|
|
228
|
-
{ surface: "modality_choice", text: rec.modality_choice.rationale },
|
|
229
|
-
...rec.key_strengths.map((b) => ({ surface: "key_strengths", text: normaliseBullet(b).text })),
|
|
230
|
-
...rec.key_risks.map((b) => ({ surface: "key_risks", text: normaliseBullet(b).text })),
|
|
231
|
-
];
|
|
232
|
-
const classDrugNames = collectClassDrugNames(dossierView);
|
|
233
|
-
const keyPmids = collectKeyPaperPmids(dossierView);
|
|
234
|
-
for (const { surface, text } of surfaces) {
|
|
235
|
-
for (const { path, resolvable } of findPathTokens(text)) {
|
|
236
|
-
total += 1;
|
|
237
|
-
// Legacy-format tokens carry a `dossier.` prefix; block-format tokens
|
|
238
|
-
// are bare. Strip the prefix so the non-dossier-prefix check works for
|
|
239
|
-
// both formats.
|
|
240
|
-
const barePath = path.startsWith("dossier.") ? path.slice("dossier.".length) : path;
|
|
241
|
-
if (NON_DOSSIER_PREFIXES.some((p) => barePath === p || barePath.startsWith(`${p}.`) || barePath.startsWith(`${p}[`))) {
|
|
242
|
-
nonDossier.push({ token: path, source: surface });
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const resolved = resolvePath(dossier, resolvable);
|
|
246
|
-
if (resolved === undefined || resolved === null) {
|
|
247
|
-
unresolved.push({ surface, path, excerpt: text.slice(0, 200) });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
for (const id of findExternalIds(text)) {
|
|
251
|
-
if (!externalIds.has(id)) {
|
|
252
|
-
unresolved.push({ surface: "external_missing", id, excerpt: text.slice(0, 200) });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
for (const m of text.matchAll(NCT_RE)) {
|
|
256
|
-
const nctId = m[0];
|
|
257
|
-
const interventions = findTrialInterventions(dossierView, nctId);
|
|
258
|
-
// Only flag wrong-class when the trial IS present in the dossier
|
|
259
|
-
// and its interventions don't reference the class. Trials absent
|
|
260
|
-
// from the collected sets are unverifiable — flagging them would
|
|
261
|
-
// produce false positives for any on-class trial that the
|
|
262
|
-
// synthesis agent cited but the discovery collector missed.
|
|
263
|
-
if (interventions.length === 0)
|
|
264
|
-
continue;
|
|
265
|
-
// Skip when no class-drug catalog is available — without a reference
|
|
266
|
-
// set (early-stage / first-in-class targets, or drug_interactions
|
|
267
|
-
// collector returning queried_no_data) every cited trial would
|
|
268
|
-
// otherwise flag as wrong-class regardless of its actual class.
|
|
269
|
-
if (classDrugNames.size === 0)
|
|
270
|
-
continue;
|
|
271
|
-
const referencesClassDrug = interventions.some((iv) => [...classDrugNames].some((name) => iv.includes(name)));
|
|
272
|
-
if (!referencesClassDrug) {
|
|
273
|
-
unresolved.push({ surface: "nct_wrong_class", id: nctId, excerpt: text.slice(0, 200) });
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
for (const m of text.matchAll(PMID_RE)) {
|
|
277
|
-
const pmid = m[1];
|
|
278
|
-
if (!keyPmids.has(pmid)) {
|
|
279
|
-
unresolved.push({ surface: "pmid_not_in_key_papers", id: pmid, excerpt: text.slice(0, 200) });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
const organLiabilities = dossierView?.safety_profile?.target_organ_liabilities?.data?.rows ?? [];
|
|
284
|
-
const tolOrgans = new Set(organLiabilities
|
|
285
|
-
.map((t) => String(t?.organ ?? "")
|
|
286
|
-
.toLowerCase()
|
|
287
|
-
.trim())
|
|
288
|
-
.filter((o) => o.length > 0));
|
|
289
|
-
for (const b of rec.key_risks ?? []) {
|
|
290
|
-
const norm = normaliseBullet(b);
|
|
291
|
-
// Detect organ-level claims by structural category when available
|
|
292
|
-
// (typed bullets) OR by keyword match on the text (plain-string
|
|
293
|
-
// bullets — the production shape).
|
|
294
|
-
const isOrganClaim = (norm.category != null && ORGAN_CATEGORIES.has(norm.category)) || ORGAN_KEYWORDS_RE.test(norm.text);
|
|
295
|
-
if (!isOrganClaim)
|
|
296
|
-
continue;
|
|
297
|
-
// "Probe pass" = the TOL section has an entry whose organ matches the
|
|
298
|
-
// organ class mentioned in the bullet. Production bullets are plain
|
|
299
|
-
// English so we extract the organ class via keyword matching and
|
|
300
|
-
// cross-reference against the canonical TOL organ field.
|
|
301
|
-
const bulletOrgans = extractOrganClassesFromText(norm.text);
|
|
302
|
-
const citesTOL = bulletOrgans.size > 0 && [...bulletOrgans].some((o) => tolOrgans.has(o));
|
|
303
|
-
if (!citesTOL) {
|
|
304
|
-
unresolved.push({
|
|
305
|
-
surface: "organ_claim_without_probe_pass",
|
|
306
|
-
excerpt: norm.text.slice(0, 200),
|
|
307
|
-
bullet_category: norm.category ?? "inferred_from_text",
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
citations_total: total,
|
|
313
|
-
citations_unresolved: unresolved,
|
|
314
|
-
non_dossier_citations: nonDossier,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-row LLM clinical-consequence annotator with Postgres caching.
|
|
3
|
-
*
|
|
4
|
-
* Any off-target row missing a `clinical_consequence` gets annotated by a
|
|
5
|
-
* focused tool-less LLM specialist that returns ONE sentence + provenance.
|
|
6
|
-
* Results are cached in `cortex_off_target_annotations` keyed by
|
|
7
|
-
* (primary_target_gene, off_target_key) so repeat assessments reuse prior
|
|
8
|
-
* work.
|
|
9
|
-
*
|
|
10
|
-
* Cache miss path: when a `deps` bundle is supplied, build a compact JSON
|
|
11
|
-
* prompt, run a single-shot harness agent loop (no tools, max 2 iterations),
|
|
12
|
-
* parse JSON out of the final assistant text, INSERT on success, return the
|
|
13
|
-
* consequence. With no `deps`, the function returns null on cache miss
|
|
14
|
-
* (no LLM call). Cache hit path: a single primary-key lookup, no LLM call.
|
|
15
|
-
* Per-row failures (LLM, schema validation, DB) are swallowed — annotation
|
|
16
|
-
* is supplementary; the dossier renders fine with null.
|
|
17
|
-
*/
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
import type { Pool } from "pg";
|
|
20
|
-
import type { AgentSession } from "../../../auth/types.js";
|
|
21
|
-
import type { AgentChat } from "../../../providers/types.js";
|
|
22
|
-
import type { UsageRecorder } from "../../../billing/usage-recorder.js";
|
|
23
|
-
export declare const OffTargetAnnotationSchema: z.ZodObject<{
|
|
24
|
-
clinical_consequence: z.ZodString;
|
|
25
|
-
provenance: z.ZodString;
|
|
26
|
-
}, z.core.$strip>;
|
|
27
|
-
export type OffTargetAnnotation = z.infer<typeof OffTargetAnnotationSchema>;
|
|
28
|
-
export interface ClinicalConsequenceAnnotatorDeps {
|
|
29
|
-
readonly provider: AgentChat;
|
|
30
|
-
readonly session: AgentSession;
|
|
31
|
-
readonly model: string;
|
|
32
|
-
/** LLM usage-accounting seam for the annotator loop; omitted falls back to the no-op recorder. */
|
|
33
|
-
readonly usageRecorder?: UsageRecorder;
|
|
34
|
-
}
|
|
35
|
-
export interface AnnotationInput {
|
|
36
|
-
primaryTargetGene: string;
|
|
37
|
-
offTargetId: string | null;
|
|
38
|
-
offTargetName: string;
|
|
39
|
-
offTargetAccession: string | null;
|
|
40
|
-
pchembl: number;
|
|
41
|
-
/** Optional one-line family-relationship hint (e.g., "obligate cofactor"). */
|
|
42
|
-
context?: string;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Annotate a single off-target row. Returns null on any failure (cache
|
|
46
|
-
* miss + no deps, cache miss + LLM unavailable, schema validation failure,
|
|
47
|
-
* DB unreachable, etc.) — callers must treat null as "no annotation" and
|
|
48
|
-
* leave `clinical_consequence` as their original fallback.
|
|
49
|
-
*/
|
|
50
|
-
export declare function annotateClinicalConsequence(pool: Pool, input: AnnotationInput, deps?: ClinicalConsequenceAnnotatorDeps): Promise<OffTargetAnnotation | null>;
|
|
51
|
-
/**
|
|
52
|
-
* Annotate every row in an off-target panel whose `clinical_consequence`
|
|
53
|
-
* is currently null. Mutates the supplied arrays in-place. Pure assembly-
|
|
54
|
-
* time helper — no return value.
|
|
55
|
-
*
|
|
56
|
-
* Per-row LLM calls are bounded by the "annotation-llm" host budget (4 in
|
|
57
|
-
* flight). Cache hits skip the LLM entirely and consume only a cheap
|
|
58
|
-
* Postgres lookup, so the budget gate is held briefly for those rows.
|
|
59
|
-
* Without `deps`, the panel is annotated cache-only — rows missing a
|
|
60
|
-
* cached entry remain with their original fallback.
|
|
61
|
-
*/
|
|
62
|
-
export declare function annotateOffTargetPanel(pool: Pool, panel: {
|
|
63
|
-
rows: Array<Record<string, unknown>>;
|
|
64
|
-
excluded_rows: Array<Record<string, unknown>>;
|
|
65
|
-
}, primaryTargetGene: string, deps?: ClinicalConsequenceAnnotatorDeps): Promise<void>;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-row LLM clinical-consequence annotator with Postgres caching.
|
|
3
|
-
*
|
|
4
|
-
* Any off-target row missing a `clinical_consequence` gets annotated by a
|
|
5
|
-
* focused tool-less LLM specialist that returns ONE sentence + provenance.
|
|
6
|
-
* Results are cached in `cortex_off_target_annotations` keyed by
|
|
7
|
-
* (primary_target_gene, off_target_key) so repeat assessments reuse prior
|
|
8
|
-
* work.
|
|
9
|
-
*
|
|
10
|
-
* Cache miss path: when a `deps` bundle is supplied, build a compact JSON
|
|
11
|
-
* prompt, run a single-shot harness agent loop (no tools, max 2 iterations),
|
|
12
|
-
* parse JSON out of the final assistant text, INSERT on success, return the
|
|
13
|
-
* consequence. With no `deps`, the function returns null on cache miss
|
|
14
|
-
* (no LLM call). Cache hit path: a single primary-key lookup, no LLM call.
|
|
15
|
-
* Per-row failures (LLM, schema validation, DB) are swallowed — annotation
|
|
16
|
-
* is supplementary; the dossier renders fine with null.
|
|
17
|
-
*/
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
import { withHost } from "../../../lib/host-concurrency.js";
|
|
20
|
-
import { tryQuery, tryMutation } from "../../../lib/db-result.js";
|
|
21
|
-
import { offTargetAnnotatorPrompt } from "../../../prompts/target-assessment/off-target-annotator.js";
|
|
22
|
-
import { composeSystemPrompt } from "../../../agents/system-prompt.js";
|
|
23
|
-
import { forSubAgent } from "../../../auth/types.js";
|
|
24
|
-
import { finalText, runAgent } from "../../../loop/run-agent.js";
|
|
25
|
-
import { passthroughStep } from "../../../loop/run-step.js";
|
|
26
|
-
export const OffTargetAnnotationSchema = z.object({
|
|
27
|
-
clinical_consequence: z.string().min(10),
|
|
28
|
-
provenance: z.string().min(3),
|
|
29
|
-
});
|
|
30
|
-
function cacheKey(input) {
|
|
31
|
-
const id = input.offTargetId?.trim();
|
|
32
|
-
if (id)
|
|
33
|
-
return id;
|
|
34
|
-
const name = input.offTargetName.trim().toLowerCase();
|
|
35
|
-
return name ? `name:${name}` : null;
|
|
36
|
-
}
|
|
37
|
-
async function readCache(pool, primaryTargetGene, key) {
|
|
38
|
-
// Annotation is supplementary — a cache-read failure must NOT surface. The
|
|
39
|
-
// `DbError` is collapsed to `null` (treated as a cache miss) via `.unwrapOr`.
|
|
40
|
-
return tryQuery("offTargetAnnotations.readCache", async () => {
|
|
41
|
-
const res = await pool.query(`SELECT clinical_consequence, provenance
|
|
42
|
-
FROM cortex_off_target_annotations
|
|
43
|
-
WHERE primary_target_gene = $1 AND off_target_key = $2
|
|
44
|
-
LIMIT 1`, [primaryTargetGene, key]);
|
|
45
|
-
if (res.rows.length === 0)
|
|
46
|
-
return null;
|
|
47
|
-
const row = res.rows[0];
|
|
48
|
-
return {
|
|
49
|
-
clinical_consequence: row.clinical_consequence,
|
|
50
|
-
provenance: row.provenance ?? "",
|
|
51
|
-
};
|
|
52
|
-
}).unwrapOr(null);
|
|
53
|
-
}
|
|
54
|
-
async function writeCache(pool, primaryTargetGene, key, offTargetName, annotation, model) {
|
|
55
|
-
// Cache-write failures must not affect the assembly result — the `DbError`
|
|
56
|
-
// is swallowed (both branches no-op) so annotation never surfaces a DB fault.
|
|
57
|
-
await tryMutation("offTargetAnnotations.writeCache", async () => {
|
|
58
|
-
await pool.query(`INSERT INTO cortex_off_target_annotations
|
|
59
|
-
(primary_target_gene, off_target_key, off_target_name, clinical_consequence, provenance, model)
|
|
60
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
61
|
-
ON CONFLICT (primary_target_gene, off_target_key) DO NOTHING`, [primaryTargetGene, key, offTargetName, annotation.clinical_consequence, annotation.provenance, model]);
|
|
62
|
-
}).match(() => { }, () => { });
|
|
63
|
-
}
|
|
64
|
-
function buildPrompt(input) {
|
|
65
|
-
return JSON.stringify({
|
|
66
|
-
primary_target_gene: input.primaryTargetGene,
|
|
67
|
-
off_target_id: input.offTargetId,
|
|
68
|
-
off_target_name: input.offTargetName,
|
|
69
|
-
off_target_accession: input.offTargetAccession,
|
|
70
|
-
pchembl: input.pchembl,
|
|
71
|
-
context: input.context ?? null,
|
|
72
|
-
}, null, 2);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Best-effort JSON extraction from an LLM text response. Strips fenced
|
|
76
|
-
* code blocks and surrounding prose so a slightly-out-of-spec reply still
|
|
77
|
-
* parses.
|
|
78
|
-
*/
|
|
79
|
-
function extractJsonObject(text) {
|
|
80
|
-
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
81
|
-
const candidate = fenced ? fenced[1] : text;
|
|
82
|
-
const start = candidate.indexOf("{");
|
|
83
|
-
const end = candidate.lastIndexOf("}");
|
|
84
|
-
if (start === -1 || end === -1 || end < start)
|
|
85
|
-
return null;
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(candidate.slice(start, end + 1));
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const ANNOTATOR_AGENT_ID = "off-target-annotator";
|
|
94
|
-
function buildAnnotatorAgent(model) {
|
|
95
|
-
return {
|
|
96
|
-
id: ANNOTATOR_AGENT_ID,
|
|
97
|
-
systemPrompt: composeSystemPrompt(offTargetAnnotatorPrompt),
|
|
98
|
-
model,
|
|
99
|
-
tools: [],
|
|
100
|
-
maxIterations: 2,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Annotate a single off-target row. Returns null on any failure (cache
|
|
105
|
-
* miss + no deps, cache miss + LLM unavailable, schema validation failure,
|
|
106
|
-
* DB unreachable, etc.) — callers must treat null as "no annotation" and
|
|
107
|
-
* leave `clinical_consequence` as their original fallback.
|
|
108
|
-
*/
|
|
109
|
-
export async function annotateClinicalConsequence(pool, input, deps) {
|
|
110
|
-
if (!input.primaryTargetGene)
|
|
111
|
-
return null;
|
|
112
|
-
const key = cacheKey(input);
|
|
113
|
-
if (!key)
|
|
114
|
-
return null;
|
|
115
|
-
const cached = await readCache(pool, input.primaryTargetGene, key);
|
|
116
|
-
if (cached)
|
|
117
|
-
return cached;
|
|
118
|
-
if (!deps)
|
|
119
|
-
return null;
|
|
120
|
-
try {
|
|
121
|
-
const agent = buildAnnotatorAgent(deps.model);
|
|
122
|
-
const session = forSubAgent(deps.session, ANNOTATOR_AGENT_ID);
|
|
123
|
-
const controller = new AbortController();
|
|
124
|
-
const { messages } = await runAgent(agent, [{ role: "user", content: buildPrompt(input) }], session, {
|
|
125
|
-
provider: deps.provider,
|
|
126
|
-
signal: controller.signal,
|
|
127
|
-
emit: () => { },
|
|
128
|
-
runStep: passthroughStep,
|
|
129
|
-
usageRecorder: deps.usageRecorder,
|
|
130
|
-
});
|
|
131
|
-
const raw = extractJsonObject(finalText(messages));
|
|
132
|
-
if (raw === null)
|
|
133
|
-
return null;
|
|
134
|
-
const parsed = OffTargetAnnotationSchema.safeParse(raw);
|
|
135
|
-
if (!parsed.success)
|
|
136
|
-
return null;
|
|
137
|
-
await writeCache(pool, input.primaryTargetGene, key, input.offTargetName, parsed.data, deps.model);
|
|
138
|
-
return parsed.data;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Annotate every row in an off-target panel whose `clinical_consequence`
|
|
146
|
-
* is currently null. Mutates the supplied arrays in-place. Pure assembly-
|
|
147
|
-
* time helper — no return value.
|
|
148
|
-
*
|
|
149
|
-
* Per-row LLM calls are bounded by the "annotation-llm" host budget (4 in
|
|
150
|
-
* flight). Cache hits skip the LLM entirely and consume only a cheap
|
|
151
|
-
* Postgres lookup, so the budget gate is held briefly for those rows.
|
|
152
|
-
* Without `deps`, the panel is annotated cache-only — rows missing a
|
|
153
|
-
* cached entry remain with their original fallback.
|
|
154
|
-
*/
|
|
155
|
-
export async function annotateOffTargetPanel(pool, panel, primaryTargetGene, deps) {
|
|
156
|
-
if (!primaryTargetGene)
|
|
157
|
-
return;
|
|
158
|
-
const allRows = [...panel.rows, ...panel.excluded_rows];
|
|
159
|
-
const needsAnnotation = allRows.filter((r) => r && (r.clinical_consequence == null || r.clinical_consequence === ""));
|
|
160
|
-
if (needsAnnotation.length === 0)
|
|
161
|
-
return;
|
|
162
|
-
await Promise.all(needsAnnotation.map((r) => withHost("annotation-llm", async () => {
|
|
163
|
-
const annotation = await annotateClinicalConsequence(pool, {
|
|
164
|
-
primaryTargetGene,
|
|
165
|
-
offTargetId: typeof r.off_target_id === "string" ? r.off_target_id : null,
|
|
166
|
-
offTargetName: typeof r.off_target_name === "string" ? r.off_target_name : "",
|
|
167
|
-
offTargetAccession: typeof r.accession === "string" ? r.accession : null,
|
|
168
|
-
pchembl: typeof r.pchembl === "number" ? r.pchembl : 0,
|
|
169
|
-
context: r.relationship === "obligate_cofactor" ? "obligate cofactor with the primary protein" : undefined,
|
|
170
|
-
}, deps);
|
|
171
|
-
if (annotation)
|
|
172
|
-
r.clinical_consequence = annotation.clinical_consequence;
|
|
173
|
-
})));
|
|
174
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { ClinicalTrialAttribution } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
2
|
-
type TrialLike = {
|
|
3
|
-
nctId: string;
|
|
4
|
-
title: string;
|
|
5
|
-
officialTitle?: string | null;
|
|
6
|
-
conditions?: string[];
|
|
7
|
-
interventions?: string[];
|
|
8
|
-
interventionDetails?: Array<{
|
|
9
|
-
name: string;
|
|
10
|
-
type?: string | null;
|
|
11
|
-
description?: string | null;
|
|
12
|
-
otherNames?: string[];
|
|
13
|
-
}>;
|
|
14
|
-
studyType?: string | null;
|
|
15
|
-
primaryPurpose?: string | null;
|
|
16
|
-
briefSummary?: string | null;
|
|
17
|
-
detailedDescription?: string | null;
|
|
18
|
-
};
|
|
19
|
-
export type KnownClassDrug = {
|
|
20
|
-
name: string;
|
|
21
|
-
moleculeChemblId?: string | null;
|
|
22
|
-
targetUniprots?: string[];
|
|
23
|
-
};
|
|
24
|
-
export type TherapeuticProgram = {
|
|
25
|
-
programId: string;
|
|
26
|
-
name: string;
|
|
27
|
-
targetSymbol: string;
|
|
28
|
-
targetUniprot?: string | null;
|
|
29
|
-
modality?: string | null;
|
|
30
|
-
nctIds?: string[];
|
|
31
|
-
};
|
|
32
|
-
export type ClinicalEvidenceAttributionContext = {
|
|
33
|
-
assessmentSymbol: string;
|
|
34
|
-
assessmentUniprot: string;
|
|
35
|
-
familyUniprots: string[];
|
|
36
|
-
knownClassDrugs: KnownClassDrug[];
|
|
37
|
-
therapeuticPrograms: TherapeuticProgram[];
|
|
38
|
-
};
|
|
39
|
-
export type ClinicalEvidenceAttributionResult = {
|
|
40
|
-
attribution: ClinicalTrialAttribution;
|
|
41
|
-
eligible_for_toxicology_aggregation: boolean;
|
|
42
|
-
};
|
|
43
|
-
export declare function classifyClinicalEvidenceTrial(trial: TrialLike, ctx: ClinicalEvidenceAttributionContext): ClinicalEvidenceAttributionResult;
|
|
44
|
-
export {};
|