"""AI PMO kernel intent llm-arbitration mixin.""" from __future__ import annotations from . import ai_pmo_kernel_common as _common for _name, _value in vars(_common).items(): if not _name.startswith("__"): globals()[_name] = _value class KernelIntentLLMMixin: async def _resolve_intent_with_llm( self, *, message_text: str, session: Optional[ConversationSession], world_model: dict[str, Any], rule_intent: Optional[CanonicalIntent] = None, ) -> Optional[CanonicalIntent]: project_snapshot = world_model.get("project") if isinstance(world_model.get("project"), dict) else {} project_names = [ str(project_snapshot.get("title") or "").strip(), str(project_snapshot.get("name") or "").strip(), ] route_decision = select_intent_route(message_text, project_names=project_names) correction_decision = kernel_resolve_correction(message_text) forced_low_tier = _should_force_low_tier_intent_route( message_text=message_text, correction_action=correction_decision.action, ) prompt = PROMPT_AI_PMO_INTENT_CLASSIFICATION.format( message_text=str(message_text or "")[:1200], session_last_intent=str(getattr(session, "last_intent", "") or "").strip() if session is not None else "", session_project_id=_safe_int(getattr(session, "current_project_id", None)) if session is not None else None, world_model_project_id=_safe_int(world_model.get("resolved_project_id")), candidate_intents_json=json.dumps(list(INTENT_CANDIDATES), ensure_ascii=False), candidate_routes_json=json.dumps( [ ROUTE_QUERY_ANSWER, ROUTE_PROJECT_CREATE, ROUTE_PROJECT_SIGNAL_CREATE, ROUTE_PROJECT_SIGNAL_DISCOVER, ROUTE_SIGNAL_SOURCE_ONBOARD, ], ensure_ascii=False, ), ).strip() total_budget_seconds = _intent_llm_total_budget_seconds() stage_started_at = time.perf_counter() async def _invoke_intent_stage( *, function_name: str, route_tier: str, route_reasons: list[str], stage_label: str, timeout_cap_seconds: Optional[float] = None, ) -> Optional[CanonicalIntent]: llm_timeout_seconds = _intent_llm_timeout_seconds(function_name=function_name) if timeout_cap_seconds is not None: llm_timeout_seconds = min(llm_timeout_seconds, max(float(timeout_cap_seconds), 0.0)) if llm_timeout_seconds <= 0.0: return None logger.info( "ai_pmo_intent_llm_route", tenant_id=self.tenant_id, stage=stage_label, function_name=function_name, route_tier=route_tier, timeout_seconds=llm_timeout_seconds, forced_low_tier=forced_low_tier, correction_action=correction_decision.action, correction_keywords=correction_decision.matched_keywords, route_reasons=route_reasons, total_budget_seconds=total_budget_seconds, ) try: extraction = await asyncio.wait_for( llm_service.generate_structured_object( prompt, db=self.db, tenant_id=self.tenant_id, function_name=function_name, use_prompt_profile=False, temperature=0.0, max_tokens=260, response_mode="fast", ), timeout=llm_timeout_seconds, ) except TimeoutError: logger.warning( "ai_pmo_intent_llm_timeout", tenant_id=self.tenant_id, stage=stage_label, function_name=function_name, timeout_seconds=llm_timeout_seconds, ) return None except Exception as exc: logger.warning( "ai_pmo_intent_llm_failed", tenant_id=self.tenant_id, stage=stage_label, function_name=function_name, error=str(exc), ) return None payload = extraction.payload if extraction and isinstance(extraction.payload, dict) else None if payload is None: logger.warning( "ai_pmo_intent_llm_empty_payload", tenant_id=self.tenant_id, stage=stage_label, function_name=function_name, error_code=str(getattr(extraction, "error_code", "") or ""), provider=str(getattr(extraction, "provider", "") or ""), model=str(getattr(extraction, "model", "") or ""), ) return None structured_route = RouteResolver.from_structured_payload( payload=payload, message_text=message_text, fallback_project_id=_safe_int(world_model.get("resolved_project_id")), ) intent_name = str(payload.get("intent") or "").strip() if structured_route is not None: intent_name = RouteResolver.canonical_intent_for_route(structured_route.key) if intent_name not in INTENT_CANDIDATES: return None confidence = float(payload.get("confidence") or 0.0) project_id = _safe_int(payload.get("project_id")) if project_id is not None and int(project_id) <= 0: project_id = None if project_id is None: project_id = _safe_int(world_model.get("resolved_project_id")) if structured_route is not None and structured_route.project_id is not None: project_id = structured_route.project_id reasoning = str(payload.get("reasoning") or "").strip() decision_trace = build_decision_trace( intent=intent_name, confidence=confidence, route_tier=route_tier, route_function=function_name, gate_action="unresolved", gate_code="intent_only", correction_action=correction_decision.action, correction_keywords=correction_decision.matched_keywords, complexity_reasons=route_reasons, ) return self._build_canonical_intent( intent_name=intent_name, message_text=message_text, project_id=project_id, confidence=confidence, metadata={ "context_source": "llm", "stage": stage_label, "reasoning": reasoning[:300], "llm_function": function_name, "route_tier": route_tier, "route_complex": route_tier == "high", "route_reasons": route_reasons, "forced_low_tier": forced_low_tier, "timeout_seconds": llm_timeout_seconds, "total_budget_seconds": total_budget_seconds, "correction_action": correction_decision.action, "correction_keywords": correction_decision.matched_keywords, "decision_trace": decision_trace, "workflow_route": structured_route.key if structured_route is not None else LEGACY_INTENT_ROUTE_MAP.get(intent_name, ROUTE_QUERY_ANSWER), "workflow_family": structured_route.family if structured_route is not None else RouteResolver._split_route_key( LEGACY_INTENT_ROUTE_MAP.get(intent_name, ROUTE_QUERY_ANSWER) )[0], "workflow_action": structured_route.action if structured_route is not None else RouteResolver._split_route_key( LEGACY_INTENT_ROUTE_MAP.get(intent_name, ROUTE_QUERY_ANSWER) )[1], "requested_count": ( structured_route.requested_count if structured_route is not None else RouteResolver.extract_requested_count(message_text=message_text, metadata=payload) ), "requires_confirmation": ( structured_route.requires_confirmation if structured_route is not None else bool(payload.get("requires_confirmation")) ), }, ) low_stage_reasons = ["stage1_low_first", *list(route_decision.reasons or [])] if forced_low_tier: low_stage_reasons.append("correction_forced_low_tier") low_intent = await _invoke_intent_stage( function_name=INTENT_LLM_FUNCTION_NAME, route_tier="low", route_reasons=low_stage_reasons, stage_label="stage1_low", ) if forced_low_tier: return low_intent should_escalate, escalate_reasons = _should_escalate_to_high_intent_route( route_is_complex=bool(route_decision.is_complex), low_intent=low_intent, rule_intent=rule_intent, ) if not should_escalate: return low_intent elapsed_seconds = time.perf_counter() - stage_started_at remaining_budget_seconds = total_budget_seconds - elapsed_seconds stage2_min_remaining_seconds = _intent_llm_stage2_min_remaining_seconds() if remaining_budget_seconds < stage2_min_remaining_seconds: logger.info( "ai_pmo_intent_stage2_skipped", tenant_id=self.tenant_id, reason="budget_insufficient", remaining_budget_seconds=max(0.0, remaining_budget_seconds), min_remaining_seconds=stage2_min_remaining_seconds, escalate_reasons=escalate_reasons, ) if low_intent is not None: return self._copy_intent_with_metadata( low_intent, stage2_skipped=True, stage2_skip_reason="budget_insufficient", stage2_reasons=escalate_reasons, ) return None high_stage_reasons = [*list(route_decision.reasons or []), *escalate_reasons, "stage2_high_retry"] high_intent = await _invoke_intent_stage( function_name="classify_intent_complex", route_tier="high", route_reasons=high_stage_reasons, stage_label="stage2_high", timeout_cap_seconds=remaining_budget_seconds, ) if high_intent is not None: if low_intent is not None: return self._copy_intent_with_metadata( high_intent, stage2_promoted=True, stage1_intent=low_intent.name, stage1_confidence=float(low_intent.confidence or 0.0), ) return high_intent if low_intent is not None: return self._copy_intent_with_metadata( low_intent, stage2_attempted=True, stage2_failed=True, stage2_reasons=escalate_reasons, ) return None