@easyoref/agent 1.21.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/__tests__/clarify.test.ts +827 -0
- package/__tests__/config.test.ts +304 -0
- package/__tests__/enrichment.integration.test.ts +871 -0
- package/__tests__/graph.test.ts +661 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +54 -0
- package/dist/auth.js.map +1 -0
- package/dist/dry-run.d.ts +12 -0
- package/dist/dry-run.d.ts.map +1 -0
- package/dist/dry-run.js +236 -0
- package/dist/dry-run.js.map +1 -0
- package/dist/extract.d.ts +180 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +210 -0
- package/dist/extract.js.map +1 -0
- package/dist/graph.d.ts +4083 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +162 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +7 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +18 -0
- package/dist/models.js.map +1 -0
- package/dist/nodes/clarify-node.d.ts +132 -0
- package/dist/nodes/clarify-node.d.ts.map +1 -0
- package/dist/nodes/clarify-node.js +118 -0
- package/dist/nodes/clarify-node.js.map +1 -0
- package/dist/nodes/clarify.d.ts +6 -0
- package/dist/nodes/clarify.d.ts.map +1 -0
- package/dist/nodes/clarify.js +124 -0
- package/dist/nodes/clarify.js.map +1 -0
- package/dist/nodes/edit-node.d.ts +71 -0
- package/dist/nodes/edit-node.d.ts.map +1 -0
- package/dist/nodes/edit-node.js +496 -0
- package/dist/nodes/edit-node.js.map +1 -0
- package/dist/nodes/edit.d.ts +6 -0
- package/dist/nodes/edit.d.ts.map +1 -0
- package/dist/nodes/edit.js +22 -0
- package/dist/nodes/edit.js.map +1 -0
- package/dist/nodes/extract-node.d.ts +174 -0
- package/dist/nodes/extract-node.d.ts.map +1 -0
- package/dist/nodes/extract-node.js +233 -0
- package/dist/nodes/extract-node.js.map +1 -0
- package/dist/nodes/extract.d.ts +6 -0
- package/dist/nodes/extract.d.ts.map +1 -0
- package/dist/nodes/extract.js +49 -0
- package/dist/nodes/extract.js.map +1 -0
- package/dist/nodes/filter-agent.d.ts +11 -0
- package/dist/nodes/filter-agent.d.ts.map +1 -0
- package/dist/nodes/filter-agent.js +60 -0
- package/dist/nodes/filter-agent.js.map +1 -0
- package/dist/nodes/filter-node.d.ts +9 -0
- package/dist/nodes/filter-node.d.ts.map +1 -0
- package/dist/nodes/filter-node.js +111 -0
- package/dist/nodes/filter-node.js.map +1 -0
- package/dist/nodes/filters.d.ts +13 -0
- package/dist/nodes/filters.d.ts.map +1 -0
- package/dist/nodes/filters.js +111 -0
- package/dist/nodes/filters.js.map +1 -0
- package/dist/nodes/message-node.d.ts +71 -0
- package/dist/nodes/message-node.d.ts.map +1 -0
- package/dist/nodes/message-node.js +491 -0
- package/dist/nodes/message-node.js.map +1 -0
- package/dist/nodes/message.d.ts +71 -0
- package/dist/nodes/message.d.ts.map +1 -0
- package/dist/nodes/message.js +496 -0
- package/dist/nodes/message.js.map +1 -0
- package/dist/nodes/vote-node.d.ts +13 -0
- package/dist/nodes/vote-node.d.ts.map +1 -0
- package/dist/nodes/vote-node.js +232 -0
- package/dist/nodes/vote-node.js.map +1 -0
- package/dist/nodes/vote.d.ts +13 -0
- package/dist/nodes/vote.d.ts.map +1 -0
- package/dist/nodes/vote.js +232 -0
- package/dist/nodes/vote.js.map +1 -0
- package/dist/queue.d.ts +15 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +41 -0
- package/dist/queue.js.map +1 -0
- package/dist/redis.d.ts +8 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +33 -0
- package/dist/redis.js.map +1 -0
- package/dist/runtime/auth.d.ts +11 -0
- package/dist/runtime/auth.d.ts.map +1 -0
- package/dist/runtime/auth.js +54 -0
- package/dist/runtime/auth.js.map +1 -0
- package/dist/runtime/dry-run.d.ts +12 -0
- package/dist/runtime/dry-run.d.ts.map +1 -0
- package/dist/runtime/dry-run.js +236 -0
- package/dist/runtime/dry-run.js.map +1 -0
- package/dist/runtime/queue.d.ts +15 -0
- package/dist/runtime/queue.d.ts.map +1 -0
- package/dist/runtime/queue.js +41 -0
- package/dist/runtime/queue.js.map +1 -0
- package/dist/runtime/redis.d.ts +8 -0
- package/dist/runtime/redis.d.ts.map +1 -0
- package/dist/runtime/redis.js +33 -0
- package/dist/runtime/redis.js.map +1 -0
- package/dist/runtime/worker.d.ts +14 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +135 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/tools/alert-history.d.ts +18 -0
- package/dist/tools/alert-history.d.ts.map +1 -0
- package/dist/tools/alert-history.js +98 -0
- package/dist/tools/alert-history.js.map +1 -0
- package/dist/tools/betterstack-log.d.ts +15 -0
- package/dist/tools/betterstack-log.d.ts.map +1 -0
- package/dist/tools/betterstack-log.js +80 -0
- package/dist/tools/betterstack-log.js.map +1 -0
- package/dist/tools/index.d.ts +44 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read-sources.d.ts +15 -0
- package/dist/tools/read-sources.d.ts.map +1 -0
- package/dist/tools/read-sources.js +67 -0
- package/dist/tools/read-sources.js.map +1 -0
- package/dist/tools/resolve-area.d.ts +19 -0
- package/dist/tools/resolve-area.d.ts.map +1 -0
- package/dist/tools/resolve-area.js +147 -0
- package/dist/tools/resolve-area.js.map +1 -0
- package/dist/tools.d.ts +115 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +439 -0
- package/dist/tools.js.map +1 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +135 -0
- package/dist/worker.js.map +1 -0
- package/package.json +26 -0
- package/src/graph.ts +200 -0
- package/src/index.ts +27 -0
- package/src/models.ts +20 -0
- package/src/nodes/clarify-node.ts +172 -0
- package/src/nodes/edit-node.ts +695 -0
- package/src/nodes/extract-node.ts +299 -0
- package/src/nodes/filter-node.ts +139 -0
- package/src/nodes/message.ts +695 -0
- package/src/nodes/vote-node.ts +354 -0
- package/src/nodes/vote.ts +354 -0
- package/src/runtime/auth.ts +63 -0
- package/src/runtime/dry-run.ts +303 -0
- package/src/runtime/queue.ts +53 -0
- package/src/runtime/redis.ts +38 -0
- package/src/runtime/worker.ts +167 -0
- package/src/tools/alert-history.ts +120 -0
- package/src/tools/betterstack-log.ts +102 -0
- package/src/tools/index.ts +23 -0
- package/src/tools/read-sources.ts +86 -0
- package/src/tools/resolve-area.ts +202 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vote Node — consensus voting, deterministic, 0 tokens.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates extraction results from multiple sources into a single
|
|
5
|
+
* voted consensus using median, majority, and weighted confidence.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as logger from "@easyoref/monitoring";
|
|
9
|
+
import type {
|
|
10
|
+
CitedSource,
|
|
11
|
+
QualitativeCount,
|
|
12
|
+
ValidatedExtraction,
|
|
13
|
+
VotedResult,
|
|
14
|
+
} from "@easyoref/shared";
|
|
15
|
+
import type { AgentStateType } from "../graph.js";
|
|
16
|
+
|
|
17
|
+
function weightedConfidence(
|
|
18
|
+
sources: Array<{ sourceTrust: number; confidence: number }>,
|
|
19
|
+
): number {
|
|
20
|
+
if (sources.length === 0) return 0;
|
|
21
|
+
return (
|
|
22
|
+
sources.reduce(
|
|
23
|
+
(accumulator, extraction) =>
|
|
24
|
+
accumulator + extraction.sourceTrust * extraction.confidence,
|
|
25
|
+
0,
|
|
26
|
+
) / sources.length
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function modeQualification(
|
|
31
|
+
sources: Array<Record<string, unknown>>,
|
|
32
|
+
key: string,
|
|
33
|
+
): QualitativeCount | undefined {
|
|
34
|
+
const values = sources
|
|
35
|
+
.map((extraction) => extraction[key] as QualitativeCount | undefined)
|
|
36
|
+
.filter((value): value is QualitativeCount => value !== undefined);
|
|
37
|
+
if (values.length === 0) return undefined;
|
|
38
|
+
const frequency = new Map<QualitativeCount, number>();
|
|
39
|
+
for (const value of values)
|
|
40
|
+
frequency.set(value, (frequency.get(value) ?? 0) + 1);
|
|
41
|
+
return [...frequency.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function medianQualificationNumber(
|
|
45
|
+
sources: Array<Record<string, unknown>>,
|
|
46
|
+
key: string,
|
|
47
|
+
): number | undefined {
|
|
48
|
+
const values = sources
|
|
49
|
+
.map((extraction) => extraction[key] as number | undefined)
|
|
50
|
+
.filter((value): value is number => value !== undefined)
|
|
51
|
+
.sort((a, b) => a - b);
|
|
52
|
+
return values.length > 0 ? values[Math.floor(values.length / 2)] : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function aggregateVote(
|
|
56
|
+
extractions: ValidatedExtraction[],
|
|
57
|
+
alertId: string,
|
|
58
|
+
): VotedResult | undefined {
|
|
59
|
+
const valid = extractions.filter((extraction) => extraction.valid);
|
|
60
|
+
|
|
61
|
+
if (valid.length === 0) return undefined;
|
|
62
|
+
|
|
63
|
+
const indexed = valid.map((extraction, index) => ({
|
|
64
|
+
...extraction,
|
|
65
|
+
citationIndex: index + 1,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const citedSources: CitedSource[] = indexed.map((extraction) => ({
|
|
69
|
+
index: extraction.citationIndex,
|
|
70
|
+
channel: extraction.channel,
|
|
71
|
+
...(extraction.messageUrl && { messageUrl: extraction.messageUrl }),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const etaSources = indexed
|
|
75
|
+
.filter((extraction) => extraction.etaRefinedMinutes !== undefined)
|
|
76
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
77
|
+
const bestEtaSource = etaSources[0];
|
|
78
|
+
|
|
79
|
+
const countryMap = new Map<
|
|
80
|
+
string,
|
|
81
|
+
{ canonical: string; citations: number[] }
|
|
82
|
+
>();
|
|
83
|
+
for (const extraction of indexed) {
|
|
84
|
+
if (extraction.countryOrigin) {
|
|
85
|
+
const key = extraction.countryOrigin.toLowerCase();
|
|
86
|
+
const entry = countryMap.get(key);
|
|
87
|
+
if (entry) {
|
|
88
|
+
entry.citations.push(extraction.citationIndex);
|
|
89
|
+
} else {
|
|
90
|
+
countryMap.set(key, {
|
|
91
|
+
canonical: extraction.countryOrigin,
|
|
92
|
+
citations: [extraction.citationIndex],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const countryOrigins =
|
|
98
|
+
countryMap.size > 0
|
|
99
|
+
? Array.from(countryMap.values()).map(({ canonical, citations }) => ({
|
|
100
|
+
name: canonical,
|
|
101
|
+
citations,
|
|
102
|
+
}))
|
|
103
|
+
: undefined;
|
|
104
|
+
|
|
105
|
+
const rocketSources = indexed.filter(
|
|
106
|
+
(extraction) => extraction.rocketCount !== undefined,
|
|
107
|
+
);
|
|
108
|
+
const rocketValues = rocketSources.map(
|
|
109
|
+
(extraction) => extraction.rocketCount as number,
|
|
110
|
+
);
|
|
111
|
+
const rocketCountMin =
|
|
112
|
+
rocketValues.length > 0 ? Math.min(...rocketValues) : undefined;
|
|
113
|
+
const rocketCountMax =
|
|
114
|
+
rocketValues.length > 0 ? Math.max(...rocketValues) : undefined;
|
|
115
|
+
const rocketCitations = rocketSources.map(
|
|
116
|
+
(extraction) => extraction.citationIndex,
|
|
117
|
+
);
|
|
118
|
+
const rocketConfidence = weightedConfidence(rocketSources);
|
|
119
|
+
|
|
120
|
+
const detailSources = indexed
|
|
121
|
+
.filter((extraction) => extraction.rocketDetail)
|
|
122
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
123
|
+
const rocketDetail = detailSources[0]?.rocketDetail;
|
|
124
|
+
|
|
125
|
+
const cassetteSources = indexed.filter(
|
|
126
|
+
(extraction) => extraction.isCassette !== undefined,
|
|
127
|
+
);
|
|
128
|
+
const cassetteValues = cassetteSources.map(
|
|
129
|
+
(extraction) => extraction.isCassette as boolean,
|
|
130
|
+
);
|
|
131
|
+
const isCassette =
|
|
132
|
+
cassetteValues.length > 0
|
|
133
|
+
? cassetteValues.filter(Boolean).length > cassetteValues.length / 2
|
|
134
|
+
: undefined;
|
|
135
|
+
const cassetteConfidence = weightedConfidence(cassetteSources);
|
|
136
|
+
|
|
137
|
+
const interceptedSources = indexed.filter(
|
|
138
|
+
(extraction) => extraction.intercepted !== undefined,
|
|
139
|
+
);
|
|
140
|
+
const interceptedQualSources = indexed.filter(
|
|
141
|
+
(extraction) => extraction.interceptedQual !== undefined,
|
|
142
|
+
);
|
|
143
|
+
const interceptedValues = interceptedSources
|
|
144
|
+
.map((extraction) => extraction.intercepted as number)
|
|
145
|
+
.sort((a, b) => a - b);
|
|
146
|
+
const intercepted =
|
|
147
|
+
interceptedValues.length > 0
|
|
148
|
+
? interceptedValues[Math.floor(interceptedValues.length / 2)]
|
|
149
|
+
: undefined;
|
|
150
|
+
const interceptedQual =
|
|
151
|
+
intercepted === undefined
|
|
152
|
+
? modeQualification(interceptedQualSources, "interceptedQual")
|
|
153
|
+
: undefined;
|
|
154
|
+
const interceptedQualNumber = interceptedQual
|
|
155
|
+
? medianQualificationNumber(interceptedQualSources, "interceptedQualNum")
|
|
156
|
+
: undefined;
|
|
157
|
+
const interceptedConfidence = weightedConfidence(
|
|
158
|
+
interceptedSources.length > 0 ? interceptedSources : interceptedQualSources,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const seaSources = indexed.filter(
|
|
162
|
+
(extraction) => extraction.seaImpact !== undefined,
|
|
163
|
+
);
|
|
164
|
+
const seaQualSources = indexed.filter(
|
|
165
|
+
(extraction) => extraction.seaImpactQual !== undefined,
|
|
166
|
+
);
|
|
167
|
+
const seaValues = seaSources
|
|
168
|
+
.map((extraction) => extraction.seaImpact as number)
|
|
169
|
+
.sort((a, b) => a - b);
|
|
170
|
+
const seaImpact =
|
|
171
|
+
seaValues.length > 0
|
|
172
|
+
? seaValues[Math.floor(seaValues.length / 2)]
|
|
173
|
+
: undefined;
|
|
174
|
+
const seaImpactQual =
|
|
175
|
+
seaImpact === undefined
|
|
176
|
+
? modeQualification(seaQualSources, "seaImpactQual")
|
|
177
|
+
: undefined;
|
|
178
|
+
const seaImpactQualNumber = seaImpactQual
|
|
179
|
+
? medianQualificationNumber(seaQualSources, "seaImpactQualNum")
|
|
180
|
+
: undefined;
|
|
181
|
+
const seaConfidence = weightedConfidence(
|
|
182
|
+
seaSources.length > 0 ? seaSources : seaQualSources,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const openSources = indexed.filter(
|
|
186
|
+
(extraction) => extraction.openAreaImpact !== undefined,
|
|
187
|
+
);
|
|
188
|
+
const openQualSources = indexed.filter(
|
|
189
|
+
(extraction) => extraction.openAreaImpactQual !== undefined,
|
|
190
|
+
);
|
|
191
|
+
const openValues = openSources
|
|
192
|
+
.map((extraction) => extraction.openAreaImpact as number)
|
|
193
|
+
.sort((a, b) => a - b);
|
|
194
|
+
const openAreaImpact =
|
|
195
|
+
openValues.length > 0
|
|
196
|
+
? openValues[Math.floor(openValues.length / 2)]
|
|
197
|
+
: undefined;
|
|
198
|
+
const openAreaImpactQual =
|
|
199
|
+
openAreaImpact === undefined
|
|
200
|
+
? modeQualification(openQualSources, "openAreaImpactQual")
|
|
201
|
+
: undefined;
|
|
202
|
+
const openAreaImpactQualNumber = openAreaImpactQual
|
|
203
|
+
? medianQualificationNumber(openQualSources, "openAreaImpactQualNum")
|
|
204
|
+
: undefined;
|
|
205
|
+
const openAreaConfidence = weightedConfidence(
|
|
206
|
+
openSources.length > 0 ? openSources : openQualSources,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const allHitsSources = indexed.filter(
|
|
210
|
+
(extraction) => extraction.hitsConfirmed !== undefined,
|
|
211
|
+
);
|
|
212
|
+
const hitsValues = allHitsSources
|
|
213
|
+
.map((extraction) => extraction.hitsConfirmed as number)
|
|
214
|
+
.sort((a, b) => a - b);
|
|
215
|
+
const hitsConfirmed =
|
|
216
|
+
hitsValues.length > 0
|
|
217
|
+
? hitsValues[Math.floor(hitsValues.length / 2)]
|
|
218
|
+
: undefined;
|
|
219
|
+
const positiveHitsSources = allHitsSources.filter(
|
|
220
|
+
(extraction) => (extraction.hitsConfirmed as number) > 0,
|
|
221
|
+
);
|
|
222
|
+
const hitsCitations =
|
|
223
|
+
positiveHitsSources.length > 0
|
|
224
|
+
? positiveHitsSources.map((extraction) => extraction.citationIndex)
|
|
225
|
+
: allHitsSources.map((extraction) => extraction.citationIndex);
|
|
226
|
+
const hitsConfidence = weightedConfidence(allHitsSources);
|
|
227
|
+
|
|
228
|
+
const hitsWithLocation = positiveHitsSources
|
|
229
|
+
.filter((extraction) => extraction.hitLocation)
|
|
230
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
231
|
+
const hitLocation = hitsWithLocation[0]?.hitLocation;
|
|
232
|
+
const hitType = hitsWithLocation[0]?.hitType;
|
|
233
|
+
|
|
234
|
+
const hitsWithDetail = positiveHitsSources
|
|
235
|
+
.filter((extraction) => extraction.hitDetail)
|
|
236
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
237
|
+
const hitDetail = hitsWithDetail[0]?.hitDetail;
|
|
238
|
+
|
|
239
|
+
const noImpactSources = allHitsSources.filter(
|
|
240
|
+
(extraction) => (extraction.hitsConfirmed as number) === 0,
|
|
241
|
+
);
|
|
242
|
+
const noImpacts = noImpactSources.length > 0 && hitsConfirmed === 0;
|
|
243
|
+
const noImpactsCitations = noImpactSources.map(
|
|
244
|
+
(extraction) => extraction.citationIndex,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const casualtySources = indexed.filter(
|
|
248
|
+
(extraction) => extraction.casualties && extraction.casualties > 0,
|
|
249
|
+
);
|
|
250
|
+
const casualtyValues = casualtySources
|
|
251
|
+
.map((extraction) => extraction.casualties as number)
|
|
252
|
+
.sort((a, b) => a - b);
|
|
253
|
+
const casualties =
|
|
254
|
+
casualtyValues.length > 0
|
|
255
|
+
? casualtyValues[Math.floor(casualtyValues.length / 2)]
|
|
256
|
+
: undefined;
|
|
257
|
+
const casualtiesCitations = casualtySources.map(
|
|
258
|
+
(extraction) => extraction.citationIndex,
|
|
259
|
+
);
|
|
260
|
+
const casualtiesConfidence = weightedConfidence(casualtySources);
|
|
261
|
+
|
|
262
|
+
const injurySources = indexed.filter(
|
|
263
|
+
(extraction) =>
|
|
264
|
+
extraction.injuries !== undefined && (extraction.injuries as number) > 0,
|
|
265
|
+
);
|
|
266
|
+
const injuryValues = injurySources
|
|
267
|
+
.map((extraction) => extraction.injuries as number)
|
|
268
|
+
.sort((a, b) => a - b);
|
|
269
|
+
const injuries =
|
|
270
|
+
injuryValues.length > 0
|
|
271
|
+
? injuryValues[Math.floor(injuryValues.length / 2)]
|
|
272
|
+
: undefined;
|
|
273
|
+
const injuriesCitations = injurySources.map(
|
|
274
|
+
(extraction) => extraction.citationIndex,
|
|
275
|
+
);
|
|
276
|
+
const injuriesConfidence = weightedConfidence(injurySources);
|
|
277
|
+
|
|
278
|
+
const injuryCauseValues = injurySources
|
|
279
|
+
.map((extraction) => extraction.injuriesCause)
|
|
280
|
+
.filter(
|
|
281
|
+
(value): value is "rocket" | "rushing_to_shelter" => value !== undefined,
|
|
282
|
+
);
|
|
283
|
+
const rocketCauseCount = injuryCauseValues.filter(
|
|
284
|
+
(value) => value === "rocket",
|
|
285
|
+
).length;
|
|
286
|
+
const shelterCauseCount = injuryCauseValues.filter(
|
|
287
|
+
(value) => value === "rushing_to_shelter",
|
|
288
|
+
).length;
|
|
289
|
+
const injuriesCause =
|
|
290
|
+
injuryCauseValues.length === 0
|
|
291
|
+
? undefined
|
|
292
|
+
: rocketCauseCount >= shelterCauseCount
|
|
293
|
+
? "rocket"
|
|
294
|
+
: "rushing_to_shelter";
|
|
295
|
+
|
|
296
|
+
const totalWeight = indexed.reduce(
|
|
297
|
+
(accumulator, extraction) =>
|
|
298
|
+
accumulator + extraction.sourceTrust * extraction.confidence,
|
|
299
|
+
0,
|
|
300
|
+
);
|
|
301
|
+
const weightedConfidenceValue = totalWeight / indexed.length;
|
|
302
|
+
|
|
303
|
+
const voted: VotedResult = {
|
|
304
|
+
etaRefinedMinutes: bestEtaSource?.etaRefinedMinutes,
|
|
305
|
+
etaCitations: bestEtaSource ? [bestEtaSource.citationIndex] : [],
|
|
306
|
+
countryOrigins: countryOrigins ?? [],
|
|
307
|
+
rocketCountMin: rocketCountMin,
|
|
308
|
+
rocketCountMax: rocketCountMax,
|
|
309
|
+
rocketCitations: rocketCitations,
|
|
310
|
+
rocketConfidence: rocketConfidence,
|
|
311
|
+
rocketDetail: rocketDetail,
|
|
312
|
+
isCassette: isCassette,
|
|
313
|
+
isCassetteConfidence: cassetteConfidence,
|
|
314
|
+
intercepted,
|
|
315
|
+
interceptedQual: interceptedQual,
|
|
316
|
+
interceptedConfidence: interceptedConfidence,
|
|
317
|
+
seaImpact: seaImpact,
|
|
318
|
+
seaImpactQual: seaImpactQual,
|
|
319
|
+
seaConfidence: seaConfidence,
|
|
320
|
+
openAreaImpact: openAreaImpact,
|
|
321
|
+
openAreaImpactQual: openAreaImpactQual,
|
|
322
|
+
openAreaConfidence: openAreaConfidence,
|
|
323
|
+
hitsConfirmed: hitsConfirmed,
|
|
324
|
+
hitsCitations: hitsCitations,
|
|
325
|
+
hitsConfidence: hitsConfidence,
|
|
326
|
+
hitLocation: hitLocation,
|
|
327
|
+
hitType: hitType,
|
|
328
|
+
hitDetail: hitDetail,
|
|
329
|
+
noImpacts: noImpacts,
|
|
330
|
+
noImpactsCitations: noImpactsCitations,
|
|
331
|
+
interceptedCitations: interceptedSources.map(
|
|
332
|
+
(extraction) => extraction.citationIndex,
|
|
333
|
+
),
|
|
334
|
+
casualties,
|
|
335
|
+
casualtiesCitations: casualtiesCitations,
|
|
336
|
+
casualtiesConfidence: casualtiesConfidence,
|
|
337
|
+
injuries,
|
|
338
|
+
injuriesCause: injuriesCause,
|
|
339
|
+
injuriesCitations: injuriesCitations,
|
|
340
|
+
injuriesConfidence: injuriesConfidence,
|
|
341
|
+
confidence: Math.round(weightedConfidenceValue * 100) / 100,
|
|
342
|
+
sourcesCount: indexed.length,
|
|
343
|
+
citedSources,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
logger.info("Agent: voted", { alertId, voted });
|
|
347
|
+
return voted;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const voteNode = (state: AgentStateType): Partial<AgentStateType> => {
|
|
351
|
+
return { votedResult: aggregateVote(state.extractions, state.alertId) };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const vote = aggregateVote;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vote Node — consensus voting, deterministic, 0 tokens.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates extraction results from multiple sources into a single
|
|
5
|
+
* voted consensus using median, majority, and weighted confidence.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as logger from "@easyoref/monitoring";
|
|
9
|
+
import type {
|
|
10
|
+
CitedSource,
|
|
11
|
+
QualitativeCount,
|
|
12
|
+
ValidatedExtraction,
|
|
13
|
+
VotedResult,
|
|
14
|
+
} from "@easyoref/shared";
|
|
15
|
+
import type { AgentStateType } from "../graph.js";
|
|
16
|
+
|
|
17
|
+
function weightedConfidence(
|
|
18
|
+
sources: Array<{ sourceTrust: number; confidence: number }>,
|
|
19
|
+
): number {
|
|
20
|
+
if (sources.length === 0) return 0;
|
|
21
|
+
return (
|
|
22
|
+
sources.reduce(
|
|
23
|
+
(accumulator, extraction) =>
|
|
24
|
+
accumulator + extraction.sourceTrust * extraction.confidence,
|
|
25
|
+
0,
|
|
26
|
+
) / sources.length
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function modeQualification(
|
|
31
|
+
sources: Array<Record<string, unknown>>,
|
|
32
|
+
key: string,
|
|
33
|
+
): QualitativeCount | undefined {
|
|
34
|
+
const values = sources
|
|
35
|
+
.map((extraction) => extraction[key] as QualitativeCount | undefined)
|
|
36
|
+
.filter((value): value is QualitativeCount => value !== undefined);
|
|
37
|
+
if (values.length === 0) return undefined;
|
|
38
|
+
const frequency = new Map<QualitativeCount, number>();
|
|
39
|
+
for (const value of values)
|
|
40
|
+
frequency.set(value, (frequency.get(value) ?? 0) + 1);
|
|
41
|
+
return [...frequency.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function medianQualificationNumber(
|
|
45
|
+
sources: Array<Record<string, unknown>>,
|
|
46
|
+
key: string,
|
|
47
|
+
): number | undefined {
|
|
48
|
+
const values = sources
|
|
49
|
+
.map((extraction) => extraction[key] as number | undefined)
|
|
50
|
+
.filter((value): value is number => value !== undefined)
|
|
51
|
+
.sort((a, b) => a - b);
|
|
52
|
+
return values.length > 0 ? values[Math.floor(values.length / 2)] : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function aggregateVote(
|
|
56
|
+
extractions: ValidatedExtraction[],
|
|
57
|
+
alertId: string,
|
|
58
|
+
): VotedResult | undefined {
|
|
59
|
+
const valid = extractions.filter((extraction) => extraction.valid);
|
|
60
|
+
|
|
61
|
+
if (valid.length === 0) return undefined;
|
|
62
|
+
|
|
63
|
+
const indexed = valid.map((extraction, index) => ({
|
|
64
|
+
...extraction,
|
|
65
|
+
citationIndex: index + 1,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const citedSources: CitedSource[] = indexed.map((extraction) => ({
|
|
69
|
+
index: extraction.citationIndex,
|
|
70
|
+
channel: extraction.channel,
|
|
71
|
+
...(extraction.messageUrl && { messageUrl: extraction.messageUrl }),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const etaSources = indexed
|
|
75
|
+
.filter((extraction) => extraction.etaRefinedMinutes !== undefined)
|
|
76
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
77
|
+
const bestEtaSource = etaSources[0];
|
|
78
|
+
|
|
79
|
+
const countryMap = new Map<
|
|
80
|
+
string,
|
|
81
|
+
{ canonical: string; citations: number[] }
|
|
82
|
+
>();
|
|
83
|
+
for (const extraction of indexed) {
|
|
84
|
+
if (extraction.countryOrigin) {
|
|
85
|
+
const key = extraction.countryOrigin.toLowerCase();
|
|
86
|
+
const entry = countryMap.get(key);
|
|
87
|
+
if (entry) {
|
|
88
|
+
entry.citations.push(extraction.citationIndex);
|
|
89
|
+
} else {
|
|
90
|
+
countryMap.set(key, {
|
|
91
|
+
canonical: extraction.countryOrigin,
|
|
92
|
+
citations: [extraction.citationIndex],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const countryOrigins =
|
|
98
|
+
countryMap.size > 0
|
|
99
|
+
? Array.from(countryMap.values()).map(({ canonical, citations }) => ({
|
|
100
|
+
name: canonical,
|
|
101
|
+
citations,
|
|
102
|
+
}))
|
|
103
|
+
: undefined;
|
|
104
|
+
|
|
105
|
+
const rocketSources = indexed.filter(
|
|
106
|
+
(extraction) => extraction.rocketCount !== undefined,
|
|
107
|
+
);
|
|
108
|
+
const rocketValues = rocketSources.map(
|
|
109
|
+
(extraction) => extraction.rocketCount as number,
|
|
110
|
+
);
|
|
111
|
+
const rocketCountMin =
|
|
112
|
+
rocketValues.length > 0 ? Math.min(...rocketValues) : undefined;
|
|
113
|
+
const rocketCountMax =
|
|
114
|
+
rocketValues.length > 0 ? Math.max(...rocketValues) : undefined;
|
|
115
|
+
const rocketCitations = rocketSources.map(
|
|
116
|
+
(extraction) => extraction.citationIndex,
|
|
117
|
+
);
|
|
118
|
+
const rocketConfidence = weightedConfidence(rocketSources);
|
|
119
|
+
|
|
120
|
+
const detailSources = indexed
|
|
121
|
+
.filter((extraction) => extraction.rocketDetail)
|
|
122
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
123
|
+
const rocketDetail = detailSources[0]?.rocketDetail;
|
|
124
|
+
|
|
125
|
+
const cassetteSources = indexed.filter(
|
|
126
|
+
(extraction) => extraction.isCassette !== undefined,
|
|
127
|
+
);
|
|
128
|
+
const cassetteValues = cassetteSources.map(
|
|
129
|
+
(extraction) => extraction.isCassette as boolean,
|
|
130
|
+
);
|
|
131
|
+
const isCassette =
|
|
132
|
+
cassetteValues.length > 0
|
|
133
|
+
? cassetteValues.filter(Boolean).length > cassetteValues.length / 2
|
|
134
|
+
: undefined;
|
|
135
|
+
const cassetteConfidence = weightedConfidence(cassetteSources);
|
|
136
|
+
|
|
137
|
+
const interceptedSources = indexed.filter(
|
|
138
|
+
(extraction) => extraction.intercepted !== undefined,
|
|
139
|
+
);
|
|
140
|
+
const interceptedQualSources = indexed.filter(
|
|
141
|
+
(extraction) => extraction.interceptedQual !== undefined,
|
|
142
|
+
);
|
|
143
|
+
const interceptedValues = interceptedSources
|
|
144
|
+
.map((extraction) => extraction.intercepted as number)
|
|
145
|
+
.sort((a, b) => a - b);
|
|
146
|
+
const intercepted =
|
|
147
|
+
interceptedValues.length > 0
|
|
148
|
+
? interceptedValues[Math.floor(interceptedValues.length / 2)]
|
|
149
|
+
: undefined;
|
|
150
|
+
const interceptedQual =
|
|
151
|
+
intercepted === undefined
|
|
152
|
+
? modeQualification(interceptedQualSources, "interceptedQual")
|
|
153
|
+
: undefined;
|
|
154
|
+
const interceptedQualNumber = interceptedQual
|
|
155
|
+
? medianQualificationNumber(interceptedQualSources, "interceptedQual_num")
|
|
156
|
+
: undefined;
|
|
157
|
+
const interceptedConfidence = weightedConfidence(
|
|
158
|
+
interceptedSources.length > 0 ? interceptedSources : interceptedQualSources,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const seaSources = indexed.filter(
|
|
162
|
+
(extraction) => extraction.seaImpact !== undefined,
|
|
163
|
+
);
|
|
164
|
+
const seaQualSources = indexed.filter(
|
|
165
|
+
(extraction) => extraction.seaImpactQual !== undefined,
|
|
166
|
+
);
|
|
167
|
+
const seaValues = seaSources
|
|
168
|
+
.map((extraction) => extraction.seaImpact as number)
|
|
169
|
+
.sort((a, b) => a - b);
|
|
170
|
+
const seaImpact =
|
|
171
|
+
seaValues.length > 0
|
|
172
|
+
? seaValues[Math.floor(seaValues.length / 2)]
|
|
173
|
+
: undefined;
|
|
174
|
+
const seaImpactQual =
|
|
175
|
+
seaImpact === undefined
|
|
176
|
+
? modeQualification(seaQualSources, "seaImpactQual")
|
|
177
|
+
: undefined;
|
|
178
|
+
const seaImpactQualNumber = seaImpactQual
|
|
179
|
+
? medianQualificationNumber(seaQualSources, "seaImpactQual_num")
|
|
180
|
+
: undefined;
|
|
181
|
+
const seaConfidence = weightedConfidence(
|
|
182
|
+
seaSources.length > 0 ? seaSources : seaQualSources,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const openSources = indexed.filter(
|
|
186
|
+
(extraction) => extraction.openAreaImpact !== undefined,
|
|
187
|
+
);
|
|
188
|
+
const openQualSources = indexed.filter(
|
|
189
|
+
(extraction) => extraction.openAreaImpactQual !== undefined,
|
|
190
|
+
);
|
|
191
|
+
const openValues = openSources
|
|
192
|
+
.map((extraction) => extraction.openAreaImpact as number)
|
|
193
|
+
.sort((a, b) => a - b);
|
|
194
|
+
const openAreaImpact =
|
|
195
|
+
openValues.length > 0
|
|
196
|
+
? openValues[Math.floor(openValues.length / 2)]
|
|
197
|
+
: undefined;
|
|
198
|
+
const openAreaImpactQual =
|
|
199
|
+
openAreaImpact === undefined
|
|
200
|
+
? modeQualification(openQualSources, "openAreaImpactQual")
|
|
201
|
+
: undefined;
|
|
202
|
+
const openAreaImpactQualNumber = openAreaImpactQual
|
|
203
|
+
? medianQualificationNumber(openQualSources, "openAreaImpactQual_num")
|
|
204
|
+
: undefined;
|
|
205
|
+
const openAreaConfidence = weightedConfidence(
|
|
206
|
+
openSources.length > 0 ? openSources : openQualSources,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const allHitsSources = indexed.filter(
|
|
210
|
+
(extraction) => extraction.hitsConfirmed !== undefined,
|
|
211
|
+
);
|
|
212
|
+
const hitsValues = allHitsSources
|
|
213
|
+
.map((extraction) => extraction.hitsConfirmed as number)
|
|
214
|
+
.sort((a, b) => a - b);
|
|
215
|
+
const hitsConfirmed =
|
|
216
|
+
hitsValues.length > 0
|
|
217
|
+
? hitsValues[Math.floor(hitsValues.length / 2)]
|
|
218
|
+
: undefined;
|
|
219
|
+
const positiveHitsSources = allHitsSources.filter(
|
|
220
|
+
(extraction) => (extraction.hitsConfirmed as number) > 0,
|
|
221
|
+
);
|
|
222
|
+
const hitsCitations =
|
|
223
|
+
positiveHitsSources.length > 0
|
|
224
|
+
? positiveHitsSources.map((extraction) => extraction.citationIndex)
|
|
225
|
+
: allHitsSources.map((extraction) => extraction.citationIndex);
|
|
226
|
+
const hitsConfidence = weightedConfidence(allHitsSources);
|
|
227
|
+
|
|
228
|
+
const hitsWithLocation = positiveHitsSources
|
|
229
|
+
.filter((extraction) => extraction.hitLocation)
|
|
230
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
231
|
+
const hitLocation = hitsWithLocation[0]?.hitLocation;
|
|
232
|
+
const hitType = hitsWithLocation[0]?.hitType;
|
|
233
|
+
|
|
234
|
+
const hitsWithDetail = positiveHitsSources
|
|
235
|
+
.filter((extraction) => extraction.hitDetail)
|
|
236
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
237
|
+
const hitDetail = hitsWithDetail[0]?.hitDetail;
|
|
238
|
+
|
|
239
|
+
const noImpactSources = allHitsSources.filter(
|
|
240
|
+
(extraction) => (extraction.hitsConfirmed as number) === 0,
|
|
241
|
+
);
|
|
242
|
+
const noImpacts = noImpactSources.length > 0 && hitsConfirmed === 0;
|
|
243
|
+
const noImpactsCitations = noImpactSources.map(
|
|
244
|
+
(extraction) => extraction.citationIndex,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const casualtySources = indexed.filter(
|
|
248
|
+
(extraction) => extraction.casualties && extraction.casualties > 0,
|
|
249
|
+
);
|
|
250
|
+
const casualtyValues = casualtySources
|
|
251
|
+
.map((extraction) => extraction.casualties as number)
|
|
252
|
+
.sort((a, b) => a - b);
|
|
253
|
+
const casualties =
|
|
254
|
+
casualtyValues.length > 0
|
|
255
|
+
? casualtyValues[Math.floor(casualtyValues.length / 2)]
|
|
256
|
+
: undefined;
|
|
257
|
+
const casualtiesCitations = casualtySources.map(
|
|
258
|
+
(extraction) => extraction.citationIndex,
|
|
259
|
+
);
|
|
260
|
+
const casualtiesConfidence = weightedConfidence(casualtySources);
|
|
261
|
+
|
|
262
|
+
const injurySources = indexed.filter(
|
|
263
|
+
(extraction) =>
|
|
264
|
+
extraction.injuries !== undefined && (extraction.injuries as number) > 0,
|
|
265
|
+
);
|
|
266
|
+
const injuryValues = injurySources
|
|
267
|
+
.map((extraction) => extraction.injuries as number)
|
|
268
|
+
.sort((a, b) => a - b);
|
|
269
|
+
const injuries =
|
|
270
|
+
injuryValues.length > 0
|
|
271
|
+
? injuryValues[Math.floor(injuryValues.length / 2)]
|
|
272
|
+
: undefined;
|
|
273
|
+
const injuriesCitations = injurySources.map(
|
|
274
|
+
(extraction) => extraction.citationIndex,
|
|
275
|
+
);
|
|
276
|
+
const injuriesConfidence = weightedConfidence(injurySources);
|
|
277
|
+
|
|
278
|
+
const injuryCauseValues = injurySources
|
|
279
|
+
.map((extraction) => extraction.injuriesCause)
|
|
280
|
+
.filter(
|
|
281
|
+
(value): value is "rocket" | "rushing_to_shelter" => value !== undefined,
|
|
282
|
+
);
|
|
283
|
+
const rocketCauseCount = injuryCauseValues.filter(
|
|
284
|
+
(value) => value === "rocket",
|
|
285
|
+
).length;
|
|
286
|
+
const shelterCauseCount = injuryCauseValues.filter(
|
|
287
|
+
(value) => value === "rushing_to_shelter",
|
|
288
|
+
).length;
|
|
289
|
+
const injuriesCause =
|
|
290
|
+
injuryCauseValues.length === 0
|
|
291
|
+
? undefined
|
|
292
|
+
: rocketCauseCount >= shelterCauseCount
|
|
293
|
+
? "rocket"
|
|
294
|
+
: "rushing_to_shelter";
|
|
295
|
+
|
|
296
|
+
const totalWeight = indexed.reduce(
|
|
297
|
+
(accumulator, extraction) =>
|
|
298
|
+
accumulator + extraction.sourceTrust * extraction.confidence,
|
|
299
|
+
0,
|
|
300
|
+
);
|
|
301
|
+
const weightedConfidenceValue = totalWeight / indexed.length;
|
|
302
|
+
|
|
303
|
+
const voted: VotedResult = {
|
|
304
|
+
etaRefinedMinutes: bestEtaSource?.etaRefinedMinutes,
|
|
305
|
+
etaCitations: bestEtaSource ? [bestEtaSource.citationIndex] : [],
|
|
306
|
+
countryOrigins: countryOrigins ?? [],
|
|
307
|
+
rocketCountMin: rocketCountMin,
|
|
308
|
+
rocketCountMax: rocketCountMax,
|
|
309
|
+
rocketCitations: rocketCitations,
|
|
310
|
+
rocketConfidence: rocketConfidence,
|
|
311
|
+
rocketDetail: rocketDetail,
|
|
312
|
+
isCassette: isCassette,
|
|
313
|
+
isCassetteConfidence: cassetteConfidence,
|
|
314
|
+
intercepted,
|
|
315
|
+
interceptedQual: interceptedQual,
|
|
316
|
+
interceptedConfidence: interceptedConfidence,
|
|
317
|
+
seaImpact: seaImpact,
|
|
318
|
+
seaImpactQual: seaImpactQual,
|
|
319
|
+
seaConfidence: seaConfidence,
|
|
320
|
+
openAreaImpact: openAreaImpact,
|
|
321
|
+
openAreaImpactQual: openAreaImpactQual,
|
|
322
|
+
openAreaConfidence: openAreaConfidence,
|
|
323
|
+
hitsConfirmed: hitsConfirmed,
|
|
324
|
+
hitsCitations: hitsCitations,
|
|
325
|
+
hitsConfidence: hitsConfidence,
|
|
326
|
+
hitLocation: hitLocation,
|
|
327
|
+
hitType: hitType,
|
|
328
|
+
hitDetail: hitDetail,
|
|
329
|
+
noImpacts: noImpacts,
|
|
330
|
+
noImpactsCitations: noImpactsCitations,
|
|
331
|
+
interceptedCitations: interceptedSources.map(
|
|
332
|
+
(extraction) => extraction.citationIndex,
|
|
333
|
+
),
|
|
334
|
+
casualties,
|
|
335
|
+
casualtiesCitations: casualtiesCitations,
|
|
336
|
+
casualtiesConfidence: casualtiesConfidence,
|
|
337
|
+
injuries,
|
|
338
|
+
injuriesCause: injuriesCause,
|
|
339
|
+
injuriesCitations: injuriesCitations,
|
|
340
|
+
injuriesConfidence: injuriesConfidence,
|
|
341
|
+
confidence: Math.round(weightedConfidenceValue * 100) / 100,
|
|
342
|
+
sourcesCount: indexed.length,
|
|
343
|
+
citedSources,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
logger.info("Agent: voted", { alertId, voted });
|
|
347
|
+
return voted;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const voteNode = (state: AgentStateType): Partial<AgentStateType> => {
|
|
351
|
+
return { votedResult: aggregateVote(state.extractions, state.alertId) };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const vote = aggregateVote;
|