"""AWOOOI SRE war-room conversation manager. Conversation generation uses the same controlled provider clients and router as incident analysis. It never performs infrastructure remediation. The fixed policy order is GCP-A Ollama -> GCP-B Ollama -> host111 Ollama -> Claude -> Gemini; paid providers remain eligible only for an explicitly sanitized, run-owned canary with a bounded cost contract. """ from __future__ import annotations import asyncio import hashlib import json import re from dataclasses import dataclass, field from decimal import Decimal, InvalidOperation from html import escape from typing import Any from uuid import uuid4 import structlog from src.core.config import get_settings from src.repositories.incident_repository import get_incident_repository from src.repositories.k8s_repository import get_k8s_repository from src.services.ai_provider_policy import ( PAID_PROVIDER_ORDER, PRODUCTION_OLLAMA_ORDER, PRODUCTION_PROVIDER_ORDER, ) from src.services.ai_providers.interfaces import ( AIResult, is_provider_enabled_by_env, ) from src.services.ai_router import get_ai_executor from src.services.sanitization_service import sanitize from src.utils.timezone import now_taipei logger = structlog.get_logger(__name__) OPENCLAW_PERSONA = """你是 OpenClaw,AWOOOI 平台的 SRE AI 首席顧問。 個性: 精準、果斷、專業,直接給出有證據的判讀。 語氣: 簡短有力,繁體中文,不超過 300 字。 稱呼用戶為「老闆」。 你只能分析與討論;不得執行、宣稱執行或跨 domain 指派修復。 """ NEMOCLAW_PERSONA = """你是 NemoClaw,AWOOOI 平台的 AI 戰術參謀。 個性: 分析型、質疑假設,補充不同角度。 語氣: 建設性、繁體中文,不超過 200 字。 稱呼用戶為「老闆」。評論 OpenClaw 時說「我補充」或「我有不同看法」。 你只能分析與討論;不得執行、宣稱執行或跨 domain 指派修復。 """ _CORRELATION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,191}$") _RECEIPT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,191}$") _PROVIDER_STATUS_TERMS = ( "狀態", "可用", "啟用", "鎖定", "硬鎖", "provider status", "route status", ) _PROVIDER_TERMS = ( "ollama", "gcp-a", "gcp-b", "111", "claude", "gemini", "provider", "模型", "路由", ) _IPV4_OCTET = r"(?:25[0-5]|2[0-4]\d|1?\d?\d)" _PRIVATE_IPV4 = re.compile( rf"\b(?:10\.{_IPV4_OCTET}\.{_IPV4_OCTET}\.{_IPV4_OCTET}" rf"|192\.168\.{_IPV4_OCTET}\.{_IPV4_OCTET}" rf"|172\.(?:1[6-9]|2\d|3[01])\.{_IPV4_OCTET}\.{_IPV4_OCTET})\b" ) _SANITIZER_PRIVATE_IP_MARKER = re.compile(r"\[PRIVATE_IP:[^\]\r\n]+\]") def _cloud_chat_dlp(text: str) -> str: """Remove RFC1918 values after the legacy sanitizer's visible marker.""" redacted = _SANITIZER_PRIVATE_IP_MARKER.sub("[PRIVATE_IP_REDACTED]", text) return _PRIVATE_IPV4.sub("[PRIVATE_IP_REDACTED]", redacted) @dataclass(frozen=True) class ChatRouteResult: """Content plus public-safe execution evidence for one chat generation.""" text: str success: bool provider: str model: str fallback_reason: str policy_order: tuple[str, ...] = PRODUCTION_PROVIDER_ORDER admitted_provider_order: tuple[str, ...] = PRODUCTION_OLLAMA_ORDER cost_receipt_id: str | None = None tokens: int = 0 cost_usd: float = 0.0 runtime_readback: dict[str, Any] = field(default_factory=dict) infrastructure_remediation_write_allowed: bool = False cross_domain_execution_allowed: bool = False def to_dict(self) -> dict[str, Any]: return { "schema_version": "sre_chat_provider_route_receipt_v1", "success": self.success, "text": self.text, "provider": self.provider, "model": self.model, "fallback_reason": self.fallback_reason, "policy_order": list(self.policy_order), "admitted_provider_order": list(self.admitted_provider_order), "cost_receipt_id": self.cost_receipt_id, "tokens": self.tokens, "cost_usd": round(self.cost_usd, 8), "runtime_readback": dict(self.runtime_readback), "infrastructure_remediation_write_allowed": False, "cross_domain_execution_allowed": False, } def render_html(self) -> str: body = escape(self.text or "AI 路由目前無可驗證回應。", quote=False) receipt = self.cost_receipt_id or "none" evidence = ( f"provider={self.provider} | model={self.model} | " f"fallback={self.fallback_reason} | cost_receipt={receipt}" ) return f"{body}\n\n🤖 {escape(evidence, quote=False)}" class ChatManager: """AWOOOI SRE chat manager with a fail-closed provider boundary.""" async def get_system_context(self) -> str: """Collect a bounded live context for advisory conversation only.""" now = now_taipei() k8s = get_k8s_repository() incidents = get_incident_repository() try: k8s_status = await k8s.get_pod_status_summary(namespace="awoooi-prod") cluster_info = ( f"Cluster: {k8s_status['running']}/{k8s_status['total']} Pods Running" ) if k8s_status.get("problem_pods"): cluster_info += f", {len(k8s_status['problem_pods'])} 異常" except Exception: cluster_info = "Cluster: 無法取得狀態" try: active_incidents = await incidents.get_active() if active_incidents: lines = [ ( f"- {incident.incident_id}: {incident.status.value} " f"(SEV {incident.severity.value})" ) for incident in active_incidents[:3] ] incident_summary = "\n".join(lines) else: incident_summary = "無活躍告警" except Exception: incident_summary = "無法取得告警" return ( f"## 系統狀態 ({now.strftime('%Y-%m-%d %H:%M')} 台北)\n" f"- {cluster_info}\n" f"- 活躍告警: {incident_summary}\n" ) @staticmethod def _is_provider_status_question(message: str) -> bool: normalized = message.casefold() return any(term in normalized for term in _PROVIDER_TERMS) and any( term in normalized for term in _PROVIDER_STATUS_TERMS ) @staticmethod def _safe_correlation(value: Any, fallback: str) -> str: candidate = str(value or "").strip() return candidate if _CORRELATION_ID.fullmatch(candidate) else fallback @staticmethod def _paid_canary_context_is_bounded(route_context: dict[str, Any]) -> bool: if route_context.get("synthetic_validation") is not True: return False if route_context.get("data_classification") != "sanitized": return False if route_context.get("infrastructure_remediation_write_allowed") is not False: return False if not all( _CORRELATION_ID.fullmatch(str(route_context.get(field) or "")) for field in ("trace_id", "run_id", "work_item_id") ): return False try: cost_cap = Decimal(str(route_context["paid_canary_max_cost_usd"])) output_cap = int(route_context["paid_canary_max_output_tokens"]) except (InvalidOperation, KeyError, TypeError, ValueError): return False return bool( cost_cap.is_finite() and Decimal("0") < cost_cap <= Decimal("0.25") and 1 <= output_cap <= 512 ) async def _paid_provider_candidates( self, route_context: dict[str, Any] | None, ) -> tuple[list[str], dict[str, Any]]: """Return paid lanes admitted before their atomic reservation step.""" payload = dict(route_context or {}) readback: dict[str, Any] = { "schema_version": "sre_chat_paid_canary_admission_v1", "status": "not_requested", "run_owned_lease_verified": False, "cost_policy_preflight_verified": False, "candidates": [], } if not self._paid_canary_context_is_bounded(payload): readback["status"] = "bounded_canary_context_missing" return [], readback run_id = str(payload["run_id"]) try: from src.services.ai_control import ( get_paid_provider_canary_control_readback, is_provider_disabled, ) control = await get_paid_provider_canary_control_readback( expected_owner=run_id, ) if not control.get("leased_enabled"): readback["status"] = "run_owned_canary_not_admitted" return [], readback readback["run_owned_lease_verified"] = True from src.services.ai_rate_limiter import get_ai_rate_limiter limiter = get_ai_rate_limiter() settings = get_settings() admitted: list[str] = [] provider_states: dict[str, str] = {} for provider in PAID_PROVIDER_ORDER: prefix = provider.upper() configured = bool(getattr(settings, f"{prefix}_API_KEY", "")) mode = ( str(getattr(settings, f"{prefix}_EXECUTION_MODE", "shadow")) .strip() .lower() ) percent = max( 0, min(100, int(getattr(settings, f"{prefix}_CANARY_PERCENT", 0))), ) bucket = int(hashlib.sha256(run_id.encode()).hexdigest()[:8], 16) % 100 if not ( configured and is_provider_enabled_by_env(provider) and mode == "canary" and percent > 0 and bucket < percent ): provider_states[provider] = "configured_canary_not_selected" continue if await is_provider_disabled(provider, run_id=run_id): provider_states[provider] = "run_owned_canary_not_admitted" continue usage = await limiter.get_usage_stats(provider) pricing = usage.get("pricing_policy") or {} if ( pricing.get("supported") is not True or usage.get("cost_exceeded") is True ): provider_states[provider] = "cost_policy_not_ready" continue admitted.append(provider) provider_states[provider] = "admitted_atomic_reservation_pending" readback.update( status=("admitted" if admitted else "no_paid_candidate_admitted"), cost_policy_preflight_verified=bool(admitted), candidates=list(admitted), provider_states=provider_states, ) return admitted, readback except Exception as error: logger.warning( "sre_chat_paid_canary_admission_unavailable", error_type=type(error).__name__, ) readback["status"] = "paid_admission_readback_unavailable" return [], readback async def get_provider_runtime_readback( self, route_context: dict[str, Any] | None = None, ) -> dict[str, Any]: """Read current provider policy/health without guessing a lock state.""" health = {provider: "not_checked" for provider in PRODUCTION_OLLAMA_ORDER} selected_primary = "ollama_gcp_a" observed_fallback: str | None = None health_status = "unavailable" try: from src.services.ollama_failover_manager import ( get_ollama_failover_manager, ) route = await get_ollama_failover_manager().select_provider( task_type="chat_status", emit_side_effects=False, ) selected_primary = route.primary.provider_name observed_fallback = ( route.observed_fallback.provider_name if route.observed_fallback else None ) health["ollama_gcp_a"] = route.health_gcp_a.status.value if route.health_gcp_b is not None: health["ollama_gcp_b"] = route.health_gcp_b.status.value if route.health_local is not None: health["ollama_local"] = route.health_local.status.value health_status = "verified" except Exception as error: logger.warning( "sre_chat_ollama_runtime_readback_unavailable", error_type=type(error).__name__, ) paid_candidates, paid_admission = await self._paid_provider_candidates( route_context ) bounded_context = self._paid_canary_context_is_bounded( dict(route_context or {}) ) paid_control: dict[str, Any] = { "readback_status": "unavailable", "route_lease_state": "readback_unavailable", "lock_present": False, "lock_ttl_seconds": -2, "disabled_verified": False, } try: from src.services.ai_control import ( get_paid_provider_canary_control_readback, ) expected_owner = ( str((route_context or {}).get("run_id") or "") if bounded_context else None ) control = await get_paid_provider_canary_control_readback( expected_owner=expected_owner, ) if paid_admission.get("run_owned_lease_verified") is True: route_lease_state = "active_for_this_run" elif control.get("legacy_persistent_state_detected") is True: route_lease_state = "legacy_persistent_state_detected" elif control.get("lock_present") is True: route_lease_state = "active_for_another_run" elif control.get("disabled_verified") is True: route_lease_state = "inactive_disabled_after_canary" else: route_lease_state = "inconsistent_fail_closed" paid_control = { "readback_status": "verified", "route_lease_state": route_lease_state, "lock_present": bool(control.get("lock_present")), "lock_ttl_seconds": int(control.get("lock_ttl_seconds") or -1), "claude_disabled": bool(control.get("claude_disabled", True)), "gemini_disabled": bool(control.get("gemini_disabled", True)), "disabled_verified": bool(control.get("disabled_verified")), "persistent_enabled": bool(control.get("persistent_enabled")), "legacy_persistent_state_detected": bool( control.get("legacy_persistent_state_detected") ), } except Exception as error: logger.warning( "sre_chat_paid_control_readback_unavailable", error_type=type(error).__name__, ) authentication_readbacks: dict[str, dict[str, Any]] = {} try: from src.services.ai_rate_limiter import get_ai_rate_limiter limiter = get_ai_rate_limiter() raw_authentication = await asyncio.gather( *( limiter.get_provider_authentication_status(provider) for provider in PAID_PROVIDER_ORDER ), return_exceptions=True, ) for provider, authentication in zip( PAID_PROVIDER_ORDER, raw_authentication, strict=True, ): if isinstance(authentication, dict): authentication_readbacks[provider] = authentication else: authentication_readbacks[provider] = { "authenticated": None, "authentication_verified": False, "verification_status": "authentication_readback_unavailable", } except Exception as error: logger.warning( "sre_chat_paid_authentication_readback_unavailable", error_type=type(error).__name__, ) settings = get_settings() paid: dict[str, Any] = {} provider_states = paid_admission.get("provider_states") or {} for provider in PAID_PROVIDER_ORDER: prefix = provider.upper() configured = bool(getattr(settings, f"{prefix}_API_KEY", "")) execution_mode = ( str(getattr(settings, f"{prefix}_EXECUTION_MODE", "shadow")) .strip() .lower() ) canary_percent = int(getattr(settings, f"{prefix}_CANARY_PERCENT", 0)) authentication = authentication_readbacks.get(provider) or { "authenticated": None, "authentication_verified": False, "verification_status": "authentication_readback_unavailable", } if provider in paid_candidates: runtime_state = "run_owned_canary_admitted" elif not configured: runtime_state = "not_configured" elif not is_provider_enabled_by_env(provider): runtime_state = "disabled_by_environment" elif execution_mode != "canary" or canary_percent <= 0: runtime_state = "configured_non_canary" elif authentication.get("authenticated") is False: runtime_state = "configured_canary_authentication_rejected" elif authentication.get("authentication_verified") is not True: runtime_state = "configured_canary_authentication_not_verified" elif paid_control["route_lease_state"] == "inactive_disabled_after_canary": runtime_state = ( "configured_authenticated_canary_waiting_for_run_owned_lease" ) else: runtime_state = str( provider_states.get(provider) or paid_admission.get("status") or "runtime_readback_unavailable" ) paid[provider] = { "configured": configured, "environment_enabled": is_provider_enabled_by_env(provider), "execution_mode": execution_mode, "canary_percent": canary_percent, "authenticated": authentication.get("authenticated"), "authentication_verified": bool( authentication.get("authentication_verified") is True ), "verification_status": str( authentication.get("verification_status") or "authentication_readback_unavailable" ), "route_lease_state": paid_control["route_lease_state"], "runtime_state": runtime_state, "run_owned_admitted": provider in paid_candidates, } return { "schema_version": "sre_chat_provider_runtime_readback_v1", "policy_order": list(PRODUCTION_PROVIDER_ORDER), "selected_primary": selected_primary, "observed_fallback": observed_fallback, "health_readback_status": health_status, "ollama_health": health, "paid": paid, "paid_admission": paid_admission, "paid_control": paid_control, "provider_route_switch_performed": False, "infrastructure_remediation_write_allowed": False, "cross_domain_execution_allowed": False, } @staticmethod def _format_provider_runtime_readback(readback: dict[str, Any]) -> str: health = readback.get("ollama_health") or {} paid = readback.get("paid") or {} def paid_line(label: str, provider: str) -> str: state = paid.get(provider) or {} authenticated = state.get("authenticated") authenticated_text = ( "true" if authenticated is True else "false" if authenticated is False else "unknown" ) return ( f"{label}:configured={str(bool(state.get('configured'))).lower()}|" f"authenticated={authenticated_text}|" f"auth_verified={str(bool(state.get('authentication_verified'))).lower()}|" f"verification={state.get('verification_status', 'authentication_readback_unavailable')}|" f"canary={state.get('execution_mode', 'unknown')} " f"{int(state.get('canary_percent') or 0)}%|" f"route_lease={state.get('route_lease_state', 'readback_unavailable')}" ) return "\n".join( [ "AI 路由即時讀回", "順序:GCP-A → GCP-B → 111 → Claude → Gemini", ( "Ollama:" f"GCP-A={health.get('ollama_gcp_a', 'not_checked')};" f"GCP-B={health.get('ollama_gcp_b', 'not_checked')};" f"111={health.get('ollama_local', 'not_checked')}" ), paid_line("Claude", "claude"), paid_line("Gemini", "gemini"), "此讀回未切換 provider,也未執行任何主機或服務修復。", ] ) @staticmethod def _result_provider(provider: str) -> str: # The registry's GCP-A compatibility alias returns ``ollama``. Within # this fixed executor order that identity can only be the first GCP-A hop. return "ollama_gcp_a" if provider == "ollama" else provider @staticmethod def _result_model(provider: str, requested_ollama_model: str) -> str: settings = get_settings() if provider in {"ollama_gcp_a", "ollama_gcp_b"}: return str(settings.OLLAMA_HEALTH_CHECK_MODEL) if provider == "ollama_local": return requested_ollama_model if provider == "claude": return str(settings.CLAUDE_MODEL) if provider == "gemini": return str(settings.GEMINI_MODEL) return "none" @staticmethod def _fallback_reason(result: AIResult, provider: str) -> str: if not result.success: status = str(result.audit_metadata.get("status") or "").strip() if status and re.fullmatch(r"[a-z0-9_:-]{1,96}", status): return f"terminal_{status}" return "canonical_route_exhausted_or_policy_blocked" if result.from_cache: return "current_policy_cache_hit" try: position = PRODUCTION_PROVIDER_ORDER.index(provider) + 1 except ValueError: return "provider_identity_outside_policy" if position == 1: return "canonical_primary_succeeded" return f"canonical_position_{position}_after_prior_hops_no_success" @staticmethod def _cost_receipt_id(result: AIResult, provider: str) -> str | None: if provider not in PAID_PROVIDER_ORDER: return None candidate = str( result.audit_metadata.get("generation_receipt_id") or "" ).strip() return candidate if _RECEIPT_ID.fullmatch(candidate) else None @staticmethod def _plain_response(raw_response: str) -> str: text = re.sub( r".*?", "", str(raw_response or ""), flags=re.DOTALL, ).strip() if not text: return "" try: payload = json.loads(text) except (TypeError, ValueError): return text if isinstance(payload, dict): for field_name in ("response", "message", "description", "reasoning"): value = payload.get(field_name) if isinstance(value, str) and value.strip(): return value.strip() return text async def route_chat( self, *, role: str, system_prompt: str, user_message: str, route_context: dict[str, Any] | None = None, ) -> ChatRouteResult: """Generate one advisory response through the controlled provider route.""" if self._is_provider_status_question(user_message): readback = await self.get_provider_runtime_readback(route_context) return ChatRouteResult( text=self._format_provider_runtime_readback(readback), success=True, provider="runtime_readback", model="deterministic_policy", fallback_reason="provider_status_request_no_generation", admitted_provider_order=PRODUCTION_OLLAMA_ORDER, runtime_readback=readback, ) persona = OPENCLAW_PERSONA if role == "openclaw" else NEMOCLAW_PERSONA requested_ollama_model = ( str(get_settings().OPENCLAW_DEFAULT_MODEL) if role == "openclaw" else "deepseek-r1:14b" ) prompt = _cloud_chat_dlp( sanitize( ( f"{persona}\n\n{system_prompt}\n\n" f"使用者問題:\n{user_message}\n\n" "只能回覆分析;不得呼叫 executor、不得聲稱完成修復。" ), source_label="awoooi_sre_group_chat", ) ) prompt_sha256 = hashlib.sha256(prompt.encode("utf-8")).hexdigest() seed = uuid4().hex requested_context = dict(route_context or {}) trace_id = self._safe_correlation( requested_context.get("trace_id"), f"sre-chat-trace:{seed}", ) run_id = self._safe_correlation( requested_context.get("run_id"), f"sre-chat-run:{seed}", ) work_item_id = self._safe_correlation( requested_context.get("work_item_id"), "SRE-CHAT-ADVISORY", ) context: dict[str, Any] = { "trace_id": trace_id, "run_id": run_id, "work_item_id": work_item_id, "intent_hint": "chat", "task_type": "chat", "data_classification": "sanitized", # Ordinary SRE conversation is real traffic, never synthetic. Its # GCP boundary is the exact prompt-bound chat receipt below. "sre_chat_sanitization_receipt": { "schema_version": "sre_chat_sanitization_receipt_v1", "status": "verified", "sanitizer": "src.services.sanitization_service.sanitize", "source_label": "awoooi_sre_group_chat", "data_classification": "sanitized", "prompt_sha256": prompt_sha256, "trace_id": trace_id, "run_id": run_id, "work_item_id": work_item_id, "raw_input_forwarded": False, "raw_log_payload_forwarded": False, "secret_value_exposed": False, "private_network_value_exposed": False, "infrastructure_remediation_write_allowed": False, }, "ollama_model": requested_ollama_model, "enforce_ollama_first": True, "cloud_transport_auto_preflight": True, "infrastructure_remediation_write_allowed": False, "executor_invocation_allowed": False, "agent99_dispatch_allowed": False, "cross_domain_execution_allowed": False, "canonical_asset_identity": { "canonical_id": "service:awoooi:sre-war-room-chat", "resolution_status": "resolved", "asset_domain": "conversation", "circuit_state": "closed", }, } bounded_canary = self._paid_canary_context_is_bounded(requested_context) require_local = False if bounded_canary: context.update( # Synthetic markers are reserved for the explicitly bounded # paid-provider canary contract and bind its exact prompt. synthetic_validation=True, synthetic_prompt_sha256=prompt_sha256, paid_canary_max_cost_usd=str( requested_context["paid_canary_max_cost_usd"] ), paid_canary_max_output_tokens=int( requested_context["paid_canary_max_output_tokens"] ), ) else: # Interactive chat must not wait indefinitely on one cold Ollama # hop. Paid validation canaries retain their separate 300s client # contract and therefore do not inherit this interactive timeout. context["provider_timeout_seconds"] = 35 context["sre_chat_sanitization_receipt"]["provider_timeout_seconds"] = 35 paid_candidates, admission = await self._paid_provider_candidates( {**requested_context, **context} if bounded_canary else None ) admitted_order = [*PRODUCTION_OLLAMA_ORDER, *paid_candidates] result = await get_ai_executor().execute( prompt, provider_order=admitted_order, context=context, cache_ttl=300, require_local=require_local, ) provider = self._result_provider(str(result.provider or "none")) receipt_id = self._cost_receipt_id(result, provider) paid_cost_cap = float(context.get("paid_canary_max_cost_usd") or 0.0) paid_receipt_invalid = bool( result.success and provider in PAID_PROVIDER_ORDER and ( not receipt_id or float(result.cost_usd or 0.0) <= 0.0 or float(result.cost_usd or 0.0) > paid_cost_cap ) ) reported_admitted_order = admitted_order if paid_receipt_invalid: logger.error( "sre_chat_paid_result_missing_cost_receipt", provider=provider, ) return ChatRouteResult( text="付費 Provider 回應缺少費用 receipt,已拒絕呈現。", success=False, provider=provider, model=self._result_model(provider, requested_ollama_model), fallback_reason="paid_generation_cost_receipt_invalid", admitted_provider_order=tuple(reported_admitted_order), runtime_readback={"paid_admission": admission}, ) body = self._plain_response(result.raw_response) if not result.success or not body: readback = await self.get_provider_runtime_readback( requested_context if bounded_canary else None ) body = ( "目前沒有可驗證的模型回應;以下狀態來自 runtime readback," "不是推測,也未執行修復。\n" + self._format_provider_runtime_readback(readback) ) else: readback = {"paid_admission": admission} logger.info( "sre_chat_provider_route_completed", provider=provider, model=self._result_model(provider, requested_ollama_model), success=bool(result.success and body), fallback_reason=self._fallback_reason(result, provider), cost_receipt_present=bool(receipt_id), infrastructure_remediation_write_allowed=False, cross_domain_execution_allowed=False, ) return ChatRouteResult( text=body, success=bool(result.success and body), provider=provider, model=self._result_model(provider, requested_ollama_model), fallback_reason=self._fallback_reason(result, provider), admitted_provider_order=tuple(reported_admitted_order), cost_receipt_id=receipt_id, tokens=max(0, int(result.tokens or 0)), cost_usd=max(0.0, float(result.cost_usd or 0.0)), runtime_readback=readback, ) async def _call_openclaw( self, system_prompt: str, user_message: str, route_context: dict[str, Any] | None = None, ) -> str | None: result = await self.route_chat( role="openclaw", system_prompt=system_prompt, user_message=user_message, route_context=route_context, ) return result.render_html() async def _call_nemotron( self, system_prompt: str, user_message: str, route_context: dict[str, Any] | None = None, ) -> str | None: result = await self.route_chat( role="nemoclaw", system_prompt=system_prompt, user_message=user_message, route_context=route_context, ) return result.render_html() async def generate_response( self, user_id: int, # noqa: ARG002 username: str, # noqa: ARG002 message_text: str, ) -> str: """Generate the requested single- or dual-persona response.""" context = await self.get_system_context() text = message_text.strip() if text.lower().startswith("@openclaw"): message = text[9:].strip() or text result = await self._call_openclaw(context, message) return f"🦞 OpenClaw:\n{result or '🔴 OpenClaw 無響應'}" if text.lower().startswith("@nemo"): message = text[5:].strip() or text result = await self._call_nemotron(context, message) return f"🤖 NemoClaw:\n{result or '🔴 NemoClaw 無響應'}" openclaw_task = asyncio.create_task(self._call_openclaw(context, text)) nemo_task = asyncio.create_task( self._call_nemotron( context, f"老闆問了: {text}\n\n請從 NemoClaw 角度補充或評論。", ) ) try: openclaw_raw = await asyncio.wait_for(openclaw_task, timeout=40.0) except TimeoutError: openclaw_raw = None openclaw_block = f"🦞 OpenClaw:\n{openclaw_raw or '🔴 無響應'}" try: nemo_raw = await asyncio.wait_for(nemo_task, timeout=60.0) except TimeoutError: nemo_raw = None if nemo_raw: return f"{openclaw_block}\n\n🤖 NemoClaw:\n{nemo_raw}" return openclaw_block _chat_manager: ChatManager | None = None def get_chat_manager() -> ChatManager: global _chat_manager if _chat_manager is None: _chat_manager = ChatManager() return _chat_manager