Add AgentSessionService and refactor agent routes
Move session-related responsibilities into a new application-layer AgentSessionService (and AgentSessionFeedbackResult dataclass), provide a bootstrap factory (get_agent_session_service), and update agent API routes to call the service instead of accessing ConversationStore directly. Routes now translate ValueError into 404 responses and use service methods for get/list/history/delete/feedback. Also update package exports and docs/READMEs to declare the backend architecture authority, enforce api -> application -> domain ports -> infrastructure boundaries, and call out legacy services/workflows as migration-only. These changes centralize session logic in the application layer and tighten architecture guidance for future backend work.
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
|
||||
`backend` 是当前正式使用的 FastAPI 后端目录,入口为 `app.main:app`。
|
||||
|
||||
## 架构约束入口
|
||||
|
||||
- Backend authoritative architecture 文档:`docs/architecture/backend-project-architecture.md`
|
||||
- Backend migration RFC:`docs/rfc/backend-api-parsing-embedding-migration-requirements.md`
|
||||
- 后续 backend 新增功能和重构默认遵守:`api -> application -> domain ports -> infrastructure`
|
||||
- `backend/app/services/*` 与 `backend/app/workflows/*` 为迁移期 legacy 目录,除迁移或兼容修复外,不应新增业务编排逻辑。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
@@ -34,10 +41,15 @@ PYTHONPATH=backend uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```text
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── api/ # FastAPI 路由与模型
|
||||
│ ├── config/ # 配置与日志
|
||||
│ ├── services/ # 文档处理、LLM、RAG、存储
|
||||
│ └── workers/ # 任务相关代码
|
||||
│ ├── api/ # FastAPI 路由与 transport models
|
||||
│ ├── application/ # 用例编排层
|
||||
│ ├── domain/ # 核心业务模型与稳定端口
|
||||
│ ├── infrastructure/ # 外部系统适配器
|
||||
│ ├── shared/ # composition root 与横切支撑
|
||||
│ ├── config/ # 配置与日志
|
||||
│ ├── services/ # legacy façade / 兼容入口
|
||||
│ ├── workflows/ # legacy workflow 入口
|
||||
│ └── workers/ # 任务相关代码
|
||||
├── .env.example
|
||||
├── requirements.txt
|
||||
└── main.py
|
||||
@@ -46,4 +58,13 @@ backend/
|
||||
## 说明
|
||||
|
||||
- 路由前缀保持为 `/api/v1`,以兼容当前前端。
|
||||
- 原 `backend/app/api/routes/docs.py`、`rag.py`、`compliance.py`、`status.py` 仍保留在仓库中,但不再作为主路由入口。
|
||||
- 当前主业务链路入口是 `documents`、`knowledge`、`agent`。
|
||||
- `compliance.py` 当前仍被挂载,但尚未满足目标架构约束;在迁移前不应继续扩展业务编排。
|
||||
- `docs.py` 与 `rag.py` 为遗留/非主入口,不应继续扩展。
|
||||
|
||||
## 开发约束
|
||||
|
||||
- backend 开发前先阅读 `docs/architecture/backend-project-architecture.md`。
|
||||
- 新增业务能力默认落在 `application` 层,由 `api` 调用,不要直接写进 route。
|
||||
- route 不应直接访问 MinIO、Milvus、Parser SDK、LLM SDK 或 `ConversationStore`。
|
||||
- `backend/app/shared/bootstrap.py` 是当前 composition root;依赖装配优先收口到这里。
|
||||
|
||||
@@ -65,7 +65,7 @@ async def chat_with_session(request: ChatRequest):
|
||||
model=request.model or settings.llm_model,
|
||||
top_k=request.top_k or settings.rag_top_k,
|
||||
)
|
||||
session = get_conversation_store().get_session(session_id)
|
||||
session = get_agent_session_service().get_session(session_id)
|
||||
return ChatResponse(
|
||||
session_id=session_id,
|
||||
answer=result.answer,
|
||||
@@ -133,45 +133,52 @@ async def chat_stream(request: ChatRequest):
|
||||
@router.get("/session/{session_id}", response_model=SessionInfo)
|
||||
async def get_session_info(session_id: str):
|
||||
"""Return session info."""
|
||||
session = get_conversation_store().get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在或已过期")
|
||||
return SessionInfo(
|
||||
session_id=session.session_id,
|
||||
message_count=len(session.messages),
|
||||
created_at=session.created_at,
|
||||
updated_at=session.updated_at,
|
||||
)
|
||||
try:
|
||||
session = get_agent_session_service().get_session(session_id)
|
||||
return SessionInfo(
|
||||
session_id=session.session_id,
|
||||
message_count=len(session.messages),
|
||||
created_at=session.created_at,
|
||||
updated_at=session.updated_at,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/session/{session_id}/history")
|
||||
async def get_session_history(session_id: str, max_turns: int = 5):
|
||||
"""Return session history."""
|
||||
session = get_conversation_store().get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在或已过期")
|
||||
history = [{"role": msg.role, "content": msg.content} for msg in session.messages[-(max_turns * 2):]]
|
||||
return {"session_id": session_id, "history": history}
|
||||
try:
|
||||
history = get_agent_session_service().get_history(session_id=session_id, max_turns=max_turns)
|
||||
return {"session_id": session_id, "history": history}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
|
||||
|
||||
@router.delete("/session/{session_id}")
|
||||
async def delete_session(session_id: str):
|
||||
"""Delete session."""
|
||||
if not get_conversation_store().delete_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return {"message": "会话已删除", "session_id": session_id}
|
||||
try:
|
||||
get_agent_session_service().delete_session(session_id)
|
||||
return {"message": "会话已删除", "session_id": session_id}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=List[SessionInfo])
|
||||
async def list_sessions():
|
||||
"""List sessions."""
|
||||
return [SessionInfo(**item) for item in get_conversation_store().list_sessions()]
|
||||
return [SessionInfo(**item) for item in get_agent_session_service().list_sessions()]
|
||||
|
||||
|
||||
@router.post("/feedback")
|
||||
async def submit_feedback(request: FeedbackRequest):
|
||||
"""Submit feedback."""
|
||||
session = get_conversation_store().get_session(request.session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return {"message": "反馈已提交", "session_id": request.session_id, "message_index": request.message_index}
|
||||
try:
|
||||
result = get_agent_session_service().submit_feedback(
|
||||
session_id=request.session_id,
|
||||
message_index=request.message_index,
|
||||
)
|
||||
return {"message": "反馈已提交", "session_id": result.session_id, "message_index": result.message_index}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Initialize the app.application.agent package."""
|
||||
|
||||
from .services import AgentConversationService
|
||||
from .services import AgentConversationService, AgentSessionFeedbackResult, AgentSessionService
|
||||
# Keep package boundaries explicit so backend imports stay predictable.
|
||||
|
||||
|
||||
__all__ = ["AgentConversationService"]
|
||||
__all__ = ["AgentConversationService", "AgentSessionFeedbackResult", "AgentSessionService"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Implement application-layer logic for services."""
|
||||
"""Implement application-layer logic for agent services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
from app.domain.conversation import AnswerGenerator, AnswerResult, ConversationStore
|
||||
@@ -141,5 +142,50 @@ class AgentConversationService:
|
||||
content=full_answer,
|
||||
sources=sources_payload,
|
||||
)
|
||||
|
||||
|
||||
return session.session_id, event_stream()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentSessionFeedbackResult:
|
||||
"""Represent the result of storing session feedback."""
|
||||
|
||||
session_id: str
|
||||
message_index: int
|
||||
|
||||
|
||||
class AgentSessionService:
|
||||
"""Provide application-layer access to session management workflows."""
|
||||
|
||||
def __init__(self, *, conversation_store: ConversationStore) -> None:
|
||||
"""Initialize the Agent Session Service instance."""
|
||||
self.conversation_store = conversation_store
|
||||
|
||||
def get_session(self, session_id: str):
|
||||
"""Return a session by id or raise when it does not exist."""
|
||||
session = self.conversation_store.get_session(session_id)
|
||||
if not session:
|
||||
raise ValueError("会话不存在或已过期")
|
||||
return session
|
||||
|
||||
def get_history(self, *, session_id: str, max_turns: int = 5) -> list[dict[str, str]]:
|
||||
"""Return the recent conversation history for a session."""
|
||||
session = self.get_session(session_id)
|
||||
return [{"role": msg.role, "content": msg.content} for msg in session.messages[-(max_turns * 2):]]
|
||||
|
||||
def delete_session(self, session_id: str) -> None:
|
||||
"""Delete a session or raise when it does not exist."""
|
||||
if not self.conversation_store.delete_session(session_id):
|
||||
raise ValueError("会话不存在")
|
||||
|
||||
def list_sessions(self) -> list[dict]:
|
||||
"""Return the list of visible sessions."""
|
||||
return self.conversation_store.list_sessions()
|
||||
|
||||
def submit_feedback(self, *, session_id: str, message_index: int) -> AgentSessionFeedbackResult:
|
||||
"""Validate feedback targets and return a normalized feedback result."""
|
||||
session = self.get_session(session_id)
|
||||
if message_index < 0 or message_index >= len(session.messages):
|
||||
raise ValueError("消息索引不存在")
|
||||
# Preserve the existing API behavior until a persistent feedback store is introduced.
|
||||
return AgentSessionFeedbackResult(session_id=session_id, message_index=message_index)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from app.application.agent import AgentConversationService
|
||||
from app.application.agent import AgentConversationService, AgentSessionService
|
||||
from app.application.documents import DocumentCommandService, DocumentQueryService
|
||||
from app.application.knowledge import KnowledgeRetrievalService
|
||||
from app.config.settings import settings
|
||||
@@ -162,3 +162,6 @@ def get_perception_service() -> PerceptionService:
|
||||
event_store=MockEventStore(),
|
||||
retrieval_service=get_retrieval_service(),
|
||||
)
|
||||
def get_agent_session_service() -> AgentSessionService:
|
||||
"""Return agent session service."""
|
||||
return AgentSessionService(conversation_store=get_conversation_store())
|
||||
|
||||
Reference in New Issue
Block a user