@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
package/dist/worker.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * BullMQ worker — processes "enrich-alert" jobs.
3
+ *
4
+ * Session-aware scheduling:
5
+ * early_warning → every 20s, up to 30 min
6
+ * siren → every 20s, up to 15 min
7
+ * resolved → every 60s, up to 10 min (tail — detailed intel)
8
+ *
9
+ * After each job, checks the session phase and re-enqueues
10
+ * with the appropriate delay. Stops when phase expires.
11
+ */
12
+ import * as logger from "@easyoref/monitoring";
13
+ import { clearSession, config, getActiveSession, getLanguagePack, isPhaseExpired, PHASE_ENRICH_DELAY_MS, } from "@easyoref/shared";
14
+ import { Worker } from "bullmq";
15
+ import { Bot } from "grammy";
16
+ import { runEnrichment } from "./graph.js";
17
+ import { MONITORING_RE, stripMonitoring } from "./nodes/edit-node.js";
18
+ import { enqueueEnrich } from "./runtime/queue.js";
19
+ let _worker = undefined;
20
+ /** Remove ⏳ monitoring indicator from all chat messages (best-effort) */
21
+ async function removeMonitoringIndicator(session) {
22
+ if (!config.botToken || !MONITORING_RE.test(session.currentText))
23
+ return;
24
+ const cleaned = stripMonitoring(session.currentText);
25
+ const tgBot = new Bot(config.botToken);
26
+ const targets = session.telegramMessages ?? [
27
+ {
28
+ chatId: session.chatId,
29
+ messageId: session.latestMessageId,
30
+ isCaption: session.isCaption,
31
+ },
32
+ ];
33
+ for (const cm of targets) {
34
+ try {
35
+ if (cm.isCaption) {
36
+ await tgBot.api.editMessageCaption(cm.chatId, cm.messageId, {
37
+ caption: cleaned,
38
+ parse_mode: "HTML",
39
+ });
40
+ }
41
+ else {
42
+ await tgBot.api.editMessageText(cm.chatId, cm.messageId, cleaned, {
43
+ parse_mode: "HTML",
44
+ });
45
+ }
46
+ }
47
+ catch (err) {
48
+ const errStr = String(err);
49
+ if (!errStr.includes("message is not modified")) {
50
+ logger.error("Failed to remove monitoring indicator", {
51
+ error: errStr,
52
+ chatId: cm.chatId,
53
+ });
54
+ }
55
+ }
56
+ }
57
+ logger.info("Removed monitoring indicator", { targets: targets.length });
58
+ }
59
+ export function startEnrichWorker() {
60
+ if (!config.agent.enabled)
61
+ return;
62
+ const connection = {
63
+ host: new URL(config.agent.redisUrl).hostname,
64
+ port: Number(new URL(config.agent.redisUrl).port || 6379),
65
+ password: new URL(config.agent.redisUrl).password || undefined,
66
+ };
67
+ _worker = new Worker("enrich-alert", async (job) => {
68
+ const { alertId } = job.data;
69
+ logger.info("Enrich worker: processing job", { alertId, jobId: job.id });
70
+ const session = await getActiveSession();
71
+ if (!session) {
72
+ logger.info("Enrich worker: no active session — skipping", { alertId });
73
+ return;
74
+ }
75
+ // Phase expired → end session
76
+ if (isPhaseExpired(session)) {
77
+ logger.info("Enrich worker: phase expired — ending session", {
78
+ alertId: session.latestAlertId,
79
+ phase: session.phase,
80
+ });
81
+ await removeMonitoringIndicator(session);
82
+ await clearSession();
83
+ return;
84
+ }
85
+ const langPack = getLanguagePack(config.language);
86
+ // Run enrichment using latest alert's message as edit target
87
+ await runEnrichment({
88
+ alertId: session.latestAlertId,
89
+ alertTs: session.latestAlertTs,
90
+ alertType: session.phase,
91
+ alertAreas: session.alertAreas,
92
+ chatId: session.chatId,
93
+ messageId: session.latestMessageId,
94
+ isCaption: session.isCaption,
95
+ telegramMessages: session.telegramMessages,
96
+ currentText: session.baseText ?? session.currentText,
97
+ monitoringLabel: langPack.labels.monitoring,
98
+ });
99
+ // Re-check session after enrichment (may have changed phase)
100
+ const after = await getActiveSession();
101
+ if (!after)
102
+ return;
103
+ if (isPhaseExpired(after)) {
104
+ logger.info("Enrich worker: phase expired post-enrich — ending session", {
105
+ phase: after.phase,
106
+ });
107
+ await removeMonitoringIndicator(after);
108
+ await clearSession();
109
+ return;
110
+ }
111
+ // Re-enqueue with phase-appropriate delay
112
+ const delay = PHASE_ENRICH_DELAY_MS[after.phase];
113
+ await enqueueEnrich(after.latestAlertId, after.latestAlertTs, delay);
114
+ }, {
115
+ connection,
116
+ concurrency: 1,
117
+ });
118
+ _worker.on("completed", (job) => {
119
+ logger.info("Enrich worker: job completed", { jobId: job.id });
120
+ });
121
+ _worker.on("failed", (job, err) => {
122
+ logger.error("Enrich worker: job failed", {
123
+ jobId: job?.id,
124
+ error: String(err),
125
+ });
126
+ });
127
+ logger.info("Enrich worker started");
128
+ }
129
+ export async function stopEnrichWorker() {
130
+ if (_worker) {
131
+ await _worker.close();
132
+ _worker = undefined;
133
+ }
134
+ }
135
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@easyoref/agent",
3
+ "version": "1.21.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@langchain/core": "^1.1.31",
13
+ "@langchain/langgraph": "^1.2.1",
14
+ "@langchain/openai": "^1.2.12",
15
+ "bullmq": "^5.0.0",
16
+ "ioredis": "^5.3.0",
17
+ "@easyoref/shared": "*",
18
+ "@easyoref/monitoring": "*",
19
+ "@easyoref/gramjs": "*",
20
+ "zod": "4.3.6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "typescript": "^5.7.0"
25
+ }
26
+ }
package/src/graph.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * LangGraph.js enrichment pipeline — phase-aware, time-validated.
3
+ *
4
+ * ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────---──┐
5
+ * │ filter │───▶│ extract │───▶│ vote │───▶│ shouldClarify │
6
+ * └─────────┘ └─────────┘ └─────────┘ └──────┬───---──┘
7
+ * │
8
+ * ┌──────────────┴──────────────┐
9
+ * │ │
10
+ * [low conf] [high conf]
11
+ * │ │
12
+ * ▼ ▼
13
+ * ┌────────────┐ ┌─────────┐
14
+ * │ clarify │ │ edit │
15
+ * └──────┬─────┘ └─────────┘
16
+ * │ ▲
17
+ * ▼ │
18
+ * ┌────────────┐ │
19
+ * │ revote │───────────────────────┘
20
+ * └────────────┘
21
+ *
22
+ * ── Node responsibilities ──────────────────────────────────────────────────
23
+ *
24
+ * filter: Collect Telegram posts from Redis, apply deterministic noise
25
+ * filters (area lists, summaries, IDF press releases). Returns
26
+ * ChannelTracking structure.
27
+ *
28
+ * extract: LLM-powered extraction pipeline:
29
+ * 1. Cheap model → which channels have relevant intel?
30
+ * 2. Expensive model → extract structured data per post
31
+ * 3. Post-filter → deterministic validation
32
+ *
33
+ * vote: Consensus voting (deterministic, 0 tokens). Aggregates multiple
34
+ * extractions into a single VotedResult using median/majority.
35
+ *
36
+ * shouldClarify: Conditional routing:
37
+ * - Low confidence (< threshold) → clarify
38
+ * - Single-source Lebanon for central Israel → clarify (suspicious)
39
+ * - Already clarified → edit
40
+ * - MCP tools disabled → edit
41
+ *
42
+ * clarify: ReAct agent with tools (read_telegram, alert_history,
43
+ * resolve_area, betterstack_log). Fetches more data to resolve
44
+ * contradictions. Output: new extractions.
45
+ *
46
+ * revote: Re-run vote with additional extractions from clarify.
47
+ *
48
+ * edit: Build enriched message text and edit Telegram message.
49
+ *
50
+ * ── Why this pipeline? ─────────────────────────────────────────────────────
51
+ *
52
+ * 1. Cheap → Expensive: Saves tokens. Pre-filter with cheap model ($0.001)
53
+ * before spending on per-post extraction ($0.01 each).
54
+ *
55
+ * 2. ReAct clarification: Low-confidence results aren't "failed" —
56
+ * they're signals that more data is needed. The LLM decides what tools
57
+ * to use rather than a hardcoded threshold.
58
+ *
59
+ * 3. Carry-forward: previousEnrichment preserves data between phases.
60
+ * If origin was confirmed in early_warning, it carries to red_alert/resolved.
61
+ *
62
+ * 4. Time validation: LLM instructions emphasize checking if sources
63
+ * are about THIS alert vs. previous attacks. Critical for accuracy.
64
+ */
65
+
66
+ import * as logger from "@easyoref/monitoring";
67
+ import {
68
+ AlertTypeSchema,
69
+ ChannelTrackingSchema,
70
+ EnrichmentDataSchema,
71
+ RunEnrichmentInputSchema,
72
+ TelegramMessageSchema,
73
+ ValidatedExtractionSchema,
74
+ VotedResultSchema,
75
+ config,
76
+ createEmptyEnrichmentData,
77
+ validateSafe,
78
+ } from "@easyoref/shared";
79
+ import {
80
+ END,
81
+ MemorySaver,
82
+ ReducedValue,
83
+ START,
84
+ StateGraph,
85
+ StateSchema,
86
+ } from "@langchain/langgraph";
87
+ import { z } from "zod";
88
+ import { clarifyNode } from "./nodes/clarify-node.js";
89
+ import { editNode } from "./nodes/edit-node.js";
90
+ import { extractNode } from "./nodes/extract-node.js";
91
+ import { filterNode } from "./nodes/filter-node.js";
92
+ import { voteNode } from "./nodes/vote-node.js";
93
+
94
+ export const AgentState = new StateSchema({
95
+ alertId: z.string(),
96
+ alertTs: z.number(),
97
+ alertType: AlertTypeSchema,
98
+ alertAreas: z.array(z.string()),
99
+ chatId: z.string(),
100
+ messageId: z.number(),
101
+ isCaption: z.boolean(),
102
+ currentText: z.string(),
103
+ tracking: ChannelTrackingSchema.optional(),
104
+ extractions: new ReducedValue(z.array(ValidatedExtractionSchema), {
105
+ reducer: (previous, current) => [...previous, ...current],
106
+ }),
107
+ votedResult: VotedResultSchema.optional(),
108
+ clarifyAttempted: z.boolean().default(false),
109
+ previousEnrichment: EnrichmentDataSchema.optional(),
110
+ monitoringLabel: z.string().optional(),
111
+ telegramMessages: new ReducedValue(z.array(TelegramMessageSchema), {
112
+ reducer: (previous, current) => [...previous, ...current],
113
+ }),
114
+ });
115
+
116
+ export type AgentStateType = typeof AgentState.State;
117
+
118
+ const shouldClarify = (state: AgentStateType): "clarify" | "edit" => {
119
+ if (state.clarifyAttempted) return "edit";
120
+ if (!config.agent.mcpTools) return "edit";
121
+ if (!state.votedResult) return "edit";
122
+
123
+ if (state.votedResult.confidence < config.agent.confidenceThreshold) {
124
+ logger.info("Agent: routing to clarify (low confidence)", {
125
+ confidence: state.votedResult.confidence,
126
+ });
127
+ return "clarify";
128
+ }
129
+
130
+ const origins = state.votedResult.countryOrigins;
131
+ if (origins && origins.length === 1 && state.votedResult.sourcesCount === 1) {
132
+ if (
133
+ origins[0]!.name === "Lebanon" &&
134
+ state.alertAreas.some(
135
+ (area) =>
136
+ area.includes("תל אביב") ||
137
+ area.includes("גוש דן") ||
138
+ area.includes("שרון") ||
139
+ area.includes("מרכז"),
140
+ )
141
+ ) {
142
+ logger.info("Agent: routing to clarify (suspicious Lebanon origin)", {});
143
+ return "clarify";
144
+ }
145
+ }
146
+
147
+ return "edit";
148
+ };
149
+
150
+ const checkpointer = new MemorySaver();
151
+
152
+ export const buildGraph = () =>
153
+ new StateGraph(AgentState)
154
+ .addNode("filter", filterNode)
155
+ .addNode("extract", extractNode)
156
+ .addNode("vote", voteNode)
157
+ .addNode("clarify", clarifyNode)
158
+ .addNode("revote", voteNode)
159
+ .addNode("edit", editNode)
160
+ .addEdge(START, "filter")
161
+ .addEdge("filter", "extract")
162
+ .addEdge("extract", "vote")
163
+ .addConditionalEdges("vote", shouldClarify, {
164
+ clarify: "clarify",
165
+ edit: "edit",
166
+ })
167
+ .addEdge("clarify", "revote")
168
+ .addEdge("revote", "edit")
169
+ .addEdge("edit", END)
170
+ .compile({ checkpointer });
171
+
172
+ export type { RunEnrichmentInput } from "@easyoref/shared";
173
+ export { RunEnrichmentInputSchema };
174
+
175
+ export const runEnrichment = async (input: unknown): Promise<void> => {
176
+ const validation = validateSafe(RunEnrichmentInputSchema, input);
177
+ if (!validation.ok) {
178
+ logger.error("Enrichment: invalid input", { error: validation.error });
179
+ throw new Error(`Invalid enrichment input: ${validation.error}`);
180
+ }
181
+
182
+ const validInput = validation.data;
183
+
184
+ await buildGraph().invoke(
185
+ {
186
+ alertId: validInput.alertId,
187
+ alertTs: validInput.alertTs,
188
+ alertType: validInput.alertType,
189
+ alertAreas: validInput.alertAreas,
190
+ chatId: validInput.chatId,
191
+ messageId: validInput.messageId,
192
+ isCaption: validInput.isCaption,
193
+ telegramMessages: validInput.telegramMessages,
194
+ currentText: validInput.currentText,
195
+ previousEnrichment: createEmptyEnrichmentData(),
196
+ monitoringLabel: validInput.monitoringLabel,
197
+ },
198
+ { configurable: { thread_id: validInput.alertId } },
199
+ );
200
+ };
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * EasyOref Agent Package
3
+ *
4
+ * LangGraph-based enrichment pipeline for Israeli missile alert processing.
5
+ */
6
+
7
+ export * from "./graph.js";
8
+
9
+ export * from "./nodes/clarify-node.js";
10
+ export * from "./nodes/edit-node.js";
11
+ export * from "./nodes/extract-node.js";
12
+ export * from "./nodes/filter-node.js";
13
+ export * from "./nodes/vote-node.js";
14
+
15
+ export * from "./models.js";
16
+
17
+ export * from "./runtime/auth.js";
18
+ export * from "./runtime/dry-run.js";
19
+ export * from "./runtime/queue.js";
20
+ export * from "./runtime/redis.js";
21
+ export * from "./runtime/worker.js";
22
+
23
+ export { alertHistoryTool } from "./tools/alert-history.js";
24
+ export { betterstackLogTool } from "./tools/betterstack-log.js";
25
+ export { clarifyTools } from "./tools/index.js";
26
+ export { readSourcesTool } from "./tools/read-sources.js";
27
+ export { resolveAreaTool } from "./tools/resolve-area.js";
package/src/models.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * LLM models configuration.
3
+ */
4
+
5
+ import { ChatOpenRouter } from "@langchain/openrouter";
6
+ import { config } from "@easyoref/shared";
7
+
8
+ export const filterModel = new ChatOpenRouter({
9
+ apiKey: config.agent.apiKey,
10
+ model: config.agent.filterModel,
11
+ temperature: 0,
12
+ maxTokens: 200,
13
+ });
14
+
15
+ export const extractModel = new ChatOpenRouter({
16
+ apiKey: config.agent.apiKey,
17
+ model: config.agent.extractModel,
18
+ temperature: 0,
19
+ maxTokens: 500,
20
+ });
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Clarify Node — optional ReAct tool calling for low-confidence enrichment.
3
+ */
4
+
5
+ import * as logger from "@easyoref/monitoring";
6
+ import {
7
+ ClarifyOutputSchema,
8
+ pushSessionPost,
9
+ type ChannelPost,
10
+ type ValidatedExtraction,
11
+ } from "@easyoref/shared";
12
+ import { createAgent, toolStrategy } from "langchain";
13
+ import type { AgentStateType } from "../graph.js";
14
+ import { filterModel } from "../models.js";
15
+ import { clarifyTools } from "../tools/index.js";
16
+
17
+ export const clarifyAgent = createAgent({
18
+ model: filterModel,
19
+ tools: clarifyTools,
20
+ responseFormat: toolStrategy(ClarifyOutputSchema),
21
+ systemPrompt: `
22
+ You are the clarification agent for EasyOref — an Israeli missile alert enrichment system.
23
+
24
+ The voting pipeline analyzed Telegram channel posts and produced a result with
25
+ low confidence or contradictions. You have access to 4 tools:
26
+
27
+ 1. read_telegram_sources — fetch last N posts from a Telegram news channel
28
+ 2. alert_history — get recent alert history from Pikud HaOref.
29
+ 3. resolve_area — check if a location mentioned in news is relevant to user's areas.
30
+ 4. betterstack_log — query recent EasyOref logs from Better Stack.
31
+
32
+ CRITICAL — TIME VALIDATION:
33
+ You receive the alert time (Israel timezone). Channel posts may be about PREVIOUS
34
+ attacks or ongoing military operations (not THIS specific alert). When in doubt:
35
+ - Use alert_history to verify if an alert really occurred at the claimed time/area.
36
+ - If a post discusses events from hours ago, it is STALE — ignore it.
37
+
38
+ You decide whether tools would help:
39
+ - If contradictions can be resolved with existing data → respond immediately, no tools.
40
+ - If an authoritative source (IDF, N12) could settle a disagreement → fetch 1-4 posts.
41
+ - If you need to verify whether an alert occurred → check alert_history.
42
+
43
+ Always respect an output format.
44
+ `,
45
+ });
46
+
47
+ function describeContradictions(
48
+ extractions: ValidatedExtraction[],
49
+ voted: {
50
+ countryOrigins?: { name: string }[];
51
+ rocketCountMin?: number;
52
+ rocketCountMax?: number;
53
+ interceptedConfidence?: number;
54
+ intercepted?: number;
55
+ hitsConfidence?: number;
56
+ hitsConfirmed?: number;
57
+ confidence: number;
58
+ sourcesCount: number;
59
+ },
60
+ ): string {
61
+ const issues: string[] = [];
62
+
63
+ if (voted.countryOrigins && voted.countryOrigins.length > 1) {
64
+ const names = voted.countryOrigins.map((c) => c.name).join(", ");
65
+ issues.push(`Multiple origin countries reported: ${names}`);
66
+ }
67
+
68
+ if (
69
+ voted.rocketCountMin &&
70
+ voted.rocketCountMax &&
71
+ voted.rocketCountMax - voted.rocketCountMin > 3
72
+ ) {
73
+ issues.push(
74
+ `Wide rocket count range: ${voted.rocketCountMin}–${voted.rocketCountMax}`,
75
+ );
76
+ }
77
+
78
+ if (
79
+ (voted.interceptedConfidence ?? 0) < 0.5 &&
80
+ voted.intercepted !== undefined
81
+ ) {
82
+ issues.push(
83
+ `Intercepted count (${voted.intercepted}) has low confidence: ${(
84
+ voted.interceptedConfidence ?? 0
85
+ ).toFixed(2)}`,
86
+ );
87
+ }
88
+
89
+ issues.push(`Overall confidence: ${voted.confidence}`);
90
+ issues.push(`Sources count: ${voted.sourcesCount}`);
91
+
92
+ return issues.join("\n");
93
+ }
94
+
95
+ export const clarifyNode = async (
96
+ state: AgentStateType,
97
+ ): Promise<Partial<AgentStateType>> => {
98
+ if (!state.votedResult) {
99
+ logger.info("Agent: clarify skipped — no voted result", {
100
+ alertId: state.alertId,
101
+ });
102
+ return { clarifyAttempted: true };
103
+ }
104
+
105
+ logger.info("Agent: clarify triggered", {
106
+ alertId: state.alertId,
107
+ confidence: state.votedResult.confidence,
108
+ });
109
+
110
+ try {
111
+ const contradictions = describeContradictions(
112
+ state.extractions,
113
+ state.votedResult,
114
+ );
115
+
116
+ const alertTimeIL = new Date(state.alertTs).toLocaleTimeString("he-IL", {
117
+ hour: "2-digit",
118
+ minute: "2-digit",
119
+ timeZone: "Asia/Jerusalem",
120
+ });
121
+
122
+ const userPrompt =
123
+ `Alert region: ${state.alertAreas.join(", ")}\n` +
124
+ `Alert type: ${state.alertType}\n` +
125
+ `Alert time: ${alertTimeIL} (Israel)\n` +
126
+ `Message ID: ${state.messageId}\n\n` +
127
+ `Current voted result:\n` +
128
+ JSON.stringify(state.votedResult, undefined, 2) +
129
+ `\n\nContradictions & issues:\n${contradictions}\n\n` +
130
+ `Existing extractions (${
131
+ state.extractions.filter((e) => e.valid).length
132
+ } valid):\n` +
133
+ state.extractions
134
+ .filter((e) => e.valid)
135
+ .map(
136
+ (e) =>
137
+ ` [${e.channel}] country=${e.countryOrigin}, rockets=${e.rocketCount}, ` +
138
+ `intercepted=${e.intercepted}, hits=${e.hitsConfirmed}, conf=${e.confidence}`,
139
+ )
140
+ .join("\n") +
141
+ `\n\nDecide: would fetching more data from authoritative channels resolve these issues?`;
142
+
143
+ const result = await clarifyAgent.invoke({ messages: [userPrompt] });
144
+ const output = result.structuredResponse;
145
+
146
+ const newPosts: ChannelPost[] = [];
147
+ if (output?.newPosts) {
148
+ for (const p of output.newPosts) {
149
+ const post: ChannelPost = {
150
+ channel: p.channel,
151
+ text: p.text,
152
+ ts: p.ts,
153
+ messageUrl: p.messageUrl,
154
+ };
155
+ newPosts.push(post);
156
+ await pushSessionPost(post);
157
+ }
158
+ }
159
+
160
+ return {
161
+ extractions: [...state.extractions, ...(output?.newExtractions ?? [])],
162
+ votedResult: undefined,
163
+ clarifyAttempted: true,
164
+ };
165
+ } catch (err) {
166
+ logger.error("Agent: clarify failed", {
167
+ alertId: state.alertId,
168
+ error: String(err),
169
+ });
170
+ return { clarifyAttempted: true };
171
+ }
172
+ };