@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.
Files changed (158) hide show
  1. package/__tests__/clarify.test.ts +827 -0
  2. package/__tests__/config.test.ts +304 -0
  3. package/__tests__/enrichment.integration.test.ts +871 -0
  4. package/__tests__/graph.test.ts +661 -0
  5. package/dist/auth.d.ts +11 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +54 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/dry-run.d.ts +12 -0
  10. package/dist/dry-run.d.ts.map +1 -0
  11. package/dist/dry-run.js +236 -0
  12. package/dist/dry-run.js.map +1 -0
  13. package/dist/extract.d.ts +180 -0
  14. package/dist/extract.d.ts.map +1 -0
  15. package/dist/extract.js +210 -0
  16. package/dist/extract.js.map +1 -0
  17. package/dist/graph.d.ts +4083 -0
  18. package/dist/graph.d.ts.map +1 -0
  19. package/dist/graph.js +162 -0
  20. package/dist/graph.js.map +1 -0
  21. package/dist/index.d.ts +23 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +23 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/models.d.ts +7 -0
  26. package/dist/models.d.ts.map +1 -0
  27. package/dist/models.js +18 -0
  28. package/dist/models.js.map +1 -0
  29. package/dist/nodes/clarify-node.d.ts +132 -0
  30. package/dist/nodes/clarify-node.d.ts.map +1 -0
  31. package/dist/nodes/clarify-node.js +118 -0
  32. package/dist/nodes/clarify-node.js.map +1 -0
  33. package/dist/nodes/clarify.d.ts +6 -0
  34. package/dist/nodes/clarify.d.ts.map +1 -0
  35. package/dist/nodes/clarify.js +124 -0
  36. package/dist/nodes/clarify.js.map +1 -0
  37. package/dist/nodes/edit-node.d.ts +71 -0
  38. package/dist/nodes/edit-node.d.ts.map +1 -0
  39. package/dist/nodes/edit-node.js +496 -0
  40. package/dist/nodes/edit-node.js.map +1 -0
  41. package/dist/nodes/edit.d.ts +6 -0
  42. package/dist/nodes/edit.d.ts.map +1 -0
  43. package/dist/nodes/edit.js +22 -0
  44. package/dist/nodes/edit.js.map +1 -0
  45. package/dist/nodes/extract-node.d.ts +174 -0
  46. package/dist/nodes/extract-node.d.ts.map +1 -0
  47. package/dist/nodes/extract-node.js +233 -0
  48. package/dist/nodes/extract-node.js.map +1 -0
  49. package/dist/nodes/extract.d.ts +6 -0
  50. package/dist/nodes/extract.d.ts.map +1 -0
  51. package/dist/nodes/extract.js +49 -0
  52. package/dist/nodes/extract.js.map +1 -0
  53. package/dist/nodes/filter-agent.d.ts +11 -0
  54. package/dist/nodes/filter-agent.d.ts.map +1 -0
  55. package/dist/nodes/filter-agent.js +60 -0
  56. package/dist/nodes/filter-agent.js.map +1 -0
  57. package/dist/nodes/filter-node.d.ts +9 -0
  58. package/dist/nodes/filter-node.d.ts.map +1 -0
  59. package/dist/nodes/filter-node.js +111 -0
  60. package/dist/nodes/filter-node.js.map +1 -0
  61. package/dist/nodes/filters.d.ts +13 -0
  62. package/dist/nodes/filters.d.ts.map +1 -0
  63. package/dist/nodes/filters.js +111 -0
  64. package/dist/nodes/filters.js.map +1 -0
  65. package/dist/nodes/message-node.d.ts +71 -0
  66. package/dist/nodes/message-node.d.ts.map +1 -0
  67. package/dist/nodes/message-node.js +491 -0
  68. package/dist/nodes/message-node.js.map +1 -0
  69. package/dist/nodes/message.d.ts +71 -0
  70. package/dist/nodes/message.d.ts.map +1 -0
  71. package/dist/nodes/message.js +496 -0
  72. package/dist/nodes/message.js.map +1 -0
  73. package/dist/nodes/vote-node.d.ts +13 -0
  74. package/dist/nodes/vote-node.d.ts.map +1 -0
  75. package/dist/nodes/vote-node.js +232 -0
  76. package/dist/nodes/vote-node.js.map +1 -0
  77. package/dist/nodes/vote.d.ts +13 -0
  78. package/dist/nodes/vote.d.ts.map +1 -0
  79. package/dist/nodes/vote.js +232 -0
  80. package/dist/nodes/vote.js.map +1 -0
  81. package/dist/queue.d.ts +15 -0
  82. package/dist/queue.d.ts.map +1 -0
  83. package/dist/queue.js +41 -0
  84. package/dist/queue.js.map +1 -0
  85. package/dist/redis.d.ts +8 -0
  86. package/dist/redis.d.ts.map +1 -0
  87. package/dist/redis.js +33 -0
  88. package/dist/redis.js.map +1 -0
  89. package/dist/runtime/auth.d.ts +11 -0
  90. package/dist/runtime/auth.d.ts.map +1 -0
  91. package/dist/runtime/auth.js +54 -0
  92. package/dist/runtime/auth.js.map +1 -0
  93. package/dist/runtime/dry-run.d.ts +12 -0
  94. package/dist/runtime/dry-run.d.ts.map +1 -0
  95. package/dist/runtime/dry-run.js +236 -0
  96. package/dist/runtime/dry-run.js.map +1 -0
  97. package/dist/runtime/queue.d.ts +15 -0
  98. package/dist/runtime/queue.d.ts.map +1 -0
  99. package/dist/runtime/queue.js +41 -0
  100. package/dist/runtime/queue.js.map +1 -0
  101. package/dist/runtime/redis.d.ts +8 -0
  102. package/dist/runtime/redis.d.ts.map +1 -0
  103. package/dist/runtime/redis.js +33 -0
  104. package/dist/runtime/redis.js.map +1 -0
  105. package/dist/runtime/worker.d.ts +14 -0
  106. package/dist/runtime/worker.d.ts.map +1 -0
  107. package/dist/runtime/worker.js +135 -0
  108. package/dist/runtime/worker.js.map +1 -0
  109. package/dist/tools/alert-history.d.ts +18 -0
  110. package/dist/tools/alert-history.d.ts.map +1 -0
  111. package/dist/tools/alert-history.js +98 -0
  112. package/dist/tools/alert-history.js.map +1 -0
  113. package/dist/tools/betterstack-log.d.ts +15 -0
  114. package/dist/tools/betterstack-log.d.ts.map +1 -0
  115. package/dist/tools/betterstack-log.js +80 -0
  116. package/dist/tools/betterstack-log.js.map +1 -0
  117. package/dist/tools/index.d.ts +44 -0
  118. package/dist/tools/index.d.ts.map +1 -0
  119. package/dist/tools/index.js +20 -0
  120. package/dist/tools/index.js.map +1 -0
  121. package/dist/tools/read-sources.d.ts +15 -0
  122. package/dist/tools/read-sources.d.ts.map +1 -0
  123. package/dist/tools/read-sources.js +67 -0
  124. package/dist/tools/read-sources.js.map +1 -0
  125. package/dist/tools/resolve-area.d.ts +19 -0
  126. package/dist/tools/resolve-area.d.ts.map +1 -0
  127. package/dist/tools/resolve-area.js +147 -0
  128. package/dist/tools/resolve-area.js.map +1 -0
  129. package/dist/tools.d.ts +115 -0
  130. package/dist/tools.d.ts.map +1 -0
  131. package/dist/tools.js +439 -0
  132. package/dist/tools.js.map +1 -0
  133. package/dist/worker.d.ts +14 -0
  134. package/dist/worker.d.ts.map +1 -0
  135. package/dist/worker.js +135 -0
  136. package/dist/worker.js.map +1 -0
  137. package/package.json +26 -0
  138. package/src/graph.ts +200 -0
  139. package/src/index.ts +27 -0
  140. package/src/models.ts +20 -0
  141. package/src/nodes/clarify-node.ts +172 -0
  142. package/src/nodes/edit-node.ts +695 -0
  143. package/src/nodes/extract-node.ts +299 -0
  144. package/src/nodes/filter-node.ts +139 -0
  145. package/src/nodes/message.ts +695 -0
  146. package/src/nodes/vote-node.ts +354 -0
  147. package/src/nodes/vote.ts +354 -0
  148. package/src/runtime/auth.ts +63 -0
  149. package/src/runtime/dry-run.ts +303 -0
  150. package/src/runtime/queue.ts +53 -0
  151. package/src/runtime/redis.ts +38 -0
  152. package/src/runtime/worker.ts +167 -0
  153. package/src/tools/alert-history.ts +120 -0
  154. package/src/tools/betterstack-log.ts +102 -0
  155. package/src/tools/index.ts +23 -0
  156. package/src/tools/read-sources.ts +86 -0
  157. package/src/tools/resolve-area.ts +202 -0
  158. 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;