595 lines
22 KiB
Python
595 lines
22 KiB
Python
"""Voice/browser/Telegram command router: export intel, webbuilder/agy, bevestiging + UI."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import time
|
|
import uuid
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app.config import settings
|
|
from app.services import webbuilder_agent
|
|
|
|
SESSION_TTL_SEC = 3600
|
|
_pending: dict[str, dict[str, Any]] = {}
|
|
|
|
VOICE_CHANNELS = frozenset({"voice", "browser", "telegram"})
|
|
|
|
LOCATION_ALIASES: dict[str, dict[str, str]] = {
|
|
"dubai": {"country": "AE", "q": "Dubai", "label": "Dubai (VAE)"},
|
|
"abu dhabi": {"country": "AE", "q": "Abu Dhabi", "label": "Abu Dhabi (VAE)"},
|
|
"sharjah": {"country": "AE", "q": "Sharjah", "label": "Sharjah (VAE)"},
|
|
"vae": {"country": "AE", "q": "", "label": "Verenigde Arabische Emiraten"},
|
|
"uae": {"country": "AE", "q": "", "label": "Verenigde Arabische Emiraten"},
|
|
"emiraten": {"country": "AE", "q": "", "label": "VAE"},
|
|
"saudi": {"country": "SA", "q": "", "label": "Saoedi-Arabië"},
|
|
"riyadh": {"country": "SA", "q": "Riyadh", "label": "Riyadh (SA)"},
|
|
"jeddah": {"country": "SA", "q": "Jeddah", "label": "Jeddah (SA)"},
|
|
"qatar": {"country": "QA", "q": "", "label": "Qatar"},
|
|
"doha": {"country": "QA", "q": "Doha", "label": "Doha (Qatar)"},
|
|
"kuwait": {"country": "KW", "q": "", "label": "Koeweit"},
|
|
"nederland": {"country": "NL", "q": "", "label": "Nederland"},
|
|
"amsterdam": {"country": "NL", "q": "Amsterdam", "label": "Amsterdam"},
|
|
"rotterdam": {"country": "NL", "q": "Rotterdam", "label": "Rotterdam"},
|
|
"belgië": {"country": "BE", "q": "", "label": "België"},
|
|
"belgie": {"country": "BE", "q": "", "label": "België"},
|
|
"antwerpen": {"country": "BE", "q": "Antwerp", "label": "Antwerpen"},
|
|
"duitsland": {"country": "DE", "q": "", "label": "Duitsland"},
|
|
"berlijn": {"country": "DE", "q": "Berlin", "label": "Berlijn"},
|
|
"frankfurt": {"country": "DE", "q": "Frankfurt", "label": "Frankfurt"},
|
|
"turkije": {"country": "TR", "q": "", "label": "Turkije"},
|
|
"istanbul": {"country": "TR", "q": "Istanbul", "label": "Istanbul"},
|
|
"marokko": {"country": "MA", "q": "", "label": "Marokko"},
|
|
"casablanca": {"country": "MA", "q": "Casablanca", "label": "Casablanca"},
|
|
}
|
|
|
|
ENTITY_ALIASES: dict[str, str] = {
|
|
"distri": "distributor,wholesaler,importer,logistics",
|
|
"distributeur": "distributor,wholesaler,importer,logistics",
|
|
"distributeurs": "distributor,wholesaler,importer,logistics",
|
|
"groothandel": "wholesaler,distributor",
|
|
"groothandels": "wholesaler,distributor",
|
|
"importeur": "importer",
|
|
"importeurs": "importer",
|
|
"logistiek": "logistics",
|
|
"restaurant": "restaurant",
|
|
"restaurants": "restaurant",
|
|
"cateraar": "caterer",
|
|
"cateraars": "caterer",
|
|
"slager": "butcher",
|
|
"slagers": "butcher",
|
|
"döner": "doner",
|
|
"doner": "doner",
|
|
}
|
|
|
|
SEARCH_TRIGGERS = (
|
|
"zoek", "opzoeken", "vind", "zoeken", "toon", "laat zien", "lijst",
|
|
"geef me", "haal op", "export intel", "wereldexport",
|
|
)
|
|
|
|
YES_RE = re.compile(
|
|
r"^(ja|jawel|jep|yep|ok|oke|oké|okay|klopt|bevestig|doe maar|graag|precies|goed|akkoord|start|uitvoeren|doorgaan)\b",
|
|
re.I,
|
|
)
|
|
NO_RE = re.compile(r"^(nee|neen|stop|annuleer|niet|cancel|laat maar|wacht)\b", re.I)
|
|
|
|
|
|
def _cleanup_sessions() -> None:
|
|
now = time.time()
|
|
dead = [k for k, v in _pending.items() if now - float(v.get("_ts", 0)) > SESSION_TTL_SEC]
|
|
for k in dead:
|
|
_pending.pop(k, None)
|
|
|
|
|
|
def is_confirmation_yes(text: str) -> bool:
|
|
t = (text or "").strip()
|
|
return bool(t and YES_RE.search(t))
|
|
|
|
|
|
def is_confirmation_no(text: str) -> bool:
|
|
t = (text or "").strip()
|
|
return bool(t and NO_RE.search(t))
|
|
|
|
|
|
def wants_export_search(text: str) -> bool:
|
|
t = (text or "").lower()
|
|
if not any(k in t for k in SEARCH_TRIGGERS):
|
|
return False
|
|
has_loc = any(alias in t for alias in LOCATION_ALIASES)
|
|
has_ent = any(alias in t for alias in ENTITY_ALIASES)
|
|
return has_loc or has_ent
|
|
|
|
|
|
def _detect_location(text: str) -> dict[str, str] | None:
|
|
t = text.lower()
|
|
best: tuple[int, dict[str, str]] | None = None
|
|
for alias, loc in LOCATION_ALIASES.items():
|
|
if alias in t:
|
|
score = len(alias)
|
|
if best is None or score > best[0]:
|
|
best = (score, loc)
|
|
return best[1] if best else None
|
|
|
|
|
|
def _detect_entity_types(text: str) -> str:
|
|
t = text.lower()
|
|
found: list[str] = []
|
|
for alias, types in ENTITY_ALIASES.items():
|
|
if alias in t:
|
|
for et in types.split(","):
|
|
if et not in found:
|
|
found.append(et)
|
|
if not found and any(w in t for w in ("distri", "distributeur", "groothandel", "b2b", "leverancier")):
|
|
return "distributor,wholesaler,importer,logistics"
|
|
return ",".join(found) if found else "distributor,wholesaler,importer,logistics"
|
|
|
|
|
|
def _entity_label(entity_types: str) -> str:
|
|
labels = {
|
|
"distributor": "distributeurs",
|
|
"wholesaler": "groothandels",
|
|
"importer": "importeurs",
|
|
"logistics": "logistiek",
|
|
"restaurant": "restaurants",
|
|
"caterer": "cateraars",
|
|
"butcher": "slagers",
|
|
"doner": "dönerzaken",
|
|
}
|
|
parts = [labels.get(x.strip(), x.strip()) for x in entity_types.split(",") if x.strip()]
|
|
return ", ".join(parts) if parts else "bedrijven"
|
|
|
|
|
|
def parse_export_search(text: str) -> dict[str, Any]:
|
|
loc = _detect_location(text)
|
|
entity_types = _detect_entity_types(text)
|
|
missing: list[str] = []
|
|
if not loc:
|
|
missing.append("locatie")
|
|
params: dict[str, Any] = {
|
|
"country": (loc or {}).get("country", ""),
|
|
"q": (loc or {}).get("q", ""),
|
|
"entity_types": entity_types,
|
|
"limit": 50,
|
|
}
|
|
label_loc = (loc or {}).get("label", "")
|
|
return {
|
|
"params": params,
|
|
"location_label": label_loc,
|
|
"entity_label": _entity_label(entity_types),
|
|
"missing": missing,
|
|
"incomplete": bool(missing),
|
|
}
|
|
|
|
|
|
def store_pending(session_id: str, action: dict[str, Any]) -> str:
|
|
_cleanup_sessions()
|
|
action_id = str(uuid.uuid4())[:12]
|
|
action = dict(action)
|
|
action["id"] = action_id
|
|
action["_ts"] = time.time()
|
|
_pending[session_id] = action
|
|
return action_id
|
|
|
|
|
|
def get_pending(session_id: str) -> dict[str, Any] | None:
|
|
_cleanup_sessions()
|
|
row = _pending.get(session_id)
|
|
if not row:
|
|
return None
|
|
if time.time() - float(row.get("_ts", 0)) > SESSION_TTL_SEC:
|
|
_pending.pop(session_id, None)
|
|
return None
|
|
return row
|
|
|
|
|
|
def clear_pending(session_id: str) -> None:
|
|
_pending.pop(session_id, None)
|
|
|
|
|
|
def build_confirmation_question(parsed: dict[str, Any]) -> str:
|
|
loc = parsed.get("location_label") or "de geselecteerde regio"
|
|
ent = parsed.get("entity_label") or "bedrijven"
|
|
return (
|
|
f"Ik ga **{ent}** in **{loc}** voor je opzoeken in Export Intel.\n\n"
|
|
"Klopt dat? Zeg **ja** om te starten, **nee** om te annuleren, "
|
|
"of geef aan wat ik moet aanpassen (bijv. alleen distributeurs, of een andere stad)."
|
|
)
|
|
|
|
|
|
def build_clarification_question(parsed: dict[str, Any]) -> str:
|
|
missing = parsed.get("missing") or []
|
|
if "locatie" in missing:
|
|
return (
|
|
"In welke **stad of welk land** wil je zoeken? "
|
|
"Bijvoorbeeld: Dubai, VAE, Nederland, Frankfurt…"
|
|
)
|
|
return "Kun je iets specifieker zijn over wat je zoekt en waar?"
|
|
|
|
|
|
async def fetch_export_entities(params: dict[str, Any]) -> dict[str, Any]:
|
|
query = {k: v for k, v in params.items() if v not in (None, "")}
|
|
url = f"{settings.TOOLS_API_URL.rstrip('/')}/export-intel/entities"
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
resp = await client.get(url, params=query)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def _open_url(params: dict[str, Any]) -> str:
|
|
qs = []
|
|
if params.get("country"):
|
|
qs.append(f"country={params['country']}")
|
|
if params.get("q"):
|
|
qs.append(f"q={params['q']}")
|
|
qs.append("tab=distributors")
|
|
return "/export-intel?" + "&".join(qs)
|
|
|
|
|
|
async def execute_search_action(action: dict[str, Any]) -> dict[str, Any]:
|
|
params = action.get("params") or {}
|
|
data = await fetch_export_entities(params)
|
|
items = data.get("items") or []
|
|
total = int(data.get("total") or len(items))
|
|
loc = action.get("location_label") or params.get("q") or params.get("country") or "markt"
|
|
ent = action.get("entity_label") or "bedrijven"
|
|
title = f"{ent.title()} — {loc}"
|
|
if not items:
|
|
reply = (
|
|
f"Ik heb gezocht maar vond **geen** {ent} in {loc}. "
|
|
"Wil je dat ik een sync start of een bredere regio probeer?"
|
|
)
|
|
else:
|
|
reply = (
|
|
f"Gevonden: **{total}** {ent} in {loc}. "
|
|
f"Ik toon de eerste {min(len(items), 50)} in het resultatenvenster."
|
|
)
|
|
return {
|
|
"agent": "sourcing",
|
|
"agent_label": "Export Intel → Herman",
|
|
"reply": reply,
|
|
"delegated_agents": ["sourcing", "export_intel"],
|
|
"routing_reason": f"Export Intel zoekopdracht: {title}",
|
|
"agent_steps": [
|
|
{"agent": "export_intel", "status": "done", "message": f"{total} resultaten"},
|
|
{"agent": "sourcing", "status": "delegated", "message": "Marktdata opgehaald"},
|
|
],
|
|
"needs_confirmation": False,
|
|
"ui_actions": [
|
|
{
|
|
"type": "show_export_results",
|
|
"title": title,
|
|
"entities": items[:50],
|
|
"total": total,
|
|
"params": params,
|
|
"open_url": _open_url(params),
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def _pending_summary(action: dict[str, Any]) -> dict[str, Any]:
|
|
atype = action.get("type", "")
|
|
base = {"id": action.get("id"), "type": atype}
|
|
if atype == "export_intel_search":
|
|
base["location_label"] = action.get("location_label")
|
|
base["entity_label"] = action.get("entity_label")
|
|
base["params"] = action.get("params")
|
|
elif atype == "webbuilder_build":
|
|
base["project"] = action.get("project")
|
|
base["entity_label"] = f"website {action.get('project', '')}"
|
|
base["location_label"] = "Agy · Antigravity"
|
|
return base
|
|
|
|
|
|
def build_webbuilder_confirmation(project: str, raw: str) -> str:
|
|
snippet = (raw or "")[:240]
|
|
return (
|
|
f"Ik stuur **Agy (Antigravity)** op Hermes aan om een website te bouwen.\n\n"
|
|
f"**Project:** {project}\n"
|
|
f"**Opdracht:** {snippet}{'…' if len(raw or '') > 240 else ''}\n\n"
|
|
"Klopt dat? Zeg **ja** om te starten, **nee** om te annuleren."
|
|
)
|
|
|
|
|
|
async def execute_webbuilder_action(action: dict[str, Any], channel: str = "voice") -> dict[str, Any]:
|
|
raw = action.get("raw_message") or ""
|
|
project = action.get("project") or webbuilder_agent.extract_project_name(raw)
|
|
try:
|
|
outcome = await webbuilder_agent.generate_from_message(raw, channel=channel, wait=False)
|
|
except Exception as exc:
|
|
return {
|
|
"agent": "webbuilder",
|
|
"agent_label": "Web Builder",
|
|
"reply": f"Agy kon niet starten op Hermes: {exc}\n\nControleer VM107 webbuilder-api (:8798).",
|
|
"delegated_agents": ["webbuilder"],
|
|
"needs_confirmation": False,
|
|
}
|
|
|
|
project = outcome.get("project") or project
|
|
preview = outcome.get("preview_url") or webbuilder_agent.HERMES_PREVIEW_BASE
|
|
nas_path = outcome.get("nas_path") or ""
|
|
reply = (
|
|
f"Agy is gestart voor **{project}**.\n"
|
|
f"Preview (na build): {preview}\n"
|
|
f"NAS: {nas_path}\n"
|
|
"Volg live: Agents → Terminals → Web Builder."
|
|
)
|
|
return {
|
|
"agent": "webbuilder",
|
|
"agent_label": "Agy / Web Builder → Herman",
|
|
"reply": reply,
|
|
"delegated_agents": ["webbuilder"],
|
|
"routing_reason": "Website via Antigravity CLI (agy) op Hermes VM107",
|
|
"agent_steps": [
|
|
{"agent": "webbuilder", "status": "running", "message": f"agy bouwt {project}"},
|
|
{"agent": "herman", "status": "delegated", "message": "Voice → build gestart"},
|
|
],
|
|
"webbuilder_project": project,
|
|
"webbuilder_preview_url": preview,
|
|
"needs_confirmation": False,
|
|
"ui_actions": [
|
|
{
|
|
"type": "open_webbuilder_build",
|
|
"title": f"Website build — {project}",
|
|
"project": project,
|
|
"preview_url": preview,
|
|
"nas_path": nas_path,
|
|
"agents_url": "/agents",
|
|
"status_hint": "Build duurt enkele minuten — preview opent na afloop",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
async def execute_pending_action(action: dict[str, Any], channel: str = "voice") -> dict[str, Any]:
|
|
atype = action.get("type")
|
|
if atype == "webbuilder_build":
|
|
return await execute_webbuilder_action(action, channel=channel)
|
|
return await execute_search_action(action)
|
|
|
|
|
|
def _confirmation_reminder(pending: dict[str, Any]) -> str:
|
|
if pending.get("type") == "webbuilder_build":
|
|
return build_webbuilder_confirmation(pending.get("project", "website"), pending.get("raw_message", ""))
|
|
return build_confirmation_question(pending)
|
|
|
|
|
|
async def handle_voice_command(
|
|
message: str,
|
|
session_id: str | None = None,
|
|
confirm_action_id: str | None = None,
|
|
channel: str = "voice",
|
|
) -> dict[str, Any] | None:
|
|
"""Unified voice/browser router: bevestiging + export + webbuilder/agy."""
|
|
sid = (session_id or "").strip() or "default"
|
|
text = (message or "").strip()
|
|
if not text:
|
|
return None
|
|
|
|
pending = get_pending(sid)
|
|
|
|
if confirm_action_id and pending and pending.get("id") == confirm_action_id:
|
|
clear_pending(sid)
|
|
return await execute_pending_action(pending, channel=channel)
|
|
|
|
if pending:
|
|
if is_confirmation_yes(text):
|
|
clear_pending(sid)
|
|
return await execute_pending_action(pending, channel=channel)
|
|
if is_confirmation_no(text):
|
|
clear_pending(sid)
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": "Oké, geannuleerd. Waar kan ik je verder mee helpen?",
|
|
"needs_confirmation": False,
|
|
}
|
|
if pending.get("awaiting") == "location" and pending.get("type") == "export_intel_search":
|
|
loc = _detect_location(text)
|
|
if loc:
|
|
merged = dict(pending)
|
|
merged["params"] = dict(pending.get("params") or {})
|
|
merged["params"]["country"] = loc["country"]
|
|
merged["params"]["q"] = loc.get("q", "")
|
|
merged["location_label"] = loc["label"]
|
|
merged.pop("awaiting", None)
|
|
merged.pop("incomplete", None)
|
|
merged.pop("missing", None)
|
|
action_id = store_pending(sid, merged)
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_confirmation_question(merged),
|
|
"needs_confirmation": True,
|
|
"pending_action": _pending_summary(merged),
|
|
}
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_clarification_question({"missing": ["locatie"]}),
|
|
"needs_confirmation": True,
|
|
"clarification": True,
|
|
}
|
|
if webbuilder_agent.wants_website(text):
|
|
project = webbuilder_agent.extract_project_name(text)
|
|
action_id = store_pending(sid, {
|
|
"type": "webbuilder_build",
|
|
"project": project,
|
|
"raw_message": text,
|
|
})
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_webbuilder_confirmation(project, text),
|
|
"needs_confirmation": True,
|
|
"pending_action": _pending_summary(get_pending(sid) or {}),
|
|
}
|
|
if wants_export_search(text):
|
|
parsed = parse_export_search(text)
|
|
if not parsed.get("incomplete"):
|
|
action_id = store_pending(sid, {**parsed, "type": "export_intel_search"})
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_confirmation_question(parsed),
|
|
"needs_confirmation": True,
|
|
"pending_action": _pending_summary(get_pending(sid) or {}),
|
|
}
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": (
|
|
f"Ik wacht nog op bevestiging.\n\n{_confirmation_reminder(pending)}\n\n"
|
|
"Zeg **ja** of **nee**."
|
|
),
|
|
"needs_confirmation": True,
|
|
"pending_action": _pending_summary(pending),
|
|
}
|
|
|
|
if webbuilder_agent.wants_website(text):
|
|
project = webbuilder_agent.extract_project_name(text)
|
|
store_pending(sid, {"type": "webbuilder_build", "project": project, "raw_message": text})
|
|
pa = get_pending(sid) or {}
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_webbuilder_confirmation(project, text),
|
|
"needs_confirmation": True,
|
|
"pending_action": _pending_summary(pa),
|
|
}
|
|
|
|
return await handle_export_intent(message, session_id=session_id, confirm_action_id=confirm_action_id)
|
|
|
|
|
|
async def handle_export_intent(
|
|
message: str,
|
|
session_id: str | None = None,
|
|
confirm_action_id: str | None = None,
|
|
) -> dict[str, Any] | None:
|
|
"""Return Herman-shaped dict when export flow applies, else None."""
|
|
sid = (session_id or "").strip() or "default"
|
|
text = (message or "").strip()
|
|
if not text:
|
|
return None
|
|
|
|
pending = get_pending(sid)
|
|
|
|
if confirm_action_id and pending and pending.get("id") == confirm_action_id:
|
|
clear_pending(sid)
|
|
return await execute_pending_action(pending)
|
|
|
|
if pending:
|
|
if is_confirmation_yes(text):
|
|
clear_pending(sid)
|
|
return await execute_pending_action(pending)
|
|
if is_confirmation_no(text):
|
|
clear_pending(sid)
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": "Oké, geannuleerd. Waar kan ik je verder mee helpen?",
|
|
"needs_confirmation": False,
|
|
}
|
|
if pending.get("awaiting") == "location":
|
|
loc = _detect_location(text)
|
|
if loc:
|
|
merged = dict(pending)
|
|
merged["params"] = dict(pending.get("params") or {})
|
|
merged["params"]["country"] = loc["country"]
|
|
merged["params"]["q"] = loc.get("q", "")
|
|
merged["location_label"] = loc["label"]
|
|
merged.pop("awaiting", None)
|
|
merged.pop("incomplete", None)
|
|
merged.pop("missing", None)
|
|
action_id = store_pending(sid, merged)
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_confirmation_question(merged),
|
|
"needs_confirmation": True,
|
|
"pending_action": {
|
|
"id": action_id,
|
|
"type": "export_intel_search",
|
|
"params": merged.get("params"),
|
|
"location_label": merged.get("location_label"),
|
|
"entity_label": merged.get("entity_label"),
|
|
},
|
|
}
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_clarification_question({"missing": ["locatie"]}),
|
|
"needs_confirmation": True,
|
|
"clarification": True,
|
|
}
|
|
if wants_export_search(text):
|
|
parsed = parse_export_search(text)
|
|
if parsed.get("incomplete"):
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_clarification_question(parsed),
|
|
"needs_confirmation": True,
|
|
"clarification": True,
|
|
}
|
|
action_id = store_pending(sid, parsed)
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_confirmation_question(parsed),
|
|
"needs_confirmation": True,
|
|
"pending_action": {
|
|
"id": action_id,
|
|
"type": "export_intel_search",
|
|
"params": parsed.get("params"),
|
|
"location_label": parsed.get("location_label"),
|
|
"entity_label": parsed.get("entity_label"),
|
|
},
|
|
}
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": (
|
|
f"Ik wacht nog op bevestiging: {build_confirmation_question(pending)}\n\n"
|
|
"Zeg **ja** of **nee**, of stel een nieuwe zoekopdracht."
|
|
),
|
|
"needs_confirmation": True,
|
|
"pending_action": {
|
|
"id": pending.get("id"),
|
|
"type": pending.get("type", "export_intel_search"),
|
|
"params": pending.get("params"),
|
|
"location_label": pending.get("location_label"),
|
|
"entity_label": pending.get("entity_label"),
|
|
},
|
|
}
|
|
|
|
if not wants_export_search(text):
|
|
return None
|
|
|
|
parsed = parse_export_search(text)
|
|
if parsed.get("incomplete"):
|
|
store_pending(sid, {**parsed, "type": "export_intel_search", "awaiting": "location"})
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_clarification_question(parsed),
|
|
"needs_confirmation": True,
|
|
"clarification": True,
|
|
}
|
|
|
|
action_id = store_pending(sid, {**parsed, "type": "export_intel_search"})
|
|
return {
|
|
"agent": "herman",
|
|
"agent_label": "Herman",
|
|
"reply": build_confirmation_question(parsed),
|
|
"needs_confirmation": True,
|
|
"pending_action": {
|
|
"id": action_id,
|
|
"type": "export_intel_search",
|
|
"params": parsed.get("params"),
|
|
"location_label": parsed.get("location_label"),
|
|
"entity_label": parsed.get("entity_label"),
|
|
},
|
|
}
|