feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
This commit is contained in:
231
crews/agromatrix_crew/session_context.py
Normal file
231
crews/agromatrix_crew/session_context.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Session Context Layer — Humanized Stepan v3 / v3.1 / v3.2 / v3.5.
|
||||
|
||||
In-memory, per-chat сесійний контекст з TTL 15 хвилин.
|
||||
Не персистується між рестартами контейнера (це очікувано — сесія коротка).
|
||||
|
||||
Структура SessionContext:
|
||||
{
|
||||
"last_messages": list[str] (max 3, найновіші),
|
||||
"last_depth": "light" | "deep" | None,
|
||||
"last_agents": list[str] (max 5),
|
||||
"last_question": str | None,
|
||||
"pending_action": dict | None — Confirmation Gate (v3.1),
|
||||
"doc_facts": dict | None — Fact Lock Layer (v3.2):
|
||||
числові факти з документу (profit_uah, area_ha тощо),
|
||||
зберігаються між запитами щоб уникнути RAG-інконсистентності,
|
||||
"fact_claims": list[dict] — Self-Correction (v3.2):
|
||||
останні 3 твердження агента, напр.
|
||||
[{"key":"profit_present","value":False,"ts":1234}],
|
||||
"active_doc_id": str | None — Doc Anchor (v3.3):
|
||||
doc_id поточного активного документу;
|
||||
при зміні → скидаємо doc_facts і fact_claims,
|
||||
"doc_focus": bool — Doc Focus Gate (v3.5):
|
||||
True = документ "приклеєний" до діалогу (активний режим).
|
||||
False = документ є, але не нав'язуємо його контекст.
|
||||
"doc_focus_ts": float — timestamp активації doc_focus (time.time()),
|
||||
"updated_at": float (time.time())
|
||||
}
|
||||
|
||||
doc_focus TTL: DOC_FOCUS_TTL (600 с = 10 хв).
|
||||
Скидається автоматично при photo/URL/vision-інтенті або вручну через /doc off.
|
||||
|
||||
Telemetry:
|
||||
AGX_STEPAN_METRIC session_loaded chat_id=h:...
|
||||
AGX_STEPAN_METRIC session_expired chat_id=h:...
|
||||
AGX_STEPAN_METRIC session_updated chat_id=h:... depth=... agents=...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from crews.agromatrix_crew.telemetry import tlog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TTL 15 хвилин
|
||||
SESSION_TTL: float = 900.0
|
||||
|
||||
# Doc Focus Gate TTL: 10 хвилин після останньої активації
|
||||
DOC_FOCUS_TTL: float = 600.0
|
||||
|
||||
# v3.6: Cooldown після auto-clear — 2 хв блокування implicit doc re-activate
|
||||
DOC_FOCUS_COOLDOWN_S: float = 120.0
|
||||
|
||||
_STORE: dict[str, dict] = {}
|
||||
|
||||
|
||||
def _default_session() -> dict:
|
||||
return {
|
||||
"last_messages": [],
|
||||
"last_depth": None,
|
||||
"last_agents": [],
|
||||
"last_question": None,
|
||||
"pending_action": None, # v3.1: Confirmation Gate
|
||||
"doc_facts": None, # v3.2: Fact Lock Layer
|
||||
"fact_claims": [], # v3.2: Self-Correction Policy
|
||||
"active_doc_id": None, # v3.3: Doc Anchor Reset
|
||||
"doc_focus": False, # v3.5: Doc Focus Gate
|
||||
"doc_focus_ts": 0.0, # v3.5: timestamp активації doc_focus
|
||||
"doc_focus_cooldown_until": 0.0, # v3.6: epoch seconds, 0=inactive
|
||||
"last_photo_ts": 0.0, # v3.5 fix: timestamp останнього фото
|
||||
"updated_at": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def is_doc_focus_cooldown_active(session: dict, now_ts: float | None = None) -> bool:
|
||||
"""
|
||||
Повертає True якщо cooldown активний (після auto-clear по web/vision домену).
|
||||
Поки cooldown — implicit doc re-activate заблокований.
|
||||
Fail-safe: будь-яка помилка → False.
|
||||
"""
|
||||
try:
|
||||
until = float(session.get("doc_focus_cooldown_until") or 0.0)
|
||||
now = now_ts if now_ts is not None else time.time()
|
||||
return until > now
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_doc_focus_active(session: dict, now_ts: float | None = None) -> bool:
|
||||
"""
|
||||
Повертає True якщо doc_focus увімкнений і TTL ще не минув.
|
||||
|
||||
Використовується в run.py для вирішення чи підмішувати doc_context в промпт.
|
||||
Fail-safe: будь-яка помилка → False.
|
||||
"""
|
||||
try:
|
||||
if not session.get("doc_focus"):
|
||||
return False
|
||||
ts = session.get("doc_focus_ts") or 0.0
|
||||
now = now_ts if now_ts is not None else time.time()
|
||||
return (now - ts) <= DOC_FOCUS_TTL
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def load_session(chat_id: str) -> dict:
|
||||
"""
|
||||
Завантажити SessionContext для chat_id.
|
||||
|
||||
- Якщо нема → повернути default (порожній).
|
||||
- Якщо протух (now - updated_at > TTL) → очистити, повернути default.
|
||||
- Fail-safe: ніяких винятків назовні.
|
||||
"""
|
||||
try:
|
||||
if not chat_id:
|
||||
return _default_session()
|
||||
|
||||
existing = _STORE.get(chat_id)
|
||||
if existing is None:
|
||||
tlog(logger, "session_loaded", chat_id=chat_id, status="new")
|
||||
return _default_session()
|
||||
|
||||
age = time.time() - existing.get("updated_at", 0.0)
|
||||
if age > SESSION_TTL:
|
||||
_STORE.pop(chat_id, None)
|
||||
tlog(logger, "session_expired", chat_id=chat_id, age_s=round(age))
|
||||
return _default_session()
|
||||
|
||||
tlog(logger, "session_loaded", chat_id=chat_id, status="hit",
|
||||
last_depth=existing.get("last_depth"))
|
||||
return deepcopy(existing)
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("load_session error (returning default): %s", exc)
|
||||
return _default_session()
|
||||
|
||||
|
||||
def update_session(
|
||||
chat_id: str,
|
||||
message: str,
|
||||
depth: str,
|
||||
agents: list[str] | None = None,
|
||||
last_question: str | None = None,
|
||||
pending_action: dict | None = None, # v3.1: Confirmation Gate
|
||||
doc_facts: dict | None = None, # v3.2: Fact Lock
|
||||
fact_claims: list | None = None, # v3.2: Self-Correction
|
||||
active_doc_id: str | None = None, # v3.3: Doc Anchor Reset
|
||||
doc_focus: bool | None = None, # v3.5: Doc Focus Gate
|
||||
doc_focus_ts: float | None = None, # v3.5: timestamp активації
|
||||
doc_focus_cooldown_until: float | None = None, # v3.6: cooldown epoch
|
||||
last_photo_ts: float | None = None, # v3.5 fix: timestamp фото
|
||||
) -> None:
|
||||
"""
|
||||
Оновити SessionContext для chat_id.
|
||||
|
||||
- last_messages: append + trim до 3 (зберігає найновіші).
|
||||
- last_agents: встановити нові; trim до 5.
|
||||
- updated_at: time.time()
|
||||
- Fail-safe: не кидає назовні.
|
||||
"""
|
||||
try:
|
||||
if not chat_id:
|
||||
return
|
||||
|
||||
current = _STORE.get(chat_id) or _default_session()
|
||||
session = deepcopy(current)
|
||||
|
||||
# last_messages: append + keep last 3
|
||||
msgs: list[str] = session.get("last_messages") or []
|
||||
if message:
|
||||
msgs.append(message[:500]) # guard against huge messages
|
||||
session["last_messages"] = msgs[-3:]
|
||||
|
||||
# depth, agents, question, pending_action
|
||||
session["last_depth"] = depth
|
||||
new_agents = list(agents or [])[:5]
|
||||
session["last_agents"] = new_agents
|
||||
session["last_question"] = last_question
|
||||
# pending_action: зберігаємо якщо є; якщо None і питання немає — скидаємо
|
||||
if pending_action is not None:
|
||||
session["pending_action"] = pending_action
|
||||
elif not last_question:
|
||||
session["pending_action"] = None
|
||||
|
||||
# v3.2: Fact Lock — merge якщо нові факти є
|
||||
if doc_facts is not None:
|
||||
session["doc_facts"] = doc_facts
|
||||
|
||||
# v3.2: Self-Correction — append новий claim, тримати max 3
|
||||
if fact_claims is not None:
|
||||
existing_claims: list = session.get("fact_claims") or []
|
||||
existing_claims.extend(fact_claims)
|
||||
session["fact_claims"] = existing_claims[-3:]
|
||||
|
||||
# v3.3: Doc Anchor — зберегти active_doc_id
|
||||
if active_doc_id is not None:
|
||||
session["active_doc_id"] = active_doc_id
|
||||
|
||||
# v3.5: Doc Focus Gate
|
||||
if doc_focus is not None:
|
||||
session["doc_focus"] = doc_focus
|
||||
if doc_focus_ts is not None:
|
||||
session["doc_focus_ts"] = doc_focus_ts
|
||||
|
||||
# v3.6: Cooldown
|
||||
if doc_focus_cooldown_until is not None:
|
||||
session["doc_focus_cooldown_until"] = doc_focus_cooldown_until
|
||||
|
||||
# v3.5 fix: Photo timestamp
|
||||
if last_photo_ts is not None:
|
||||
session["last_photo_ts"] = last_photo_ts
|
||||
|
||||
session["updated_at"] = time.time()
|
||||
|
||||
_STORE[chat_id] = session
|
||||
|
||||
tlog(logger, "session_updated", chat_id=chat_id, depth=depth,
|
||||
agents=new_agents)
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("update_session error: %s", exc)
|
||||
|
||||
|
||||
def clear_session(chat_id: str) -> None:
|
||||
"""Примусово очистити сесію (для тестів та ops-команд)."""
|
||||
_STORE.pop(chat_id, None)
|
||||
Reference in New Issue
Block a user