Files
DEMO-AGENT/review_agent/llm.py

191 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import logging
from urllib import error, request
from django.conf import settings
class LLMConfigurationError(RuntimeError):
"""Raised when the project has not been configured with a usable LLM provider."""
class LLMRequestError(RuntimeError):
"""Raised when the remote LLM provider call fails."""
logger = logging.getLogger(__name__)
def generate_reply(conversation, user_message: str, knowledge_context: str = "") -> str:
"""Calls the SiliconFlow OpenAI-compatible chat endpoint and returns assistant text."""
if not settings.LLM_API_KEY:
raise LLMConfigurationError("缺少 LLM_API_KEY 配置。")
if not settings.LLM_MODEL:
raise LLMConfigurationError("缺少 LLM_MODEL 配置。")
payload = {
"model": settings.LLM_MODEL,
"messages": build_messages(conversation, user_message, knowledge_context=knowledge_context),
"temperature": 0.3,
}
body = json.dumps(payload).encode("utf-8")
endpoint = f"{settings.LLM_BASE_URL.rstrip('/')}/chat/completions"
http_request = request.Request(
endpoint,
data=body,
headers={
"Authorization": f"Bearer {settings.LLM_API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with request.urlopen(http_request, timeout=60) as response:
data = json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
details = exc.read().decode("utf-8", errors="ignore")
raise LLMRequestError(f"模型接口调用失败HTTP {exc.code} {details}") from exc
except error.URLError as exc:
raise LLMRequestError(f"模型接口调用失败:{exc.reason}") from exc
try:
return data["choices"][0]["message"]["content"].strip()
except (KeyError, IndexError, TypeError) as exc:
raise LLMRequestError("模型接口返回格式不符合预期。") from exc
def generate_completion(messages: list[dict[str, str]], *, temperature: float = 0.0, timeout: float = 60) -> str:
"""Calls the configured chat endpoint with explicit messages and returns assistant text."""
if not settings.LLM_API_KEY:
raise LLMConfigurationError("缺少 LLM_API_KEY 配置。")
if not settings.LLM_MODEL:
raise LLMConfigurationError("缺少 LLM_MODEL 配置。")
payload = {
"model": settings.LLM_MODEL,
"messages": messages,
"temperature": temperature,
}
body = json.dumps(payload).encode("utf-8")
endpoint = f"{settings.LLM_BASE_URL.rstrip('/')}/chat/completions"
http_request = request.Request(
endpoint,
data=body,
headers={
"Authorization": f"Bearer {settings.LLM_API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with request.urlopen(http_request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
details = exc.read().decode("utf-8", errors="ignore")
raise LLMRequestError(f"模型接口调用失败HTTP {exc.code} {details}") from exc
except error.URLError as exc:
raise LLMRequestError(f"模型接口调用失败:{exc.reason}") from exc
try:
return data["choices"][0]["message"]["content"].strip()
except (KeyError, IndexError, TypeError) as exc:
raise LLMRequestError("模型接口返回格式不符合预期。") from exc
def stream_reply(conversation, user_message: str, knowledge_context: str = ""):
"""Streams incremental assistant text from the SiliconFlow chat endpoint."""
if not settings.LLM_API_KEY:
raise LLMConfigurationError("缺少 LLM_API_KEY 配置。")
if not settings.LLM_MODEL:
raise LLMConfigurationError("缺少 LLM_MODEL 配置。")
payload = {
"model": settings.LLM_MODEL,
"messages": build_messages(conversation, user_message, knowledge_context=knowledge_context),
"temperature": 0.3,
"stream": True,
}
body = json.dumps(payload).encode("utf-8")
endpoint = f"{settings.LLM_BASE_URL.rstrip('/')}/chat/completions"
http_request = request.Request(
endpoint,
data=body,
headers={
"Authorization": f"Bearer {settings.LLM_API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with request.urlopen(http_request, timeout=300) as response:
for raw_line in response:
line = raw_line.decode("utf-8", errors="ignore").strip()
if not line or not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
try:
payload = json.loads(data)
except json.JSONDecodeError:
logger.warning("Skipping malformed LLM stream data", extra={"data": data[:200]})
continue
delta = (
payload.get("choices", [{}])[0]
.get("delta", {})
.get("content", "")
)
if delta:
yield delta
except error.HTTPError as exc:
details = exc.read().decode("utf-8", errors="ignore")
raise LLMRequestError(f"模型接口调用失败HTTP {exc.code} {details}") from exc
except error.URLError as exc:
raise LLMRequestError(f"模型接口调用失败:{exc.reason}") from exc
def build_messages(conversation, latest_user_message: str, knowledge_context: str = "") -> list[dict[str, str]]:
"""Builds system and conversation history messages for the provider call."""
messages = [{"role": "system", "content": system_prompt()}]
if knowledge_context.strip():
messages.append(
{
"role": "system",
"content": (
"以下是全局知识库检索到的材料片段。回答用户时优先依据这些片段;"
"如果片段不足以支持结论,请明确说明信息不足,不要编造。\n\n"
f"{knowledge_context.strip()}"
),
}
)
for message in conversation.messages.all():
messages.append({"role": message.role, "content": message.content})
if not conversation.messages.filter(role="user", content=latest_user_message.strip()).exists():
messages.append({"role": "user", "content": latest_user_message.strip()})
return messages
def system_prompt() -> str:
"""Defines the initial assistant behavior for the review workspace."""
return (
"你是“审核智能体”,服务于体外诊断试剂临床注册文件准备与审核场景。"
"你的回答要专业、简洁、结构清楚,优先帮助用户完成法规核查、说明书审核、"
"风险识别、资料补充建议和审评思路梳理。"
"当信息不足时,明确指出缺失信息,并给出下一步建议。"
"除非用户明确要求英文,否则始终使用中文回答。"
)