@inflexa-ai/harness 0.35.8 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/genai-spans.d.ts +14 -0
- package/dist/loop/genai-spans.js +35 -0
- package/dist/loop/run-agent.d.ts +1 -1
- package/dist/loop/run-agent.js +37 -4
- 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,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-drug regulatory-action collector. Queries openFDA Structured Product
|
|
3
|
-
* Labels (boxed warning, Section 5 warnings_and_cautions, REMS indicator)
|
|
4
|
-
* and the EMA referrals catalogue (Article 31 / Article 20 procedures)
|
|
5
|
-
* for each drug, then maps both into the dossier's RegulatoryActionRow
|
|
6
|
-
* shape.
|
|
7
|
-
*
|
|
8
|
-
* Coverage that legitimately has no
|
|
9
|
-
* machine-readable source (e.g., EMA assessment-report procedure codes like
|
|
10
|
-
* EMEA/H/A-31/1291) is omitted rather than backfilled with curation — the
|
|
11
|
-
* source_url points at the canonical document where the human reader can
|
|
12
|
-
* extract the additional context.
|
|
13
|
-
*/
|
|
14
|
-
import { withHost } from "../../../lib/host-concurrency.js";
|
|
15
|
-
import { getReferralsByDrug } from "../../../tools/lib/ema-client.js";
|
|
16
|
-
import { getDrugLabelActions } from "../../../tools/lib/openfda-client.js";
|
|
17
|
-
import { segmentLabelSafety } from "./fda-label-safety.js";
|
|
18
|
-
function formatEffectiveTime(yyyymmdd) {
|
|
19
|
-
if (!yyyymmdd || yyyymmdd.length < 6)
|
|
20
|
-
return "";
|
|
21
|
-
return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}`;
|
|
22
|
-
}
|
|
23
|
-
function formatEmaDate(ddmmyyyy) {
|
|
24
|
-
if (!ddmmyyyy)
|
|
25
|
-
return "";
|
|
26
|
-
const parts = ddmmyyyy.split("/");
|
|
27
|
-
if (parts.length !== 3)
|
|
28
|
-
return "";
|
|
29
|
-
return `${parts[2]}-${parts[1]}`;
|
|
30
|
-
}
|
|
31
|
-
const FINDING_CHAR_LIMIT = 2000;
|
|
32
|
-
function clipFinding(text) {
|
|
33
|
-
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
34
|
-
if (cleaned.length <= FINDING_CHAR_LIMIT)
|
|
35
|
-
return cleaned;
|
|
36
|
-
return `${cleaned.slice(0, FINDING_CHAR_LIMIT - 1).trimEnd()}…`;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Project a fetched label onto the shared typed safety form. Returns null when
|
|
40
|
-
* the label carries no application number: its warnings could not then be cited
|
|
41
|
-
* back to a document, and an uncitable warning is not evidence.
|
|
42
|
-
*/
|
|
43
|
-
function labelSafetyOf(drug, label) {
|
|
44
|
-
if (!label.applicationNumber)
|
|
45
|
-
return null;
|
|
46
|
-
const sections = [];
|
|
47
|
-
if (label.boxedWarning)
|
|
48
|
-
sections.push({ section: "boxed_warning", text: label.boxedWarning });
|
|
49
|
-
if (label.warningsAndCautions)
|
|
50
|
-
sections.push({ section: "warnings_and_precautions", text: label.warningsAndCautions });
|
|
51
|
-
if (sections.length === 0)
|
|
52
|
-
return null;
|
|
53
|
-
return {
|
|
54
|
-
application_number: label.applicationNumber,
|
|
55
|
-
drug_name: label.genericName ?? label.brandName ?? drug.name,
|
|
56
|
-
effective_time: label.effectiveTime ?? undefined,
|
|
57
|
-
source_url: label.sourceUrl || undefined,
|
|
58
|
-
sections,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function fdaRowsFor(drug, labels) {
|
|
62
|
-
if (labels.length === 0)
|
|
63
|
-
return [];
|
|
64
|
-
const sorted = [...labels].sort((a, b) => (b.effectiveTime ?? "").localeCompare(a.effectiveTime ?? ""));
|
|
65
|
-
const latest = sorted[0];
|
|
66
|
-
const rows = [];
|
|
67
|
-
const actionDate = formatEffectiveTime(latest.effectiveTime);
|
|
68
|
-
// The label's own prose, segmented once and attributed to organs, is what
|
|
69
|
-
// lets each row cite the section a reader can open rather than only the
|
|
70
|
-
// agency and the date.
|
|
71
|
-
const labelSafety = labelSafetyOf(drug, latest);
|
|
72
|
-
const signals = labelSafety ? segmentLabelSafety([labelSafety]).signals : [];
|
|
73
|
-
const evidenceFrom = (section) => signals.filter((s) => s.source_section === section).map((s) => s.evidence);
|
|
74
|
-
if (latest.boxedWarning) {
|
|
75
|
-
rows.push({
|
|
76
|
-
drug_chembl_id: drug.chemblId,
|
|
77
|
-
drug_name: drug.name,
|
|
78
|
-
agency: "FDA",
|
|
79
|
-
action_kind: "black_box",
|
|
80
|
-
source_kind: "boxed_warning",
|
|
81
|
-
action_date: actionDate,
|
|
82
|
-
source_date: actionDate,
|
|
83
|
-
application_number: latest.applicationNumber ?? undefined,
|
|
84
|
-
finding: clipFinding(latest.boxedWarning),
|
|
85
|
-
source_url: latest.sourceUrl || undefined,
|
|
86
|
-
evidence: evidenceFrom("boxed_warning"),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
if (latest.warningsAndCautions) {
|
|
90
|
-
rows.push({
|
|
91
|
-
drug_chembl_id: drug.chemblId,
|
|
92
|
-
drug_name: drug.name,
|
|
93
|
-
agency: "FDA",
|
|
94
|
-
action_kind: "label_warning",
|
|
95
|
-
source_kind: "label_warning",
|
|
96
|
-
action_date: actionDate,
|
|
97
|
-
source_date: actionDate,
|
|
98
|
-
application_number: latest.applicationNumber ?? undefined,
|
|
99
|
-
label_section: "5",
|
|
100
|
-
finding: clipFinding(latest.warningsAndCautions),
|
|
101
|
-
source_url: latest.sourceUrl || undefined,
|
|
102
|
-
evidence: evidenceFrom("warnings_and_precautions"),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
if (latest.hasRems) {
|
|
106
|
-
rows.push({
|
|
107
|
-
drug_chembl_id: drug.chemblId,
|
|
108
|
-
drug_name: drug.name,
|
|
109
|
-
agency: "FDA",
|
|
110
|
-
action_kind: "REMS",
|
|
111
|
-
source_kind: "rems",
|
|
112
|
-
action_date: actionDate,
|
|
113
|
-
source_date: actionDate,
|
|
114
|
-
application_number: latest.applicationNumber ?? undefined,
|
|
115
|
-
finding: `Risk Evaluation and Mitigation Strategy referenced in current label (${latest.brandName ?? latest.genericName ?? drug.name}).`,
|
|
116
|
-
source_url: latest.sourceUrl || undefined,
|
|
117
|
-
evidence: [],
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return rows;
|
|
121
|
-
}
|
|
122
|
-
function classifyEmaActionKind(referral) {
|
|
123
|
-
const status = referral.currentStatus.toLowerCase();
|
|
124
|
-
if (status.includes("withdraw"))
|
|
125
|
-
return "withdrawal";
|
|
126
|
-
return "referral";
|
|
127
|
-
}
|
|
128
|
-
function emaRowsFor(drug, referrals) {
|
|
129
|
-
return referrals.map((r) => {
|
|
130
|
-
const date = formatEmaDate(r.europeanCommissionDecisionDate) ||
|
|
131
|
-
formatEmaDate(r.chmpOpinionDate) ||
|
|
132
|
-
formatEmaDate(r.cmdhPositionDate) ||
|
|
133
|
-
formatEmaDate(r.pracRecommendationDate) ||
|
|
134
|
-
formatEmaDate(r.procedureStartDate);
|
|
135
|
-
const findingParts = [];
|
|
136
|
-
if (r.referralType)
|
|
137
|
-
findingParts.push(r.referralType);
|
|
138
|
-
if (r.referenceNumber)
|
|
139
|
-
findingParts.push(`(${r.referenceNumber})`);
|
|
140
|
-
if (r.currentStatus)
|
|
141
|
-
findingParts.push(`— ${r.currentStatus}`);
|
|
142
|
-
if (r.referralName && r.referralName.toLowerCase() !== drug.name.toLowerCase()) {
|
|
143
|
-
findingParts.push(`re: ${r.referralName}`);
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
drug_chembl_id: drug.chemblId,
|
|
147
|
-
drug_name: drug.name,
|
|
148
|
-
agency: "EMA",
|
|
149
|
-
action_kind: classifyEmaActionKind(r),
|
|
150
|
-
source_kind: classifyEmaActionKind(r) === "withdrawal" ? "withdrawal" : "referral",
|
|
151
|
-
action_date: date,
|
|
152
|
-
source_date: date,
|
|
153
|
-
finding: clipFinding(findingParts.join(" ")),
|
|
154
|
-
source_url: r.referralUrl || undefined,
|
|
155
|
-
evidence: [],
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Fetch regulatory actions (FDA label + EMA referrals) for a set of drugs.
|
|
161
|
-
* Per-drug failures are isolated — one drug throwing does not blank out
|
|
162
|
-
* the rest. Returns an empty array when no drugs are provided or no source
|
|
163
|
-
* returned any actions.
|
|
164
|
-
*/
|
|
165
|
-
export async function fetchRegulatoryActions(drugs) {
|
|
166
|
-
if (drugs.length === 0)
|
|
167
|
-
return [];
|
|
168
|
-
const perDrug = await Promise.all(drugs.map(async (drug) => {
|
|
169
|
-
const [fdaLabels, emaReferrals] = await Promise.all([
|
|
170
|
-
withHost("openfda", () => getDrugLabelActions(drug.name)).catch(() => []),
|
|
171
|
-
withHost("ema", () => getReferralsByDrug(drug.name)).catch(() => []),
|
|
172
|
-
]);
|
|
173
|
-
return [...fdaRowsFor(drug, fdaLabels), ...emaRowsFor(drug, emaReferrals)];
|
|
174
|
-
}));
|
|
175
|
-
return perDrug.flat();
|
|
176
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The safety-evidence corroboration spine.
|
|
3
|
-
*
|
|
4
|
-
* A run queries a dozen-plus independent sources and files each one's output in
|
|
5
|
-
* its own dossier section. This fold asks the question none of those sections
|
|
6
|
-
* can: for a given organ, do the sources agree? Agreement across independent
|
|
7
|
-
* sources is itself evidence, and it is what separates a real organ liability
|
|
8
|
-
* from a single-source artefact.
|
|
9
|
-
*
|
|
10
|
-
* The fold re-fetches nothing. Every signal it reads is already in hand when it
|
|
11
|
-
* runs, and a source that also populates a section of its own is referenced
|
|
12
|
-
* here as a contribution rather than restated — there is one evidence path, not
|
|
13
|
-
* two.
|
|
14
|
-
*
|
|
15
|
-
* **The source list is open.** A source is one entry in `SIGNAL_SOURCES`
|
|
16
|
-
* declaring its id and how to read its signals out of the run's inputs.
|
|
17
|
-
* Everything downstream — grouping, counting, thresholding, evidence assembly —
|
|
18
|
-
* is written against the id-bearing record, so a fifteenth collector joins the
|
|
19
|
-
* spine by adding an entry and changes neither the fold nor the schema.
|
|
20
|
-
*/
|
|
21
|
-
import type { SafetyCorroboration } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
22
|
-
import type { Phase1Bundle } from "../schemas.js";
|
|
23
|
-
import type { OrganSignalProjection } from "./fda-label-safety.js";
|
|
24
|
-
/** Distinct sources an organ needs before the spine calls it corroborated. */
|
|
25
|
-
export declare const MIN_INDEPENDENT_SOURCES = 2;
|
|
26
|
-
/** What the section reports as having emptied it. */
|
|
27
|
-
export declare const CORROBORATION_FILTER: string;
|
|
28
|
-
/** Everything the fold reads. Already collected — nothing here is fetched. */
|
|
29
|
-
export interface CorroborationInput {
|
|
30
|
-
readonly phase1: Phase1Bundle;
|
|
31
|
-
/** Per-organ FDA label warnings segmented at the approval-precedent step. */
|
|
32
|
-
readonly regulatoryOrganSignals: OrganSignalProjection | null;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Fold the run's per-organ safety signals into one corroboration record per
|
|
36
|
-
* organ.
|
|
37
|
-
*
|
|
38
|
-
* `dropped_count` counts signals — the fold's input unit — discarded because
|
|
39
|
-
* their organ did not resolve, because their source produced no locator, or
|
|
40
|
-
* because their organ drew fewer than `MIN_INDEPENDENT_SOURCES` distinct
|
|
41
|
-
* sources. A fold that ends with no record reports `filtered` with that count
|
|
42
|
-
* rather than an empty `available`.
|
|
43
|
-
*/
|
|
44
|
-
export declare function assembleSafetyCorroboration(input: CorroborationInput): SafetyCorroboration;
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The safety-evidence corroboration spine.
|
|
3
|
-
*
|
|
4
|
-
* A run queries a dozen-plus independent sources and files each one's output in
|
|
5
|
-
* its own dossier section. This fold asks the question none of those sections
|
|
6
|
-
* can: for a given organ, do the sources agree? Agreement across independent
|
|
7
|
-
* sources is itself evidence, and it is what separates a real organ liability
|
|
8
|
-
* from a single-source artefact.
|
|
9
|
-
*
|
|
10
|
-
* The fold re-fetches nothing. Every signal it reads is already in hand when it
|
|
11
|
-
* runs, and a source that also populates a section of its own is referenced
|
|
12
|
-
* here as a contribution rather than restated — there is one evidence path, not
|
|
13
|
-
* two.
|
|
14
|
-
*
|
|
15
|
-
* **The source list is open.** A source is one entry in `SIGNAL_SOURCES`
|
|
16
|
-
* declaring its id and how to read its signals out of the run's inputs.
|
|
17
|
-
* Everything downstream — grouping, counting, thresholding, evidence assembly —
|
|
18
|
-
* is written against the id-bearing record, so a fifteenth collector joins the
|
|
19
|
-
* spine by adding an entry and changes neither the fold nor the schema.
|
|
20
|
-
*/
|
|
21
|
-
import { isSafetyRelevant, meetsTpmFloor, resolveTissueOrganSystem } from "../assemblers/safety.js";
|
|
22
|
-
import { resolveHpoOrgan } from "./hpo-organ-map.js";
|
|
23
|
-
import { resolveImpcOrgan } from "./impc-organ-map.js";
|
|
24
|
-
import { classifyOrgan } from "./meddra-organ-map.js";
|
|
25
|
-
/** Distinct sources an organ needs before the spine calls it corroborated. */
|
|
26
|
-
export const MIN_INDEPENDENT_SOURCES = 2;
|
|
27
|
-
/** What the section reports as having emptied it. */
|
|
28
|
-
export const CORROBORATION_FILTER = `organ signals with no resolvable organ or locator, or whose organ drew fewer than ` + `${MIN_INDEPENDENT_SOURCES} independent sources`;
|
|
29
|
-
/**
|
|
30
|
-
* Curated human phenotypes ranked per organ. A well-annotated gene carries
|
|
31
|
-
* hundreds of HPO terms under one organ system; beyond the ranked head they
|
|
32
|
-
* restate the same organ-level signal, so the source raises it once at the
|
|
33
|
-
* granularity a reader can act on.
|
|
34
|
-
*/
|
|
35
|
-
const PHENOTYPES_PER_ORGAN = 3;
|
|
36
|
-
// ── Source extractors ────────────────────────────────────────────────
|
|
37
|
-
/** Warning fragments from FDA prescribing information, already organ-keyed. */
|
|
38
|
-
function extractRegulatoryLabels(input) {
|
|
39
|
-
const projection = input.regulatoryOrganSignals;
|
|
40
|
-
if (!projection)
|
|
41
|
-
return [];
|
|
42
|
-
return projection.signals.map((s) => ({
|
|
43
|
-
organ: s.organ,
|
|
44
|
-
signal: `${s.drug_name} label §${s.label_section}: ${s.excerpt}`,
|
|
45
|
-
evidence: s.evidence.regulatory_reference ? s.evidence : null,
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
/** Mouse knockout phenotype systems, keyed by the MGI accession for the gene. */
|
|
49
|
-
function extractKnockoutPhenotypes(input) {
|
|
50
|
-
const impc = input.phase1.collectors.impc;
|
|
51
|
-
if (impc.coverage !== "available")
|
|
52
|
-
return [];
|
|
53
|
-
const accession = impc.data.mgiAccessionId;
|
|
54
|
-
return impc.data.organSystems.map((bucket) => ({
|
|
55
|
-
organ: resolveImpcOrgan(bucket),
|
|
56
|
-
signal: `mouse knockout phenotype in the ${bucket} system`,
|
|
57
|
-
evidence: accession
|
|
58
|
-
? {
|
|
59
|
-
source: "impc",
|
|
60
|
-
accession,
|
|
61
|
-
predicate: "knockout_phenotype",
|
|
62
|
-
excerpt: `${impc.data.mouseMarkerSymbol ?? ""} knockout phenotypes in the ${bucket} system`.trim(),
|
|
63
|
-
metadata: { bucket, phenotype_count: impc.data.phenotypeCount },
|
|
64
|
-
}
|
|
65
|
-
: null,
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
/** Curated human phenotypes, resolved by HPO ancestry rather than by label. */
|
|
69
|
-
function extractHumanPhenotypes(input) {
|
|
70
|
-
const monarch = input.phase1.collectors.monarch;
|
|
71
|
-
if (monarch.coverage !== "available")
|
|
72
|
-
return [];
|
|
73
|
-
const ranked = [...monarch.data.phenotypes].sort((a, b) => b.publications.length - a.publications.length || (b.frequencyPercent ?? 0) - (a.frequencyPercent ?? 0) || a.label.localeCompare(b.label));
|
|
74
|
-
const perOrgan = new Map();
|
|
75
|
-
const out = [];
|
|
76
|
-
for (const p of ranked) {
|
|
77
|
-
const organ = resolveHpoOrgan(p.hpoId, p.ancestorIds);
|
|
78
|
-
if (organ === null) {
|
|
79
|
-
out.push({ organ: null, signal: p.label, evidence: null });
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const seen = perOrgan.get(organ) ?? 0;
|
|
83
|
-
if (seen >= PHENOTYPES_PER_ORGAN)
|
|
84
|
-
continue;
|
|
85
|
-
perOrgan.set(organ, seen + 1);
|
|
86
|
-
const pmid = p.publications.find((pub) => pub.startsWith("PMID:"))?.slice("PMID:".length);
|
|
87
|
-
out.push({
|
|
88
|
-
organ,
|
|
89
|
-
signal: `curated human phenotype: ${p.label}`,
|
|
90
|
-
evidence: {
|
|
91
|
-
source: "monarch",
|
|
92
|
-
accession: p.hpoId,
|
|
93
|
-
...(pmid ? { pmid } : {}),
|
|
94
|
-
predicate: "human_phenotype",
|
|
95
|
-
is_human: true,
|
|
96
|
-
excerpt: p.label,
|
|
97
|
-
metadata: {
|
|
98
|
-
...(p.diseaseContext ? { disease_context: p.diseaseContext } : {}),
|
|
99
|
-
...(p.frequencyPercent != null ? { frequency_percent: p.frequencyPercent } : {}),
|
|
100
|
-
...(p.primaryKnowledgeSource ? { knowledge_source: p.primaryKnowledgeSource } : {}),
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
/** Open Targets' curated safety liabilities, keyed by the Ensembl accession. */
|
|
108
|
-
function extractCuratedSafetyLiabilities(input) {
|
|
109
|
-
const ot = input.phase1.collectors.opentargets;
|
|
110
|
-
if (ot.coverage !== "available")
|
|
111
|
-
return [];
|
|
112
|
-
return ot.data.safetyLiabilities.map((liability) => ({
|
|
113
|
-
organ: classifyOrgan(liability.event) ?? classifyOrgan(liability.biosamples.join(" ")),
|
|
114
|
-
signal: `curated safety liability: ${liability.event}`,
|
|
115
|
-
evidence: {
|
|
116
|
-
source: "opentargets:safety",
|
|
117
|
-
accession: ot.data.ensemblId,
|
|
118
|
-
predicate: "known_safety_liability",
|
|
119
|
-
excerpt: liability.effects ? `${liability.event} — ${liability.effects}` : liability.event,
|
|
120
|
-
metadata: { biosamples: liability.biosamples, curator: liability.source },
|
|
121
|
-
},
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Baseline expression in safety-relevant tissues.
|
|
126
|
-
*
|
|
127
|
-
* The atlas reaches the dossier through the Open Targets `baselineExpression`
|
|
128
|
-
* surface, narrowed to the GTEx bulk-tissue rows — the same path
|
|
129
|
-
* `reference_biology.normal_tissue_expression` and the off-tissue risk rows
|
|
130
|
-
* read, with the same safety-relevance and floor rules applied. Only the
|
|
131
|
-
* highest-expressing tissue per organ is raised: several tissues mapping to one
|
|
132
|
-
* organ are one expression signal about that organ.
|
|
133
|
-
*/
|
|
134
|
-
function extractExpressionAtlas(input) {
|
|
135
|
-
const expression = input.phase1.collectors.expressionHuman;
|
|
136
|
-
if (expression.coverage !== "available")
|
|
137
|
-
return [];
|
|
138
|
-
const accession = input.phase1.resolved.ids.ensembl;
|
|
139
|
-
const best = new Map();
|
|
140
|
-
const unresolved = [];
|
|
141
|
-
for (const t of expression.data.tissues) {
|
|
142
|
-
const value = t.value ?? 0;
|
|
143
|
-
if (!isSafetyRelevant(t.tissueLabel, t.organSystem) || !meetsTpmFloor(t.tissueLabel, value))
|
|
144
|
-
continue;
|
|
145
|
-
const organ = resolveTissueOrganSystem(t.tissueLabel, t.organSystem);
|
|
146
|
-
if (organ === null) {
|
|
147
|
-
unresolved.push({ organ: null, signal: `${t.tissueLabel} expression`, evidence: null });
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const current = best.get(organ);
|
|
151
|
-
if (!current || value > current.value)
|
|
152
|
-
best.set(organ, { tissue: t.tissueLabel, value });
|
|
153
|
-
}
|
|
154
|
-
const resolved = [...best.entries()].map(([organ, hit]) => {
|
|
155
|
-
const signal = `${expression.data.unit} expression ${hit.value} in ${hit.tissue}`;
|
|
156
|
-
return {
|
|
157
|
-
organ,
|
|
158
|
-
signal,
|
|
159
|
-
evidence: accession
|
|
160
|
-
? {
|
|
161
|
-
source: `expression:${expression.data.source}`,
|
|
162
|
-
accession,
|
|
163
|
-
predicate: "expressed_in_safety_tissue",
|
|
164
|
-
score: hit.value,
|
|
165
|
-
excerpt: `${hit.tissue}: ${hit.value} (${expression.data.unit})`,
|
|
166
|
-
metadata: {
|
|
167
|
-
tissue: hit.tissue,
|
|
168
|
-
unit: expression.data.unit,
|
|
169
|
-
normalization_notes: expression.data.normalization_notes,
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
: null,
|
|
173
|
-
};
|
|
174
|
-
});
|
|
175
|
-
return [...resolved, ...unresolved];
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* The registered sources.
|
|
179
|
-
*
|
|
180
|
-
* This array is the whole integration surface: an id, and how to read that
|
|
181
|
-
* source's organ-bearing signals out of what the run already collected.
|
|
182
|
-
*/
|
|
183
|
-
const SIGNAL_SOURCES = [
|
|
184
|
-
{ id: "fda_label", extract: extractRegulatoryLabels },
|
|
185
|
-
{ id: "impc", extract: extractKnockoutPhenotypes },
|
|
186
|
-
{ id: "monarch", extract: extractHumanPhenotypes },
|
|
187
|
-
{ id: "opentargets_safety", extract: extractCuratedSafetyLiabilities },
|
|
188
|
-
{ id: "expression_atlas", extract: extractExpressionAtlas },
|
|
189
|
-
];
|
|
190
|
-
/**
|
|
191
|
-
* Fold the run's per-organ safety signals into one corroboration record per
|
|
192
|
-
* organ.
|
|
193
|
-
*
|
|
194
|
-
* `dropped_count` counts signals — the fold's input unit — discarded because
|
|
195
|
-
* their organ did not resolve, because their source produced no locator, or
|
|
196
|
-
* because their organ drew fewer than `MIN_INDEPENDENT_SOURCES` distinct
|
|
197
|
-
* sources. A fold that ends with no record reports `filtered` with that count
|
|
198
|
-
* rather than an empty `available`.
|
|
199
|
-
*/
|
|
200
|
-
export function assembleSafetyCorroboration(input) {
|
|
201
|
-
const sourcesConsidered = [];
|
|
202
|
-
const admitted = [];
|
|
203
|
-
let dropped = 0;
|
|
204
|
-
for (const source of SIGNAL_SOURCES) {
|
|
205
|
-
const signals = source.extract(input);
|
|
206
|
-
if (signals.length === 0)
|
|
207
|
-
continue;
|
|
208
|
-
sourcesConsidered.push(source.id);
|
|
209
|
-
const seen = new Set();
|
|
210
|
-
for (const raw of signals) {
|
|
211
|
-
if (raw.organ === null || raw.evidence === null) {
|
|
212
|
-
dropped += 1;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const key = `${raw.organ}\u0000${raw.signal}`;
|
|
216
|
-
if (seen.has(key))
|
|
217
|
-
continue;
|
|
218
|
-
seen.add(key);
|
|
219
|
-
admitted.push({
|
|
220
|
-
source: source.id,
|
|
221
|
-
organ: raw.organ,
|
|
222
|
-
contribution: { source: source.id, signal: raw.signal, evidence: raw.evidence },
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (sourcesConsidered.length === 0) {
|
|
227
|
-
return {
|
|
228
|
-
coverage: "queried_no_data",
|
|
229
|
-
error: { message: "no source produced an organ-bearing safety signal" },
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
const byOrgan = new Map();
|
|
233
|
-
for (const item of admitted) {
|
|
234
|
-
const bucket = byOrgan.get(item.organ);
|
|
235
|
-
if (bucket)
|
|
236
|
-
bucket.push(item);
|
|
237
|
-
else
|
|
238
|
-
byOrgan.set(item.organ, [item]);
|
|
239
|
-
}
|
|
240
|
-
const rows = [];
|
|
241
|
-
for (const [organ, items] of byOrgan) {
|
|
242
|
-
const corroborating = [...new Set(items.map((i) => i.source))].sort();
|
|
243
|
-
if (corroborating.length < MIN_INDEPENDENT_SOURCES) {
|
|
244
|
-
dropped += items.length;
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
const contributions = items.map((i) => i.contribution);
|
|
248
|
-
rows.push({
|
|
249
|
-
organ,
|
|
250
|
-
contributions,
|
|
251
|
-
corroborating_sources: corroborating,
|
|
252
|
-
independent_source_count: corroborating.length,
|
|
253
|
-
support: { state: "scored", evidence: contributions.map((c) => c.evidence) },
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
rows.sort((a, b) => b.independent_source_count - a.independent_source_count || a.organ.localeCompare(b.organ));
|
|
257
|
-
// Reaching here means at least one source emitted a signal — that is what put
|
|
258
|
-
// it in `sourcesConsidered` — and every emitted signal was either admitted or
|
|
259
|
-
// counted as dropped. So no record left standing means the fold's own
|
|
260
|
-
// thresholds took them all, and `dropped` is necessarily positive.
|
|
261
|
-
if (rows.length === 0) {
|
|
262
|
-
return { coverage: "filtered", filter: CORROBORATION_FILTER, dropped_count: dropped };
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
coverage: "available",
|
|
266
|
-
data: {
|
|
267
|
-
rows,
|
|
268
|
-
sources_considered: [...sourcesConsidered].sort(),
|
|
269
|
-
min_independent_sources: MIN_INDEPENDENT_SOURCES,
|
|
270
|
-
},
|
|
271
|
-
...(dropped > 0 ? { dropped_count: dropped } : {}),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured-output LLM helper for the target-assessment DBOS workflow.
|
|
3
|
-
*
|
|
4
|
-
* Uses a tool-choice forcing pattern: a single Anthropic tool named `submit`
|
|
5
|
-
* carries the Zod schema as its `input_schema`, and
|
|
6
|
-
* `tool_choice: { type: "tool", name: "submit" }` forces the model to fill
|
|
7
|
-
* it. The tool's `input` IS the structured output.
|
|
8
|
-
*
|
|
9
|
-
* This intentionally does NOT pull in the harness `runAgent` loop — Phase-2
|
|
10
|
-
* decisions are zero-tool single-shot calls. Phase-5 syntheses today use
|
|
11
|
-
* regulatory-guidance + approval-precedent retrieval tools; the harness
|
|
12
|
-
* port drops those grounding tools (PR #4 ships single-shot synthesis; a
|
|
13
|
-
* follow-up may reintroduce the retrieval tools through the agent loop).
|
|
14
|
-
*
|
|
15
|
-
* Calls are routed through `runLlmStep`, so each LLM call lives in its own
|
|
16
|
-
* `DBOS.runStep({name})` cache slot and a billing-gateway 402 self-cancels the
|
|
17
|
-
* workflow rather than returning a coverage envelope.
|
|
18
|
-
*/
|
|
19
|
-
import { z } from "zod";
|
|
20
|
-
import type { AgentSession } from "../../../auth/types.js";
|
|
21
|
-
import type { Logger } from "../../../lib/logger.js";
|
|
22
|
-
import type { AgentChat } from "../../../providers/types.js";
|
|
23
|
-
import { BUDGET_EXCEEDED_SENTINEL } from "./llm-step.js";
|
|
24
|
-
export interface StructuredLlmCallOptions<TSchema extends z.ZodType> {
|
|
25
|
-
/** Operational logging seam, forwarded to `runLlmStep`; omitted falls back to no-op. */
|
|
26
|
-
readonly logger?: Logger;
|
|
27
|
-
/** Attempt-numbered durable-step name (e.g. `"ta-decision:modulator-triage:0"`). */
|
|
28
|
-
readonly stepName: string;
|
|
29
|
-
/** Agent id stamped on telemetry and billing-gateway provenance. */
|
|
30
|
-
readonly agentId: string;
|
|
31
|
-
readonly provider: AgentChat;
|
|
32
|
-
readonly session: AgentSession;
|
|
33
|
-
readonly system: string;
|
|
34
|
-
readonly prompt: string;
|
|
35
|
-
readonly schema: TSchema;
|
|
36
|
-
readonly model: string;
|
|
37
|
-
readonly signal?: AbortSignal;
|
|
38
|
-
}
|
|
39
|
-
export type StructuredLlmResult<TSchema extends z.ZodType> = {
|
|
40
|
-
readonly kind: "ok";
|
|
41
|
-
readonly value: z.infer<TSchema>;
|
|
42
|
-
} | {
|
|
43
|
-
readonly kind: "budget-exceeded";
|
|
44
|
-
readonly sentinel: typeof BUDGET_EXCEEDED_SENTINEL;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Thrown when the model returned a structured-output payload that does
|
|
48
|
-
* not parse against the declared Zod schema. The caller decides whether
|
|
49
|
-
* to wrap as `coverage: "queried_no_data"` or rethrow as a hard failure.
|
|
50
|
-
*/
|
|
51
|
-
export declare class StructuredOutputParseError extends Error {
|
|
52
|
-
readonly cause?: unknown | undefined;
|
|
53
|
-
constructor(message: string, cause?: unknown | undefined);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Run one structured-output LLM call. Forces the model to call the
|
|
57
|
-
* `submit` tool whose input schema is `opts.schema`; parses the tool
|
|
58
|
-
* input via `safeParse` and returns the typed value. A billing-gateway 402 returns
|
|
59
|
-
* the budget-exceeded sentinel; non-402 throws + parse errors throw.
|
|
60
|
-
*/
|
|
61
|
-
export declare function structuredLlmCall<TSchema extends z.ZodType>(opts: StructuredLlmCallOptions<TSchema>): Promise<StructuredLlmResult<TSchema>>;
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured-output LLM helper for the target-assessment DBOS workflow.
|
|
3
|
-
*
|
|
4
|
-
* Uses a tool-choice forcing pattern: a single Anthropic tool named `submit`
|
|
5
|
-
* carries the Zod schema as its `input_schema`, and
|
|
6
|
-
* `tool_choice: { type: "tool", name: "submit" }` forces the model to fill
|
|
7
|
-
* it. The tool's `input` IS the structured output.
|
|
8
|
-
*
|
|
9
|
-
* This intentionally does NOT pull in the harness `runAgent` loop — Phase-2
|
|
10
|
-
* decisions are zero-tool single-shot calls. Phase-5 syntheses today use
|
|
11
|
-
* regulatory-guidance + approval-precedent retrieval tools; the harness
|
|
12
|
-
* port drops those grounding tools (PR #4 ships single-shot synthesis; a
|
|
13
|
-
* follow-up may reintroduce the retrieval tools through the agent loop).
|
|
14
|
-
*
|
|
15
|
-
* Calls are routed through `runLlmStep`, so each LLM call lives in its own
|
|
16
|
-
* `DBOS.runStep({name})` cache slot and a billing-gateway 402 self-cancels the
|
|
17
|
-
* workflow rather than returning a coverage envelope.
|
|
18
|
-
*/
|
|
19
|
-
import { jsonSchema, tool as aiTool } from "ai";
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import { runLlmStep } from "./llm-step.js";
|
|
22
|
-
const SUBMIT_TOOL_NAME = "submit";
|
|
23
|
-
/**
|
|
24
|
-
* Thrown when the model returned a structured-output payload that does
|
|
25
|
-
* not parse against the declared Zod schema. The caller decides whether
|
|
26
|
-
* to wrap as `coverage: "queried_no_data"` or rethrow as a hard failure.
|
|
27
|
-
*/
|
|
28
|
-
export class StructuredOutputParseError extends Error {
|
|
29
|
-
cause;
|
|
30
|
-
constructor(message, cause) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.cause = cause;
|
|
33
|
-
this.name = "StructuredOutputParseError";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Convert a Zod schema to the JSON Schema shape Anthropic accepts as
|
|
38
|
-
* `tool.input_schema`. Strips `$schema` (Anthropic rejects it) and asserts
|
|
39
|
-
* a top-level `"type": "object"` (Anthropic rejects oneOf at the root).
|
|
40
|
-
*/
|
|
41
|
-
function schemaToJsonSchema(schema, stepName) {
|
|
42
|
-
const jsonSchema = z.toJSONSchema(schema);
|
|
43
|
-
delete jsonSchema.$schema;
|
|
44
|
-
if (jsonSchema.type !== "object") {
|
|
45
|
-
throw new Error(`structuredLlmCall(${stepName}): schema must emit top-level "type":"object", ` + `got ${JSON.stringify(jsonSchema.type)}`);
|
|
46
|
-
}
|
|
47
|
-
// `z.toJSONSchema()` returns an untyped JSON-schema object; it is structurally
|
|
48
|
-
// Anthropic's `input_schema` once the top-level `type:"object"` check above passes
|
|
49
|
-
// (the one shape Anthropic requires), which is the invariant making this cast safe.
|
|
50
|
-
return jsonSchema;
|
|
51
|
-
}
|
|
52
|
-
function extractToolUseInput(blocks) {
|
|
53
|
-
for (const block of blocks) {
|
|
54
|
-
const part = block;
|
|
55
|
-
if (part.type === "tool-call" && part.toolName === SUBMIT_TOOL_NAME) {
|
|
56
|
-
return part.input;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Run one structured-output LLM call. Forces the model to call the
|
|
63
|
-
* `submit` tool whose input schema is `opts.schema`; parses the tool
|
|
64
|
-
* input via `safeParse` and returns the typed value. A billing-gateway 402 returns
|
|
65
|
-
* the budget-exceeded sentinel; non-402 throws + parse errors throw.
|
|
66
|
-
*/
|
|
67
|
-
export async function structuredLlmCall(opts) {
|
|
68
|
-
const inputSchema = schemaToJsonSchema(opts.schema, opts.stepName);
|
|
69
|
-
const req = {
|
|
70
|
-
system: opts.system,
|
|
71
|
-
messages: [{ role: "user", content: opts.prompt }],
|
|
72
|
-
tools: {
|
|
73
|
-
[SUBMIT_TOOL_NAME]: aiTool({
|
|
74
|
-
description: `Submit the structured output. Call this tool exactly once with the final answer.`,
|
|
75
|
-
inputSchema: jsonSchema(inputSchema),
|
|
76
|
-
}),
|
|
77
|
-
},
|
|
78
|
-
toolChoice: { type: "tool", toolName: SUBMIT_TOOL_NAME },
|
|
79
|
-
};
|
|
80
|
-
const result = await runLlmStep({
|
|
81
|
-
...(opts.logger ? { logger: opts.logger } : {}),
|
|
82
|
-
stepName: opts.stepName,
|
|
83
|
-
agentId: opts.agentId,
|
|
84
|
-
provider: opts.provider,
|
|
85
|
-
req,
|
|
86
|
-
session: opts.session,
|
|
87
|
-
signal: opts.signal,
|
|
88
|
-
});
|
|
89
|
-
if (result.kind === "budget-exceeded") {
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
const content = result.response.message.content;
|
|
93
|
-
const raw = extractToolUseInput(Array.isArray(content) ? content : []);
|
|
94
|
-
if (raw === undefined) {
|
|
95
|
-
throw new StructuredOutputParseError(`structuredLlmCall(${opts.stepName}): model returned no submit tool_use block`);
|
|
96
|
-
}
|
|
97
|
-
const parsed = opts.schema.safeParse(raw);
|
|
98
|
-
if (!parsed.success) {
|
|
99
|
-
throw new StructuredOutputParseError(`structuredLlmCall(${opts.stepName}): submit payload failed schema validation: ${parsed.error.message}`, parsed.error);
|
|
100
|
-
}
|
|
101
|
-
return { kind: "ok", value: parsed.data };
|
|
102
|
-
}
|