copilot-swe-agent[bot] IngSeb0 commited on
Commit
d0f3cbe
·
unverified ·
1 Parent(s): 29b4967

feat: build complete ReliefLensAI backend

Browse files

Full multimodal disaster triage backend including:
- FastAPI app with CORS, lifespan, health check
- Pydantic v2 schemas: report, signal, incident, resource, dispatch, amd, crisis_room
- 11 skills: transcribe_audio, caption_image, extract_location, normalize_signal,
detect_duplicates, classify_priority, recommend_resources, generate_dispatch_message,
calculate_confidence, fetch_amd_metrics, export_incident_report
- 8 agents: intake, transcription, vision, normalization, dedup, triage, resource, dispatch
- Services: vLLM client (demo+real mode), storage (file-backed JSON cache), pipeline
- API routes: /crisis-room, /reports, /incidents, /amd, /demo
- Demo data: 29-report Santa Ana flood scenario (text/audio/image/csv)
- Tests: 30 passing across schemas, skills, and API
- Docker, .env.example, setup/run scripts
- Demo mode enabled by default (no external dependencies required)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: IngSeb0 <141876214+IngSeb0@users.noreply.github.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backend/.env.example +13 -0
  2. backend/Dockerfile +18 -0
  3. backend/__pycache__/main.cpython-312.pyc +0 -0
  4. backend/agents/__init__.py +19 -0
  5. backend/agents/__pycache__/__init__.cpython-312.pyc +0 -0
  6. backend/agents/__pycache__/dedup_agent.cpython-312.pyc +0 -0
  7. backend/agents/__pycache__/dispatch_agent.cpython-312.pyc +0 -0
  8. backend/agents/__pycache__/intake_agent.cpython-312.pyc +0 -0
  9. backend/agents/__pycache__/normalization_agent.cpython-312.pyc +0 -0
  10. backend/agents/__pycache__/resource_agent.cpython-312.pyc +0 -0
  11. backend/agents/__pycache__/transcription_agent.cpython-312.pyc +0 -0
  12. backend/agents/__pycache__/triage_agent.cpython-312.pyc +0 -0
  13. backend/agents/__pycache__/vision_agent.cpython-312.pyc +0 -0
  14. backend/agents/dedup_agent.py +19 -0
  15. backend/agents/dispatch_agent.py +39 -0
  16. backend/agents/intake_agent.py +39 -0
  17. backend/agents/normalization_agent.py +45 -0
  18. backend/agents/resource_agent.py +23 -0
  19. backend/agents/transcription_agent.py +29 -0
  20. backend/agents/triage_agent.py +106 -0
  21. backend/agents/vision_agent.py +28 -0
  22. backend/api/__init__.py +0 -0
  23. backend/api/__pycache__/__init__.cpython-312.pyc +0 -0
  24. backend/api/routes/__init__.py +0 -0
  25. backend/api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
  26. backend/api/routes/__pycache__/amd.cpython-312.pyc +0 -0
  27. backend/api/routes/__pycache__/crisis_room.cpython-312.pyc +0 -0
  28. backend/api/routes/__pycache__/demo.cpython-312.pyc +0 -0
  29. backend/api/routes/__pycache__/incidents.cpython-312.pyc +0 -0
  30. backend/api/routes/__pycache__/reports.cpython-312.pyc +0 -0
  31. backend/api/routes/amd.py +15 -0
  32. backend/api/routes/crisis_room.py +42 -0
  33. backend/api/routes/demo.py +70 -0
  34. backend/api/routes/incidents.py +90 -0
  35. backend/api/routes/reports.py +93 -0
  36. backend/core/__init__.py +0 -0
  37. backend/core/__pycache__/__init__.cpython-312.pyc +0 -0
  38. backend/core/__pycache__/config.cpython-312.pyc +0 -0
  39. backend/core/config.py +20 -0
  40. backend/data/dispatch/03a16c03-94e5-4e6f-ba8a-237658e28a5b.json +1 -0
  41. backend/data/dispatch/12791726-6e83-4d2f-aea9-9ba2733edbe9.json +1 -0
  42. backend/data/dispatch/143f2fa6-e703-4a7e-84a4-92a5ee09d550.json +1 -0
  43. backend/data/dispatch/2048c8c5-ca83-467f-959c-a6838a3897b2.json +1 -0
  44. backend/data/dispatch/22eb6be8-481a-4e35-a3f1-007b71fe871d.json +1 -0
  45. backend/data/dispatch/36562d51-406e-4ff6-8fb8-4a97135aa904.json +1 -0
  46. backend/data/dispatch/36d98de7-9007-4c70-a694-e066d515ba9c.json +1 -0
  47. backend/data/dispatch/372c2577-8f0a-42f5-ad2b-ae205ec79e1d.json +1 -0
  48. backend/data/dispatch/3ec4f10a-3f9d-427d-aca8-3134efa35105.json +1 -0
  49. backend/data/dispatch/3f2d7e95-ee1c-443e-8825-cc32c7bea9b4.json +1 -0
  50. backend/data/dispatch/46f1763e-8b79-4726-9f58-42f572b1634a.json +1 -0
backend/.env.example ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AMD / vLLM Settings
2
+ VLLM_BASE_URL=http://localhost:8000/v1
3
+ VLLM_API_KEY=not-needed
4
+ VLLM_MODEL=Qwen/Qwen2.5-72B-Instruct
5
+ VLLM_VISION_MODEL=Qwen/Qwen2-VL-7B-Instruct
6
+
7
+ # App Settings
8
+ APP_ENV=development
9
+ DEBUG=true
10
+ DEMO_MODE=true
11
+
12
+ # Storage
13
+ STORAGE_PATH=./data
backend/Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ RUN mkdir -p data
15
+
16
+ EXPOSE 8080
17
+
18
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
backend/__pycache__/main.cpython-312.pyc ADDED
Binary file (3.05 kB). View file
 
backend/agents/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .intake_agent import IntakeAgent
2
+ from .transcription_agent import TranscriptionAgent
3
+ from .vision_agent import VisionAgent
4
+ from .normalization_agent import NormalizationAgent
5
+ from .dedup_agent import DedupAgent
6
+ from .triage_agent import TriageAgent
7
+ from .resource_agent import ResourceAgent
8
+ from .dispatch_agent import DispatchAgent
9
+
10
+ __all__ = [
11
+ "IntakeAgent",
12
+ "TranscriptionAgent",
13
+ "VisionAgent",
14
+ "NormalizationAgent",
15
+ "DedupAgent",
16
+ "TriageAgent",
17
+ "ResourceAgent",
18
+ "DispatchAgent",
19
+ ]
backend/agents/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (661 Bytes). View file
 
backend/agents/__pycache__/dedup_agent.cpython-312.pyc ADDED
Binary file (1.41 kB). View file
 
backend/agents/__pycache__/dispatch_agent.cpython-312.pyc ADDED
Binary file (2.16 kB). View file
 
backend/agents/__pycache__/intake_agent.cpython-312.pyc ADDED
Binary file (1.97 kB). View file
 
backend/agents/__pycache__/normalization_agent.cpython-312.pyc ADDED
Binary file (2.55 kB). View file
 
backend/agents/__pycache__/resource_agent.cpython-312.pyc ADDED
Binary file (1.68 kB). View file
 
backend/agents/__pycache__/transcription_agent.cpython-312.pyc ADDED
Binary file (1.79 kB). View file
 
backend/agents/__pycache__/triage_agent.cpython-312.pyc ADDED
Binary file (6.56 kB). View file
 
backend/agents/__pycache__/vision_agent.cpython-312.pyc ADDED
Binary file (1.85 kB). View file
 
backend/agents/dedup_agent.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import List
4
+
5
+ from schemas.signal import NormalizedSignal
6
+ from skills.detect_duplicates import detect_duplicates
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class DedupAgent:
12
+ def __init__(self, threshold: float = 0.75) -> None:
13
+ self.threshold = threshold
14
+
15
+ async def run(self, signals: List[NormalizedSignal]) -> List[NormalizedSignal]:
16
+ logger.info("DedupAgent: processing %d signals", len(signals))
17
+ unique = await detect_duplicates(signals, threshold=self.threshold)
18
+ logger.info("DedupAgent: %d unique signals after dedup", len(unique))
19
+ return unique
backend/agents/dispatch_agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import Any, List
4
+
5
+ from schemas.dispatch import DispatchMessage
6
+ from schemas.incident import Incident
7
+ from schemas.resource import ResourceRecommendation
8
+ from skills.generate_dispatch_message import generate_dispatch_message
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DispatchAgent:
14
+ def __init__(self, vllm_client: Any = None) -> None:
15
+ self.vllm_client = vllm_client
16
+
17
+ async def run(
18
+ self,
19
+ incidents: List[Incident],
20
+ all_resources: List[ResourceRecommendation],
21
+ ) -> List[DispatchMessage]:
22
+ logger.info("DispatchAgent: generating messages for %d incidents", len(incidents))
23
+ messages: List[DispatchMessage] = []
24
+ resource_by_incident = {}
25
+ for r in all_resources:
26
+ resource_by_incident.setdefault(r.incident_id, []).append(r)
27
+
28
+ for incident in incidents:
29
+ incident_resources = resource_by_incident.get(incident.id, [])
30
+ msg = await generate_dispatch_message(
31
+ incident,
32
+ incident_resources,
33
+ channel="radio",
34
+ vllm_client=self.vllm_client,
35
+ )
36
+ messages.append(msg)
37
+
38
+ logger.info("DispatchAgent: generated %d dispatch messages", len(messages))
39
+ return messages
backend/agents/intake_agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ from schemas.report import ReportInput, ReportType
6
+ from schemas.signal import NormalizedSignal, SignalType
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class IntakeAgent:
12
+ async def run(self, report: ReportInput) -> NormalizedSignal:
13
+ logger.debug("IntakeAgent processing report %s (type=%s)", report.id, report.report_type)
14
+
15
+ content = report.content or ""
16
+ modality = report.report_type.value
17
+
18
+ if report.report_type == ReportType.LOCATION:
19
+ signal_type = SignalType.FLOOD
20
+ description = f"Location data received: {content[:200]}"
21
+ confidence = 0.70
22
+ elif report.report_type == ReportType.CSV:
23
+ signal_type = SignalType.FLOOD
24
+ description = f"CSV batch data: {content[:200]}"
25
+ confidence = 0.70
26
+ else:
27
+ signal_type = SignalType.UNKNOWN
28
+ description = content[:200] if content else f"Report from {modality}"
29
+ confidence = 0.60
30
+
31
+ return NormalizedSignal(
32
+ source_report_id=report.id,
33
+ signal_type=signal_type,
34
+ description=description,
35
+ raw_text=content,
36
+ confidence=confidence,
37
+ modality=modality,
38
+ created_at=datetime.utcnow(),
39
+ )
backend/agents/normalization_agent.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ from schemas.signal import NormalizedSignal, SignalType
6
+ from skills.normalize_signal import normalize_signal
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class NormalizationAgent:
12
+ def __init__(self, vllm_client=None) -> None:
13
+ self.vllm_client = vllm_client
14
+
15
+ async def run(self, raw_text: str, report_id: str, modality: str) -> NormalizedSignal:
16
+ logger.debug("NormalizationAgent processing text from report %s", report_id)
17
+ data = await normalize_signal(raw_text, modality, report_id, vllm_client=self.vllm_client)
18
+
19
+ signal_type_str = data.get("signal_type", "unknown")
20
+ try:
21
+ signal_type = SignalType(signal_type_str)
22
+ except ValueError:
23
+ signal_type = SignalType.UNKNOWN
24
+
25
+ created_raw = data.get("created_at", datetime.utcnow().isoformat())
26
+ if isinstance(created_raw, str):
27
+ try:
28
+ created_at = datetime.fromisoformat(created_raw)
29
+ except ValueError:
30
+ created_at = datetime.utcnow()
31
+ else:
32
+ created_at = created_raw
33
+
34
+ return NormalizedSignal(
35
+ source_report_id=report_id,
36
+ signal_type=signal_type,
37
+ description=data.get("description", raw_text[:200]),
38
+ location=data.get("location"),
39
+ coordinates=data.get("coordinates"),
40
+ affected_people=data.get("affected_people"),
41
+ raw_text=raw_text,
42
+ confidence=data.get("confidence", 0.7),
43
+ modality=modality,
44
+ created_at=created_at,
45
+ )
backend/agents/resource_agent.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import Any, List
4
+
5
+ from schemas.incident import Incident
6
+ from schemas.resource import ResourceRecommendation
7
+ from skills.recommend_resources import recommend_resources
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ResourceAgent:
13
+ def __init__(self, vllm_client: Any = None) -> None:
14
+ self.vllm_client = vllm_client
15
+
16
+ async def run(self, incidents: List[Incident]) -> List[ResourceRecommendation]:
17
+ logger.info("ResourceAgent: generating recommendations for %d incidents", len(incidents))
18
+ all_recommendations: List[ResourceRecommendation] = []
19
+ for incident in incidents:
20
+ recs = await recommend_resources(incident, self.vllm_client)
21
+ all_recommendations.extend(recs)
22
+ logger.info("ResourceAgent: generated %d total recommendations", len(all_recommendations))
23
+ return all_recommendations
backend/agents/transcription_agent.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ from core.config import get_settings
5
+ from schemas.report import ReportInput
6
+ from skills.transcribe_audio import transcribe_audio
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class TranscriptionAgent:
12
+ def __init__(self) -> None:
13
+ self.settings = get_settings()
14
+
15
+ async def run(self, report: ReportInput) -> str:
16
+ logger.debug("TranscriptionAgent processing report %s", report.id)
17
+ file_path = report.file_path or ""
18
+ if not file_path and report.content:
19
+ file_path = report.content
20
+
21
+ result = await transcribe_audio(file_path, demo_mode=self.settings.demo_mode)
22
+ transcription: str = result.get("transcription", "")
23
+ logger.info(
24
+ "Transcribed audio report %s: %.50s... (confidence=%.2f)",
25
+ report.id,
26
+ transcription,
27
+ result.get("confidence", 0),
28
+ )
29
+ return transcription
backend/agents/triage_agent.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ import uuid
4
+ from datetime import datetime
5
+ from typing import Any, List, Optional
6
+
7
+ from schemas.incident import EvidenceItem, Incident, IncidentStatus, Priority
8
+ from schemas.signal import NormalizedSignal, SignalType
9
+ from skills.calculate_confidence import calculate_confidence
10
+ from skills.classify_priority import classify_priority
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _PRIORITY_ORDER = {Priority.P0: 0, Priority.P1: 1, Priority.P2: 2, Priority.P3: 3}
15
+
16
+
17
+ class TriageAgent:
18
+ def __init__(self, session_id: str, vllm_client: Any = None) -> None:
19
+ self.session_id = session_id
20
+ self.vllm_client = vllm_client
21
+
22
+ async def run(self, signals: List[NormalizedSignal]) -> List[Incident]:
23
+ logger.info("TriageAgent: creating incidents from %d signals", len(signals))
24
+ incidents: List[Incident] = []
25
+
26
+ groups: dict = {}
27
+ for signal in signals:
28
+ key = (signal.signal_type, signal.location or "unknown")
29
+ groups.setdefault(key, []).append(signal)
30
+
31
+ for (signal_type, location), group_signals in groups.items():
32
+ priority = await classify_priority(group_signals[0], self.vllm_client)
33
+
34
+ total_affected: Optional[int] = None
35
+ for s in group_signals:
36
+ if s.affected_people:
37
+ total_affected = (total_affected or 0) + s.affected_people
38
+
39
+ confidence = await calculate_confidence(group_signals)
40
+ title = self._make_title(signal_type, location)
41
+ description = self._make_description(signal_type, location, group_signals, total_affected)
42
+
43
+ coords = next((s.coordinates for s in group_signals if s.coordinates), None)
44
+ evidence = [
45
+ EvidenceItem(
46
+ report_id=s.source_report_id,
47
+ modality=s.modality,
48
+ description=s.description[:150],
49
+ )
50
+ for s in group_signals
51
+ ]
52
+
53
+ incident = Incident(
54
+ id=str(uuid.uuid4()),
55
+ session_id=self.session_id,
56
+ title=title,
57
+ description=description,
58
+ priority=priority,
59
+ status=IncidentStatus.NEW,
60
+ signal_ids=[s.id for s in group_signals],
61
+ evidence=evidence,
62
+ location=location if location != "unknown" else None,
63
+ coordinates=coords,
64
+ affected_people=total_affected,
65
+ confidence=confidence,
66
+ created_at=datetime.utcnow(),
67
+ updated_at=datetime.utcnow(),
68
+ )
69
+ incidents.append(incident)
70
+
71
+ incidents.sort(key=lambda x: _PRIORITY_ORDER.get(x.priority, 99))
72
+ logger.info("TriageAgent: created %d incidents", len(incidents))
73
+ return incidents
74
+
75
+ def _make_title(self, signal_type: SignalType, location: str) -> str:
76
+ type_labels = {
77
+ SignalType.PERSON_TRAPPED: "Persona(s) Atrapada(s)",
78
+ SignalType.MEDICAL_EMERGENCY: "Emergencia Médica",
79
+ SignalType.STRUCTURAL_DAMAGE: "Daño Estructural",
80
+ SignalType.FLOOD: "Inundación",
81
+ SignalType.FIRE: "Incendio",
82
+ SignalType.MISSING_PERSON: "Persona Desaparecida",
83
+ SignalType.RESOURCE_REQUEST: "Solicitud de Recursos",
84
+ SignalType.SAFE_STATUS: "Reporte de Seguridad",
85
+ SignalType.UNKNOWN: "Incidente Desconocido",
86
+ }
87
+ label = type_labels.get(signal_type, "Incidente")
88
+ loc = location.title() if location and location != "unknown" else "Barrio Santa Ana"
89
+ return f"{label} — {loc}"
90
+
91
+ def _make_description(
92
+ self,
93
+ signal_type: SignalType,
94
+ location: str,
95
+ signals: List[NormalizedSignal],
96
+ affected: Optional[int],
97
+ ) -> str:
98
+ parts = [f"Tipo: {signal_type.value.replace('_', ' ').title()}."]
99
+ if location and location != "unknown":
100
+ parts.append(f"Ubicación: {location}.")
101
+ if affected:
102
+ parts.append(f"Personas afectadas estimadas: {affected}.")
103
+ parts.append(f"Basado en {len(signals)} señal(es) recibida(s).")
104
+ if signals:
105
+ parts.append(f"Reporte más reciente: {signals[-1].description[:100]}.")
106
+ return " ".join(parts)
backend/agents/vision_agent.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ from core.config import get_settings
5
+ from schemas.report import ReportInput
6
+ from skills.caption_image import caption_image
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class VisionAgent:
12
+ def __init__(self) -> None:
13
+ self.settings = get_settings()
14
+
15
+ async def run(self, report: ReportInput) -> str:
16
+ logger.debug("VisionAgent processing report %s", report.id)
17
+ image_path = report.file_path or ""
18
+ if not image_path and report.content:
19
+ image_path = report.content
20
+
21
+ result = await caption_image(image_path, demo_mode=self.settings.demo_mode)
22
+ caption: str = result.get("caption", "")
23
+ hazards = result.get("hazards", [])
24
+ if hazards:
25
+ caption += f" Hazards identified: {', '.join(hazards)}."
26
+
27
+ logger.info("Captioned image report %s: %.80s...", report.id, caption)
28
+ return caption
backend/api/__init__.py ADDED
File without changes
backend/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (164 Bytes). View file
 
backend/api/routes/__init__.py ADDED
File without changes
backend/api/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (171 Bytes). View file
 
backend/api/routes/__pycache__/amd.cpython-312.pyc ADDED
Binary file (956 Bytes). View file
 
backend/api/routes/__pycache__/crisis_room.cpython-312.pyc ADDED
Binary file (2.46 kB). View file
 
backend/api/routes/__pycache__/demo.cpython-312.pyc ADDED
Binary file (3.56 kB). View file
 
backend/api/routes/__pycache__/incidents.cpython-312.pyc ADDED
Binary file (5.66 kB). View file
 
backend/api/routes/__pycache__/reports.cpython-312.pyc ADDED
Binary file (5.01 kB). View file
 
backend/api/routes/amd.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from core.config import get_settings
6
+ from schemas.amd import AMDPerformanceMetric
7
+ from skills.fetch_amd_metrics import fetch_amd_metrics
8
+
9
+ router = APIRouter(prefix="/amd", tags=["amd"])
10
+
11
+
12
+ @router.get("/performance", response_model=AMDPerformanceMetric)
13
+ async def get_amd_performance() -> AMDPerformanceMetric:
14
+ settings = get_settings()
15
+ return await fetch_amd_metrics(settings.vllm_base_url, demo_mode=settings.demo_mode)
backend/api/routes/crisis_room.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ import uuid
4
+ from typing import Optional
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from schemas.crisis_room import CrisisRoomSummary
10
+ from schemas.report import UploadBatch
11
+ from services.pipeline import Pipeline
12
+ from services.storage import get_storage
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter(prefix="/crisis-room", tags=["crisis-room"])
16
+
17
+
18
+ class CrisisRoomRequest(BaseModel):
19
+ batch: UploadBatch
20
+ session_id: Optional[str] = None
21
+
22
+
23
+ @router.post("", response_model=CrisisRoomSummary)
24
+ async def create_crisis_room(request: CrisisRoomRequest) -> CrisisRoomSummary:
25
+ if request.session_id:
26
+ request.batch.session_id = request.session_id
27
+ pipeline = Pipeline()
28
+ try:
29
+ summary = await pipeline.process_batch(request.batch)
30
+ except Exception as exc:
31
+ logger.exception("Pipeline error: %s", exc)
32
+ raise HTTPException(status_code=500, detail=f"Pipeline error: {exc}")
33
+ return summary
34
+
35
+
36
+ @router.get("/{session_id}", response_model=dict)
37
+ async def get_crisis_room(session_id: str) -> dict:
38
+ storage = get_storage()
39
+ session = await storage.get_session(session_id)
40
+ if session is None:
41
+ raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
42
+ return session
backend/api/routes/demo.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import json
3
+ import logging
4
+ import uuid
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ from schemas.report import ReportInput, ReportType, UploadBatch
12
+ from services.pipeline import Pipeline
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter(prefix="/demo", tags=["demo"])
16
+
17
+ _SCENARIO_PATH = Path(__file__).parent.parent.parent.parent / "demo_data" / "scenario_flood_santa_ana.json"
18
+
19
+
20
+ def _load_scenario() -> Dict[str, Any]:
21
+ if _SCENARIO_PATH.exists():
22
+ return json.loads(_SCENARIO_PATH.read_text(encoding="utf-8"))
23
+ return {
24
+ "scenario_name": "Inundación Barrio Santa Ana",
25
+ "description": "Demo scenario — file not found",
26
+ "reports": [],
27
+ }
28
+
29
+
30
+ @router.get("/scenario")
31
+ async def get_demo_scenario() -> Dict[str, Any]:
32
+ return _load_scenario()
33
+
34
+
35
+ @router.post("/run")
36
+ async def run_demo() -> Dict[str, Any]:
37
+ scenario = _load_scenario()
38
+ session_id = str(uuid.uuid4())
39
+ reports: List[ReportInput] = []
40
+
41
+ for raw in scenario.get("reports", []):
42
+ rtype_str = raw.get("report_type", "text")
43
+ try:
44
+ rtype = ReportType(rtype_str)
45
+ except ValueError:
46
+ rtype = ReportType.TEXT
47
+
48
+ reports.append(ReportInput(
49
+ id=raw.get("id", str(uuid.uuid4())),
50
+ session_id=session_id,
51
+ report_type=rtype,
52
+ content=raw.get("content"),
53
+ metadata=raw.get("metadata", {}),
54
+ created_at=datetime.utcnow(),
55
+ ))
56
+
57
+ batch = UploadBatch(
58
+ session_id=session_id,
59
+ reports=reports,
60
+ scenario_name=scenario.get("scenario_name", "Demo"),
61
+ )
62
+
63
+ pipeline = Pipeline()
64
+ try:
65
+ summary = await pipeline.process_batch(batch)
66
+ except Exception as exc:
67
+ logger.exception("Demo pipeline error: %s", exc)
68
+ raise HTTPException(status_code=500, detail=str(exc))
69
+
70
+ return summary.model_dump(mode="json")
backend/api/routes/incidents.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ from datetime import datetime
4
+ from typing import List, Optional
5
+
6
+ from fastapi import APIRouter, HTTPException, Query
7
+ from pydantic import BaseModel
8
+
9
+ from schemas.dispatch import DispatchMessage
10
+ from schemas.incident import Incident, IncidentStatus, Priority
11
+ from services.storage import get_storage
12
+ from skills.generate_dispatch_message import generate_dispatch_message
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter(prefix="/incidents", tags=["incidents"])
16
+
17
+
18
+ class IncidentUpdate(BaseModel):
19
+ status: Optional[IncidentStatus] = None
20
+ human_approved: Optional[bool] = None
21
+ notes: Optional[str] = None
22
+ priority: Optional[Priority] = None
23
+
24
+
25
+ @router.get("", response_model=List[dict])
26
+ async def list_incidents(
27
+ session_id: Optional[str] = Query(default=None),
28
+ priority: Optional[Priority] = Query(default=None),
29
+ status: Optional[IncidentStatus] = Query(default=None),
30
+ ) -> List[dict]:
31
+ storage = get_storage()
32
+ incidents = await storage.list_incidents()
33
+
34
+ if session_id:
35
+ incidents = [i for i in incidents if i.get("session_id") == session_id]
36
+ if priority:
37
+ incidents = [i for i in incidents if i.get("priority") == priority.value]
38
+ if status:
39
+ incidents = [i for i in incidents if i.get("status") == status.value]
40
+
41
+ return incidents
42
+
43
+
44
+ @router.get("/{incident_id}", response_model=dict)
45
+ async def get_incident(incident_id: str) -> dict:
46
+ storage = get_storage()
47
+ incident = await storage.get_incident(incident_id)
48
+ if incident is None:
49
+ raise HTTPException(status_code=404, detail=f"Incident {incident_id} not found")
50
+ return incident
51
+
52
+
53
+ @router.patch("/{incident_id}", response_model=dict)
54
+ async def update_incident(incident_id: str, update: IncidentUpdate) -> dict:
55
+ storage = get_storage()
56
+ updates = {k: v for k, v in update.model_dump().items() if v is not None}
57
+ updates["updated_at"] = datetime.utcnow().isoformat()
58
+ if "priority" in updates and isinstance(updates["priority"], Priority):
59
+ updates["priority"] = updates["priority"].value
60
+ if "status" in updates and isinstance(updates["status"], IncidentStatus):
61
+ updates["status"] = updates["status"].value
62
+
63
+ updated = await storage.update_incident(incident_id, updates)
64
+ if updated is None:
65
+ raise HTTPException(status_code=404, detail=f"Incident {incident_id} not found")
66
+ return updated
67
+
68
+
69
+ @router.post("/{incident_id}/dispatch-message", response_model=dict)
70
+ async def create_dispatch_message(incident_id: str, channel: str = "radio") -> dict:
71
+ storage = get_storage()
72
+ incident_data = await storage.get_incident(incident_id)
73
+ if incident_data is None:
74
+ raise HTTPException(status_code=404, detail=f"Incident {incident_id} not found")
75
+
76
+ incident = Incident(**incident_data)
77
+ all_resources_raw = await storage.list_resources()
78
+ resources = [r for r in all_resources_raw if r.get("incident_id") == incident_id]
79
+
80
+ from schemas.resource import ResourceRecommendation
81
+ resource_objs = []
82
+ for r in resources:
83
+ try:
84
+ resource_objs.append(ResourceRecommendation(**r))
85
+ except Exception:
86
+ pass
87
+
88
+ msg = await generate_dispatch_message(incident, resource_objs, channel=channel)
89
+ await storage.save_dispatch(msg.id, msg.model_dump(mode="json"))
90
+ return msg.model_dump(mode="json")
backend/api/routes/reports.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import logging
3
+ import uuid
4
+ from datetime import datetime
5
+ from typing import List, Optional
6
+
7
+ from fastapi import APIRouter, File, Form, HTTPException, UploadFile
8
+ from pydantic import BaseModel
9
+
10
+ from schemas.report import ReportInput, ReportType, UploadBatch
11
+ from services.pipeline import Pipeline
12
+ from services.storage import get_storage
13
+
14
+ logger = logging.getLogger(__name__)
15
+ router = APIRouter(prefix="/reports", tags=["reports"])
16
+
17
+
18
+ @router.post("/upload")
19
+ async def upload_reports(
20
+ session_id: Optional[str] = Form(default=None),
21
+ scenario_name: Optional[str] = Form(default=None),
22
+ files: List[UploadFile] = File(default=[]),
23
+ text_messages: Optional[str] = Form(default=None),
24
+ ) -> dict:
25
+ sid = session_id or str(uuid.uuid4())
26
+ reports: List[ReportInput] = []
27
+
28
+ if text_messages:
29
+ for i, msg in enumerate(text_messages.split("|||")):
30
+ msg = msg.strip()
31
+ if msg:
32
+ reports.append(ReportInput(
33
+ id=str(uuid.uuid4()),
34
+ session_id=sid,
35
+ report_type=ReportType.TEXT,
36
+ content=msg,
37
+ metadata={"source": "upload", "index": i},
38
+ created_at=datetime.utcnow(),
39
+ ))
40
+
41
+ for file in files:
42
+ filename = file.filename or "unknown"
43
+ name_lower = filename.lower()
44
+ if name_lower.endswith((".mp3", ".wav", ".ogg", ".m4a")):
45
+ rtype = ReportType.AUDIO
46
+ elif name_lower.endswith((".jpg", ".jpeg", ".png", ".webp")):
47
+ rtype = ReportType.IMAGE
48
+ elif name_lower.endswith(".csv"):
49
+ rtype = ReportType.CSV
50
+ else:
51
+ rtype = ReportType.TEXT
52
+
53
+ content = await file.read()
54
+ reports.append(ReportInput(
55
+ id=str(uuid.uuid4()),
56
+ session_id=sid,
57
+ report_type=rtype,
58
+ content=content.decode("utf-8", errors="replace") if rtype in (ReportType.TEXT, ReportType.CSV) else None,
59
+ file_path=filename,
60
+ metadata={"original_filename": filename, "size_bytes": len(content)},
61
+ created_at=datetime.utcnow(),
62
+ ))
63
+
64
+ storage = get_storage()
65
+ await storage.save_session(sid, {
66
+ "session_id": sid,
67
+ "scenario_name": scenario_name,
68
+ "total_reports": len(reports),
69
+ "status": "uploaded",
70
+ "created_at": datetime.utcnow().isoformat(),
71
+ })
72
+
73
+ return {"session_id": sid, "batch_id": sid, "report_count": len(reports), "status": "uploaded"}
74
+
75
+
76
+ @router.post("/process")
77
+ async def process_batch(batch: UploadBatch) -> dict:
78
+ pipeline = Pipeline()
79
+ try:
80
+ summary = await pipeline.process_batch(batch)
81
+ except Exception as exc:
82
+ logger.exception("Batch processing error: %s", exc)
83
+ raise HTTPException(status_code=500, detail=str(exc))
84
+ return summary.model_dump(mode="json")
85
+
86
+
87
+ @router.get("/{session_id}")
88
+ async def list_session_reports(session_id: str) -> dict:
89
+ storage = get_storage()
90
+ session = await storage.get_session(session_id)
91
+ if session is None:
92
+ raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
93
+ return {"session_id": session_id, "session": session}
backend/core/__init__.py ADDED
File without changes
backend/core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (165 Bytes). View file
 
backend/core/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.21 kB). View file
 
backend/core/config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ from pydantic_settings import BaseSettings
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ vllm_base_url: str = "http://localhost:8000/v1"
7
+ vllm_api_key: str = "not-needed"
8
+ vllm_model: str = "Qwen/Qwen2.5-72B-Instruct"
9
+ vllm_vision_model: str = "Qwen/Qwen2-VL-7B-Instruct"
10
+ app_env: str = "development"
11
+ debug: bool = True
12
+ demo_mode: bool = True
13
+ storage_path: str = "./data"
14
+
15
+ model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
16
+
17
+
18
+ @lru_cache()
19
+ def get_settings() -> Settings:
20
+ return Settings()
backend/data/dispatch/03a16c03-94e5-4e6f-ba8a-237658e28a5b.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "03a16c03-94e5-4e6f-ba8a-237658e28a5b", "incident_id": "53250214-30ee-4f0b-8be8-5b04aed8e1ef", "channel": "radio", "message": "ALERTA P0 \u2014 BARRIO SANTA ANA: Tipo: Flood. Ubicaci\u00f3n: Barrio Santa Ana. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte m \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 53250214. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.503776", "approved": false}
backend/data/dispatch/12791726-6e83-4d2f-aea9-9ba2733edbe9.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "12791726-6e83-4d2f-aea9-9ba2733edbe9", "incident_id": "1c43f156-18ad-4322-8aa3-725416683112", "channel": "radio", "message": "ALERTA P0 \u2014 CENTRO COMUNITARIO: Tipo: Flood. Ubicaci\u00f3n: Centro Comunitario. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 1c43f156. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.503795", "approved": false}
backend/data/dispatch/143f2fa6-e703-4a7e-84a4-92a5ee09d550.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "143f2fa6-e703-4a7e-84a4-92a5ee09d550", "incident_id": "e76d4ac7-3a14-40c5-b13d-ade472600a0a", "channel": "radio", "message": "ALERTA P0 \u2014 Barrio Santa Ana: Tipo: Flood. Ubicaci\u00f3n: Barrio Santa Ana. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte m \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: e76d4ac7. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:30.587101", "approved": false}
backend/data/dispatch/2048c8c5-ca83-467f-959c-a6838a3897b2.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "2048c8c5-ca83-467f-959c-a6838a3897b2", "incident_id": "d8ebe4ff-807a-4469-b7fb-1c2e215a7827", "channel": "radio", "message": "ALERTA P0 \u2014 PUENTE: Tipo: Flood. Ubicaci\u00f3n: Puente. Personas afectadas estimadas: 60. Basado en 2 se\u00f1al(es) recibida(s). Reporte m\u00e1s recient \u2014 60 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: d8ebe4ff. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.415505", "approved": false}
backend/data/dispatch/22eb6be8-481a-4e35-a3f1-007b71fe871d.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "22eb6be8-481a-4e35-a3f1-007b71fe871d", "incident_id": "4588efff-8ded-444f-8d4d-e48651a094ca", "channel": "radio", "message": "ALERTA P0 \u2014 CENTRO COMUNITARIO: Tipo: Flood. Ubicaci\u00f3n: Centro Comunitario. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 4588efff. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.415568", "approved": false}
backend/data/dispatch/36562d51-406e-4ff6-8fb8-4a97135aa904.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "36562d51-406e-4ff6-8fb8-4a97135aa904", "incident_id": "cc64a7fe-38af-4e72-b78b-eb05aa3176e8", "channel": "radio", "message": "ALERTA P0 \u2014 SECTOR EL PROGRESO INFORMAMOS QUE EL NIVEL DEL AGUA COMENZ\u00d3 A BAJAR LEVEMENTE EN LAS \u00daLTIMAS 2 HORAS: Tipo: Flood. Ubicaci\u00f3n: Sector El Progreso Informamos Que El Nivel Del Agua Comenz\u00f3 A Bajar Levemente En Las \u00daltimas 2 H \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: cc64a7fe. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.415598", "approved": false}
backend/data/dispatch/36d98de7-9007-4c70-a694-e066d515ba9c.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "36d98de7-9007-4c70-a694-e066d515ba9c", "incident_id": "9ca75909-d9ea-49ad-90e0-e371a8b3576c", "channel": "radio", "message": "ALERTA P0 \u2014 CALLE SUCRE EST\u00c1 COMPLETAMENTE INUNDADA: Tipo: Flood. Ubicaci\u00f3n: Calle Sucre Est\u00e1 Completamente Inundada. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 9ca75909. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:12.415578", "approved": false}
backend/data/dispatch/372c2577-8f0a-42f5-ad2b-ae205ec79e1d.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "372c2577-8f0a-42f5-ad2b-ae205ec79e1d", "incident_id": "5e88dc0d-5c0c-4b05-98cf-0674cd48392a", "channel": "radio", "message": "ALERTA P0 \u2014 Calle Sucre Est\u00e1 Completamente Inundada: Tipo: Flood. Ubicaci\u00f3n: Calle Sucre Est\u00e1 Completamente Inundada. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 5e88dc0d. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:30.498178", "approved": false}
backend/data/dispatch/3ec4f10a-3f9d-427d-aca8-3134efa35105.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "3ec4f10a-3f9d-427d-aca8-3134efa35105", "incident_id": "6cac476b-78e0-4226-b31f-1839b7185642", "channel": "radio", "message": "ALERTA P0 \u2014 Santa Ana: Tipo: Flood. Ubicaci\u00f3n: Santa Ana. Personas afectadas estimadas: 180. Basado en 6 se\u00f1al(es) recibida(s). Reporte m\u00e1s rec \u2014 180 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 6cac476b. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:30.498064", "approved": false}
backend/data/dispatch/3f2d7e95-ee1c-443e-8825-cc32c7bea9b4.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "3f2d7e95-ee1c-443e-8825-cc32c7bea9b4", "incident_id": "7de575c3-3ed9-4645-a814-f446bfda0714", "channel": "radio", "message": "ALERTA P0 \u2014 Centro Comunitario: Tipo: Flood. Ubicaci\u00f3n: Centro Comunitario. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 7de575c3. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:30.587121", "approved": false}
backend/data/dispatch/46f1763e-8b79-4726-9f58-42f572b1634a.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "46f1763e-8b79-4726-9f58-42f572b1634a", "incident_id": "1816328e-f3ed-4e13-a87d-552b278a58a1", "channel": "radio", "message": "ALERTA P0 \u2014 Calle Bolivar: Tipo: Flood. Ubicaci\u00f3n: Calle Bolivar. Personas afectadas estimadas: 30. Basado en 1 se\u00f1al(es) recibida(s). Reporte m\u00e1s \u2014 30 personas afectadas. Recursos necesarios: 2x Equipo de rescate acu\u00e1tico, 3x Lanchas de evacuaci\u00f3n, 200x Agua potable embotellada. Coordinar con: Brigada Alfa \u2014 Rescate Cr\u00edtico. ID incidente: 1816328e. CAMBIO.", "brigade_target": "Brigada Alfa \u2014 Rescate Cr\u00edtico", "created_at": "2026-05-06T01:07:30.498241", "approved": false}