Files
life-echo/api/app/agents/chat/interview_agent.py

223 lines
8.9 KiB
Python
Raw Normal View History

"""
InterviewAgent正式访谈 Specialist
负责状态感知回复开场白不负责 Redis 持久化 Orchestrator 统一处理
"""
2026-03-19 14:36:14 +08:00
from typing import Any, List, Optional
2026-03-20 17:25:42 +08:00
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 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__)
2026-03-20 17:25:42 +08:00
# 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,
2026-03-20 17:25:42 +08:00
) -> AgentChatTurn:
"""生成状态感知的访谈回复,不持久化(由 Orchestrator 负责)"""
if not self.llm:
2026-03-20 17:25:42 +08:00
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
2026-03-19 14:36:14 +08:00
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)
2026-03-19 14:36:14 +08:00
response_text = (
response.content if hasattr(response, "content") else str(response)
)
log_agent_payload(
logger, "InterviewAgent.generate_response.raw_response", response_text
)
2026-03-19 14:36:14 +08:00
messages = [
msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()
]
raw_list = messages if messages else [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
]
]
log_agent_summary(
logger,
"InterviewAgent.generate_response segments={} conversation_id={}",
len(out),
conversation_id,
)
2026-03-20 17:25:42 +08:00
return AgentChatTurn(messages=out, skip_tts=False)
except Exception as e:
logger.error("生成回应失败: {}", e, exc_info=True)
2026-03-20 17:25:42 +08:00
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:
2026-03-20 15:15:35 +08:00
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)
2026-03-19 14:36:14 +08:00
response_text = (
response.content if hasattr(response, "content") else str(response)
)
log_agent_payload(
logger, "InterviewAgent.opening.raw_response", response_text
)
2026-03-19 14:36:14 +08:00
messages = [
msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()
]
raw_list = messages if messages else [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,
)
return (
out
if out
else [
response_text.strip()[
: settings.chat_interview_max_chars_per_segment
]
]
)
except Exception as e:
logger.error("生成开场白失败: {}", e, exc_info=True)
2026-03-20 15:15:35 +08:00
return ["你好呀~ 又见面了,最近有没有什么事想跟我说说?"]