feat(demo): 初始化审核智能体演示基线
This commit is contained in:
1
review_agent/__init__.py
Normal file
1
review_agent/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
7
review_agent/apps.py
Normal file
7
review_agent/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ReviewAgentConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "review_agent"
|
||||
verbose_name = "审核智能体"
|
||||
79
review_agent/llm.py
Normal file
79
review_agent/llm.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import json
|
||||
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."""
|
||||
|
||||
|
||||
def generate_reply(conversation, user_message: 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),
|
||||
"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 build_messages(conversation, latest_user_message: str) -> list[dict[str, str]]:
|
||||
"""Builds system and conversation history messages for the provider call."""
|
||||
|
||||
messages = [{"role": "system", "content": system_prompt()}]
|
||||
|
||||
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 (
|
||||
"你是“审核智能体”,服务于体外诊断试剂临床注册文件准备与审核场景。"
|
||||
"你的回答要专业、简洁、结构清楚,优先帮助用户完成法规核查、说明书审核、"
|
||||
"风险识别、资料补充建议和审评思路梳理。"
|
||||
"当信息不足时,明确指出缺失信息,并给出下一步建议。"
|
||||
"除非用户明确要求英文,否则始终使用中文回答。"
|
||||
)
|
||||
78
review_agent/migrations/0001_initial.py
Normal file
78
review_agent/migrations/0001_initial.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-04 15:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Conversation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(blank=True, max_length=120)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="review_conversations",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-updated_at", "-id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Message",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.CharField(
|
||||
choices=[("user", "用户"), ("assistant", "助手")], max_length=20
|
||||
),
|
||||
),
|
||||
("content", models.TextField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"conversation",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="messages",
|
||||
to="review_agent.conversation",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["created_at", "id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
1
review_agent/migrations/__init__.py
Normal file
1
review_agent/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
44
review_agent/models.py
Normal file
44
review_agent/models.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Conversation(models.Model):
|
||||
"""Stores a user's review-agent conversation shell."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="review_conversations",
|
||||
)
|
||||
title = models.CharField(max_length=120, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at", "-id"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title or f"对话 {self.pk}"
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
"""Stores one user or assistant message in a conversation."""
|
||||
|
||||
class Role(models.TextChoices):
|
||||
USER = "user", "用户"
|
||||
ASSISTANT = "assistant", "助手"
|
||||
|
||||
conversation = models.ForeignKey(
|
||||
Conversation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="messages",
|
||||
)
|
||||
role = models.CharField(max_length=20, choices=Role.choices)
|
||||
content = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["created_at", "id"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.get_role_display()} - {self.conversation_id}"
|
||||
90
review_agent/services.py
Normal file
90
review_agent/services.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.utils import timezone
|
||||
|
||||
from .llm import LLMConfigurationError, LLMRequestError, generate_reply
|
||||
from .models import Conversation, Message
|
||||
|
||||
|
||||
def list_conversations(user, search: str = "") -> QuerySet[Conversation]:
|
||||
"""Returns a user's conversations, optionally filtered by title or content."""
|
||||
|
||||
conversations = Conversation.objects.filter(user=user)
|
||||
if not search:
|
||||
return conversations
|
||||
|
||||
return conversations.filter(
|
||||
Q(title__icontains=search) | Q(messages__content__icontains=search)
|
||||
).distinct()
|
||||
|
||||
|
||||
def get_conversation_for_user(user, conversation_id: int | None) -> Conversation | None:
|
||||
"""Loads a conversation only when it belongs to the current user."""
|
||||
|
||||
if not conversation_id:
|
||||
return None
|
||||
return Conversation.objects.filter(user=user, pk=conversation_id).first()
|
||||
|
||||
|
||||
def create_conversation(user) -> Conversation:
|
||||
"""Creates an empty conversation that can immediately accept messages."""
|
||||
|
||||
now = timezone.localtime()
|
||||
return Conversation.objects.create(
|
||||
user=user,
|
||||
title=f"新对话 {now.strftime('%m-%d %H:%M')}",
|
||||
)
|
||||
|
||||
|
||||
def append_user_message(conversation: Conversation, content: str) -> Message:
|
||||
"""Appends a user message and updates the conversation title from the first prompt."""
|
||||
|
||||
message = Message.objects.create(
|
||||
conversation=conversation,
|
||||
role=Message.Role.USER,
|
||||
content=content.strip(),
|
||||
)
|
||||
|
||||
if conversation.messages.filter(role=Message.Role.USER).count() == 1:
|
||||
conversation.title = build_conversation_title(content)
|
||||
conversation.save(update_fields=["title", "updated_at"])
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def append_assistant_message(conversation: Conversation, content: str) -> Message:
|
||||
"""Appends the deterministic assistant reply."""
|
||||
|
||||
return Message.objects.create(
|
||||
conversation=conversation,
|
||||
role=Message.Role.ASSISTANT,
|
||||
content=content,
|
||||
)
|
||||
|
||||
|
||||
def send_message(conversation: Conversation, content: str) -> tuple[Message, Message]:
|
||||
"""Stores one user message and one provider-backed assistant reply."""
|
||||
|
||||
user_message = append_user_message(conversation, content)
|
||||
try:
|
||||
reply_content = generate_reply(conversation, content)
|
||||
except (LLMConfigurationError, LLMRequestError) as exc:
|
||||
reply_content = f"模型调用失败:{exc}"
|
||||
|
||||
assistant_message = append_assistant_message(conversation, reply_content)
|
||||
|
||||
if conversation.title.startswith("新对话"):
|
||||
conversation.title = build_conversation_title(content)
|
||||
conversation.save(update_fields=["title", "updated_at"])
|
||||
|
||||
return user_message, assistant_message
|
||||
|
||||
|
||||
def build_conversation_title(content: str) -> str:
|
||||
"""Creates a concise title from the first user message."""
|
||||
|
||||
normalized = " ".join(content.strip().split())
|
||||
if not normalized:
|
||||
return "新对话"
|
||||
return normalized[:24]
|
||||
47
review_agent/views.py
Normal file
47
review_agent/views.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from .services import create_conversation, get_conversation_for_user, list_conversations, send_message
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def workspace(request: HttpRequest) -> HttpResponse:
|
||||
"""Renders the review-agent workspace and handles conversation actions."""
|
||||
|
||||
if request.method == "POST":
|
||||
action = request.POST.get("action")
|
||||
conversation = get_conversation_for_user(request.user, request.POST.get("conversation_id"))
|
||||
|
||||
if action == "new_conversation":
|
||||
conversation = create_conversation(request.user)
|
||||
return redirect(f"/?conversation={conversation.pk}")
|
||||
|
||||
if action == "send_message":
|
||||
content = (request.POST.get("prompt") or "").strip()
|
||||
if not conversation:
|
||||
conversation = create_conversation(request.user)
|
||||
if content:
|
||||
send_message(conversation, content)
|
||||
return redirect(f"/?conversation={conversation.pk}")
|
||||
|
||||
search = (request.GET.get("q") or "").strip()
|
||||
conversations = list_conversations(request.user, search)
|
||||
current = get_conversation_for_user(request.user, request.GET.get("conversation"))
|
||||
|
||||
if current is None and conversations.exists():
|
||||
current = conversations.first()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"home.html",
|
||||
{
|
||||
"page_title": "审核智能体",
|
||||
"search_query": search,
|
||||
"conversations": conversations,
|
||||
"current_conversation": current,
|
||||
"messages": current.messages.all() if current else [],
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user