Files
microdao-daarion/crews/agromatrix_crew/session_context.py
Apple 129e4ea1fc 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
2026-03-03 07:14:14 -08:00

232 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)