Files
TJWaterServerBinary/app/api/v1/endpoints/copilot.py
T

121 lines
3.9 KiB
Python

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.keycloak_dependencies import get_current_keycloak_username
from app.core.config import settings
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,
username: str = Depends(get_current_keycloak_username),
):
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": 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",
},
)