feat(api): 收敛对话与记忆流程边界,引入 LLM 网关与专用服务
- MemoryService 异步路径委托 MemoryIngestService / MemoryRetrievalService;富化派发经 MemoryEnrichmentScheduler - WebSocket pipeline 经 ChatTurnService 与显式 DTO 编排单轮对话;回忆录片段入队由 MemoirIngestScheduler 封装 - 新增 LlmGateway(LlmUseCase),各 agent、任务与适配器对齐 ports - 补充 memory 提示适配、runtime 类型、memory-retrieval 文档、ai-touchpoints 说明与扫描脚本及配套测试 Made-with: Cursor
This commit is contained in:
116
api/app/features/conversation/chat_turn.py
Normal file
116
api/app/features/conversation/chat_turn.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Conversation chat turn boundary.
|
||||
|
||||
This module gives the WebSocket pipeline a small, explicit contract for one
|
||||
user turn. It deliberately keeps the existing ``ChatOrchestrator`` behavior
|
||||
intact while making the runtime inputs/outputs visible and testable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.chat import ChatOrchestrator
|
||||
from app.agents.chat.agent_turn import AgentChatTurn
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatTurnInput:
|
||||
"""Transport-neutral input for a single user chat turn."""
|
||||
|
||||
conversation_id: str
|
||||
user_message: str
|
||||
is_from_voice: bool = False
|
||||
voice_session_id: str | None = None
|
||||
user_message_timestamp: datetime | None = None
|
||||
audio_duration_seconds: int | None = None
|
||||
force_skip_tts: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatTurnContext:
|
||||
"""Runtime dependencies needed to execute a turn."""
|
||||
|
||||
db: AsyncSession
|
||||
user: Any | None
|
||||
conversation: Any | None
|
||||
apply_extracted_profile_fn: Callable[..., Any]
|
||||
get_missing_profile_fields_fn: Callable[[Any], list[str]]
|
||||
get_filled_profile_fields_fn: Callable[[Any], dict[str, Any]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatTurnDecision:
|
||||
"""Observable decision metadata for the chat runtime boundary."""
|
||||
|
||||
engine: str = "ChatOrchestrator"
|
||||
route_hint: str = "auto"
|
||||
force_skip_tts: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatTurnResult:
|
||||
"""Stable result shape consumed by conversation persistence and delivery."""
|
||||
|
||||
messages: list[str]
|
||||
skip_tts: bool
|
||||
memory_retrieval_trace: dict[str, Any] | None = None
|
||||
interview_state_meta: dict[str, Any] | None = None
|
||||
decision: ChatTurnDecision = ChatTurnDecision()
|
||||
|
||||
@classmethod
|
||||
def from_agent_turn(
|
||||
cls,
|
||||
turn: AgentChatTurn,
|
||||
*,
|
||||
decision: ChatTurnDecision,
|
||||
) -> "ChatTurnResult":
|
||||
return cls(
|
||||
messages=list(turn.messages or []),
|
||||
skip_tts=bool(turn.skip_tts or decision.force_skip_tts),
|
||||
memory_retrieval_trace=turn.memory_retrieval_trace,
|
||||
interview_state_meta=turn.interview_state_meta,
|
||||
decision=decision,
|
||||
)
|
||||
|
||||
|
||||
class ChatTurnService:
|
||||
"""Executes one chat turn behind an explicit internal contract."""
|
||||
|
||||
def __init__(self, orchestrator: ChatOrchestrator | None = None) -> None:
|
||||
self._orchestrator = orchestrator or ChatOrchestrator()
|
||||
|
||||
async def process_turn(
|
||||
self,
|
||||
turn_input: ChatTurnInput,
|
||||
context: ChatTurnContext,
|
||||
) -> ChatTurnResult:
|
||||
decision = ChatTurnDecision(force_skip_tts=turn_input.force_skip_tts)
|
||||
turn = await self._orchestrator.process_user_message(
|
||||
conversation_id=turn_input.conversation_id,
|
||||
user_message=turn_input.user_message,
|
||||
user=context.user,
|
||||
conversation=context.conversation,
|
||||
is_from_voice=turn_input.is_from_voice,
|
||||
voice_session_id=turn_input.voice_session_id,
|
||||
db=context.db,
|
||||
apply_extracted_profile_fn=context.apply_extracted_profile_fn,
|
||||
get_missing_profile_fields_fn=context.get_missing_profile_fields_fn,
|
||||
get_filled_profile_fields_fn=context.get_filled_profile_fields_fn,
|
||||
user_message_timestamp=turn_input.user_message_timestamp,
|
||||
audio_duration_seconds=turn_input.audio_duration_seconds,
|
||||
)
|
||||
return ChatTurnResult.from_agent_turn(turn, decision=decision)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ChatTurnContext",
|
||||
"ChatTurnDecision",
|
||||
"ChatTurnInput",
|
||||
"ChatTurnResult",
|
||||
"ChatTurnService",
|
||||
]
|
||||
Reference in New Issue
Block a user