easyoref 1.14.2 → 1.15.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.
- package/dist/agent/extract.d.ts +48 -0
- package/dist/agent/extract.d.ts.map +1 -0
- package/dist/agent/extract.js +375 -0
- package/dist/agent/extract.js.map +1 -0
- package/dist/agent/filters.d.ts +48 -0
- package/dist/agent/filters.d.ts.map +1 -0
- package/dist/agent/filters.js +124 -0
- package/dist/agent/filters.js.map +1 -0
- package/dist/agent/graph.d.ts +9 -93
- package/dist/agent/graph.d.ts.map +1 -1
- package/dist/agent/graph.js +110 -1118
- package/dist/agent/graph.js.map +1 -1
- package/dist/agent/helpers.d.ts +6 -0
- package/dist/agent/helpers.d.ts.map +1 -0
- package/dist/agent/helpers.js +15 -0
- package/dist/agent/helpers.js.map +1 -0
- package/dist/agent/message.d.ts +48 -0
- package/dist/agent/message.d.ts.map +1 -0
- package/dist/agent/message.js +353 -0
- package/dist/agent/message.js.map +1 -0
- package/dist/agent/store.d.ts +2 -0
- package/dist/agent/store.d.ts.map +1 -1
- package/dist/agent/store.js +12 -1
- package/dist/agent/store.js.map +1 -1
- package/dist/agent/types.d.ts +18 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/vote.d.ts +13 -0
- package/dist/agent/vote.d.ts.map +1 -0
- package/dist/agent/vote.js +197 -0
- package/dist/agent/vote.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM extraction pipeline — two-tier: cheap pre-filter + expensive extraction.
|
|
3
|
+
*
|
|
4
|
+
* 1. Cheap model: single call — "which channels have important intel?"
|
|
5
|
+
* 2. Expensive model: per-post — "extract structured data"
|
|
6
|
+
* 3. Post-filter: deterministic validation on extraction results.
|
|
7
|
+
*/
|
|
8
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
9
|
+
import type { AlertType, ChannelTracking, TrackedMessage, ValidatedExtraction } from "./types.js";
|
|
10
|
+
/** Cheap model for channel pre-filtering (single call, short output) */
|
|
11
|
+
export declare function getFilterLLM(): ChatOpenAI;
|
|
12
|
+
/** Expensive model for structured data extraction (per-post) */
|
|
13
|
+
export declare function getExtractLLM(): ChatOpenAI;
|
|
14
|
+
/**
|
|
15
|
+
* Single cheap LLM call — filter channels by relevance.
|
|
16
|
+
* Returns channel names containing important military intel.
|
|
17
|
+
*/
|
|
18
|
+
export declare function filterChannelsCheap(tracking: ChannelTracking, alertAreas: string[], alertTs: number, alertType: AlertType): Promise<string[]>;
|
|
19
|
+
/** Phase-specific extraction instructions */
|
|
20
|
+
export declare function getPhaseInstructions(alertType: AlertType): string;
|
|
21
|
+
export declare const EXTRACT_SYSTEM_PROMPT = "You analyze Telegram channel messages about a missile/rocket attack on Israel.\nYour job: extract factual data, assess quality, AND validate temporal relevance.\n\nCRITICAL \u2014 TIME VALIDATION:\nYou will receive the alert time and the post time. You MUST determine if this post\nis about the CURRENT attack or about a previous/different event.\n- If post discusses events clearly BEFORE the alert time \u2192 time_relevance=0\n- If post is generic military news not specific to this attack \u2192 time_relevance=0.2\n- If post discusses the current attack \u2192 time_relevance=1.0\n- If uncertain \u2192 time_relevance=0.5 (the system will use alert_history to verify)\n\nReturn ONLY valid JSON (no markdown, no explanation):\n{\n \"region_relevance\": float, // 0\u20131: does this message discuss the specified alert region?\n \"source_trust\": float, // 0\u20131: factual reporting (1.0) vs unverified rumors/panic (0.0)\n \"tone\": \"calm\"|\"neutral\"|\"alarmist\",\n \"time_relevance\": float, // 0\u20131: is this post about the CURRENT attack? (see rules above)\n \"country_origin\": string|null, // \"Iran\",\"Yemen\",\"Lebanon\",\"Gaza\",\"Iraq\",\"Syria\" or null\n \"rocket_count\": int|null,\n \"is_cassette\": bool|null,\n \"intercepted\": int|null,\n \"intercepted_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"intercepted_qual_num\": int|null,\n \"sea_impact\": int|null,\n \"sea_impact_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"sea_impact_qual_num\": int|null,\n \"open_area_impact\": int|null,\n \"open_area_impact_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"open_area_impact_qual_num\": int|null,\n \"hits_confirmed\": int|null,\n \"casualties\": int|null,\n \"injuries\": int|null,\n \"eta_refined_minutes\": int|null,\n \"confidence\": float\n}\n\nRules:\n- If unrelated to the alert region, set region_relevance=0 and all data fields to null.\n- If message is speculative/unconfirmed rumor, set source_trust < 0.4.\n- If message uses excessive caps, exclamation marks, panic language \u2192 tone=\"alarmist\".\n- Only extract concrete numbers explicitly stated in the text. Never guess.\n- *_qual fields: use ONLY when NO exact count is given. If exact number present, set *_qual=null.\n- \"none\" qual is only valid if explicitly stated (e.g., \"\u0432\u0441\u0435 \u043F\u0435\u0440\u0435\u0445\u0432\u0430\u0447\u0435\u043D\u044B\", \"\u043D\u0435 \u0443\u043F\u0430\u043B\u043E \u0432 \u043C\u043E\u0440\u0435\").\n- For IDF (@idf_telegram) posts about ongoing operations (not this specific attack) \u2192 time_relevance=0.\n- LANGUAGE NEUTRALITY: Posts may be in Hebrew, Russian, Arabic, or English. The language of the post\n MUST NOT affect source_trust or confidence. Russian-language Israeli channels are equally reliable\n and often break news faster than Hebrew ones. Judge ONLY by factual content and tone.\n- TRUST INTERCEPTION & IMPACT REPORTS: When a channel explicitly states interception results\n (e.g., \"\u043F\u0435\u0440\u0435\u0445\u0432\u0430\u0447\u0435\u043D\u044B\", \"intercepted\", \"\u05D9\u05D9\u05E8\u05D5\u05D8\", \"\u0443\u043F\u0430\u043B\u0438 \u0432 \u043C\u043E\u0440\u0435\", \"fell in the sea\", \"\u05E0\u05E4\u05DC\u05D5 \u05D1\u05D9\u05DD\",\n \"open area impact\", \"\u05E9\u05D8\u05D7 \u05E4\u05EA\u05D5\u05D7\"), trust these claims with source_trust >= 0.7 and confidence >= 0.7.\n Israeli Telegram channels often report interception results before official confirmation,\n and these reports are typically accurate. Do NOT downgrade these just because they lack official source.";
|
|
22
|
+
export interface ExtractContext {
|
|
23
|
+
alertTs: number;
|
|
24
|
+
alertType: AlertType;
|
|
25
|
+
alertAreas: string[];
|
|
26
|
+
alertId: string;
|
|
27
|
+
language: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract structured data from posts using expensive LLM.
|
|
31
|
+
* Uses post-level dedup cache to avoid re-extracting unchanged posts.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractPosts(posts: TrackedMessage[], ctx: ExtractContext): Promise<ValidatedExtraction[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Deterministic validation of extraction results.
|
|
36
|
+
* Rejects: stale posts, irrelevant regions, untrusted sources,
|
|
37
|
+
* alarmist tone, no data, low confidence.
|
|
38
|
+
*/
|
|
39
|
+
export declare function postFilter(extractions: ValidatedExtraction[], alertId: string): ValidatedExtraction[];
|
|
40
|
+
export declare const _test: {
|
|
41
|
+
readonly getFilterLLM: typeof getFilterLLM;
|
|
42
|
+
readonly getExtractLLM: typeof getExtractLLM;
|
|
43
|
+
readonly getPhaseInstructions: typeof getPhaseInstructions;
|
|
44
|
+
readonly EXTRACT_SYSTEM_PROMPT: "You analyze Telegram channel messages about a missile/rocket attack on Israel.\nYour job: extract factual data, assess quality, AND validate temporal relevance.\n\nCRITICAL — TIME VALIDATION:\nYou will receive the alert time and the post time. You MUST determine if this post\nis about the CURRENT attack or about a previous/different event.\n- If post discusses events clearly BEFORE the alert time → time_relevance=0\n- If post is generic military news not specific to this attack → time_relevance=0.2\n- If post discusses the current attack → time_relevance=1.0\n- If uncertain → time_relevance=0.5 (the system will use alert_history to verify)\n\nReturn ONLY valid JSON (no markdown, no explanation):\n{\n \"region_relevance\": float, // 0–1: does this message discuss the specified alert region?\n \"source_trust\": float, // 0–1: factual reporting (1.0) vs unverified rumors/panic (0.0)\n \"tone\": \"calm\"|\"neutral\"|\"alarmist\",\n \"time_relevance\": float, // 0–1: is this post about the CURRENT attack? (see rules above)\n \"country_origin\": string|null, // \"Iran\",\"Yemen\",\"Lebanon\",\"Gaza\",\"Iraq\",\"Syria\" or null\n \"rocket_count\": int|null,\n \"is_cassette\": bool|null,\n \"intercepted\": int|null,\n \"intercepted_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"intercepted_qual_num\": int|null,\n \"sea_impact\": int|null,\n \"sea_impact_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"sea_impact_qual_num\": int|null,\n \"open_area_impact\": int|null,\n \"open_area_impact_qual\": \"all\"|\"most\"|\"many\"|\"few\"|\"exists\"|\"none\"|\"more_than\"|\"less_than\"|null,\n \"open_area_impact_qual_num\": int|null,\n \"hits_confirmed\": int|null,\n \"casualties\": int|null,\n \"injuries\": int|null,\n \"eta_refined_minutes\": int|null,\n \"confidence\": float\n}\n\nRules:\n- If unrelated to the alert region, set region_relevance=0 and all data fields to null.\n- If message is speculative/unconfirmed rumor, set source_trust < 0.4.\n- If message uses excessive caps, exclamation marks, panic language → tone=\"alarmist\".\n- Only extract concrete numbers explicitly stated in the text. Never guess.\n- *_qual fields: use ONLY when NO exact count is given. If exact number present, set *_qual=null.\n- \"none\" qual is only valid if explicitly stated (e.g., \"все перехвачены\", \"не упало в море\").\n- For IDF (@idf_telegram) posts about ongoing operations (not this specific attack) → time_relevance=0.\n- LANGUAGE NEUTRALITY: Posts may be in Hebrew, Russian, Arabic, or English. The language of the post\n MUST NOT affect source_trust or confidence. Russian-language Israeli channels are equally reliable\n and often break news faster than Hebrew ones. Judge ONLY by factual content and tone.\n- TRUST INTERCEPTION & IMPACT REPORTS: When a channel explicitly states interception results\n (e.g., \"перехвачены\", \"intercepted\", \"יירוט\", \"упали в море\", \"fell in the sea\", \"נפלו בים\",\n \"open area impact\", \"שטח פתוח\"), trust these claims with source_trust >= 0.7 and confidence >= 0.7.\n Israeli Telegram channels often report interception results before official confirmation,\n and these reports are typically accurate. Do NOT downgrade these just because they lack official source.";
|
|
45
|
+
readonly FILTER_SYSTEM_PROMPT: "You pre-filter Telegram channels for an Israeli missile alert system.\nGiven channels with their latest messages, identify which contain IMPORTANT military intel:\n- Country of origin (where rockets/missiles launched from)\n- Impact location (where they hit)\n- Warhead type / cassette munitions\n- Weapon type (ballistic, cruise, drones)\n- Damage / destruction reports\n- Interception reports (Iron Dome, David's Sling)\n- Casualty / injury reports\n\nIGNORE channels that only contain:\n- Panic, speculation, or unverified rumors\n- Rehashes of official alerts without new data\n- General commentary without actionable facts\n\nReturn ONLY valid JSON (no markdown):\n{\"relevant_channels\": [\"@channel1\", \"@channel2\"]}\nIf NO channels have important intel, return: {\"relevant_channels\": []}";
|
|
46
|
+
readonly postFilter: typeof postFilter;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=extract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/agent/extract.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAK/C,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAEf,cAAc,EACd,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAIpB,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,UAAU,CAczC;AAED,gEAAgE;AAChE,wBAAgB,aAAa,IAAI,UAAU,CAc1C;AAuBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,eAAe,EACzB,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAkDnB;AAOD,6CAA6C;AAC7C,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAmBjE;AAED,eAAO,MAAM,qBAAqB,gsHAmDyE,CAAC;AAE5G,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,cAAc,EAAE,EACvB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAiJhC;AAID;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,mBAAmB,EAAE,EAClC,OAAO,EAAE,MAAM,GACd,mBAAmB,EAAE,CAmDvB;AAID,eAAO,MAAM,KAAK;;;;;;;CAOR,CAAC"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM extraction pipeline — two-tier: cheap pre-filter + expensive extraction.
|
|
3
|
+
*
|
|
4
|
+
* 1. Cheap model: single call — "which channels have important intel?"
|
|
5
|
+
* 2. Expensive model: per-post — "extract structured data"
|
|
6
|
+
* 3. Post-filter: deterministic validation on extraction results.
|
|
7
|
+
*/
|
|
8
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
9
|
+
import { config } from "../config.js";
|
|
10
|
+
import * as logger from "../logger.js";
|
|
11
|
+
import { textHash, toIsraelTime } from "./helpers.js";
|
|
12
|
+
import { getCachedExtractions, saveCachedExtractions } from "./store.js";
|
|
13
|
+
// ── LLM instances ──────────────────────────────────────
|
|
14
|
+
/** Cheap model for channel pre-filtering (single call, short output) */
|
|
15
|
+
export function getFilterLLM() {
|
|
16
|
+
return new ChatOpenAI({
|
|
17
|
+
model: config.agent.model,
|
|
18
|
+
configuration: {
|
|
19
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
20
|
+
defaultHeaders: {
|
|
21
|
+
"HTTP-Referer": "https://github.com/mikhailkogan17/EasyOref",
|
|
22
|
+
"X-Title": "EasyOref",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
apiKey: config.agent.apiKey,
|
|
26
|
+
temperature: 0,
|
|
27
|
+
maxTokens: 200,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Expensive model for structured data extraction (per-post) */
|
|
31
|
+
export function getExtractLLM() {
|
|
32
|
+
return new ChatOpenAI({
|
|
33
|
+
model: config.agent.extractModel,
|
|
34
|
+
configuration: {
|
|
35
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
36
|
+
defaultHeaders: {
|
|
37
|
+
"HTTP-Referer": "https://github.com/mikhailkogan17/EasyOref",
|
|
38
|
+
"X-Title": "EasyOref",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
apiKey: config.agent.apiKey,
|
|
42
|
+
temperature: 0,
|
|
43
|
+
maxTokens: 500,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// ── Cheap pre-filter ───────────────────────────────────
|
|
47
|
+
const FILTER_SYSTEM_PROMPT = `You pre-filter Telegram channels for an Israeli missile alert system.
|
|
48
|
+
Given channels with their latest messages, identify which contain IMPORTANT military intel:
|
|
49
|
+
- Country of origin (where rockets/missiles launched from)
|
|
50
|
+
- Impact location (where they hit)
|
|
51
|
+
- Warhead type / cassette munitions
|
|
52
|
+
- Weapon type (ballistic, cruise, drones)
|
|
53
|
+
- Damage / destruction reports
|
|
54
|
+
- Interception reports (Iron Dome, David's Sling)
|
|
55
|
+
- Casualty / injury reports
|
|
56
|
+
|
|
57
|
+
IGNORE channels that only contain:
|
|
58
|
+
- Panic, speculation, or unverified rumors
|
|
59
|
+
- Rehashes of official alerts without new data
|
|
60
|
+
- General commentary without actionable facts
|
|
61
|
+
|
|
62
|
+
Return ONLY valid JSON (no markdown):
|
|
63
|
+
{"relevant_channels": ["@channel1", "@channel2"]}
|
|
64
|
+
If NO channels have important intel, return: {"relevant_channels": []}`;
|
|
65
|
+
/**
|
|
66
|
+
* Single cheap LLM call — filter channels by relevance.
|
|
67
|
+
* Returns channel names containing important military intel.
|
|
68
|
+
*/
|
|
69
|
+
export async function filterChannelsCheap(tracking, alertAreas, alertTs, alertType) {
|
|
70
|
+
const channels = tracking.channels_with_updates;
|
|
71
|
+
if (channels.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
const channelSummaries = channels
|
|
74
|
+
.map((ch) => {
|
|
75
|
+
const posts = ch.last_tracked_messages
|
|
76
|
+
.map((m) => ` [${toIsraelTime(m.timestamp)}] ${m.text.slice(0, 200)}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
return `${ch.channel} (${ch.last_tracked_messages.length} new):\n${posts}`;
|
|
79
|
+
})
|
|
80
|
+
.join("\n\n");
|
|
81
|
+
const regionHint = alertAreas.length > 0 ? alertAreas.join(", ") : "Israel";
|
|
82
|
+
const userPrompt = `Alert: ${regionHint} at ${toIsraelTime(alertTs)}, phase: ${alertType}\n\n` + `Channels:\n${channelSummaries}`;
|
|
83
|
+
try {
|
|
84
|
+
const llm = getFilterLLM();
|
|
85
|
+
const response = await llm.invoke([
|
|
86
|
+
{ role: "system", content: FILTER_SYSTEM_PROMPT },
|
|
87
|
+
{ role: "user", content: userPrompt },
|
|
88
|
+
]);
|
|
89
|
+
const raw = typeof response.content === "string"
|
|
90
|
+
? response.content
|
|
91
|
+
: JSON.stringify(response.content);
|
|
92
|
+
const text = raw
|
|
93
|
+
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
94
|
+
.replace(/\n?```\s*$/i, "");
|
|
95
|
+
const parsed = JSON.parse(text.trim());
|
|
96
|
+
logger.info("Agent: cheap pre-filter", {
|
|
97
|
+
total_channels: channels.length,
|
|
98
|
+
relevant: parsed.relevant_channels.length,
|
|
99
|
+
relevant_channels: parsed.relevant_channels,
|
|
100
|
+
});
|
|
101
|
+
return parsed.relevant_channels;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.warn("Agent: cheap pre-filter failed, passing all channels", {
|
|
105
|
+
error: String(err),
|
|
106
|
+
});
|
|
107
|
+
// Fallback: pass all channels through
|
|
108
|
+
return channels.map((c) => c.channel);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── Expensive extraction ───────────────────────────────
|
|
112
|
+
const QUAL_VALUES = '"all"|"most"|"many"|"few"|"exists"|"none"|"more_than"|"less_than"';
|
|
113
|
+
/** Phase-specific extraction instructions */
|
|
114
|
+
export function getPhaseInstructions(alertType) {
|
|
115
|
+
switch (alertType) {
|
|
116
|
+
case "early_warning":
|
|
117
|
+
return `PHASE: EARLY WARNING (radar detected launches, sirens not yet).
|
|
118
|
+
Focus on: country_origin (WHERE were rockets launched from?), eta_refined_minutes, rocket_count, is_cassette.
|
|
119
|
+
Do NOT extract: intercepted, sea_impact, open_area_impact, hits_confirmed, casualties, injuries — these are IMPOSSIBLE at this stage.
|
|
120
|
+
If a message discusses interception results, it is about a PREVIOUS attack — set time_relevance=0.`;
|
|
121
|
+
case "siren":
|
|
122
|
+
return `PHASE: SIREN (rockets incoming, impact imminent).
|
|
123
|
+
Focus on: country_origin (if not known yet), rocket_count, intercepted, sea_impact, open_area_impact, is_cassette.
|
|
124
|
+
Do NOT extract: hits_confirmed, casualties, injuries — too early for confirmed damage reports.
|
|
125
|
+
If a message discusses casualties or confirmed hits, verify the timing carefully - it may be about a previous attack.`;
|
|
126
|
+
case "resolved":
|
|
127
|
+
return `PHASE: RESOLVED (incident over, assessing damage).
|
|
128
|
+
Focus on: intercepted (final count), hits_confirmed, casualties, injuries, open_area_impact.
|
|
129
|
+
All fields are valid at this stage. Prioritize confirmed official reports.`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export const EXTRACT_SYSTEM_PROMPT = `You analyze Telegram channel messages about a missile/rocket attack on Israel.
|
|
133
|
+
Your job: extract factual data, assess quality, AND validate temporal relevance.
|
|
134
|
+
|
|
135
|
+
CRITICAL — TIME VALIDATION:
|
|
136
|
+
You will receive the alert time and the post time. You MUST determine if this post
|
|
137
|
+
is about the CURRENT attack or about a previous/different event.
|
|
138
|
+
- If post discusses events clearly BEFORE the alert time → time_relevance=0
|
|
139
|
+
- If post is generic military news not specific to this attack → time_relevance=0.2
|
|
140
|
+
- If post discusses the current attack → time_relevance=1.0
|
|
141
|
+
- If uncertain → time_relevance=0.5 (the system will use alert_history to verify)
|
|
142
|
+
|
|
143
|
+
Return ONLY valid JSON (no markdown, no explanation):
|
|
144
|
+
{
|
|
145
|
+
"region_relevance": float, // 0–1: does this message discuss the specified alert region?
|
|
146
|
+
"source_trust": float, // 0–1: factual reporting (1.0) vs unverified rumors/panic (0.0)
|
|
147
|
+
"tone": "calm"|"neutral"|"alarmist",
|
|
148
|
+
"time_relevance": float, // 0–1: is this post about the CURRENT attack? (see rules above)
|
|
149
|
+
"country_origin": string|null, // "Iran","Yemen","Lebanon","Gaza","Iraq","Syria" or null
|
|
150
|
+
"rocket_count": int|null,
|
|
151
|
+
"is_cassette": bool|null,
|
|
152
|
+
"intercepted": int|null,
|
|
153
|
+
"intercepted_qual": ${QUAL_VALUES}|null,
|
|
154
|
+
"intercepted_qual_num": int|null,
|
|
155
|
+
"sea_impact": int|null,
|
|
156
|
+
"sea_impact_qual": ${QUAL_VALUES}|null,
|
|
157
|
+
"sea_impact_qual_num": int|null,
|
|
158
|
+
"open_area_impact": int|null,
|
|
159
|
+
"open_area_impact_qual": ${QUAL_VALUES}|null,
|
|
160
|
+
"open_area_impact_qual_num": int|null,
|
|
161
|
+
"hits_confirmed": int|null,
|
|
162
|
+
"casualties": int|null,
|
|
163
|
+
"injuries": int|null,
|
|
164
|
+
"eta_refined_minutes": int|null,
|
|
165
|
+
"confidence": float
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Rules:
|
|
169
|
+
- If unrelated to the alert region, set region_relevance=0 and all data fields to null.
|
|
170
|
+
- If message is speculative/unconfirmed rumor, set source_trust < 0.4.
|
|
171
|
+
- If message uses excessive caps, exclamation marks, panic language → tone="alarmist".
|
|
172
|
+
- Only extract concrete numbers explicitly stated in the text. Never guess.
|
|
173
|
+
- *_qual fields: use ONLY when NO exact count is given. If exact number present, set *_qual=null.
|
|
174
|
+
- "none" qual is only valid if explicitly stated (e.g., "все перехвачены", "не упало в море").
|
|
175
|
+
- For IDF (@idf_telegram) posts about ongoing operations (not this specific attack) → time_relevance=0.
|
|
176
|
+
- LANGUAGE NEUTRALITY: Posts may be in Hebrew, Russian, Arabic, or English. The language of the post
|
|
177
|
+
MUST NOT affect source_trust or confidence. Russian-language Israeli channels are equally reliable
|
|
178
|
+
and often break news faster than Hebrew ones. Judge ONLY by factual content and tone.
|
|
179
|
+
- TRUST INTERCEPTION & IMPACT REPORTS: When a channel explicitly states interception results
|
|
180
|
+
(e.g., "перехвачены", "intercepted", "יירוט", "упали в море", "fell in the sea", "נפלו בים",
|
|
181
|
+
"open area impact", "שטח פתוח"), trust these claims with source_trust >= 0.7 and confidence >= 0.7.
|
|
182
|
+
Israeli Telegram channels often report interception results before official confirmation,
|
|
183
|
+
and these reports are typically accurate. Do NOT downgrade these just because they lack official source.`;
|
|
184
|
+
/**
|
|
185
|
+
* Extract structured data from posts using expensive LLM.
|
|
186
|
+
* Uses post-level dedup cache to avoid re-extracting unchanged posts.
|
|
187
|
+
*/
|
|
188
|
+
export async function extractPosts(posts, ctx) {
|
|
189
|
+
if (posts.length === 0)
|
|
190
|
+
return [];
|
|
191
|
+
// ── Post-level dedup ───────────────────────────────
|
|
192
|
+
const postHashMap = new Map();
|
|
193
|
+
for (const post of posts) {
|
|
194
|
+
const hash = textHash(post.channel + "|" + post.text.slice(0, 800));
|
|
195
|
+
postHashMap.set(hash, post);
|
|
196
|
+
}
|
|
197
|
+
const allHashes = [...postHashMap.keys()];
|
|
198
|
+
const cached = await getCachedExtractions(allHashes);
|
|
199
|
+
const cachedResults = [];
|
|
200
|
+
const newPosts = [];
|
|
201
|
+
for (const [hash, post] of postHashMap) {
|
|
202
|
+
const cachedJson = cached.get(hash);
|
|
203
|
+
if (cachedJson) {
|
|
204
|
+
cachedResults.push(JSON.parse(cachedJson));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
newPosts.push(post);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
logger.info("Agent: extraction dedup", {
|
|
211
|
+
alertId: ctx.alertId,
|
|
212
|
+
total: posts.length,
|
|
213
|
+
cached: cachedResults.length,
|
|
214
|
+
new: newPosts.length,
|
|
215
|
+
});
|
|
216
|
+
if (newPosts.length === 0) {
|
|
217
|
+
return cachedResults;
|
|
218
|
+
}
|
|
219
|
+
// ── Extract new posts ──────────────────────────────
|
|
220
|
+
const llm = getExtractLLM();
|
|
221
|
+
const regionHint = ctx.alertAreas.length > 0
|
|
222
|
+
? ctx.alertAreas.join(", ")
|
|
223
|
+
: Object.keys(config.agent.areaLabels).join(", ") || "Israel";
|
|
224
|
+
const alertTimeIL = toIsraelTime(ctx.alertTs);
|
|
225
|
+
const nowIL = toIsraelTime(Date.now());
|
|
226
|
+
const phaseInst = getPhaseInstructions(ctx.alertType);
|
|
227
|
+
const systemPrompt = EXTRACT_SYSTEM_PROMPT + "\n\n" + phaseInst;
|
|
228
|
+
const newResults = await Promise.all(newPosts.map(async (post) => {
|
|
229
|
+
const postTimeIL = toIsraelTime(post.timestamp);
|
|
230
|
+
const postAgeMin = Math.round((ctx.alertTs - post.timestamp) / 60_000);
|
|
231
|
+
const postAgeSuffix = postAgeMin > 0
|
|
232
|
+
? `(${postAgeMin} min BEFORE alert)`
|
|
233
|
+
: postAgeMin < 0
|
|
234
|
+
? `(${Math.abs(postAgeMin)} min AFTER alert)`
|
|
235
|
+
: "(same time as alert)";
|
|
236
|
+
const contextHeader = `Alert time: ${alertTimeIL} (Israel)\n` +
|
|
237
|
+
`Post time: ${postTimeIL} (Israel) ${postAgeSuffix}\n` +
|
|
238
|
+
`Current time: ${nowIL} (Israel)\n` +
|
|
239
|
+
`Alert region: ${regionHint}\n` +
|
|
240
|
+
`UI language: ${ctx.language}\n`;
|
|
241
|
+
try {
|
|
242
|
+
const response = await llm.invoke([
|
|
243
|
+
{ role: "system", content: systemPrompt },
|
|
244
|
+
{
|
|
245
|
+
role: "user",
|
|
246
|
+
content: `${contextHeader}Channel: ${post.channel}\n\nMessage:\n${post.text.slice(0, 800)}`,
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
const raw = typeof response.content === "string"
|
|
250
|
+
? response.content
|
|
251
|
+
: JSON.stringify(response.content);
|
|
252
|
+
const text = raw
|
|
253
|
+
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
254
|
+
.replace(/\n?```\s*$/i, "");
|
|
255
|
+
const parsed = JSON.parse(text.trim());
|
|
256
|
+
return {
|
|
257
|
+
...parsed,
|
|
258
|
+
channel: post.channel,
|
|
259
|
+
messageUrl: post.url,
|
|
260
|
+
time_relevance: parsed.time_relevance ?? 0.5,
|
|
261
|
+
valid: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
logger.warn("Agent: extraction failed", {
|
|
266
|
+
channel: post.channel,
|
|
267
|
+
error: String(err),
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
channel: post.channel,
|
|
271
|
+
region_relevance: 0,
|
|
272
|
+
source_trust: 0,
|
|
273
|
+
tone: "neutral",
|
|
274
|
+
time_relevance: 0,
|
|
275
|
+
country_origin: null,
|
|
276
|
+
rocket_count: null,
|
|
277
|
+
is_cassette: null,
|
|
278
|
+
intercepted: null,
|
|
279
|
+
intercepted_qual: null,
|
|
280
|
+
intercepted_qual_num: null,
|
|
281
|
+
sea_impact: null,
|
|
282
|
+
sea_impact_qual: null,
|
|
283
|
+
sea_impact_qual_num: null,
|
|
284
|
+
open_area_impact: null,
|
|
285
|
+
open_area_impact_qual: null,
|
|
286
|
+
open_area_impact_qual_num: null,
|
|
287
|
+
hits_confirmed: null,
|
|
288
|
+
casualties: null,
|
|
289
|
+
injuries: null,
|
|
290
|
+
eta_refined_minutes: null,
|
|
291
|
+
confidence: 0,
|
|
292
|
+
valid: false,
|
|
293
|
+
reject_reason: "extraction_error",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}));
|
|
297
|
+
// Cache new results
|
|
298
|
+
const cacheEntries = {};
|
|
299
|
+
newPosts.forEach((post, i) => {
|
|
300
|
+
const hash = textHash(post.channel + "|" + post.text.slice(0, 800));
|
|
301
|
+
cacheEntries[hash] = JSON.stringify(newResults[i]);
|
|
302
|
+
});
|
|
303
|
+
await saveCachedExtractions(cacheEntries);
|
|
304
|
+
const results = [...cachedResults, ...newResults];
|
|
305
|
+
logger.info("Agent: extracted", {
|
|
306
|
+
alertId: ctx.alertId,
|
|
307
|
+
count: results.length,
|
|
308
|
+
newLLMCalls: newResults.length,
|
|
309
|
+
cachedReused: cachedResults.length,
|
|
310
|
+
});
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
// ── Post-filter (deterministic, 0 tokens) ──────────────
|
|
314
|
+
/**
|
|
315
|
+
* Deterministic validation of extraction results.
|
|
316
|
+
* Rejects: stale posts, irrelevant regions, untrusted sources,
|
|
317
|
+
* alarmist tone, no data, low confidence.
|
|
318
|
+
*/
|
|
319
|
+
export function postFilter(extractions, alertId) {
|
|
320
|
+
const validated = extractions.map((ext) => {
|
|
321
|
+
// V0: TIME RELEVANCE — most important check
|
|
322
|
+
if (ext.time_relevance < 0.5) {
|
|
323
|
+
return { ...ext, valid: false, reject_reason: "stale_post" };
|
|
324
|
+
}
|
|
325
|
+
// V1: region relevance
|
|
326
|
+
if (ext.region_relevance < 0.5) {
|
|
327
|
+
return { ...ext, valid: false, reject_reason: "region_irrelevant" };
|
|
328
|
+
}
|
|
329
|
+
// V2: source trust
|
|
330
|
+
if (ext.source_trust < 0.4) {
|
|
331
|
+
return { ...ext, valid: false, reject_reason: "untrusted_source" };
|
|
332
|
+
}
|
|
333
|
+
// V3: tone — reject alarmist
|
|
334
|
+
if (ext.tone === "alarmist") {
|
|
335
|
+
return { ...ext, valid: false, reject_reason: "alarmist_tone" };
|
|
336
|
+
}
|
|
337
|
+
// V4: at least one data field must be non-null
|
|
338
|
+
const hasData = ext.country_origin !== null ||
|
|
339
|
+
ext.rocket_count !== null ||
|
|
340
|
+
ext.is_cassette !== null ||
|
|
341
|
+
ext.intercepted !== null ||
|
|
342
|
+
ext.intercepted_qual !== null ||
|
|
343
|
+
ext.hits_confirmed !== null ||
|
|
344
|
+
ext.casualties !== null ||
|
|
345
|
+
ext.injuries !== null ||
|
|
346
|
+
ext.eta_refined_minutes !== null;
|
|
347
|
+
if (!hasData) {
|
|
348
|
+
return { ...ext, valid: false, reject_reason: "no_data" };
|
|
349
|
+
}
|
|
350
|
+
// V5: overall confidence floor
|
|
351
|
+
if (ext.confidence < 0.3) {
|
|
352
|
+
return { ...ext, valid: false, reject_reason: "low_confidence" };
|
|
353
|
+
}
|
|
354
|
+
return { ...ext, valid: true };
|
|
355
|
+
});
|
|
356
|
+
const passed = validated.filter((e) => e.valid);
|
|
357
|
+
const rejected = validated.filter((e) => !e.valid);
|
|
358
|
+
logger.info("Agent: post-filter", {
|
|
359
|
+
alertId,
|
|
360
|
+
passed: passed.length,
|
|
361
|
+
rejected: rejected.length,
|
|
362
|
+
reasons: rejected.map((r) => `${r.channel}:${r.reject_reason}`),
|
|
363
|
+
});
|
|
364
|
+
return validated;
|
|
365
|
+
}
|
|
366
|
+
// ── Exported for testing ───────────────────────────────
|
|
367
|
+
export const _test = {
|
|
368
|
+
getFilterLLM,
|
|
369
|
+
getExtractLLM,
|
|
370
|
+
getPhaseInstructions,
|
|
371
|
+
EXTRACT_SYSTEM_PROMPT,
|
|
372
|
+
FILTER_SYSTEM_PROMPT,
|
|
373
|
+
postFilter,
|
|
374
|
+
};
|
|
375
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/agent/extract.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AASzE,0DAA0D;AAE1D,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,UAAU,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK;QACzB,aAAa,EAAE;YACb,OAAO,EAAE,8BAA8B;YACvC,cAAc,EAAE;gBACd,cAAc,EAAE,4CAA4C;gBAC5D,SAAS,EAAE,UAAU;aACtB;SACF;QACD,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;QAC3B,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,GAAG;KACf,CAAC,CAAC;AACL,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,UAAU,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY;QAChC,aAAa,EAAE;YACb,OAAO,EAAE,8BAA8B;YACvC,cAAc,EAAE;gBACd,cAAc,EAAE,4CAA4C;gBAC5D,SAAS,EAAE,UAAU;aACtB;SACF;QACD,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;QAC3B,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,GAAG;KACf,CAAC,CAAC;AACL,CAAC;AAED,0DAA0D;AAE1D,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;uEAiB0C,CAAC;AAExE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAyB,EACzB,UAAoB,EACpB,OAAe,EACf,SAAoB;IAEpB,MAAM,QAAQ,GAAG,QAAQ,CAAC,qBAAqB,CAAC;IAChD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,MAAM,gBAAgB,GAAG,QAAQ;SAC9B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACV,MAAM,KAAK,GAAG,EAAE,CAAC,qBAAqB;aACnC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;aACtE,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,OAAO,GAAG,EAAE,CAAC,OAAO,KAAK,EAAE,CAAC,qBAAqB,CAAC,MAAM,WAAW,KAAK,EAAE,CAAC;IAC7E,CAAC,CAAC;SACD,IAAI,CAAC,MAAM,CAAC,CAAC;IAEhB,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE5E,MAAM,UAAU,GACd,UAAU,UAAU,OAAO,YAAY,CACrC,OAAO,CACR,YAAY,SAAS,MAAM,GAAG,cAAc,gBAAgB,EAAE,CAAC;IAElE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAChC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,oBAAoB,EAAE;YACjD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE;SACtC,CAAC,CAAC;QAEH,MAAM,GAAG,GACP,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ;YAClC,CAAC,CAAC,QAAQ,CAAC,OAAO;YAClB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,GAAG;aACb,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC;aACnC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAoC,CAAC;QAE1E,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE;YACrC,cAAc,EAAE,QAAQ,CAAC,MAAM;YAC/B,QAAQ,EAAE,MAAM,CAAC,iBAAiB,CAAC,MAAM;YACzC,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;SAC5C,CAAC,CAAC;QAEH,OAAO,MAAM,CAAC,iBAAiB,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,sDAAsD,EAAE;YAClE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC;SACnB,CAAC,CAAC;QACH,sCAAsC;QACtC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;AACH,CAAC;AAED,0DAA0D;AAE1D,MAAM,WAAW,GACf,mEAAmE,CAAC;AAEtE,6CAA6C;AAC7C,MAAM,UAAU,oBAAoB,CAAC,SAAoB;IACvD,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,eAAe;YAClB,OAAO;;;mGAGsF,CAAC;QAEhG,KAAK,OAAO;YACV,OAAO;;;sHAGyG,CAAC;QAEnH,KAAK,UAAU;YACb,OAAO;;2EAE8D,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;wBAqBb,WAAW;;;uBAGZ,WAAW;;;6BAGL,WAAW;;;;;;;;;;;;;;;;;;;;;;;;2GAwBmE,CAAC;AAU5G;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAuB,EACvB,GAAmB;IAEnB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElC,sDAAsD;IACtD,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACpE,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAErD,MAAM,aAAa,GAA0B,EAAE,CAAC;IAChD,MAAM,QAAQ,GAAqB,EAAE,CAAC;IAEtC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC;QACvC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,UAAU,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAwB,CAAC,CAAC;QACpE,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE;QACrC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,KAAK,CAAC,MAAM;QACnB,MAAM,EAAE,aAAa,CAAC,MAAM;QAC5B,GAAG,EAAE,QAAQ,CAAC,MAAM;KACrB,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,sDAAsD;IACtD,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAE5B,MAAM,UAAU,GACd,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QACvB,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;IAClE,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,qBAAqB,GAAG,MAAM,GAAG,SAAS,CAAC;IAEhE,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAgC,EAAE;QACxD,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC;QACvE,MAAM,aAAa,GACjB,UAAU,GAAG,CAAC;YACZ,CAAC,CAAC,IAAI,UAAU,oBAAoB;YACpC,CAAC,CAAC,UAAU,GAAG,CAAC;gBAChB,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,mBAAmB;gBAC7C,CAAC,CAAC,sBAAsB,CAAC;QAE7B,MAAM,aAAa,GACjB,eAAe,WAAW,aAAa;YACvC,eAAe,UAAU,aAAa,aAAa,IAAI;YACvD,iBAAiB,KAAK,aAAa;YACnC,iBAAiB,UAAU,IAAI;YAC/B,gBAAgB,GAAG,CAAC,QAAQ,IAAI,CAAC;QAEnC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;gBAChC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;gBACzC;oBACE,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE,GAAG,aAAa,YACvB,IAAI,CAAC,OACP,iBAAiB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;iBAC3C;aACF,CAAC,CAAC;YAEH,MAAM,GAAG,GACP,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ;gBAClC,CAAC,CAAC,QAAQ,CAAC,OAAO;gBAClB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACvC,MAAM,IAAI,GAAG,GAAG;iBACb,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC;iBACnC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAqB,CAAC;YAC3D,OAAO;gBACL,GAAG,MAAM;gBACT,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,UAAU,EAAE,IAAI,CAAC,GAAG;gBACpB,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,GAAG;gBAC5C,KAAK,EAAE,IAAI;aACZ,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE;gBACtC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC;aACnB,CAAC,CAAC;YACH,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,gBAAgB,EAAE,CAAC;gBACnB,YAAY,EAAE,CAAC;gBACf,IAAI,EAAE,SAAkB;gBACxB,cAAc,EAAE,CAAC;gBACjB,cAAc,EAAE,IAAI;gBACpB,YAAY,EAAE,IAAI;gBAClB,WAAW,EAAE,IAAI;gBACjB,WAAW,EAAE,IAAI;gBACjB,gBAAgB,EAAE,IAAI;gBACtB,oBAAoB,EAAE,IAAI;gBAC1B,UAAU,EAAE,IAAI;gBAChB,eAAe,EAAE,IAAI;gBACrB,mBAAmB,EAAE,IAAI;gBACzB,gBAAgB,EAAE,IAAI;gBACtB,qBAAqB,EAAE,IAAI;gBAC3B,yBAAyB,EAAE,IAAI;gBAC/B,cAAc,EAAE,IAAI;gBACpB,UAAU,EAAE,IAAI;gBAChB,QAAQ,EAAE,IAAI;gBACd,mBAAmB,EAAE,IAAI;gBACzB,UAAU,EAAE,CAAC;gBACb,KAAK,EAAE,KAAK;gBACZ,aAAa,EAAE,kBAAkB;aAClC,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CACH,CAAC;IAEF,oBAAoB;IACpB,MAAM,YAAY,GAA2B,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACpE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IACH,MAAM,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,CAAC,GAAG,aAAa,EAAE,GAAG,UAAU,CAAC,CAAC;IAElD,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE;QAC9B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,WAAW,EAAE,UAAU,CAAC,MAAM;QAC9B,YAAY,EAAE,aAAa,CAAC,MAAM;KACnC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,0DAA0D;AAE1D;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,WAAkC,EAClC,OAAe;IAEf,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAuB,EAAE;QAC7D,4CAA4C;QAC5C,IAAI,GAAG,CAAC,cAAc,GAAG,GAAG,EAAE,CAAC;YAC7B,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;QAC/D,CAAC;QACD,uBAAuB;QACvB,IAAI,GAAG,CAAC,gBAAgB,GAAG,GAAG,EAAE,CAAC;YAC/B,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC;QACtE,CAAC;QACD,mBAAmB;QACnB,IAAI,GAAG,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC;YAC3B,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,CAAC;QACrE,CAAC;QACD,6BAA6B;QAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC5B,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC;QAClE,CAAC;QACD,+CAA+C;QAC/C,MAAM,OAAO,GACX,GAAG,CAAC,cAAc,KAAK,IAAI;YAC3B,GAAG,CAAC,YAAY,KAAK,IAAI;YACzB,GAAG,CAAC,WAAW,KAAK,IAAI;YACxB,GAAG,CAAC,WAAW,KAAK,IAAI;YACxB,GAAG,CAAC,gBAAgB,KAAK,IAAI;YAC7B,GAAG,CAAC,cAAc,KAAK,IAAI;YAC3B,GAAG,CAAC,UAAU,KAAK,IAAI;YACvB,GAAG,CAAC,QAAQ,KAAK,IAAI;YACrB,GAAG,CAAC,mBAAmB,KAAK,IAAI,CAAC;QACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;QAC5D,CAAC;QACD,+BAA+B;QAC/B,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC;YACzB,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC;QACnE,CAAC;QAED,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAEnD,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE;QAChC,OAAO;QACP,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;KAChE,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,0DAA0D;AAE1D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,YAAY;IACZ,aAAa;IACb,oBAAoB;IACpB,qBAAqB;IACrB,oBAAoB;IACpB,UAAU;CACF,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-filters — zero LLM tokens.
|
|
3
|
+
*
|
|
4
|
+
* Filters out noise:
|
|
5
|
+
* - Pikud HaOref area list "простыни" (high comma count)
|
|
6
|
+
* - Summary/recap posts with timestamp patterns "(HH:MM)", "X минут"
|
|
7
|
+
* - IDF/Tsahal press releases (long official texts)
|
|
8
|
+
*
|
|
9
|
+
* Builds ChannelTracking structure for the LLM pipeline.
|
|
10
|
+
*/
|
|
11
|
+
import type { ChannelPost } from "./store.js";
|
|
12
|
+
import type { ChannelTracking, TrackedMessage } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Pikud HaOref "простыня" — area list with many commas.
|
|
15
|
+
*/
|
|
16
|
+
declare function isAreaListNoise(text: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Summary/recap posts with timestamp patterns.
|
|
19
|
+
* Real-time intel doesn't contain multiple "(HH:MM)" timestamps
|
|
20
|
+
* or "X минут/минуты" duration references.
|
|
21
|
+
*/
|
|
22
|
+
declare function isSummaryPost(text: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* IDF/Tsahal press releases — long official texts (>400 chars from IDF channels).
|
|
25
|
+
*/
|
|
26
|
+
declare function isIdfPressRelease(channel: string, text: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Combined noise filter. Returns true if post should be filtered OUT.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isNoise(post: ChannelPost): boolean;
|
|
31
|
+
declare function toTrackedMessage(post: ChannelPost): TrackedMessage;
|
|
32
|
+
/**
|
|
33
|
+
* Build ChannelTracking from session posts.
|
|
34
|
+
*
|
|
35
|
+
* Splits posts per channel into prev (already processed) and last (new).
|
|
36
|
+
* Applies deterministic noise filter on all posts.
|
|
37
|
+
* Only includes channels that have new (last) messages.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildChannelTracking(posts: ChannelPost[], sessionStartTs: number, lastUpdateTs: number): ChannelTracking;
|
|
40
|
+
export declare const _test: {
|
|
41
|
+
readonly isAreaListNoise: typeof isAreaListNoise;
|
|
42
|
+
readonly isSummaryPost: typeof isSummaryPost;
|
|
43
|
+
readonly isIdfPressRelease: typeof isIdfPressRelease;
|
|
44
|
+
readonly isNoise: typeof isNoise;
|
|
45
|
+
readonly toTrackedMessage: typeof toTrackedMessage;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.d.ts","sourceRoot":"","sources":["../../src/agent/filters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EACf,MAAM,YAAY,CAAC;AAQpB;;GAEG;AACH,iBAAS,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI9C;AAED;;;;GAIG;AACH,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO5C;AAED;;GAEG;AACH,iBAAS,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAUlD;AAID,iBAAS,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,cAAc,CAO3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,WAAW,EAAE,EACpB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,GACnB,eAAe,CAuCjB;AAID,eAAO,MAAM,KAAK;;;;;;CAMR,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-filters — zero LLM tokens.
|
|
3
|
+
*
|
|
4
|
+
* Filters out noise:
|
|
5
|
+
* - Pikud HaOref area list "простыни" (high comma count)
|
|
6
|
+
* - Summary/recap posts with timestamp patterns "(HH:MM)", "X минут"
|
|
7
|
+
* - IDF/Tsahal press releases (long official texts)
|
|
8
|
+
*
|
|
9
|
+
* Builds ChannelTracking structure for the LLM pipeline.
|
|
10
|
+
*/
|
|
11
|
+
// ── Noise detectors ────────────────────────────────────
|
|
12
|
+
const OREF_LINK_RE = /oref\.org\.il/i;
|
|
13
|
+
const OREF_CHANNEL_RE = /pikud|פיקוד|oref/i;
|
|
14
|
+
const IDF_CHANNEL_RE = /idf|צה"?ל|tsahal/i;
|
|
15
|
+
/**
|
|
16
|
+
* Pikud HaOref "простыня" — area list with many commas.
|
|
17
|
+
*/
|
|
18
|
+
function isAreaListNoise(text) {
|
|
19
|
+
if (OREF_LINK_RE.test(text))
|
|
20
|
+
return true;
|
|
21
|
+
const commaCount = (text.match(/,/g) || []).length;
|
|
22
|
+
return commaCount >= 8;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Summary/recap posts with timestamp patterns.
|
|
26
|
+
* Real-time intel doesn't contain multiple "(HH:MM)" timestamps
|
|
27
|
+
* or "X минут/минуты" duration references.
|
|
28
|
+
*/
|
|
29
|
+
function isSummaryPost(text) {
|
|
30
|
+
// Multiple "(HH:MM)" timestamps in one post = recap/summary
|
|
31
|
+
const timeParenCount = (text.match(/\(\d{1,2}:\d{2}\)/g) || []).length;
|
|
32
|
+
if (timeParenCount >= 2)
|
|
33
|
+
return true;
|
|
34
|
+
// "X минуты" / "X минут" — Russian time duration references (recap formatting)
|
|
35
|
+
if (/\d+\s+минут[ыа]?\b/i.test(text))
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* IDF/Tsahal press releases — long official texts (>400 chars from IDF channels).
|
|
41
|
+
*/
|
|
42
|
+
function isIdfPressRelease(channel, text) {
|
|
43
|
+
if (!IDF_CHANNEL_RE.test(channel))
|
|
44
|
+
return false;
|
|
45
|
+
return text.length > 400;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Combined noise filter. Returns true if post should be filtered OUT.
|
|
49
|
+
*/
|
|
50
|
+
export function isNoise(post) {
|
|
51
|
+
// Pikud HaOref channels with long posts are area lists
|
|
52
|
+
if (OREF_CHANNEL_RE.test(post.channel) && post.text.length > 300)
|
|
53
|
+
return true;
|
|
54
|
+
// Area list spam (any channel)
|
|
55
|
+
if (isAreaListNoise(post.text))
|
|
56
|
+
return true;
|
|
57
|
+
// Summary/recap posts
|
|
58
|
+
if (isSummaryPost(post.text))
|
|
59
|
+
return true;
|
|
60
|
+
// IDF press releases
|
|
61
|
+
if (isIdfPressRelease(post.channel, post.text))
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// ── Channel tracking structure ─────────────────────────
|
|
66
|
+
function toTrackedMessage(post) {
|
|
67
|
+
return {
|
|
68
|
+
timestamp: post.ts,
|
|
69
|
+
text: post.text,
|
|
70
|
+
url: post.messageUrl,
|
|
71
|
+
channel: post.channel,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build ChannelTracking from session posts.
|
|
76
|
+
*
|
|
77
|
+
* Splits posts per channel into prev (already processed) and last (new).
|
|
78
|
+
* Applies deterministic noise filter on all posts.
|
|
79
|
+
* Only includes channels that have new (last) messages.
|
|
80
|
+
*/
|
|
81
|
+
export function buildChannelTracking(posts, sessionStartTs, lastUpdateTs) {
|
|
82
|
+
const channelMap = new Map();
|
|
83
|
+
for (const post of posts) {
|
|
84
|
+
if (isNoise(post))
|
|
85
|
+
continue;
|
|
86
|
+
if (post.ts < sessionStartTs)
|
|
87
|
+
continue;
|
|
88
|
+
if (!channelMap.has(post.channel)) {
|
|
89
|
+
channelMap.set(post.channel, { prev: [], last: [] });
|
|
90
|
+
}
|
|
91
|
+
const bucket = channelMap.get(post.channel);
|
|
92
|
+
const msg = toTrackedMessage(post);
|
|
93
|
+
if (lastUpdateTs > 0 && post.ts <= lastUpdateTs) {
|
|
94
|
+
bucket.prev.push(msg);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
bucket.last.push(msg);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const channels_with_updates = [];
|
|
101
|
+
for (const [channel, { prev, last }] of channelMap) {
|
|
102
|
+
if (last.length > 0) {
|
|
103
|
+
channels_with_updates.push({
|
|
104
|
+
channel,
|
|
105
|
+
prev_tracked_messages: prev.sort((a, b) => a.timestamp - b.timestamp),
|
|
106
|
+
last_tracked_messages: last.sort((a, b) => a.timestamp - b.timestamp),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
track_start_timestamp: sessionStartTs,
|
|
112
|
+
last_update_timestamp: lastUpdateTs,
|
|
113
|
+
channels_with_updates,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// ── Exported for testing ───────────────────────────────
|
|
117
|
+
export const _test = {
|
|
118
|
+
isAreaListNoise,
|
|
119
|
+
isSummaryPost,
|
|
120
|
+
isIdfPressRelease,
|
|
121
|
+
isNoise,
|
|
122
|
+
toTrackedMessage,
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=filters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.js","sourceRoot":"","sources":["../../src/agent/filters.ts"],"names":[],"mappings":"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"}
|