Files
AI_engine/core/decisions.py

67 lines
2.6 KiB
Python

"""
Records the engine's model decisions in the backend's decision log.
Until Phase 5 of the plan (krow_talent_app/docs/agent-platform-plan.md) the
stall and assignment-failure decisions existed only as log lines, so the
console's Insights tab could not show them. Each decision is now sent to
``POST /api/v1/internal/agent-decisions`` — the same log routemate's
assignment decisions go to.
Fire-and-forget: the post runs as a background task, so a slow or down
backend never delays an agent's reaction to a stall. A failure is logged by
the HTTP client and otherwise ignored — the decision was still made and acted
on; only its record is missing.
"""
import asyncio
from typing import Any, Dict, Optional, Set
from config.system_config import GO_API_BASE_URL, INTERNAL_API_KEY
from core.http_client import api_post
_pending: Set[asyncio.Task] = set() # keep references so tasks are not garbage-collected
def _as_booking_id(value: Any) -> Optional[int]:
"""The backend's booking_id is an unsigned integer; anything else is omitted."""
try:
n = int(value)
except (TypeError, ValueError):
return None
return n if n > 0 else None
def build_payload(decision_type: str, booking_id: Any, facts: Optional[Dict[str, Any]],
action: str, confidence: float, reasoning: str, model: str) -> Dict[str, Any]:
"""The request body. Pure, so its shape is tested without a network."""
return {
"decision_type": decision_type,
"booking_id": _as_booking_id(booking_id),
"context": {"facts": facts or {}, "model": model},
"decision": {"action": action, "confidence": round(float(confidence), 3)},
"reasoning": reasoning or "",
}
async def _post(payload: Dict[str, Any]) -> None:
await api_post(
f"{GO_API_BASE_URL}/api/v1/internal/agent-decisions",
json=payload,
headers={"X-Internal-Key": INTERNAL_API_KEY},
)
def record_decision(decision_type: str, booking_id: Any, facts: Optional[Dict[str, Any]],
action: str, confidence: float, reasoning: str, model: str) -> Optional[asyncio.Task]:
"""Schedule the record and return at once. Returns the task (for tests), or
None when there is no running loop or no key to authenticate with."""
if not INTERNAL_API_KEY:
return None
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return None
task = loop.create_task(_post(build_payload(decision_type, booking_id, facts, action, confidence, reasoning, model)))
_pending.add(task)
task.add_done_callback(_pending.discard)
return task