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:
Kevin
2026-04-30 09:17:01 +08:00
parent eddb2c3078
commit ac436b87a2
37 changed files with 1400 additions and 199 deletions

View File

@@ -24,19 +24,36 @@ from app.core.agent_logging import agent_span, log_agent_payload, log_agent_summ
from app.core.config import settings
from app.core.dependencies import get_llm_provider
from app.core.llm_call import allm_json_call
from app.core.llm_gateway import LlmGateway, LlmUseCase
from app.core.logging import get_logger
from app.ports.llm import LLMProvider
logger = get_logger(__name__)
def _get_langchain_llm():
try:
provider = get_llm_provider()
return getattr(provider, "langchain_llm", None)
return LlmGateway().langchain_llm_for(LlmUseCase("chat.profile"))
except Exception:
return None
def _langchain_messages_to_port(messages: List[Any]) -> list[dict]:
"""LangChain message 列表 → ``LLMProvider.complete`` 的 ``role/content`` 结构。"""
out: list[dict] = []
for m in messages:
if isinstance(m, SystemMessage):
out.append({"role": "system", "content": str(m.content)})
elif isinstance(m, HumanMessage):
out.append({"role": "user", "content": str(m.content)})
elif isinstance(m, AIMessage):
out.append({"role": "assistant", "content": str(m.content)})
else:
c = getattr(m, "content", None)
out.append({"role": "user", "content": str(c) if c is not None else ""})
return out
def _message_contents_char_count(messages: List[Any]) -> int:
n = 0
for m in messages:
@@ -49,9 +66,15 @@ def _message_contents_char_count(messages: List[Any]) -> int:
class ProfileAgent:
"""用户资料收集 Specialist Agent"""
def __init__(self):
def __init__(self, llm_provider: LLMProvider | None = None):
self._llm_provider = llm_provider
self.llm = _get_langchain_llm()
def _provider(self) -> LLMProvider:
if self._llm_provider is not None:
return self._llm_provider
return get_llm_provider()
async def _invoke_chat(
self,
messages: List[Any],
@@ -60,20 +83,21 @@ class ProfileAgent:
conversation_id: Optional[str],
agent_name: str,
) -> str:
chat_llm = self.llm.bind(max_tokens=max_tokens)
port_messages = _langchain_messages_to_port(messages)
llm_t0 = time.perf_counter()
with agent_span(
logger, f"{agent_name}.llm", conversation_id=conversation_id or ""
):
response = await chat_llm.ainvoke(messages)
response_text = await self._provider().complete(
port_messages,
max_tokens=max_tokens,
)
logger.info(
"event=chat_llm_done agent={} response_latency_ms={:.2f}",
agent_name,
(time.perf_counter() - llm_t0) * 1000,
)
return (
response.content if hasattr(response, "content") else str(response)
) or ""
return response_text or ""
async def _segments_from_response(
self,