easyoref 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/agent/auth.d.ts +11 -0
  2. package/dist/agent/auth.d.ts.map +1 -0
  3. package/dist/agent/auth.js +54 -0
  4. package/dist/agent/auth.js.map +1 -0
  5. package/dist/agent/clarify.d.ts +44 -0
  6. package/dist/agent/clarify.d.ts.map +1 -0
  7. package/dist/agent/clarify.js +283 -0
  8. package/dist/agent/clarify.js.map +1 -0
  9. package/dist/agent/dry-run.d.ts +12 -0
  10. package/dist/agent/dry-run.d.ts.map +1 -0
  11. package/dist/agent/dry-run.js +229 -0
  12. package/dist/agent/dry-run.js.map +1 -0
  13. package/dist/agent/gramjs-monitor.d.ts +26 -0
  14. package/dist/agent/gramjs-monitor.d.ts.map +1 -0
  15. package/dist/agent/gramjs-monitor.js +320 -0
  16. package/dist/agent/gramjs-monitor.js.map +1 -0
  17. package/dist/agent/graph.d.ts +111 -0
  18. package/dist/agent/graph.d.ts.map +1 -0
  19. package/dist/agent/graph.js +1142 -0
  20. package/dist/agent/graph.js.map +1 -0
  21. package/dist/agent/queue.d.ts +15 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +41 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/redis.d.ts +8 -0
  26. package/dist/agent/redis.d.ts.map +1 -0
  27. package/dist/agent/redis.js +33 -0
  28. package/dist/agent/redis.js.map +1 -0
  29. package/dist/agent/store.d.ts +71 -0
  30. package/dist/agent/store.d.ts.map +1 -0
  31. package/dist/agent/store.js +104 -0
  32. package/dist/agent/store.js.map +1 -0
  33. package/dist/agent/tools.d.ts +159 -0
  34. package/dist/agent/tools.d.ts.map +1 -0
  35. package/dist/agent/tools.js +439 -0
  36. package/dist/agent/tools.js.map +1 -0
  37. package/dist/agent/types.d.ts +163 -0
  38. package/dist/agent/types.d.ts.map +1 -0
  39. package/dist/agent/types.js +26 -0
  40. package/dist/agent/types.js.map +1 -0
  41. package/dist/agent/worker.d.ts +14 -0
  42. package/dist/agent/worker.d.ts.map +1 -0
  43. package/dist/agent/worker.js +90 -0
  44. package/dist/agent/worker.js.map +1 -0
  45. package/dist/bin.d.ts +17 -0
  46. package/dist/bin.d.ts.map +1 -0
  47. package/dist/bin.js +82 -0
  48. package/dist/bin.js.map +1 -0
  49. package/dist/bot.d.ts +16 -0
  50. package/dist/bot.d.ts.map +1 -0
  51. package/dist/bot.js +600 -0
  52. package/dist/bot.js.map +1 -0
  53. package/dist/config.d.ts +125 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +145 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/gif-state.d.ts +17 -0
  58. package/dist/gif-state.d.ts.map +1 -0
  59. package/dist/gif-state.js +67 -0
  60. package/dist/gif-state.js.map +1 -0
  61. package/dist/i18n.d.ts +49 -0
  62. package/dist/i18n.d.ts.map +1 -0
  63. package/dist/i18n.js +229 -0
  64. package/dist/i18n.js.map +1 -0
  65. package/dist/init.d.ts +7 -0
  66. package/dist/init.d.ts.map +1 -0
  67. package/dist/init.js +163 -0
  68. package/dist/init.js.map +1 -0
  69. package/dist/logger.d.ts +14 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +45 -0
  72. package/dist/logger.js.map +1 -0
  73. package/dist/service.d.ts +19 -0
  74. package/dist/service.d.ts.map +1 -0
  75. package/dist/service.js +165 -0
  76. package/dist/service.js.map +1 -0
  77. package/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * GramJS QR Auth — run once to generate session_string.
4
+ *
5
+ * Usage:
6
+ * npx tsx packages/bot/src/agent/auth.ts
7
+ *
8
+ * Scan QR with burner phone → session_string printed → done.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/agent/auth.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG"}
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * GramJS QR Auth — run once to generate session_string.
4
+ *
5
+ * Usage:
6
+ * npx tsx packages/bot/src/agent/auth.ts
7
+ *
8
+ * Scan QR with burner phone → session_string printed → done.
9
+ */
10
+ import qrcode from "qrcode-terminal";
11
+ import { TelegramClient } from "telegram";
12
+ import { StringSession } from "telegram/sessions/index.js";
13
+ // Telegram Desktop public api_id/api_hash (from open-source TDesktop)
14
+ const apiId = 2040;
15
+ const apiHash = "b18441a1ff607e10a989891a5462e627";
16
+ const session = new StringSession("");
17
+ const client = new TelegramClient(session, apiId, apiHash, {
18
+ connectionRetries: 3,
19
+ deviceModel: "EasyOref Auth",
20
+ appVersion: "1.0.0",
21
+ });
22
+ console.log("\n🔑 EasyOref — Telegram QR Auth\n");
23
+ console.log("Connecting to Telegram...\n");
24
+ // GramJS requires phoneNumber callback to enter QR flow.
25
+ // Throwing RESTART_AUTH_WITH_QR makes it switch to QR login.
26
+ await client.start({
27
+ phoneNumber: async () => {
28
+ throw { errorMessage: "RESTART_AUTH_WITH_QR" };
29
+ },
30
+ password: async () => "",
31
+ phoneCode: async () => "",
32
+ qrCode: async ({ token }) => {
33
+ const url = `tg://login?token=${token.toString("base64url")}`;
34
+ console.clear();
35
+ console.log("📱 Scan this QR in Telegram → Settings → Devices → Link Desktop\n");
36
+ qrcode.generate(url, { small: true });
37
+ console.log("");
38
+ },
39
+ onError: (err) => {
40
+ if (String(err).includes("RESTART_AUTH"))
41
+ return Promise.resolve(true);
42
+ console.error("Auth error:", err);
43
+ return Promise.resolve(true);
44
+ },
45
+ });
46
+ console.log("\n✅ Authenticated!\n");
47
+ const sessionString = client.session.save();
48
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
49
+ console.log("SESSION STRING (copy this to config.yaml):\n");
50
+ console.log(sessionString);
51
+ console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
52
+ await client.disconnect();
53
+ process.exit(0);
54
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/agent/auth.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,MAAM,MAAM,iBAAiB,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,sEAAsE;AACtE,MAAM,KAAK,GAAG,IAAI,CAAC;AACnB,MAAM,OAAO,GAAG,kCAAkC,CAAC;AAEnD,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC,CAAC;AACtC,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;IACzD,iBAAiB,EAAE,CAAC;IACpB,WAAW,EAAE,eAAe;IAC5B,UAAU,EAAE,OAAO;CACpB,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AAClD,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAE3C,yDAAyD;AACzD,6DAA6D;AAC7D,MAAM,MAAM,CAAC,KAAK,CAAC;IACjB,WAAW,EAAE,KAAK,IAAI,EAAE;QACtB,MAAM,EAAE,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACjD,CAAC;IACD,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE;IACxB,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE;IACzB,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAC1B,MAAM,GAAG,GAAG,oBAAoB,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC9D,OAAO,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,CACT,mEAAmE,CACpE,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACf,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAClC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;CACF,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAEpC,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAuB,CAAC;AAEjE,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;AACjE,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;AAC5D,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AAC3B,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;AAEnE,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;AAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Clarify Node — optional ReAct tool calling for low-confidence enrichment.
3
+ *
4
+ * When the voting pipeline produces a result below the confidence threshold,
5
+ * the clarify node gives the LLM access to 4 tools and lets it decide:
6
+ *
7
+ * - LLM sees the voted result, contradictions, and existing extractions
8
+ * - LLM MAY call tools if it thinks more data would help
9
+ * - LLM MAY respond immediately without tools if data is sufficient
10
+ * - Max 3 tool call iterations, then returns either way
11
+ *
12
+ * Tools:
13
+ * 1. read_telegram_sources — fetch N posts from a Telegram channel
14
+ * 2. alert_history — recent Oref alert history (was there really an alert?)
15
+ * 3. resolve_area — is a mentioned location relevant to user's areas?
16
+ * 4. betterstack_log — query recent EasyOref logs from Better Stack
17
+ *
18
+ * The LLM decides — not a deterministic threshold.
19
+ * Interview answer: "tools are available, agent is autonomous."
20
+ */
21
+ import { type ChannelPost } from "./store.js";
22
+ import type { ValidatedExtraction, VotedResult } from "./types.js";
23
+ export interface ClarifyInput {
24
+ alertId: string;
25
+ alertAreas: string[];
26
+ alertType: string;
27
+ alertTs: number;
28
+ messageId: number;
29
+ currentText: string;
30
+ extractions: ValidatedExtraction[];
31
+ votedResult: VotedResult;
32
+ }
33
+ export interface ClarifyOutput {
34
+ /** New posts discovered by tool calls (to merge into session) */
35
+ newPosts: ChannelPost[];
36
+ /** New extractions from tool-fetched posts (to add to existing) */
37
+ newExtractions: ValidatedExtraction[];
38
+ /** Number of tool calls made */
39
+ toolCallCount: number;
40
+ /** Whether clarification gathered useful new data */
41
+ clarified: boolean;
42
+ }
43
+ export declare function runClarify(input: ClarifyInput): Promise<ClarifyOutput>;
44
+ //# sourceMappingURL=clarify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clarify.d.ts","sourceRoot":"","sources":["../../src/agent/clarify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAaH,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAInE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mEAAmE;IACnE,cAAc,EAAE,mBAAmB,EAAE,CAAC;IACtC,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,SAAS,EAAE,OAAO,CAAC;CACpB;AAwHD,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAqN5E"}
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Clarify Node — optional ReAct tool calling for low-confidence enrichment.
3
+ *
4
+ * When the voting pipeline produces a result below the confidence threshold,
5
+ * the clarify node gives the LLM access to 4 tools and lets it decide:
6
+ *
7
+ * - LLM sees the voted result, contradictions, and existing extractions
8
+ * - LLM MAY call tools if it thinks more data would help
9
+ * - LLM MAY respond immediately without tools if data is sufficient
10
+ * - Max 3 tool call iterations, then returns either way
11
+ *
12
+ * Tools:
13
+ * 1. read_telegram_sources — fetch N posts from a Telegram channel
14
+ * 2. alert_history — recent Oref alert history (was there really an alert?)
15
+ * 3. resolve_area — is a mentioned location relevant to user's areas?
16
+ * 4. betterstack_log — query recent EasyOref logs from Better Stack
17
+ *
18
+ * The LLM decides — not a deterministic threshold.
19
+ * Interview answer: "tools are available, agent is autonomous."
20
+ */
21
+ import { AIMessage as AIMessageClass, HumanMessage, SystemMessage, ToolMessage, } from "@langchain/core/messages";
22
+ import { ChatOpenAI } from "@langchain/openai";
23
+ import { config } from "../config.js";
24
+ import * as logger from "../logger.js";
25
+ import { pushSessionPost } from "./store.js";
26
+ import { clarifyTools } from "./tools.js";
27
+ // ── Constants ──────────────────────────────────────────
28
+ const MAX_REACT_ITERATIONS = 3;
29
+ const CLARIFY_SYSTEM_PROMPT = `You are the clarification agent for EasyOref — an Israeli missile alert enrichment system.
30
+
31
+ The voting pipeline analyzed Telegram channel posts and produced a result with
32
+ low confidence or contradictions. You have access to 4 tools:
33
+
34
+ 1. read_telegram_sources — fetch last N posts from a Telegram news channel
35
+ (IDF, N12, etc). Returns actual message texts.
36
+ 2. alert_history — get recent alert history from Pikud HaOref.
37
+ Answers: "was there really an alert in area X in the last N minutes?"
38
+ 3. resolve_area — check if a location mentioned in news is relevant to the
39
+ user's monitored areas. Uses defense-zone proximity mapping.
40
+ 4. betterstack_log — query recent EasyOref logs from Better Stack.
41
+ See what the enrichment pipeline did recently (extractions, confidence, errors).
42
+
43
+ CRITICAL — TIME VALIDATION:
44
+ You receive the alert time (Israel timezone). Channel posts may be about PREVIOUS
45
+ attacks or ongoing military operations (not THIS specific alert). When in doubt:
46
+ - Use alert_history to verify if an alert really occurred at the claimed time/area.
47
+ - If a post discusses events from hours ago, it is STALE — ignore it.
48
+ - If the voted result has a country_origin that seems unlikely for the alert region
49
+ (e.g., "Lebanon" for central Israel) — verify with alert_history and fresh sources.
50
+
51
+ You decide whether tools would help:
52
+ - If contradictions can be resolved with existing data → respond immediately, no tools.
53
+ - If an authoritative source (IDF, N12) could settle a disagreement → fetch 1-4 posts.
54
+ - If you need to verify whether an alert occurred → check alert_history.
55
+ - If news mentions a city/region and you're unsure if it's relevant → use resolve_area.
56
+ - If the attack origin seems stale (about a previous event) → use alert_history to verify.
57
+ - You can call 0, 1, 2, or 3+ tools. Your choice.
58
+
59
+ When done (with or without tools), respond with ONLY valid JSON (no markdown):
60
+ {
61
+ "clarified": true/false,
62
+ "new_data": {
63
+ "country_origin": string|null,
64
+ "rocket_count": int|null,
65
+ "intercepted": int|null,
66
+ "hits_confirmed": int|null,
67
+ "casualties": int|null,
68
+ "injuries": int|null,
69
+ "is_cassette": bool|null
70
+ },
71
+ "confidence_boost": float, // 0-0.3 (0 if no new info)
72
+ "reasoning": "brief explanation of decision"
73
+ }`;
74
+ // ── LLM ───────────────────────────────────────────────
75
+ function getClarifyLLM() {
76
+ return new ChatOpenAI({
77
+ model: config.agent.model,
78
+ configuration: {
79
+ baseURL: "https://openrouter.ai/api/v1",
80
+ defaultHeaders: {
81
+ "HTTP-Referer": "https://github.com/mikhailkogan17/EasyOref",
82
+ "X-Title": "EasyOref-Clarify",
83
+ },
84
+ },
85
+ apiKey: config.agent.apiKey,
86
+ temperature: 0,
87
+ maxTokens: 600,
88
+ });
89
+ }
90
+ // ── Contradiction detection ───────────────────────────
91
+ function describeContradictions(extractions, voted) {
92
+ const issues = [];
93
+ // Country origin disagreement
94
+ if (voted.country_origins && voted.country_origins.length > 1) {
95
+ const names = voted.country_origins.map((c) => c.name).join(", ");
96
+ issues.push(`Multiple origin countries reported: ${names}`);
97
+ }
98
+ // Rocket count spread
99
+ if (voted.rocket_count_min !== null &&
100
+ voted.rocket_count_max !== null &&
101
+ voted.rocket_count_max - voted.rocket_count_min > 3) {
102
+ issues.push(`Wide rocket count range: ${voted.rocket_count_min}–${voted.rocket_count_max}`);
103
+ }
104
+ // Low sub-field confidence
105
+ if (voted.intercepted_confidence < 0.5 && voted.intercepted !== null) {
106
+ issues.push(`Intercepted count (${voted.intercepted}) has low confidence: ${voted.intercepted_confidence.toFixed(2)}`);
107
+ }
108
+ if (voted.hits_confidence < 0.5 && voted.hits_confirmed !== null) {
109
+ issues.push(`Hits confirmed (${voted.hits_confirmed}) has low confidence: ${voted.hits_confidence.toFixed(2)}`);
110
+ }
111
+ // Overall
112
+ issues.push(`Overall confidence: ${voted.confidence}`);
113
+ issues.push(`Sources count: ${voted.sources_count}`);
114
+ return issues.join("\n");
115
+ }
116
+ // ── ReAct loop ────────────────────────────────────────
117
+ export async function runClarify(input) {
118
+ const llm = getClarifyLLM();
119
+ const llmWithTools = llm.bindTools(clarifyTools);
120
+ const toolMap = new Map(clarifyTools.map((t) => [t.name, t]));
121
+ const contradictions = describeContradictions(input.extractions, input.votedResult);
122
+ const alertTimeIL = new Date(input.alertTs).toLocaleTimeString("he-IL", {
123
+ hour: "2-digit",
124
+ minute: "2-digit",
125
+ timeZone: "Asia/Jerusalem",
126
+ });
127
+ const userPrompt = `Alert region: ${input.alertAreas.join(", ")}\n` +
128
+ `Alert type: ${input.alertType}\n` +
129
+ `Alert time: ${alertTimeIL} (Israel)\n` +
130
+ `Message ID: ${input.messageId}\n\n` +
131
+ `Current voted result:\n` +
132
+ JSON.stringify(input.votedResult, null, 2) +
133
+ `\n\nContradictions & issues:\n${contradictions}\n\n` +
134
+ `Existing extractions (${input.extractions.filter((e) => e.valid).length} valid):\n` +
135
+ input.extractions
136
+ .filter((e) => e.valid)
137
+ .map((e) => ` [${e.channel}] country=${e.country_origin}, rockets=${e.rocket_count}, ` +
138
+ `intercepted=${e.intercepted}, hits=${e.hits_confirmed}, conf=${e.confidence}`)
139
+ .join("\n") +
140
+ `\n\nDecide: would fetching more data from authoritative channels or ` +
141
+ `the official API resolve these issues? If not, respond directly.`;
142
+ // Message history for the ReAct loop
143
+ const messages = [new SystemMessage(CLARIFY_SYSTEM_PROMPT), new HumanMessage(userPrompt)];
144
+ const newPosts = [];
145
+ let toolCallCount = 0;
146
+ for (let iter = 0; iter < MAX_REACT_ITERATIONS; iter++) {
147
+ const response = (await llmWithTools.invoke(messages));
148
+ messages.push(new AIMessageClass({
149
+ content: typeof response.content === "string"
150
+ ? response.content
151
+ : JSON.stringify(response.content),
152
+ tool_calls: response.tool_calls,
153
+ }));
154
+ // No tool calls → LLM is done
155
+ if (!response.tool_calls || response.tool_calls.length === 0) {
156
+ logger.info("Clarify: LLM finished without tool calls", {
157
+ iteration: iter,
158
+ alertId: input.alertId,
159
+ });
160
+ break;
161
+ }
162
+ // Execute each tool call
163
+ for (const tc of response.tool_calls) {
164
+ toolCallCount++;
165
+ const foundTool = toolMap.get(tc.name);
166
+ if (!foundTool) {
167
+ logger.warn("Clarify: unknown tool requested", { tool: tc.name });
168
+ messages.push(new ToolMessage({
169
+ content: JSON.stringify({ error: `Unknown tool: ${tc.name}` }),
170
+ tool_call_id: tc.id ?? `call_${toolCallCount}`,
171
+ }));
172
+ continue;
173
+ }
174
+ try {
175
+ logger.info("Clarify: calling tool", {
176
+ tool: tc.name,
177
+ args: tc.args,
178
+ alertId: input.alertId,
179
+ });
180
+ const result = await foundTool.invoke(tc.args);
181
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
182
+ messages.push(new ToolMessage({
183
+ content: resultStr,
184
+ tool_call_id: tc.id ?? `call_${toolCallCount}`,
185
+ }));
186
+ // If read_sources returned posts, store them for the session
187
+ if (tc.name === "read_telegram_sources" &&
188
+ resultStr.includes('"posts"')) {
189
+ try {
190
+ const parsed = JSON.parse(resultStr);
191
+ if (Array.isArray(parsed.posts)) {
192
+ for (const p of parsed.posts) {
193
+ const post = {
194
+ channel: parsed.channel ?? tc.args.channel,
195
+ text: p.text ?? "",
196
+ ts: p.ts ?? Date.now(),
197
+ messageUrl: p.messageUrl,
198
+ };
199
+ newPosts.push(post);
200
+ await pushSessionPost(post);
201
+ }
202
+ }
203
+ }
204
+ catch {
205
+ // JSON parse failed — ignore
206
+ }
207
+ }
208
+ }
209
+ catch (err) {
210
+ logger.warn("Clarify: tool execution failed", {
211
+ tool: tc.name,
212
+ error: String(err),
213
+ });
214
+ messages.push(new ToolMessage({
215
+ content: JSON.stringify({
216
+ error: `Tool execution failed: ${String(err)}`,
217
+ retry: false,
218
+ }),
219
+ tool_call_id: tc.id ?? `call_${toolCallCount}`,
220
+ }));
221
+ }
222
+ }
223
+ }
224
+ // Parse the final LLM response for structured findings
225
+ const lastMsg = messages[messages.length - 1];
226
+ const lastContent = lastMsg && "content" in lastMsg && typeof lastMsg.content === "string"
227
+ ? lastMsg.content
228
+ : "";
229
+ let clarified = false;
230
+ let newExtractions = [];
231
+ try {
232
+ const cleaned = lastContent
233
+ .replace(/^```(?:json)?\s*\n?/i, "")
234
+ .replace(/\n?```\s*$/i, "");
235
+ const findings = JSON.parse(cleaned.trim());
236
+ clarified = findings.clarified === true;
237
+ // Convert new_data into a synthetic validated extraction
238
+ if (findings.new_data && clarified) {
239
+ const syntheticExtraction = {
240
+ channel: "mcp_clarify",
241
+ region_relevance: 1.0,
242
+ source_trust: 0.85,
243
+ tone: "calm",
244
+ time_relevance: 1.0,
245
+ country_origin: findings.new_data.country_origin ?? null,
246
+ rocket_count: findings.new_data.rocket_count ?? null,
247
+ is_cassette: findings.new_data.is_cassette ?? null,
248
+ intercepted: findings.new_data.intercepted ?? null,
249
+ intercepted_qual: null,
250
+ intercepted_qual_num: null,
251
+ sea_impact: null,
252
+ sea_impact_qual: null,
253
+ sea_impact_qual_num: null,
254
+ open_area_impact: null,
255
+ open_area_impact_qual: null,
256
+ open_area_impact_qual_num: null,
257
+ hits_confirmed: findings.new_data.hits_confirmed ?? null,
258
+ casualties: findings.new_data.casualties ?? null,
259
+ injuries: findings.new_data.injuries ?? null,
260
+ eta_refined_minutes: null,
261
+ confidence: Math.min(0.9, (input.votedResult.confidence ?? 0.5) +
262
+ (findings.confidence_boost ?? 0.15)),
263
+ valid: true,
264
+ messageUrl: undefined,
265
+ };
266
+ newExtractions = [syntheticExtraction];
267
+ }
268
+ }
269
+ catch {
270
+ logger.info("Clarify: could not parse final LLM response as JSON", {
271
+ alertId: input.alertId,
272
+ });
273
+ }
274
+ logger.info("Clarify: completed", {
275
+ alertId: input.alertId,
276
+ toolCallCount,
277
+ clarified,
278
+ newPosts: newPosts.length,
279
+ newExtractions: newExtractions.length,
280
+ });
281
+ return { newPosts, newExtractions, toolCallCount, clarified };
282
+ }
283
+ //# sourceMappingURL=clarify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clarify.js","sourceRoot":"","sources":["../../src/agent/clarify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Dry-run: test vote + buildEnrichedMessage without Redis / Telegram / LLM.
3
+ *
4
+ * Usage:
5
+ * npx tsx packages/bot/src/agent/dry-run.ts
6
+ *
7
+ * Prints the enriched message HTML to stdout.
8
+ * Strip HTML tags to preview plain text:
9
+ * npx tsx packages/bot/src/agent/dry-run.ts | sed 's/<[^>]*>//g'
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=dry-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../../src/agent/dry-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Dry-run: test vote + buildEnrichedMessage without Redis / Telegram / LLM.
3
+ *
4
+ * Usage:
5
+ * npx tsx packages/bot/src/agent/dry-run.ts
6
+ *
7
+ * Prints the enriched message HTML to stdout.
8
+ * Strip HTML tags to preview plain text:
9
+ * npx tsx packages/bot/src/agent/dry-run.ts | sed 's/<[^>]*>//g'
10
+ */
11
+ // ── Mock base message (as formatMessage() would produce) ──────────────────────
12
+ const BASE_MESSAGE = [
13
+ "<b>🚀 Ракетная атака</b>",
14
+ "Ожидаются прилёты. Пройдите в укрытие.",
15
+ "",
16
+ "<b>Район:</b> Тель-Авив — Яффо",
17
+ "<b>Подлётное время:</b> ~5–12 мин",
18
+ "<b>Время оповещения:</b> 03:47",
19
+ ].join("\n");
20
+ // ── Mock validated extractions (normally come from LLM) ───────────────────────
21
+ const NOW = Date.now();
22
+ const ALERT_TS = NOW - 90_000; // alert was 90s ago
23
+ const MOCK_EXTRACTIONS = [
24
+ {
25
+ channel: "@newsflashhhj",
26
+ messageUrl: "https://t.me/newsflashhhj/12340",
27
+ region_relevance: 0.9,
28
+ source_trust: 0.85,
29
+ tone: "calm",
30
+ country_origin: "Iran",
31
+ rocket_count: 6,
32
+ is_cassette: false,
33
+ hits_confirmed: null,
34
+ eta_refined_minutes: 8,
35
+ confidence: 0.88,
36
+ valid: true,
37
+ },
38
+ {
39
+ channel: "@israelsecurity",
40
+ messageUrl: "https://t.me/israelsecurity/5521",
41
+ region_relevance: 0.85,
42
+ source_trust: 0.78,
43
+ tone: "neutral",
44
+ country_origin: "Lebanon",
45
+ rocket_count: 7,
46
+ is_cassette: true,
47
+ hits_confirmed: null,
48
+ eta_refined_minutes: 9,
49
+ confidence: 0.75,
50
+ valid: true,
51
+ },
52
+ {
53
+ channel: "@N12LIVE",
54
+ messageUrl: "https://t.me/N12LIVE/8802",
55
+ region_relevance: 0.7,
56
+ source_trust: 0.9,
57
+ tone: "calm",
58
+ country_origin: "Iran",
59
+ rocket_count: 5,
60
+ is_cassette: null,
61
+ hits_confirmed: 2,
62
+ eta_refined_minutes: null,
63
+ confidence: 0.82,
64
+ valid: true,
65
+ },
66
+ ];
67
+ // ── Inline copy of vote() + buildEnrichedMessage() ────────────────────────────
68
+ // (avoids importing config / redis which require a real config.yaml)
69
+ const SUPERSCRIPTS = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
70
+ function sup(indices) {
71
+ return indices
72
+ .map((n) => String(n)
73
+ .split("")
74
+ .map((d) => SUPERSCRIPTS[Number(d)])
75
+ .join(""))
76
+ .join("");
77
+ }
78
+ const COUNTRY_RU = {
79
+ Iran: "Иран",
80
+ Yemen: "Йемен",
81
+ Lebanon: "Ливан",
82
+ Gaza: "Газа",
83
+ Iraq: "Ирак",
84
+ Syria: "Сирия",
85
+ Hezbollah: "Хезболла",
86
+ };
87
+ function vote(extractions) {
88
+ const indexed = extractions.map((e, i) => ({ ...e, idx: i + 1 }));
89
+ const citedSources = indexed.map((e) => ({
90
+ index: e.idx,
91
+ channel: e.channel,
92
+ messageUrl: e.messageUrl ?? null,
93
+ }));
94
+ // ETA: highest-confidence source
95
+ const withEta = indexed
96
+ .filter((e) => e.eta_refined_minutes !== null)
97
+ .sort((a, b) => b.confidence - a.confidence);
98
+ const bestEta = withEta[0] ?? null;
99
+ // Countries: group, collect citations
100
+ const countryMap = new Map();
101
+ for (const e of indexed) {
102
+ if (e.country_origin) {
103
+ const list = countryMap.get(e.country_origin) ?? [];
104
+ list.push(e.idx);
105
+ countryMap.set(e.country_origin, list);
106
+ }
107
+ }
108
+ const country_origins = countryMap.size > 0
109
+ ? Array.from(countryMap.entries()).map(([name, citations]) => ({
110
+ name,
111
+ citations,
112
+ }))
113
+ : null;
114
+ // Rocket range
115
+ const rocketSrcs = indexed.filter((e) => e.rocket_count !== null);
116
+ const rocketVals = rocketSrcs.map((e) => e.rocket_count);
117
+ const rocket_count_min = rocketVals.length > 0 ? Math.min(...rocketVals) : null;
118
+ const rocket_count_max = rocketVals.length > 0 ? Math.max(...rocketVals) : null;
119
+ const rocket_citations = rocketSrcs.map((e) => e.idx);
120
+ // Cassette: majority
121
+ const cassVals = indexed
122
+ .filter((e) => e.is_cassette !== null)
123
+ .map((e) => e.is_cassette);
124
+ const is_cassette = cassVals.length > 0
125
+ ? cassVals.filter(Boolean).length > cassVals.length / 2
126
+ : null;
127
+ // Hits: median
128
+ const hitsVals = indexed
129
+ .filter((e) => e.hits_confirmed !== null)
130
+ .map((e) => e.hits_confirmed)
131
+ .sort((a, b) => a - b);
132
+ const hits_confirmed = hitsVals.length > 0 ? hitsVals[Math.floor(hitsVals.length / 2)] : null;
133
+ // Hits citations
134
+ const hitsSrcs = indexed.filter((e) => e.hits_confirmed !== null && e.hits_confirmed > 0);
135
+ const hits_citations = hitsSrcs.map((e) => e.idx);
136
+ // Weighted confidence
137
+ const totalWeight = indexed.reduce((s, e) => s + e.source_trust * e.confidence, 0);
138
+ return {
139
+ eta_refined_minutes: bestEta?.eta_refined_minutes ?? null,
140
+ eta_citations: bestEta ? [bestEta.idx] : [],
141
+ country_origins,
142
+ rocket_count_min,
143
+ rocket_count_max,
144
+ is_cassette,
145
+ rocket_citations,
146
+ hits_confirmed,
147
+ hits_citations,
148
+ confidence: Math.round((totalWeight / indexed.length) * 100) / 100,
149
+ sources_count: indexed.length,
150
+ citedSources,
151
+ };
152
+ }
153
+ function insertBeforeTimeLine(text, line) {
154
+ const timeLinePattern = /(<b>Время оповещения:<\/b>)/;
155
+ const match = text.match(timeLinePattern);
156
+ if (match?.index !== undefined) {
157
+ return text.slice(0, match.index) + line + "\n" + text.slice(match.index);
158
+ }
159
+ const lines = text.split("\n");
160
+ lines.splice(Math.max(lines.length - 1, 0), 0, line);
161
+ return lines.join("\n");
162
+ }
163
+ function refineEtaInPlace(text, minutes, alertTs, citations) {
164
+ const absTime = new Date(alertTs + minutes * 60_000).toLocaleTimeString("he-IL", { hour: "2-digit", minute: "2-digit", timeZone: "Asia/Jerusalem" });
165
+ const refined = `~${absTime}${sup(citations)}`;
166
+ const etaPatterns = [
167
+ /~\d+[–-]\d+\s*мин/,
168
+ /~\d+[–-]\d+\s*min/,
169
+ /~\d+[–-]\d+\s*דקות/,
170
+ /1\.5\s*мин/,
171
+ /1\.5\s*min/,
172
+ ];
173
+ for (const pattern of etaPatterns) {
174
+ if (pattern.test(text))
175
+ return text.replace(pattern, refined);
176
+ }
177
+ return text;
178
+ }
179
+ function buildEnrichedMessage(currentText, alertTs, r) {
180
+ let text = currentText;
181
+ if (r.eta_refined_minutes !== null && r.eta_citations.length > 0) {
182
+ text = refineEtaInPlace(text, r.eta_refined_minutes, alertTs, r.eta_citations);
183
+ }
184
+ if (r.country_origins && r.country_origins.length > 0) {
185
+ const parts = r.country_origins.map((c) => {
186
+ const ru = COUNTRY_RU[c.name] ?? c.name;
187
+ return `${ru}${sup(c.citations)}`;
188
+ });
189
+ // Leading \n creates blank line between ETA and intel block
190
+ text = insertBeforeTimeLine(text, `\n<b>Откуда:</b> ${parts.join(" + ")}`);
191
+ }
192
+ if (r.rocket_count_min !== null && r.rocket_count_max !== null) {
193
+ const countStr = r.rocket_count_min === r.rocket_count_max
194
+ ? `${r.rocket_count_min}`
195
+ : `~${r.rocket_count_min}-${r.rocket_count_max}`;
196
+ const cassette = r.is_cassette ? " (кассет.)" : "";
197
+ text = insertBeforeTimeLine(text, `<b>Ракет:</b> ${countStr}${cassette}`);
198
+ }
199
+ if (r.hits_confirmed !== null && r.hits_confirmed > 0) {
200
+ const hitsCite = r.hits_citations.length > 0 ? sup(r.hits_citations) : "";
201
+ text = insertBeforeTimeLine(text, `<b>Попадания (Дан центр):</b> ${r.hits_confirmed}${hitsCite}`);
202
+ }
203
+ const sourcesWithUrl = r.citedSources.filter((s) => s.messageUrl);
204
+ if (sourcesWithUrl.length > 0) {
205
+ const links = sourcesWithUrl
206
+ .map((s) => `<a href="${s.messageUrl}">[${s.index}]</a>`)
207
+ .join(" ");
208
+ text += `\n—\n<i>Источники: ${links}</i>`;
209
+ }
210
+ return text;
211
+ }
212
+ // ── Run ───────────────────────────────────────────────────────────────────────
213
+ const voted = vote(MOCK_EXTRACTIONS);
214
+ console.log("\n=== VOTE RESULT ===");
215
+ console.log(JSON.stringify(voted, null, 2));
216
+ const enriched = buildEnrichedMessage(BASE_MESSAGE, ALERT_TS, voted);
217
+ console.log("\n=== ENRICHED MESSAGE (HTML) ===");
218
+ console.log(enriched);
219
+ console.log("\n=== PLAIN TEXT PREVIEW ===");
220
+ console.log(enriched
221
+ .replace(/<[^>]*>/g, "")
222
+ .replace(/&lt;/g, "<")
223
+ .replace(/&gt;/g, ">"));
224
+ console.log(`\n=== STATS ===`);
225
+ console.log(`Confidence: ${voted.confidence}`);
226
+ console.log(`Sources: ${voted.sources_count}`);
227
+ console.log(`Chars: ${enriched.length} (TG caption limit: 1024)`);
228
+ export {};
229
+ //# sourceMappingURL=dry-run.js.map