@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,120 @@
1
+ /**
2
+ * Alert history tool — query Pikud HaOref history.
3
+ */
4
+
5
+ /** Format date as DD.MM.YYYY for Oref history API */
6
+ function formatOrefDate(d: Date): string {
7
+ const dd = String(d.getDate()).padStart(2, "0");
8
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
9
+ const yyyy = d.getFullYear();
10
+ return `${dd}.${mm}.${yyyy}`;
11
+ }
12
+
13
+ export { formatOrefDate };
14
+
15
+ import * as logger from "@easyoref/monitoring";
16
+ import { config } from "@easyoref/shared";
17
+ import { tool } from "@langchain/core/tools";
18
+ import { z } from "zod";
19
+
20
+ export const alertHistoryTool = tool(
21
+ async ({
22
+ area,
23
+ lastMinutes,
24
+ }: {
25
+ area: string;
26
+ lastMinutes: number;
27
+ }): Promise<string> => {
28
+ try {
29
+ const historyUrl =
30
+ config.orefHistoryUrl ??
31
+ "https://www.oref.org.il/Shared/Ajax/GetAlarmsHistory.aspx?lang=he&fromDate=" +
32
+ formatOrefDate(new Date(Date.now() - lastMinutes * 60_000)) +
33
+ "&toDate=" +
34
+ formatOrefDate(new Date());
35
+
36
+ const res = await fetch(historyUrl, {
37
+ headers: {
38
+ "User-Agent":
39
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
40
+ "X-Requested-With": "XMLHttpRequest",
41
+ Referer: "https://www.oref.org.il/",
42
+ Accept: "application/json, text/plain, */*",
43
+ },
44
+ signal: AbortSignal.timeout(5000),
45
+ });
46
+
47
+ if (!res.ok) {
48
+ return JSON.stringify({
49
+ error: `Oref history API returned ${res.status}`,
50
+ retry: true,
51
+ });
52
+ }
53
+
54
+ const text = await res.text();
55
+ if (!text.trim()) {
56
+ return JSON.stringify({
57
+ area,
58
+ alerts: [],
59
+ note: "No alert history returned for this period",
60
+ });
61
+ }
62
+
63
+ const parsed: unknown = JSON.parse(text);
64
+ const alerts = Array.isArray(parsed) ? parsed : [parsed];
65
+
66
+ const relevant = alerts.filter((a: Record<string, unknown>) => {
67
+ const data = (a.data as string) ?? "";
68
+ return data.includes(area) || area.includes(data.split(" ")[0] ?? "");
69
+ });
70
+
71
+ const result = {
72
+ area,
73
+ last_minutes: lastMinutes,
74
+ alerts: relevant.slice(0, 20).map((a: Record<string, unknown>) => ({
75
+ date: a.alertDate ?? a.date,
76
+ title: a.title,
77
+ data: a.data,
78
+ category: a.category_desc ?? a.category,
79
+ })),
80
+ total_in_period: alerts.length,
81
+ relevant_count: relevant.length,
82
+ queried_at: new Date().toISOString(),
83
+ };
84
+
85
+ logger.info("Tool: alert_history executed", {
86
+ area,
87
+ last_minutes: lastMinutes,
88
+ total: alerts.length,
89
+ relevant: relevant.length,
90
+ });
91
+
92
+ return JSON.stringify(result);
93
+ } catch (err) {
94
+ logger.warn("Tool: alert_history failed", { error: String(err) });
95
+ return JSON.stringify({
96
+ error: `Oref history API call failed: ${String(err)}`,
97
+ retry: true,
98
+ });
99
+ }
100
+ },
101
+ {
102
+ name: "alert_history",
103
+ description:
104
+ "Get recent alert history from Pikud HaOref (Israel Home Front Command). " +
105
+ "Answers: 'was there really an alert in area X in the last N minutes?' " +
106
+ "More useful than active alerts (the bot already has the current alert). " +
107
+ "Use to verify channel claims about attacks in specific areas.",
108
+ schema: z.object({
109
+ area: z
110
+ .string()
111
+ .describe("Hebrew area name to search for in history (e.g. תל אביב)"),
112
+ lastMinutes: z
113
+ .number()
114
+ .min(5)
115
+ .max(120)
116
+ .default(30)
117
+ .describe("How many minutes of history to search (5-120)"),
118
+ }),
119
+ },
120
+ );
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Better Stack log query tool.
3
+ */
4
+
5
+ import * as logger from "@easyoref/monitoring";
6
+ import { config } from "@easyoref/shared";
7
+ import { tool } from "@langchain/core/tools";
8
+ import { z } from "zod";
9
+
10
+ export const betterstackLogTool = tool(
11
+ async ({
12
+ query,
13
+ lastMinutes,
14
+ }: {
15
+ query: string;
16
+ lastMinutes: number;
17
+ }): Promise<string> => {
18
+ const token = config.logtailToken;
19
+ if (!token) {
20
+ return JSON.stringify({
21
+ error: "Better Stack token not configured",
22
+ hint: "Set observability.betterstack_token in config.yaml",
23
+ });
24
+ }
25
+
26
+ try {
27
+ const fromDate = new Date(Date.now() - lastMinutes * 60_000).toISOString();
28
+
29
+ const res = await fetch(
30
+ `https://in.logtail.com/queries/logs?query=${encodeURIComponent(query)}&from=${fromDate}&to=${new Date().toISOString()}&limit=20`,
31
+ {
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ Accept: "application/json",
35
+ },
36
+ signal: AbortSignal.timeout(8000),
37
+ },
38
+ );
39
+
40
+ if (!res.ok) {
41
+ const status = res.status;
42
+ if (status === 401 || status === 403) {
43
+ return JSON.stringify({
44
+ error: "Invalid Better Stack credentials",
45
+ hint: "Check your observability.betterstack_token in config.yaml",
46
+ });
47
+ }
48
+ return JSON.stringify({
49
+ error: `Better Stack API returned ${status}`,
50
+ retry: status >= 500,
51
+ });
52
+ }
53
+
54
+ const json = (await res.json()) as {
55
+ results?: Array<{ message: string; timestamp: string }>;
56
+ };
57
+ const logs = json.results ?? [];
58
+
59
+ const formattedLogs = logs.map((entry) => ({
60
+ timestamp: entry.timestamp,
61
+ message: entry.message.slice(0, 500),
62
+ }));
63
+
64
+ logger.info("Tool: betterstack_log executed", {
65
+ query,
66
+ last_minutes: lastMinutes,
67
+ returned: logs.length,
68
+ });
69
+
70
+ return JSON.stringify({
71
+ query,
72
+ logs: formattedLogs,
73
+ count: logs.length,
74
+ });
75
+ } catch (err) {
76
+ logger.warn("Tool: betterstack_log failed", { error: String(err) });
77
+ return JSON.stringify({
78
+ error: `Better Stack query failed: ${String(err)}`,
79
+ retry: true,
80
+ });
81
+ }
82
+ },
83
+ {
84
+ name: "betterstack_log",
85
+ description:
86
+ "Query recent EasyOref logs from Better Stack (formerly Logtail). " +
87
+ "Use to check: what happened in the enrichment pipeline recently, " +
88
+ "any errors or unusual patterns. " +
89
+ "Good for debugging: 'why did this alert enrichment fail?'",
90
+ schema: z.object({
91
+ query: z
92
+ .string()
93
+ .describe("Search query for log messages (e.g. 'error', 'alert-123')"),
94
+ lastMinutes: z
95
+ .number()
96
+ .min(5)
97
+ .max(1440)
98
+ .default(30)
99
+ .describe("How many minutes back to search (5-1440)"),
100
+ }),
101
+ },
102
+ );
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool re-exports for agentic clarification.
3
+ */
4
+
5
+ export { readSourcesTool } from "./read-sources.js";
6
+ export { alertHistoryTool, formatOrefDate } from "./alert-history.js";
7
+ export { resolveAreaTool, resolveAreaProximity } from "./resolve-area.js";
8
+ export { betterstackLogTool } from "./betterstack-log.js";
9
+
10
+ export { resolveAreaProximity as _resolveAreaProximity } from "./resolve-area.js";
11
+ export { formatOrefDate as _formatOrefDate } from "./alert-history.js";
12
+
13
+ import { readSourcesTool } from "./read-sources.js";
14
+ import { alertHistoryTool } from "./alert-history.js";
15
+ import { resolveAreaTool } from "./resolve-area.js";
16
+ import { betterstackLogTool } from "./betterstack-log.js";
17
+
18
+ export const clarifyTools = [
19
+ readSourcesTool,
20
+ alertHistoryTool,
21
+ resolveAreaTool,
22
+ betterstackLogTool,
23
+ ];
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Telegram sources tool — fetch recent posts from a channel.
3
+ */
4
+
5
+ import { fetchRecentChannelPosts } from "@easyoref/gramjs";
6
+ import * as logger from "@easyoref/monitoring";
7
+ import { config } from "@easyoref/shared";
8
+ import { tool } from "@langchain/core/tools";
9
+ import { z } from "zod";
10
+
11
+ export const readSourcesTool = tool(
12
+ async ({
13
+ channel,
14
+ limit,
15
+ }: {
16
+ channel: string;
17
+ limit: number;
18
+ }): Promise<string> => {
19
+ try {
20
+ const safeLimit = Math.min(limit, config.agent.clarifyFetchCount);
21
+ const posts = await fetchRecentChannelPosts(channel, safeLimit);
22
+
23
+ if (posts.length === 0) {
24
+ return JSON.stringify({
25
+ channel,
26
+ posts: [],
27
+ note: "No recent posts found or channel not accessible",
28
+ });
29
+ }
30
+
31
+ const result = {
32
+ channel,
33
+ posts: posts.map(
34
+ (p: { text: string; ts: number; messageUrl?: string }) => ({
35
+ text: p.text.slice(0, 800),
36
+ ts: p.ts,
37
+ messageUrl: p.messageUrl,
38
+ }),
39
+ ),
40
+ count: posts.length,
41
+ };
42
+
43
+ logger.info("Tool: read_telegram_sources executed", {
44
+ channel,
45
+ limit: safeLimit,
46
+ returned: posts.length,
47
+ });
48
+
49
+ return JSON.stringify(result);
50
+ } catch (err) {
51
+ const errStr = String(err);
52
+ const isRateLimit =
53
+ errStr.includes("FLOOD") || errStr.includes("rate limit");
54
+
55
+ logger.warn("Tool: read_telegram_sources failed", {
56
+ channel,
57
+ error: errStr,
58
+ });
59
+
60
+ return JSON.stringify({
61
+ error: `Failed to fetch from ${channel}: ${errStr}`,
62
+ retry: !isRateLimit,
63
+ });
64
+ }
65
+ },
66
+ {
67
+ name: "read_telegram_sources",
68
+ description:
69
+ "Fetch last 1-4 posts from a Telegram news channel via MTProto. " +
70
+ "Returns actual message texts you can extract data from. " +
71
+ "Authoritative channels: @idf_telegram (IDF official), @N12LIVE (news), " +
72
+ "@israelsecurity (security), @ynetalerts (Ynet). " +
73
+ "Use when existing sources contradict each other or you need confirmation.",
74
+ schema: z.object({
75
+ channel: z
76
+ .string()
77
+ .describe("Channel username with @ prefix (e.g. @idf_telegram)"),
78
+ limit: z
79
+ .number()
80
+ .min(1)
81
+ .max(4)
82
+ .default(3)
83
+ .describe("Number of recent posts to fetch (1-4)"),
84
+ }),
85
+ },
86
+ );
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Resolve area relevance tool — check if location is in user's defense zone.
3
+ */
4
+
5
+ import * as logger from "@easyoref/monitoring";
6
+ import { config } from "@easyoref/shared";
7
+ import { tool } from "@langchain/core/tools";
8
+ import { z } from "zod";
9
+
10
+ const AREA_PROXIMITY_GROUPS: Record<string, string[]> = {
11
+ "גוש דן": [
12
+ "תל אביב", "רמת גן", "גבעתיים", "בני ברק", "חולון", "בת ים",
13
+ "פתח תקווה", "גבעת שמואל", "אור יהודה", "יהוד", "קריית אונו",
14
+ ],
15
+ שרון: ["הרצליה", "רעננה", "כפר סבא", "הוד השרון", "נתניה", "רמת השרון", "כוכב יאיר"],
16
+ מרכז: ["ראשון לציון", "רחובות", "נס ציונה", "לוד", "רמלה", "מודיעין", "יבנה", "שוהם"],
17
+ ירושלים: ["ירושלים", "בית שמש", "מעלה אדומים", "מבשרת ציון"],
18
+ חיפה: ["חיפה", "קריות", "קריית אתא", "קריית ביאליק", "קריית מוצקין", "טירת כרמל", "נשר"],
19
+ "דרום-מערב": ["אשקלון", "אשדוד", "גן יבנה", "קריית מלאכי"],
20
+ "עוטף עזה": ["שדרות", "עוטף עזה", "נתיבות", "אופקים"],
21
+ "באר שבע": ["באר שבע", "ערד", "דימונה"],
22
+ "גליל עליון": ["קריית שמונה", "מטולה", "צפת", "ראש פינה"],
23
+ };
24
+
25
+ function resolveAreaProximity(
26
+ mentioned: string,
27
+ monitoredAreas: string[],
28
+ ): {
29
+ relevant: boolean;
30
+ sameZone: string | undefined;
31
+ monitoredMatch: string[];
32
+ reasoning: string;
33
+ } {
34
+ const normalizedMentioned = mentioned.toLowerCase().trim();
35
+ const normalizedMentionedHebrew = mentioned.replace(/\s+/g, "");
36
+
37
+ for (const area of monitoredAreas) {
38
+ const normalizedArea = area.toLowerCase().trim();
39
+ const normalizedAreaHebrew = area.replace(/\s+/g, "");
40
+
41
+ if (
42
+ normalizedMentioned === normalizedArea ||
43
+ normalizedMentioned.includes(normalizedArea) ||
44
+ normalizedArea.includes(normalizedMentioned) ||
45
+ normalizedMentionedHebrew === normalizedAreaHebrew ||
46
+ normalizedMentionedHebrew.includes(normalizedAreaHebrew)
47
+ ) {
48
+ return {
49
+ relevant: true,
50
+ sameZone: undefined,
51
+ monitoredMatch: [area],
52
+ reasoning: `"${mentioned}" directly matches monitored area "${area}"`,
53
+ };
54
+ }
55
+ }
56
+
57
+ for (const [zone, cities] of Object.entries(AREA_PROXIMITY_GROUPS)) {
58
+ const mentionedNormalized = mentioned.toLowerCase();
59
+ const mentionedHebrew = mentioned.replace(/\s+/g, "");
60
+
61
+ for (const city of cities) {
62
+ const cityNormalized = city.toLowerCase();
63
+ const cityHebrew = city.replace(/\s+/g, "");
64
+
65
+ if (
66
+ mentionedNormalized === cityNormalized ||
67
+ mentionedNormalized.includes(cityNormalized) ||
68
+ cityNormalized.includes(mentionedNormalized) ||
69
+ mentionedHebrew === cityHebrew ||
70
+ mentionedHebrew.includes(cityHebrew)
71
+ ) {
72
+ const matchedMonitored = monitoredAreas.filter((mArea) => {
73
+ const mAreaNormalized = mArea.toLowerCase().replace(/\s+/g, "");
74
+ return cities.some(
75
+ (c) =>
76
+ c.toLowerCase().replace(/\s+/g, "") === mAreaNormalized ||
77
+ mAreaNormalized.includes(c.toLowerCase().replace(/\s+/g, "")) ||
78
+ c.toLowerCase().replace(/\s+/g, "").includes(mAreaNormalized),
79
+ );
80
+ });
81
+
82
+ if (matchedMonitored.length > 0) {
83
+ return {
84
+ relevant: true,
85
+ sameZone: zone,
86
+ monitoredMatch: matchedMonitored,
87
+ reasoning:
88
+ `"${mentioned}" is in zone "${zone}" together with monitored: ` +
89
+ matchedMonitored.join(", "),
90
+ };
91
+ }
92
+
93
+ return {
94
+ relevant: false,
95
+ sameZone: zone,
96
+ monitoredMatch: [],
97
+ reasoning:
98
+ `"${mentioned}" is in zone "${zone}" but none of user's monitored ` +
99
+ `areas (${monitoredAreas.join(", ")}) are in that zone`,
100
+ };
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ relevant: false,
107
+ sameZone: undefined,
108
+ monitoredMatch: [],
109
+ reasoning:
110
+ `"${mentioned}" could not be matched to any monitored area ` +
111
+ `(${monitoredAreas.join(", ")})`,
112
+ };
113
+ }
114
+
115
+ const REGION_KEYWORDS: Record<string, string[]> = {
116
+ מרכז: ["תל אביב", "רמת גן", "פתח תקווה", "ראשון לציון", "הרצליה", "חולון"],
117
+ צפון: ["חיפה", "קריות", "צפת", "קריית שמונה", "נצרת", "עכו", "טבריה"],
118
+ דרום: ["באר שבע", "אשדוד", "אשקלון", "שדרות", "אילת"],
119
+ };
120
+
121
+ function resolveAreaProximityWithRegions(
122
+ mentioned: string,
123
+ monitoredAreas: string[],
124
+ ): {
125
+ relevant: boolean;
126
+ sameZone: string | undefined;
127
+ monitoredMatch: string[];
128
+ reasoning: string;
129
+ } {
130
+ const baseResult = resolveAreaProximity(mentioned, monitoredAreas);
131
+ if (baseResult.relevant) {
132
+ return baseResult;
133
+ }
134
+
135
+ const mentionedLower = mentioned.toLowerCase();
136
+
137
+ for (const [region, cities] of Object.entries(REGION_KEYWORDS)) {
138
+ if (!mentionedLower.includes(region)) continue;
139
+ const matchedMonitored = monitoredAreas.filter((m) =>
140
+ cities.some((c) => m.includes(c) || c.includes(m.split(" ")[0] ?? "")),
141
+ );
142
+ if (matchedMonitored.length > 0) {
143
+ return {
144
+ relevant: true,
145
+ sameZone: region,
146
+ monitoredMatch: matchedMonitored,
147
+ reasoning:
148
+ `"${mentioned}" refers to region "${region}" which includes ` +
149
+ matchedMonitored.join(", "),
150
+ };
151
+ }
152
+ }
153
+
154
+ return baseResult;
155
+ }
156
+
157
+ export { resolveAreaProximityWithRegions as resolveAreaProximity };
158
+
159
+ export const resolveAreaTool = tool(
160
+ async ({ location }: { location: string }): Promise<string> => {
161
+ const monitoredAreas = config.areas;
162
+
163
+ if (monitoredAreas.length === 0) {
164
+ return JSON.stringify({
165
+ error: "No monitored areas configured",
166
+ hint: "User has not set up city monitoring",
167
+ });
168
+ }
169
+
170
+ const result = resolveAreaProximityWithRegions(location, monitoredAreas);
171
+
172
+ logger.info("Tool: resolve_area executed", {
173
+ location,
174
+ relevant: result.relevant,
175
+ zone: result.sameZone,
176
+ });
177
+
178
+ return JSON.stringify({
179
+ location,
180
+ monitored_areas: monitoredAreas,
181
+ ...result,
182
+ });
183
+ },
184
+ {
185
+ name: "resolve_area",
186
+ description:
187
+ "Determine if a location mentioned in news is relevant to the user's " +
188
+ "monitored areas. Uses defense-zone proximity: cities in the same Iron Dome " +
189
+ "coverage zone are considered relevant. " +
190
+ 'Example: "попадание в Петах Тикве" → relevant for Herzliya user ' +
191
+ "(both in Gush Dan / Sharon zone). " +
192
+ 'Use when a news post mentions a city or region like "center" and you need ' +
193
+ "to determine if it affects the user.",
194
+ schema: z.object({
195
+ location: z
196
+ .string()
197
+ .describe(
198
+ "City or region name in Hebrew as mentioned in news (e.g. פתח תקווה, מרכז)",
199
+ ),
200
+ }),
201
+ },
202
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "references": [
8
+ { "path": "../shared" },
9
+ { "path": "../monitoring" },
10
+ { "path": "../gramjs" }
11
+ ],
12
+ "include": ["src/**/*"],
13
+ "exclude": ["src/**/__tests__/**", "src/**/*.test.ts", "dist"]
14
+ }