添加 Copilot 聊天流式响应功能及相关配置

This commit is contained in:
2026-03-23 18:03:00 +08:00
parent b0acfb21ec
commit 21dd393aee
6 changed files with 316 additions and 1 deletions
+121
View File
@@ -0,0 +1,121 @@
from __future__ import annotations
import json
from typing import AsyncGenerator, Optional
import httpx
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app.auth.dependencies import get_current_active_user
from app.core.config import settings
from app.domain.schemas.user import UserInDB
router = APIRouter()
class CopilotChatStreamRequest(BaseModel):
message: str = Field(..., min_length=1, max_length=10000)
conversation_id: Optional[str] = Field(default=None, max_length=128)
def _sse_event(event: str, data: dict) -> str:
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
@router.post(
"/chat/stream",
summary="Copilot 聊天流式响应",
description="向 Python Copilot sidecar 转发请求并通过 SSE 返回增量内容",
)
async def copilot_chat_stream(
payload: CopilotChatStreamRequest,
request: Request,
current_user: UserInDB = Depends(get_current_active_user),
):
timeout = httpx.Timeout(
connect=10.0,
read=float(settings.COPILOT_STREAM_TIMEOUT_SECONDS),
write=10.0,
pool=10.0,
)
sidecar_url = settings.COPILOT_SIDECAR_URL.rstrip("/")
upstream_url = f"{sidecar_url}/chat/stream"
async def event_generator() -> AsyncGenerator[str, None]:
headers: dict[str, str] = {}
auth_header = request.headers.get("authorization")
project_id = request.headers.get("x-project-id")
if auth_header:
headers["authorization"] = auth_header
if project_id:
headers["x-project-id"] = project_id
body = {
"message": payload.message,
"conversationId": payload.conversation_id,
"userId": current_user.username,
}
try:
async with httpx.AsyncClient(timeout=timeout) as client:
async with client.stream(
"POST",
upstream_url,
json=body,
headers=headers,
) as response:
if response.status_code >= 400:
detail_text = await response.aread()
detail = detail_text.decode("utf-8", errors="replace")
yield _sse_event(
"error",
{
"message": "Copilot sidecar request failed",
"status": response.status_code,
"detail": detail,
},
)
return
async for line in response.aiter_lines():
if await request.is_disconnected():
return
yield f"{line}\n"
except httpx.ReadTimeout:
yield _sse_event(
"error",
{
"message": "Copilot stream timeout",
"status": status.HTTP_504_GATEWAY_TIMEOUT,
},
)
except httpx.ConnectError as exc:
yield _sse_event(
"error",
{
"message": "Copilot sidecar unavailable",
"status": status.HTTP_503_SERVICE_UNAVAILABLE,
"detail": str(exc),
},
)
except Exception as exc:
yield _sse_event(
"error",
{
"message": "Unexpected stream proxy error",
"status": status.HTTP_500_INTERNAL_SERVER_ERROR,
"detail": str(exc),
},
)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
+4
View File
@@ -18,6 +18,7 @@ from app.api.v1.endpoints import (
user_management, # 新增:用户管理
audit, # 新增:审计日志
meta,
copilot_chat,
)
from app.api.v1.endpoints.network import (
general,
@@ -110,3 +111,6 @@ api_router.include_router(project_data.router, tags=["Project Data"])
# Extension
api_router.include_router(extension.router, tags=["Extension"])
# Copilot Chat
api_router.include_router(copilot_chat.router, prefix="/copilot", tags=["Copilot"])