feat: build complete ReliefLensAI backend
Browse filesFull 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>
- backend/.env.example +13 -0
- backend/Dockerfile +18 -0
- backend/__pycache__/main.cpython-312.pyc +0 -0
- backend/agents/__init__.py +19 -0
- backend/agents/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/agents/__pycache__/dedup_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/dispatch_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/intake_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/normalization_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/resource_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/transcription_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/triage_agent.cpython-312.pyc +0 -0
- backend/agents/__pycache__/vision_agent.cpython-312.pyc +0 -0
- backend/agents/dedup_agent.py +19 -0
- backend/agents/dispatch_agent.py +39 -0
- backend/agents/intake_agent.py +39 -0
- backend/agents/normalization_agent.py +45 -0
- backend/agents/resource_agent.py +23 -0
- backend/agents/transcription_agent.py +29 -0
- backend/agents/triage_agent.py +106 -0
- backend/agents/vision_agent.py +28 -0
- backend/api/__init__.py +0 -0
- backend/api/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/api/routes/__pycache__/amd.cpython-312.pyc +0 -0
- backend/api/routes/__pycache__/crisis_room.cpython-312.pyc +0 -0
- backend/api/routes/__pycache__/demo.cpython-312.pyc +0 -0
- backend/api/routes/__pycache__/incidents.cpython-312.pyc +0 -0
- backend/api/routes/__pycache__/reports.cpython-312.pyc +0 -0
- backend/api/routes/amd.py +15 -0
- backend/api/routes/crisis_room.py +42 -0
- backend/api/routes/demo.py +70 -0
- backend/api/routes/incidents.py +90 -0
- backend/api/routes/reports.py +93 -0
- backend/core/__init__.py +0 -0
- backend/core/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/core/__pycache__/config.cpython-312.pyc +0 -0
- backend/core/config.py +20 -0
- backend/data/dispatch/03a16c03-94e5-4e6f-ba8a-237658e28a5b.json +1 -0
- backend/data/dispatch/12791726-6e83-4d2f-aea9-9ba2733edbe9.json +1 -0
- backend/data/dispatch/143f2fa6-e703-4a7e-84a4-92a5ee09d550.json +1 -0
- backend/data/dispatch/2048c8c5-ca83-467f-959c-a6838a3897b2.json +1 -0
- backend/data/dispatch/22eb6be8-481a-4e35-a3f1-007b71fe871d.json +1 -0
- backend/data/dispatch/36562d51-406e-4ff6-8fb8-4a97135aa904.json +1 -0
- backend/data/dispatch/36d98de7-9007-4c70-a694-e066d515ba9c.json +1 -0
- backend/data/dispatch/372c2577-8f0a-42f5-ad2b-ae205ec79e1d.json +1 -0
- backend/data/dispatch/3ec4f10a-3f9d-427d-aca8-3134efa35105.json +1 -0
- backend/data/dispatch/3f2d7e95-ee1c-443e-8825-cc32c7bea9b4.json +1 -0
- backend/data/dispatch/46f1763e-8b79-4726-9f58-42f572b1634a.json +1 -0
|
@@ -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
|
|
@@ -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"]
|
|
Binary file (3.05 kB). View file
|
|
|
|
@@ -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 |
+
]
|
|
Binary file (661 Bytes). View file
|
|
|
|
Binary file (1.41 kB). View file
|
|
|
|
Binary file (2.16 kB). View file
|
|
|
|
Binary file (1.97 kB). View file
|
|
|
|
Binary file (2.55 kB). View file
|
|
|
|
Binary file (1.68 kB). View file
|
|
|
|
Binary file (1.79 kB). View file
|
|
|
|
Binary file (6.56 kB). View file
|
|
|
|
Binary file (1.85 kB). View file
|
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 |
+
)
|
|
@@ -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 |
+
)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
Binary file (164 Bytes). View file
|
|
|
|
File without changes
|
|
Binary file (171 Bytes). View file
|
|
|
|
Binary file (956 Bytes). View file
|
|
|
|
Binary file (2.46 kB). View file
|
|
|
|
Binary file (3.56 kB). View file
|
|
|
|
Binary file (5.66 kB). View file
|
|
|
|
Binary file (5.01 kB). View file
|
|
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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")
|
|
@@ -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")
|
|
@@ -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}
|
|
File without changes
|
|
Binary file (165 Bytes). View file
|
|
|
|
Binary file (1.21 kB). View file
|
|
|
|
@@ -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()
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|
|
@@ -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}
|