""" InterviewAgent:正式访谈 Specialist 负责状态感知回复、开场白,不负责 Redis 持久化(由 Orchestrator 统一处理) """ from typing import Any, List, Optional from app.agents.chat.agent_turn import AgentChatTurn from app.agents.chat.stage_detection import keyword_fallback_primary_stage from app.core.dependencies import get_llm_provider from app.core.logging import get_logger from app.agents.chat.helpers import format_history_string, get_history_messages from app.agents.chat.prompts_conversation import ( SLOT_NAME_MAP, get_guided_conversation_prompt, get_opening_prompt, ) from app.agents.state_schema import MemoirStateSchema from app.agents.chat.reply_limits import ( nonempty_segments_or_fallback, segments_from_llm_response, truncate_chat_segments, ) from app.core.agent_logging import ( agent_span, log_agent_payload, log_agent_summary, ) from app.core.config import settings logger = get_logger(__name__) # LLM 不可用或调用失败时对用户展示(不暴露异常细节、不触发 TTS) _FALLBACK_REPLY = "刚才网络不太稳,没接上。你可以再说一遍,或稍后再试。" def _get_langchain_llm(): try: provider = get_llm_provider() return getattr(provider, "langchain_llm", None) except Exception: return None class InterviewAgent: """正式访谈 Specialist Agent""" def __init__(self): self.llm = _get_langchain_llm() def _detect_user_stage(self, user_message: str) -> str: """关键词回退:与 stage_detection 一致(多阶段打分)。""" return keyword_fallback_primary_stage(user_message) def _estimate_same_topic_turns( self, history_messages: List[Any], current_filled_slots: dict ) -> int: """估算同一话题的连续轮数""" if len(history_messages) < 4: return len(history_messages) // 2 recent_messages = history_messages[-6:] keywords_per_turn = [] for i in range(0, len(recent_messages), 2): if i + 1 < len(recent_messages): human_msg = ( recent_messages[i].content if hasattr(recent_messages[i], "content") else str(recent_messages[i]) ) ai_msg = ( recent_messages[i + 1].content if hasattr(recent_messages[i + 1], "content") else str(recent_messages[i + 1]) ) keywords_per_turn.append((human_msg + ai_msg)[:100]) if len(keywords_per_turn) >= 3: return 3 return len(keywords_per_turn) async def generate_response_with_state( self, conversation_id: str, user_message: str, memoir_state: MemoirStateSchema, user_profile_context: str = "", detected_user_stage: Optional[str] = None, ) -> AgentChatTurn: """生成状态感知的访谈回复,不持久化(由 Orchestrator 负责)""" if not self.llm: logger.warning("InterviewAgent: LLM 未配置,返回兜底文案") return AgentChatTurn(messages=[_FALLBACK_REPLY], skip_tts=True) try: empty_slots = memoir_state.empty_slots_for_current_stage() filled_slots = { key: value.snippet for key, value in memoir_state.slots.get( memoir_state.current_stage, {} ).items() if value.snippet } if detected_user_stage is not None: du = detected_user_stage else: du = self._detect_user_stage(user_message) history_messages = await get_history_messages(conversation_id) conversation_turn = len(history_messages) // 2 same_topic_turns = self._estimate_same_topic_turns( history_messages, filled_slots ) all_stages_coverage = memoir_state.all_stages_coverage() system_prompt = get_guided_conversation_prompt( current_stage=memoir_state.current_stage, empty_slots=empty_slots, filled_slots=filled_slots, user_message=user_message, conversation_turn=conversation_turn, same_topic_turns=same_topic_turns, all_stages_coverage=all_stages_coverage, detected_user_stage=du, user_profile_context=user_profile_context, ) history_string = format_history_string(history_messages) full_prompt = f"{system_prompt}\n\n{history_string}\n\nHuman: {user_message}\n\nAssistant:" log_agent_payload( logger, "InterviewAgent.generate_response.prompt", full_prompt ) chat_llm = self.llm.bind(max_tokens=settings.chat_interview_max_tokens) with agent_span( logger, "InterviewAgent.generate_response.llm", conversation_id=conversation_id, stage=memoir_state.current_stage, ): response = await chat_llm.ainvoke(full_prompt) response_text = ( response.content if hasattr(response, "content") else str(response) ) log_agent_payload( logger, "InterviewAgent.generate_response.raw_response", response_text ) raw_list = segments_from_llm_response( response_text, max_segments=settings.chat_interview_max_segments, ) if not raw_list: raw_list = [response_text.strip()] out = truncate_chat_segments( raw_list, max_segments=settings.chat_interview_max_segments, max_chars_per_segment=settings.chat_interview_max_chars_per_segment, ) if not out: out = [ response_text.strip()[ : settings.chat_interview_max_chars_per_segment ] ] out = nonempty_segments_or_fallback(out, fallback=_FALLBACK_REPLY) log_agent_summary( logger, "InterviewAgent.generate_response segments={} conversation_id={}", len(out), conversation_id, ) return AgentChatTurn(messages=out, skip_tts=False) except Exception as e: logger.error("生成回应失败: {}", e, exc_info=True) return AgentChatTurn(messages=[_FALLBACK_REPLY], skip_tts=True) async def generate_opening_message( self, conversation_id: str, memoir_state: MemoirStateSchema, user_profile_context: str = "", ) -> List[str]: """生成空对话开场白,不持久化(由 Orchestrator 负责)""" if not self.llm: return ["你好呀~ 又见面了,今天有没有哪段回忆或近况想聊聊?"] try: empty_slots = memoir_state.empty_slots_for_current_stage() empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots] prompt = get_opening_prompt( current_stage=memoir_state.current_stage, empty_slots_readable=empty_slots_readable, user_profile_context=user_profile_context, ) full_prompt = f"{prompt}\n\nAssistant:" log_agent_payload(logger, "InterviewAgent.opening.prompt", full_prompt) opening_llm = self.llm.bind(max_tokens=settings.chat_opening_max_tokens) with agent_span( logger, "InterviewAgent.opening.llm", conversation_id=conversation_id, ): response = await opening_llm.ainvoke(full_prompt) response_text = ( response.content if hasattr(response, "content") else str(response) ) log_agent_payload( logger, "InterviewAgent.opening.raw_response", response_text ) raw_list = segments_from_llm_response(response_text, max_segments=2) if not raw_list: raw_list = [response_text.strip()] out = truncate_chat_segments( raw_list, max_segments=2, max_chars_per_segment=settings.chat_interview_max_chars_per_segment, ) log_agent_summary( logger, "InterviewAgent.opening segments={} conversation_id={}", len(out), conversation_id, ) segments = ( out if out else [ response_text.strip()[ : settings.chat_interview_max_chars_per_segment ] ] ) return nonempty_segments_or_fallback( segments, fallback="你好呀~ 又见面了,最近有没有什么事想跟我说说?", ) except Exception as e: logger.error("生成开场白失败: {}", e, exc_info=True) return ["你好呀~ 又见面了,最近有没有什么事想跟我说说?"]