feat(api): nest chat endpoints under projects and refactor dependencies

- Change chat router prefix from `/chat` to `/projects/{project_id}/chat`
- Add `dep_project` dependency to inject Project instance into endpoints
- Remove manual project validation in `stream_chat` and use dependency instead
- Add placeholder `list_sessions` endpoint for future session listing
- Update `get_history` endpoint to use project dependency and adjust path
- Improve code organization and error handling for project retrieval
This commit is contained in:
YueGuobin 2026-03-04 12:43:36 +08:00
parent 479afc01b0
commit 2df05dff6e
3 changed files with 47 additions and 65 deletions

View File

@ -156,6 +156,6 @@ router.include_router(
router.include_router(
chat.router,
prefix="/chat",
prefix="/projects/{project_id}/chat",
tags=["Chat"]
)

View File

@ -16,19 +16,22 @@
"""
API routes for GNS3 Copilot Chat integration.
Nested under projects: /v3/projects/{project_id}/chat/...
"""
import json
import logging
import uuid
from typing import Optional, List
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.controller import Controller
from gns3server.controller.project import Project
from gns3server.controller.controller_error import ControllerNotFoundError
from gns3server.agent.gns3_copilot.project_agent_manager import get_project_agent_manager
@ -41,9 +44,18 @@ responses = {404: {"model": schemas.ErrorMessage, "description": "Resource not f
router = APIRouter(responses=responses)
async def dep_project(project_id: UUID):
def dep_project(project_id: UUID) -> Project:
"""
Dependency to retrieve a project.
Args:
project_id: GNS3 project ID
Returns:
Project instance
Raises:
ControllerNotFoundError: If project not found
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
@ -61,6 +73,7 @@ async def dep_project(project_id: UUID):
async def stream_chat(
request: schemas.ChatRequest,
http_request: Request,
project: Project = Depends(dep_project),
current_user: schemas.User = Depends(get_current_active_user),
) -> StreamingResponse:
"""
@ -71,21 +84,6 @@ async def stream_chat(
the message kind (content, tool_call, tool_start, tool_end, error, done, heartbeat).
"""
# Validate project exists and get path
try:
controller = Controller.instance()
project = controller.get_project(str(request.project_id))
if not project:
raise ControllerNotFoundError(f"Project '{request.project_id}' not found")
project_path = project.path
except ControllerNotFoundError:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid project: {e}"
)
# Get user authentication info
user_id = str(current_user.user_id)
@ -95,7 +93,7 @@ async def stream_chat(
# Get or create AgentService for this project
agent_manager = await get_project_agent_manager()
agent_service = await agent_manager.get_agent(str(request.project_id), project_path)
agent_service = await agent_manager.get_agent(str(project.id), project.path)
# Generate session_id if not provided
session_id = request.session_id or str(uuid.uuid4())
@ -106,7 +104,7 @@ async def stream_chat(
async for chunk in agent_service.stream_chat(
message=request.message,
session_id=session_id,
project_id=str(request.project_id),
project_id=str(project.id),
user_id=user_id,
jwt_token=jwt_token,
mode=request.mode
@ -138,47 +136,6 @@ async def stream_chat(
)
@router.get(
"/history/{session_id}",
response_model=schemas.ConversationHistory,
summary="Get conversation history",
description="Retrieve the conversation history for a specific session/thread."
)
async def get_history(
session_id: str,
project_id: UUID,
limit: int = 100,
current_user: schemas.User = Depends(get_current_active_user),
) -> schemas.ConversationHistory:
"""
Get conversation history for a session.
"""
# Validate project exists
try:
controller = Controller.instance()
project = controller.get_project(str(project_id))
if not project:
raise ControllerNotFoundError(f"Project '{project_id}' not found")
project_path = project.path
except ControllerNotFoundError:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid project: {e}"
)
# Get AgentService for this project
agent_manager = await get_project_agent_manager()
agent_service = await agent_manager.get_agent(str(project_id), project_path)
# Get history
history = await agent_service.get_history(session_id, limit)
return schemas.ConversationHistory(**history)
@router.get(
"/sessions",
response_model=List[schemas.ChatSession],
@ -186,7 +143,7 @@ async def get_history(
description="List all chat sessions for a project (not yet implemented)."
)
async def list_sessions(
project_id: UUID = Query(..., description="GNS3 project ID"),
project: Project = Depends(dep_project),
current_user: schemas.User = Depends(get_current_active_user),
) -> list[schemas.ChatSession]:
"""
@ -199,6 +156,32 @@ async def list_sessions(
return []
@router.get(
"/sessions/{session_id}/history",
response_model=schemas.ConversationHistory,
summary="Get conversation history",
description="Retrieve the conversation history for a specific session/thread."
)
async def get_history(
session_id: str,
project: Project = Depends(dep_project),
limit: int = 100,
current_user: schemas.User = Depends(get_current_active_user),
) -> schemas.ConversationHistory:
"""
Get conversation history for a session.
"""
# Get AgentService for this project
agent_manager = await get_project_agent_manager()
agent_service = await agent_manager.get_agent(str(project.id), project.path)
# Get history
history = await agent_service.get_history(session_id, limit)
return schemas.ConversationHistory(**history)
@router.delete(
"/sessions/{session_id}",
status_code=status.HTTP_204_NO_CONTENT,
@ -207,7 +190,7 @@ async def list_sessions(
)
async def delete_session(
session_id: str,
project_id: UUID,
project: Project = Depends(dep_project),
current_user: schemas.User = Depends(get_current_active_user),
):
"""

View File

@ -35,7 +35,6 @@ class ChatRequest(BaseModel):
message: str = Field(..., description="User message content")
session_id: Optional[str] = Field(None, description="Session ID (auto-generated if not provided)")
project_id: str = Field(..., description="GNS3 project ID")
stream: bool = Field(default=True, description="Enable streaming response")
temperature: Optional[float] = Field(None, description="LLM temperature parameter")
mode: Literal["text"] = Field(default="text", description="Interaction mode")