Files
microdao-daarion/services/sofiia-console/app/adapters/aistalk.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

263 lines
9.3 KiB
Python

"""
AISTALK Adapter — HTTP bridge integration.
Enables forwarding BFF events/messages to an external AISTALK bridge service.
The adapter is best-effort and non-blocking for callers.
"""
from __future__ import annotations
import logging
import os
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional
import httpx
logger = logging.getLogger(__name__)
def _split_paths(raw: str, default: str) -> List[str]:
src = (raw or "").strip()
if not src:
src = default
parts = [p.strip() for p in src.split(",") if p.strip()]
normalized: List[str] = []
for p in parts:
if not p.startswith("/"):
p = "/" + p
normalized.append(p)
return normalized
class AISTALKAdapter:
"""
AISTALK relay adapter.
Env overrides (optional):
AISTALK_HEALTH_PATHS=/healthz,/health,/api/health
AISTALK_EVENT_PATHS=/api/events,/events,/v1/events
AISTALK_TEXT_PATHS=/api/text,/text,/v1/text
AISTALK_AUDIO_PATHS=/api/audio,/audio,/v1/audio
"""
def __init__(self, base_url: str, api_key: Optional[str] = None) -> None:
self.base_url = base_url.rstrip("/") if base_url else ""
self.api_key = api_key or ""
self._enabled = bool(self.base_url)
self._health_paths = _split_paths(
os.getenv("AISTALK_HEALTH_PATHS", ""),
"/healthz,/health,/api/health",
)
self._event_paths = _split_paths(
os.getenv("AISTALK_EVENT_PATHS", ""),
"/api/events,/events,/v1/events",
)
self._text_paths = _split_paths(
os.getenv("AISTALK_TEXT_PATHS", ""),
"/api/text,/text,/v1/text",
)
self._audio_paths = _split_paths(
os.getenv("AISTALK_AUDIO_PATHS", ""),
"/api/audio,/audio,/v1/audio",
)
self._lock = threading.Lock()
self._last_ok_at: Optional[float] = None
self._last_error: str = ""
self._last_endpoint: str = ""
self._last_probe_ok: Optional[bool] = None
self._last_probe_at: Optional[float] = None
# Fire-and-forget outbound queue to avoid adding latency to BFF handlers.
self._pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="aistalk-relay")
if self._enabled:
logger.info("AISTALKAdapter init: url=%s (HTTP relay mode)", self.base_url)
else:
logger.info("AISTALKAdapter init: no base_url, adapter disabled")
@property
def enabled(self) -> bool:
return self._enabled
def _headers(self) -> Dict[str, str]:
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
headers["X-API-Key"] = self.api_key
return headers
def _mark_ok(self, endpoint: str) -> None:
with self._lock:
self._last_ok_at = time.time()
self._last_error = ""
self._last_endpoint = endpoint
def _mark_err(self, err: str) -> None:
with self._lock:
self._last_error = (err or "")[:300]
def _post_json(self, payload: Dict[str, Any], paths: List[str], kind: str) -> bool:
if not self._enabled:
return False
last_err = "unreachable"
timeout = httpx.Timeout(connect=0.6, read=1.8, write=1.8, pool=0.6)
for path in paths:
endpoint = f"{self.base_url}{path}"
try:
with httpx.Client(timeout=timeout) as client:
r = client.post(endpoint, headers=self._headers(), json=payload)
if 200 <= r.status_code < 300:
self._mark_ok(endpoint)
return True
last_err = f"HTTP {r.status_code} @ {path}"
except Exception as e:
last_err = f"{e.__class__.__name__}: {str(e)[:180]} @ {path}"
continue
self._mark_err(last_err)
logger.debug("AISTALK %s relay failed: %s", kind, last_err)
return False
def _post_audio(self, payload: Dict[str, Any], audio_bytes: bytes, mime: str) -> bool:
if not self._enabled:
return False
last_err = "unreachable"
timeout = httpx.Timeout(connect=0.8, read=2.5, write=2.5, pool=0.8)
for path in self._audio_paths:
endpoint = f"{self.base_url}{path}"
files = {"audio": ("chunk", audio_bytes, mime or "audio/wav")}
data = {"meta": str(payload)}
try:
with httpx.Client(timeout=timeout) as client:
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
headers["X-API-Key"] = self.api_key
r = client.post(endpoint, headers=headers, data=data, files=files)
if 200 <= r.status_code < 300:
self._mark_ok(endpoint)
return True
last_err = f"HTTP {r.status_code} @ {path}"
except Exception as e:
last_err = f"{e.__class__.__name__}: {str(e)[:180]} @ {path}"
continue
self._mark_err(last_err)
logger.debug("AISTALK audio relay failed: %s", last_err)
return False
def _dispatch(self, fn, *args: Any) -> None:
if not self._enabled:
return
try:
self._pool.submit(fn, *args)
except Exception as e:
self._mark_err(str(e))
logger.debug("AISTALK dispatch failed: %s", e)
def send_text(
self,
project_id: str,
session_id: str,
text: str,
user_id: str = "console_user",
) -> None:
if not self._enabled:
return
payload = {
"v": 1,
"type": "chat.reply",
"project_id": project_id,
"session_id": session_id,
"user_id": user_id,
"data": {"text": text},
}
self._dispatch(self._post_json, payload, self._text_paths, "text")
def send_audio(
self,
project_id: str,
session_id: str,
audio_bytes: bytes,
mime: str = "audio/wav",
) -> None:
if not self._enabled:
return
payload = {
"v": 1,
"type": "voice.tts",
"project_id": project_id,
"session_id": session_id,
"user_id": "console_user",
"data": {"mime": mime, "bytes": len(audio_bytes)},
}
self._dispatch(self._post_audio, payload, audio_bytes, mime)
def handle_event(self, event: Dict[str, Any]) -> None:
if not self._enabled:
return
self._dispatch(self._post_json, event, self._event_paths, "event")
def on_event(self, event: Dict[str, Any]) -> None:
self.handle_event(event)
def probe_health(self) -> Dict[str, Any]:
if not self._enabled:
return {"enabled": False, "ok": False, "error": "disabled"}
timeout = httpx.Timeout(connect=0.5, read=1.2, write=1.2, pool=0.5)
last_err = "unreachable"
for path in self._health_paths:
endpoint = f"{self.base_url}{path}"
try:
with httpx.Client(timeout=timeout) as client:
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
headers["X-API-Key"] = self.api_key
r = client.get(endpoint, headers=headers)
if r.status_code < 500:
with self._lock:
self._last_probe_ok = r.status_code == 200
self._last_probe_at = time.time()
if r.status_code == 200:
self._mark_ok(endpoint)
return {"enabled": True, "ok": True, "url": endpoint, "status": r.status_code}
last_err = f"HTTP {r.status_code} @ {path}"
else:
last_err = f"HTTP {r.status_code} @ {path}"
except Exception as e:
last_err = f"{e.__class__.__name__}: {str(e)[:180]} @ {path}"
continue
with self._lock:
self._last_probe_ok = False
self._last_probe_at = time.time()
self._mark_err(last_err)
return {"enabled": True, "ok": False, "error": last_err}
def status(self) -> Dict[str, Any]:
with self._lock:
return {
"enabled": self._enabled,
"base_url": self.base_url,
"last_ok_at": self._last_ok_at,
"last_endpoint": self._last_endpoint,
"last_error": self._last_error,
"last_probe_ok": self._last_probe_ok,
"last_probe_at": self._last_probe_at,
"paths": {
"health": self._health_paths,
"events": self._event_paths,
"text": self._text_paths,
"audio": self._audio_paths,
},
}
def __repr__(self) -> str:
s = self.status()
return (
f"AISTALKAdapter(url={s['base_url']!r}, enabled={s['enabled']}, "
f"last_probe_ok={s['last_probe_ok']}, last_endpoint={s['last_endpoint']!r})"
)