File size: 6,672 Bytes
b4f2238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | """API endpoints."""
"""
LLM Ninja API
使用 Ninja 框架的 LLM API 接口,集成到主 API 结构中.
"""
import logging
from typing import Any, ClassVar
from django.db import transaction
from ninja import Router
from ninja.schema import Schema
from apps.core.exceptions import PermissionDenied
from apps.core.infrastructure.throttling import rate_limit_from_settings
from apps.core.security.auth import JWTOrSessionAuth
from .llm_common import achat_with_context as achat_with_context_impl
from .llm_common import get_conversation_history as get_conversation_history_impl
logger = logging.getLogger(__name__)
# 创建 LLM 路由
llm_router = Router(tags=["LLM 服务"], auth=JWTOrSessionAuth())
# ============================================================
# 请求/响应 Schema
# ============================================================
class ChatRequest(Schema):
"""对话请求"""
message: str
session_id: str | None = None
user_id: str | None = None
system_prompt: str | None = None
class ChatResponse(Schema):
"""对话响应"""
response: str
session_id: str
class ConversationMessage(Schema):
"""对话消息"""
role: str
content: str
created_at: str
metadata: ClassVar[dict[str, Any]] = {}
class ConversationHistoryResponse(Schema):
"""对话历史响应"""
session_id: str
messages: list[ConversationMessage]
class PromptTemplateSyncResponse(Schema):
"""Prompt 模板同步响应"""
synced_count: int
def sync_prompt_templates_impl(*, overwrite: bool = True) -> dict[str, int]:
"""
将代码内置 Prompt 模板同步到数据库。
该函数保留模块级命名,兼容旧测试对 `sync_prompt_templates_impl` 的 monkeypatch。
"""
from apps.core.llm.prompts import PromptManager
from apps.core.models import PromptTemplate
templates = list(PromptManager._templates.values())
synced_count = 0
with transaction.atomic():
for item in templates:
defaults = {
"title": (item.description or item.name)[:200],
"template": item.template,
"description": item.description,
"variables": item.variables,
"category": (item.name.split("_", maxsplit=1)[0] or "general"),
"is_active": True,
"version": "1.0",
}
if overwrite:
PromptTemplate.objects.update_or_create(name=item.name, defaults=defaults)
synced_count += 1
continue
_, created = PromptTemplate.objects.get_or_create(name=item.name, defaults=defaults)
if created:
synced_count += 1
return {"synced_count": synced_count}
# ============================================================
# API 端点
# ============================================================
@llm_router.post("/chat", response=ChatResponse)
@rate_limit_from_settings("LLM", by_user=True)
async def chat_with_context(request: Any, payload: ChatRequest) -> Any:
"""
带上下文的对话
支持多轮对话和上下文记忆功能.
"""
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
user = getattr(request, "auth", None)
user_id = str(getattr(user, "id", "") or "")
from apps.core.interfaces import ServiceLocator
result = await achat_with_context_impl(
message=payload.message,
session_id=payload.session_id,
user_id=user_id,
system_prompt=payload.system_prompt,
conversation_service_factory=ServiceLocator.get_conversation_service,
)
return ChatResponse(
response=result["response"],
session_id=result["session_id"],
)
@llm_router.post("/chat/stream")
@rate_limit_from_settings("LLM", by_user=True)
async def chat_with_context_stream(request: Any, payload: ChatRequest) -> Any:
from django.http import StreamingHttpResponse
from apps.core.interfaces import ServiceLocator
from apps.core.services.llm_stream_service import build_chat_stream
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
user = getattr(request, "auth", None)
user_id = str(getattr(user, "id", "") or "")
stream = build_chat_stream(
message=payload.message,
session_id=payload.session_id,
user_id=user_id,
system_prompt=payload.system_prompt,
conversation_service_factory=ServiceLocator.get_conversation_service,
llm_service_factory=ServiceLocator.get_llm_service,
)
resp = StreamingHttpResponse(stream, content_type="text/event-stream")
resp["Cache-Control"] = "no-cache"
return resp
@llm_router.get("/conversation/{session_id}/history", response=ConversationHistoryResponse)
@rate_limit_from_settings("LLM_HISTORY", by_user=True)
def get_conversation_history(request: Any, session_id: str) -> Any:
"""
获取对话历史
返回指定会话的对话记录.
"""
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
user = getattr(request, "auth", None)
user_id = str(getattr(user, "id", "") or "")
is_admin = bool(
getattr(user, "is_admin", False) or getattr(user, "is_superuser", False) or getattr(user, "is_staff", False)
)
result = get_conversation_history_impl(session_id=session_id, user_id=(None if is_admin else user_id), limit=50)
messages = [
ConversationMessage( # type: ignore[call-arg]
role=m["role"],
content=m["content"],
created_at=m["created_at"],
metadata=m.get("metadata") or {},
)
for m in result["messages"]
]
return ConversationHistoryResponse(session_id=session_id, messages=messages)
@llm_router.post("/templates/sync", response=PromptTemplateSyncResponse)
@rate_limit_from_settings("LLM", by_user=True)
def sync_prompt_templates(request: Any) -> Any:
"""
同步代码内置 Prompt 模板到数据库(仅管理员)。
"""
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
user = getattr(request, "auth", None)
is_admin = bool(getattr(user, "is_superuser", False) or getattr(user, "is_staff", False))
if not is_admin:
raise PermissionDenied(message="需要管理员权限", code="PERMISSION_DENIED")
result = sync_prompt_templates_impl(overwrite=True)
return PromptTemplateSyncResponse(synced_count=int(result.get("synced_count", 0)))
|