""" ModalityRetryMixin — Modality failure handling for ChatAgent. When a modality skill (TTS, image gen) fails after 2 silent tool retries: 1. Inject failure context into Actor re-run 2. Re-run Express through normal engine pipeline 3. Engine naturally decides response (may choose text, retry modality, etc.) 4. Result goes through normal _flush_buffer delivery No standalone LLM calls — everything goes through the engine. """ from __future__ import annotations class ModalityRetryMixin: """Modality skill failure handling — re-run Express via engine pipeline.""" async def _modality_failure_with_retry( self, failed_modality: str, original_reply: str, express_content: str ) -> str: """Handle modality skill failure by re-running Express through the engine. Called after a modality skill has exhausted its 2 internal tool retries. Injects failure context and re-runs the Actor pass through the normal pipeline (extract_reply -> JSON context -> skill -> _flush_buffer). Returns the new reply text (from re-run Express), or original_reply if the re-run also fails. """ from agent.parser import extract_reply from providers.llm.base import ChatMessage as _CM self._pending_retry = None # reset # ── Inject failure context into Express re-run ── failure_hint = ( f"\n\n(系统提示:角色刚才尝试发送{failed_modality},但发送失败了。" f"请重新选择表达方式回复用户。原始回复内容:「{original_reply[:300]}」)" ) express_prompt = getattr(self, '_last_express_prompt', None) if not express_prompt: print(f" [retry] ⚠ No cached Express prompt, falling back to text") return original_reply try: express_messages = [ _CM(role="system", content=express_prompt + failure_hint), _CM(role="user", content=getattr(self, '_last_user_message', "")), ] retry_response = await self.llm.chat(express_messages, temperature=0.9, max_tokens=500) retry_text = retry_response.content.strip() print(f" [retry] 📝 Re-run Express: {retry_text[:100]}...") # Parse through normal pipeline _, retry_reply, retry_modality = extract_reply(retry_text) # Run modality skill if LLM chose one (through normal JSON context path) if self.modality_skill_engine and retry_modality not in ("文字", "静默", ""): import json as _json import re _SECTION_RE = re.compile( r'(?:【(?P内心独白|最终回复|表达方式)】' r'|\[(?PInner Monologue|Final Reply|Expression Mode)\])' ) _matches = list(_SECTION_RE.finditer(retry_text)) _raw_mod = retry_text[_matches[-1].end():].strip() if _matches else "" structured_context = _json.dumps({ "reply": retry_reply, "modality": retry_modality, "raw_modality": _raw_mod, }, ensure_ascii=False) skill_results = await self.modality_skill_engine.plan_and_execute( raw_modality=_raw_mod, raw_output=structured_context, persona=self.persona, llm=self.llm, ) for skill_result in skill_results: if skill_result.success: self._skill_outputs.update(skill_result.output) print(f" [retry] ✅ Re-run skill succeeded: {retry_modality}") return retry_reply # Re-run skill also failed — fall through to text print(f" [retry] ⚠ Re-run skill also failed, delivering as text") return retry_reply or original_reply except Exception as e: print(f" [retry] ✗ Re-run Express failed: {e}") return original_reply