Firemedic15 commited on
Commit
81ff4a8
Β·
verified Β·
1 Parent(s): cc6e7c5

Update brief.py

Browse files
Files changed (1) hide show
  1. brief.py +154 -64
brief.py CHANGED
@@ -1,6 +1,5 @@
1
  """
2
- brief.py β€” Threat brief output schema and post-processing formatter.
3
- The agent synthesizes raw OSINT into this structure.
4
  """
5
 
6
  import json
@@ -13,13 +12,26 @@ from typing import List, Optional
13
  # Data schema
14
  # ---------------------------------------------------------------------------
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  @dataclass
17
  class ThreatBrief:
18
  region: str = ""
19
  country: str = ""
20
  assessment_date: str = ""
21
- severity: str = "" # Critical / High / Medium / Low / Minimal
22
- confidence: str = "" # High / Medium / Low
23
  primary_actors: List[str] = field(default_factory=list)
24
  event_types: List[str] = field(default_factory=list)
25
  key_locations: List[str] = field(default_factory=list)
@@ -29,32 +41,35 @@ class ThreatBrief:
29
  indicators_of_escalation: List[str] = field(default_factory=list)
30
  recommended_watch_items: List[str] = field(default_factory=list)
31
  source_types_used: List[str] = field(default_factory=list)
 
32
 
33
  def to_dict(self) -> dict:
34
- return asdict(self)
 
 
35
 
36
 
37
  # ---------------------------------------------------------------------------
38
- # Severity color mapping for Gradio display
39
  # ---------------------------------------------------------------------------
40
 
41
  SEVERITY_COLORS = {
42
- "Critical": "#FF0000",
43
- "High": "#FF6600",
44
- "Medium": "#FFA500",
45
- "Low": "#28A745",
46
- "Minimal": "#6C757D",
47
  }
48
 
49
  CONFIDENCE_COLORS = {
50
- "High": "#28A745",
51
- "Medium": "#FFA500",
52
- "Low": "#DC3545",
53
  }
54
 
55
 
56
  # ---------------------------------------------------------------------------
57
- # Parser β€” extract structured brief from LLM output
58
  # ---------------------------------------------------------------------------
59
 
60
  BRIEF_PROMPT_SCHEMA = """
@@ -62,41 +77,65 @@ You are an OSINT intelligence analyst. Based on the collected source data above,
62
  produce a threat brief as a JSON object with EXACTLY these fields:
63
 
64
  {
65
- "region": "<geopolitical region, e.g. 'East Africa'>",
66
  "country": "<primary country of concern>",
67
  "assessment_date": "<today's date, YYYY-MM-DD>",
68
  "severity": "<one of: Critical | High | Medium | Low | Minimal>",
69
  "confidence": "<one of: High | Medium | Low>",
70
- "primary_actors": ["<actor1>", "<actor2>", ...],
71
- "event_types": ["<type1>", "<type2>", ...],
72
- "key_locations": ["<location1>", ...],
73
  "fatalities_reported": <integer or null>,
74
- "narrative_summary": "<2-4 sentence analytical narrative>",
75
  "key_findings": ["<finding1>", "<finding2>", "<finding3>"],
76
- "indicators_of_escalation": ["<indicator1>", ...],
77
- "recommended_watch_items": ["<watch_item1>", ...],
78
- "source_types_used": ["ACLED", "RSS"]
 
 
 
 
 
 
 
 
 
 
79
  }
80
 
 
 
 
 
 
81
  Return ONLY the JSON object. No preamble, no markdown fences.
82
  """
83
 
84
 
 
 
 
 
85
  def parse_brief_from_llm(raw_text: str) -> ThreatBrief:
86
- """
87
- Attempts to parse a ThreatBrief from LLM output.
88
- Falls back gracefully if JSON is malformed.
89
- """
90
- # Strip markdown fences if present
91
  cleaned = re.sub(r"```json|```", "", raw_text).strip()
92
-
93
- # Extract first JSON block
94
  match = re.search(r"\{.*\}", cleaned, re.DOTALL)
95
  if not match:
96
  return _fallback_brief(raw_text)
97
 
98
  try:
99
  data = json.loads(match.group())
 
 
 
 
 
 
 
 
 
 
 
 
100
  return ThreatBrief(
101
  region=data.get("region", ""),
102
  country=data.get("country", ""),
@@ -112,6 +151,7 @@ def parse_brief_from_llm(raw_text: str) -> ThreatBrief:
112
  indicators_of_escalation=data.get("indicators_of_escalation", []),
113
  recommended_watch_items=data.get("recommended_watch_items", []),
114
  source_types_used=data.get("source_types_used", []),
 
115
  )
116
  except json.JSONDecodeError:
117
  return _fallback_brief(raw_text)
@@ -127,105 +167,155 @@ def _fallback_brief(raw_text: str) -> ThreatBrief:
127
 
128
 
129
  # ---------------------------------------------------------------------------
130
- # Renderer β€” convert ThreatBrief to Gradio-friendly HTML
131
  # ---------------------------------------------------------------------------
132
 
133
  def render_brief_html(brief: ThreatBrief) -> str:
134
- sev_color = SEVERITY_COLORS.get(brief.severity, "#999")
135
  conf_color = CONFIDENCE_COLORS.get(brief.confidence, "#999")
136
 
137
  def badge(label: str, color: str) -> str:
138
  return (
139
- f'<span style="background:{color};color:white;padding:2px 10px;'
140
  f'border-radius:12px;font-weight:bold;font-size:0.85em">{label}</span>'
141
  )
142
 
143
  def bullet_list(items: list) -> str:
144
  if not items:
145
  return "<li><em>None identified</em></li>"
146
- return "".join(f"<li>{i}</li>" for i in items)
147
 
148
  fatalities_str = (
149
  str(brief.fatalities_reported)
150
  if brief.fatalities_reported is not None
151
  else "Unknown"
152
  )
153
-
154
  sources_str = ", ".join(brief.source_types_used) if brief.source_types_used else "N/A"
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  html = f"""
157
- <div style="font-family:system-ui,sans-serif;max-width:900px;margin:auto">
158
 
 
159
  <div style="background:#1a1a2e;color:white;padding:16px 24px;border-radius:8px 8px 0 0">
160
  <h2 style="margin:0 0 4px 0">πŸ›‘οΈ OSINT Threat Brief</h2>
161
  <p style="margin:0;opacity:0.7;font-size:0.9em">
162
- {brief.country or 'Unknown Region'} &nbsp;|&nbsp;
163
  {brief.region} &nbsp;|&nbsp;
164
  {brief.assessment_date}
165
  </p>
166
  </div>
167
 
168
- <div style="background:#f8f9fa;padding:16px 24px;border:1px solid #dee2e6">
169
- <span style="margin-right:12px">
170
- <strong>Severity:</strong> {badge(brief.severity, sev_color)}
171
- </span>
172
- <span style="margin-right:12px">
173
- <strong>Confidence:</strong> {badge(brief.confidence, conf_color)}
174
- </span>
175
- <span>
176
- <strong>Sources:</strong> {sources_str}
177
- </span>
178
  </div>
179
 
 
180
  <div style="background:white;padding:20px 24px;border:1px solid #dee2e6;border-top:none">
181
  <h3 style="color:#1a1a2e;margin-top:0">Analytical Summary</h3>
182
- <p style="line-height:1.7">{brief.narrative_summary or '<em>Not available</em>'}</p>
183
  </div>
184
 
185
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:0;border:1px solid #dee2e6;border-top:none">
186
-
187
  <div style="padding:16px 20px;border-right:1px solid #dee2e6">
188
  <h4 style="color:#1a1a2e;margin-top:0">πŸ” Key Findings</h4>
189
- <ul style="padding-left:18px;line-height:1.8">
190
  {bullet_list(brief.key_findings)}
191
  </ul>
192
  </div>
193
-
194
  <div style="padding:16px 20px">
195
  <h4 style="color:#1a1a2e;margin-top:0">⚠️ Escalation Indicators</h4>
196
- <ul style="padding-left:18px;line-height:1.8">
197
  {bullet_list(brief.indicators_of_escalation)}
198
  </ul>
199
  </div>
 
200
 
201
- <div style="padding:16px 20px;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6">
 
 
202
  <h4 style="color:#1a1a2e;margin-top:0">🎭 Primary Actors</h4>
203
- <ul style="padding-left:18px;line-height:1.8">
204
  {bullet_list(brief.primary_actors)}
205
  </ul>
206
  </div>
207
-
208
- <div style="padding:16px 20px;border-top:1px solid #dee2e6">
209
  <h4 style="color:#1a1a2e;margin-top:0">πŸ“ Key Locations</h4>
210
- <ul style="padding-left:18px;line-height:1.8">
211
  {bullet_list(brief.key_locations)}
212
  </ul>
213
  </div>
214
-
215
  </div>
216
 
 
 
 
 
217
  <div style="background:#fff8e1;padding:16px 24px;border:1px solid #dee2e6;border-top:none">
218
  <h4 style="color:#1a1a2e;margin-top:0">πŸ“‘ Recommended Watch Items</h4>
219
- <ul style="padding-left:18px;line-height:1.8;margin:0">
220
  {bullet_list(brief.recommended_watch_items)}
221
  </ul>
222
  </div>
223
 
224
- <div style="background:#f8f9fa;padding:12px 24px;border:1px solid #dee2e6;border-top:none;
225
- border-radius:0 0 8px 8px;font-size:0.8em;color:#6c757d">
226
- Reported fatalities: {fatalities_str} &nbsp;|&nbsp;
227
  Event types: {', '.join(brief.event_types) or 'N/A'} &nbsp;|&nbsp;
228
- <em>This brief is AI-generated from open sources. Verify before operational use.</em>
229
  </div>
230
 
231
  </div>
 
1
  """
2
+ brief.py β€” Threat brief output schema and HTML renderer.
 
3
  """
4
 
5
  import json
 
12
  # Data schema
13
  # ---------------------------------------------------------------------------
14
 
15
+ @dataclass
16
+ class NewsItem:
17
+ title: str = ""
18
+ source: str = ""
19
+ published: str = ""
20
+ summary: str = ""
21
+ url: str = ""
22
+ notable: bool = False
23
+
24
+ def to_dict(self) -> dict:
25
+ return asdict(self)
26
+
27
+
28
  @dataclass
29
  class ThreatBrief:
30
  region: str = ""
31
  country: str = ""
32
  assessment_date: str = ""
33
+ severity: str = ""
34
+ confidence: str = ""
35
  primary_actors: List[str] = field(default_factory=list)
36
  event_types: List[str] = field(default_factory=list)
37
  key_locations: List[str] = field(default_factory=list)
 
41
  indicators_of_escalation: List[str] = field(default_factory=list)
42
  recommended_watch_items: List[str] = field(default_factory=list)
43
  source_types_used: List[str] = field(default_factory=list)
44
+ notable_news: List[NewsItem] = field(default_factory=list)
45
 
46
  def to_dict(self) -> dict:
47
+ d = asdict(self)
48
+ d["notable_news"] = [n.to_dict() for n in self.notable_news]
49
+ return d
50
 
51
 
52
  # ---------------------------------------------------------------------------
53
+ # Severity / confidence colors
54
  # ---------------------------------------------------------------------------
55
 
56
  SEVERITY_COLORS = {
57
+ "Critical": "#C0392B",
58
+ "High": "#E67E22",
59
+ "Medium": "#F39C12",
60
+ "Low": "#27AE60",
61
+ "Minimal": "#7F8C8D",
62
  }
63
 
64
  CONFIDENCE_COLORS = {
65
+ "High": "#27AE60",
66
+ "Medium": "#F39C12",
67
+ "Low": "#E74C3C",
68
  }
69
 
70
 
71
  # ---------------------------------------------------------------------------
72
+ # Prompt schema β€” tells the LLM exactly what JSON to return
73
  # ---------------------------------------------------------------------------
74
 
75
  BRIEF_PROMPT_SCHEMA = """
 
77
  produce a threat brief as a JSON object with EXACTLY these fields:
78
 
79
  {
80
+ "region": "<geopolitical region, e.g. 'Latin America'>",
81
  "country": "<primary country of concern>",
82
  "assessment_date": "<today's date, YYYY-MM-DD>",
83
  "severity": "<one of: Critical | High | Medium | Low | Minimal>",
84
  "confidence": "<one of: High | Medium | Low>",
85
+ "primary_actors": ["<actor1>", "<actor2>"],
86
+ "event_types": ["<type1>", "<type2>"],
87
+ "key_locations": ["<location1>", "<location2>"],
88
  "fatalities_reported": <integer or null>,
89
+ "narrative_summary": "<2-4 sentence analytical narrative synthesizing all sources>",
90
  "key_findings": ["<finding1>", "<finding2>", "<finding3>"],
91
+ "indicators_of_escalation": ["<indicator1>", "<indicator2>"],
92
+ "recommended_watch_items": ["<watch_item1>", "<watch_item2>"],
93
+ "source_types_used": ["ACLED", "RSS"],
94
+ "notable_news": [
95
+ {
96
+ "title": "<article headline>",
97
+ "source": "<news source name>",
98
+ "published": "<publication date>",
99
+ "summary": "<1-2 sentence summary of why this article is significant>",
100
+ "url": "<article URL>",
101
+ "notable": true
102
+ }
103
+ ]
104
  }
105
 
106
+ For notable_news: include the 3-6 most significant articles from the RSS results.
107
+ Prioritize articles marked as NOTABLE in the RSS output. Write the summary field
108
+ in your own analytical words β€” explain WHY the article matters to the threat picture,
109
+ not just what it says. If no notable articles were found, use an empty array [].
110
+
111
  Return ONLY the JSON object. No preamble, no markdown fences.
112
  """
113
 
114
 
115
+ # ---------------------------------------------------------------------------
116
+ # Parser
117
+ # ---------------------------------------------------------------------------
118
+
119
  def parse_brief_from_llm(raw_text: str) -> ThreatBrief:
 
 
 
 
 
120
  cleaned = re.sub(r"```json|```", "", raw_text).strip()
 
 
121
  match = re.search(r"\{.*\}", cleaned, re.DOTALL)
122
  if not match:
123
  return _fallback_brief(raw_text)
124
 
125
  try:
126
  data = json.loads(match.group())
127
+
128
+ news_items = []
129
+ for n in data.get("notable_news", []):
130
+ news_items.append(NewsItem(
131
+ title=n.get("title", ""),
132
+ source=n.get("source", ""),
133
+ published=n.get("published", ""),
134
+ summary=n.get("summary", ""),
135
+ url=n.get("url", ""),
136
+ notable=n.get("notable", True),
137
+ ))
138
+
139
  return ThreatBrief(
140
  region=data.get("region", ""),
141
  country=data.get("country", ""),
 
151
  indicators_of_escalation=data.get("indicators_of_escalation", []),
152
  recommended_watch_items=data.get("recommended_watch_items", []),
153
  source_types_used=data.get("source_types_used", []),
154
+ notable_news=news_items,
155
  )
156
  except json.JSONDecodeError:
157
  return _fallback_brief(raw_text)
 
167
 
168
 
169
  # ---------------------------------------------------------------------------
170
+ # HTML Renderer
171
  # ---------------------------------------------------------------------------
172
 
173
  def render_brief_html(brief: ThreatBrief) -> str:
174
+ sev_color = SEVERITY_COLORS.get(brief.severity, "#999")
175
  conf_color = CONFIDENCE_COLORS.get(brief.confidence, "#999")
176
 
177
  def badge(label: str, color: str) -> str:
178
  return (
179
+ f'<span style="background:{color};color:white;padding:3px 12px;'
180
  f'border-radius:12px;font-weight:bold;font-size:0.85em">{label}</span>'
181
  )
182
 
183
  def bullet_list(items: list) -> str:
184
  if not items:
185
  return "<li><em>None identified</em></li>"
186
+ return "".join(f"<li>{i}</li>" for i in items if i and i != "N/A")
187
 
188
  fatalities_str = (
189
  str(brief.fatalities_reported)
190
  if brief.fatalities_reported is not None
191
  else "Unknown"
192
  )
 
193
  sources_str = ", ".join(brief.source_types_used) if brief.source_types_used else "N/A"
194
 
195
+ # Build notable news section
196
+ news_html = ""
197
+ if brief.notable_news:
198
+ cards = []
199
+ for item in brief.notable_news:
200
+ source_badge = (
201
+ f'<span style="background:#EBF5FB;color:#2E86C1;padding:2px 8px;'
202
+ f'border-radius:8px;font-size:0.78em;font-weight:600">{item.source}</span>'
203
+ )
204
+ date_str = (
205
+ f'<span style="color:#888;font-size:0.8em;margin-left:8px">{item.published[:25] if item.published else ""}</span>'
206
+ )
207
+ link_html = (
208
+ f'<a href="{item.url}" target="_blank" style="color:#2E86C1;font-size:0.82em;'
209
+ f'text-decoration:none">Read full article β†’</a>'
210
+ if item.url else ""
211
+ )
212
+ cards.append(f"""
213
+ <div style="border:1px solid #D5E8F5;border-radius:6px;padding:12px 16px;
214
+ margin-bottom:10px;background:#FAFCFF">
215
+ <div style="margin-bottom:6px">{source_badge}{date_str}</div>
216
+ <div style="font-weight:600;color:#1a1a2e;margin-bottom:5px;line-height:1.4">
217
+ {item.title}
218
+ </div>
219
+ <div style="color:#444;font-size:0.9em;line-height:1.6;margin-bottom:6px">
220
+ {item.summary}
221
+ </div>
222
+ {link_html}
223
+ </div>
224
+ """)
225
+ news_html = f"""
226
+ <div style="background:white;padding:20px 24px;border:1px solid #dee2e6;border-top:none">
227
+ <h3 style="color:#1a1a2e;margin-top:0;margin-bottom:14px">
228
+ πŸ“° Notable News
229
+ <span style="font-size:0.7em;font-weight:normal;color:#666;margin-left:8px">
230
+ {len(brief.notable_news)} articles
231
+ </span>
232
+ </h3>
233
+ {"".join(cards)}
234
+ </div>
235
+ """
236
+ else:
237
+ news_html = """
238
+ <div style="background:#f8f9fa;padding:14px 24px;border:1px solid #dee2e6;border-top:none">
239
+ <p style="margin:0;color:#888;font-style:italic">No notable news articles retrieved.</p>
240
+ </div>
241
+ """
242
+
243
  html = f"""
244
+ <div style="font-family:system-ui,sans-serif;max-width:920px;margin:auto">
245
 
246
+ <!-- Header -->
247
  <div style="background:#1a1a2e;color:white;padding:16px 24px;border-radius:8px 8px 0 0">
248
  <h2 style="margin:0 0 4px 0">πŸ›‘οΈ OSINT Threat Brief</h2>
249
  <p style="margin:0;opacity:0.7;font-size:0.9em">
250
+ {brief.country or 'Unknown'} &nbsp;|&nbsp;
251
  {brief.region} &nbsp;|&nbsp;
252
  {brief.assessment_date}
253
  </p>
254
  </div>
255
 
256
+ <!-- Status bar -->
257
+ <div style="background:#f0f3f8;padding:14px 24px;border:1px solid #dee2e6;
258
+ display:flex;align-items:center;gap:16px;flex-wrap:wrap">
259
+ <span><strong>Severity:</strong> &nbsp;{badge(brief.severity, sev_color)}</span>
260
+ <span><strong>Confidence:</strong> &nbsp;{badge(brief.confidence, conf_color)}</span>
261
+ <span style="color:#666;font-size:0.9em"><strong>Sources:</strong> {sources_str}</span>
262
+ <span style="color:#666;font-size:0.9em"><strong>Fatalities:</strong> {fatalities_str}</span>
 
 
 
263
  </div>
264
 
265
+ <!-- Narrative -->
266
  <div style="background:white;padding:20px 24px;border:1px solid #dee2e6;border-top:none">
267
  <h3 style="color:#1a1a2e;margin-top:0">Analytical Summary</h3>
268
+ <p style="line-height:1.8;color:#333">{brief.narrative_summary or '<em>Not available</em>'}</p>
269
  </div>
270
 
271
+ <!-- Key findings + escalation -->
272
+ <div style="display:grid;grid-template-columns:1fr 1fr;border:1px solid #dee2e6;border-top:none">
273
  <div style="padding:16px 20px;border-right:1px solid #dee2e6">
274
  <h4 style="color:#1a1a2e;margin-top:0">πŸ” Key Findings</h4>
275
+ <ul style="padding-left:18px;line-height:1.9;margin:0">
276
  {bullet_list(brief.key_findings)}
277
  </ul>
278
  </div>
 
279
  <div style="padding:16px 20px">
280
  <h4 style="color:#1a1a2e;margin-top:0">⚠️ Escalation Indicators</h4>
281
+ <ul style="padding-left:18px;line-height:1.9;margin:0">
282
  {bullet_list(brief.indicators_of_escalation)}
283
  </ul>
284
  </div>
285
+ </div>
286
 
287
+ <!-- Actors + locations -->
288
+ <div style="display:grid;grid-template-columns:1fr 1fr;border:1px solid #dee2e6;border-top:none">
289
+ <div style="padding:16px 20px;border-right:1px solid #dee2e6">
290
  <h4 style="color:#1a1a2e;margin-top:0">🎭 Primary Actors</h4>
291
+ <ul style="padding-left:18px;line-height:1.9;margin:0">
292
  {bullet_list(brief.primary_actors)}
293
  </ul>
294
  </div>
295
+ <div style="padding:16px 20px">
 
296
  <h4 style="color:#1a1a2e;margin-top:0">πŸ“ Key Locations</h4>
297
+ <ul style="padding-left:18px;line-height:1.9;margin:0">
298
  {bullet_list(brief.key_locations)}
299
  </ul>
300
  </div>
 
301
  </div>
302
 
303
+ <!-- Notable news -->
304
+ {news_html}
305
+
306
+ <!-- Watch items -->
307
  <div style="background:#fff8e1;padding:16px 24px;border:1px solid #dee2e6;border-top:none">
308
  <h4 style="color:#1a1a2e;margin-top:0">πŸ“‘ Recommended Watch Items</h4>
309
+ <ul style="padding-left:18px;line-height:1.9;margin:0">
310
  {bullet_list(brief.recommended_watch_items)}
311
  </ul>
312
  </div>
313
 
314
+ <!-- Footer -->
315
+ <div style="background:#f8f9fa;padding:10px 24px;border:1px solid #dee2e6;border-top:none;
316
+ border-radius:0 0 8px 8px;font-size:0.78em;color:#6c757d">
317
  Event types: {', '.join(brief.event_types) or 'N/A'} &nbsp;|&nbsp;
318
+ <em>AI-generated from open sources. Verify before operational use.</em>
319
  </div>
320
 
321
  </div>