feat(agent): 增加 LLM 路由与诊断日志
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
from review_agent.file_summary.skills.base import BaseSkill, SkillResult, WorkflowContext
|
||||
from review_agent.file_summary.skills.registry import SkillRegistry
|
||||
@@ -25,3 +26,21 @@ def test_skill_registry_executes_registered_skill(django_user_model):
|
||||
|
||||
assert result.success is True
|
||||
assert result.data == {"batch_id": batch.id}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_skill_registry_logs_skill_lifecycle(caplog, django_user_model):
|
||||
from review_agent.models import Conversation, FileSummaryBatch
|
||||
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
batch = FileSummaryBatch.objects.create(conversation=conversation, user=user, batch_no="FS-LOG")
|
||||
registry = SkillRegistry()
|
||||
registry.register(EchoSkill())
|
||||
|
||||
with caplog.at_level(logging.INFO, logger="review_agent.file_summary"):
|
||||
registry.execute("echo", WorkflowContext(batch=batch))
|
||||
|
||||
messages = [record.getMessage() for record in caplog.records]
|
||||
assert any("Skill started" in message and "echo" in message for message in messages)
|
||||
assert any("Skill finished" in message and "echo" in message for message in messages)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from review_agent.file_summary.workflow_trigger import evaluate_file_summary_trigger
|
||||
from review_agent.file_summary.workflow_trigger import (
|
||||
evaluate_attachment_reader_trigger,
|
||||
evaluate_file_summary_trigger,
|
||||
)
|
||||
from review_agent.models import Conversation, FileAttachment
|
||||
|
||||
|
||||
@@ -30,3 +33,41 @@ def test_trigger_matches_keywords_only_when_active_attachment_exists(django_user
|
||||
normal = evaluate_file_summary_trigger(conversation, "你好,帮我解释法规")
|
||||
assert normal.should_start is False
|
||||
assert normal.reason == "not_matched"
|
||||
|
||||
|
||||
def test_attachment_reader_trigger_matches_file_content_phrases(django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
|
||||
missing = evaluate_attachment_reader_trigger(conversation, "根据提供的简历文件内容,简要介绍")
|
||||
assert missing.should_start is False
|
||||
assert missing.reason == "missing_attachment"
|
||||
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="resume.docx",
|
||||
storage_path="x/resume.docx",
|
||||
file_size=1,
|
||||
)
|
||||
|
||||
matched = evaluate_attachment_reader_trigger(conversation, "根据提供的简历文件内容,简要介绍")
|
||||
assert matched.should_start is True
|
||||
assert matched.workflow_type == "attachment_reader"
|
||||
|
||||
|
||||
def test_attachment_reader_trigger_matches_resume_project_experience_request(django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="resume.docx",
|
||||
storage_path="x/resume.docx",
|
||||
file_size=1,
|
||||
)
|
||||
|
||||
matched = evaluate_attachment_reader_trigger(conversation, "阅读下附件简历中的项目经历")
|
||||
|
||||
assert matched.should_start is True
|
||||
assert matched.workflow_type == "attachment_reader"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from review_agent.file_summary.workflow import create_file_summary_batch, start_file_summary_workflow
|
||||
from review_agent.skill_router import SkillRoute
|
||||
from review_agent.models import (
|
||||
Conversation,
|
||||
FileAttachment,
|
||||
@@ -102,6 +103,21 @@ def test_stream_message_uses_normal_llm_path_when_not_triggered(monkeypatch, dja
|
||||
assert "workflow_started" not in joined
|
||||
|
||||
|
||||
def test_stream_message_meta_uses_first_prompt_title_for_new_conversation(monkeypatch, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="新对话 01-01 10:00")
|
||||
|
||||
def fake_stream_reply(conversation, content):
|
||||
yield "普通回复"
|
||||
|
||||
monkeypatch.setattr("review_agent.services.stream_reply", fake_stream_reply)
|
||||
|
||||
frames = list(stream_message(conversation, "这是第一条新对话消息"))
|
||||
|
||||
assert '"title": "这是第一条新对话消息"' in frames[0]
|
||||
assert '"title": "这是第一条新对话消息"' in frames[-1]
|
||||
|
||||
|
||||
def test_stream_message_reads_active_attachment_when_requested(settings, tmp_path, django_user_model):
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
@@ -124,3 +140,91 @@ def test_stream_message_reads_active_attachment_when_requested(settings, tmp_pat
|
||||
assert "detail.txt" in joined
|
||||
assert "RA-2026" in joined
|
||||
assert "workflow_started" not in joined
|
||||
|
||||
|
||||
def test_stream_message_returns_error_event_when_unexpected_stream_error(monkeypatch, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
|
||||
def broken_stream_reply(conversation, content):
|
||||
yield "已生成部分内容"
|
||||
raise RuntimeError("provider connection reset")
|
||||
|
||||
monkeypatch.setattr("review_agent.services.stream_reply", broken_stream_reply)
|
||||
|
||||
frames = list(stream_message(conversation, "普通问题"))
|
||||
|
||||
joined = "".join(frames)
|
||||
assert "已生成部分内容" in joined
|
||||
assert "回复生成中断" in joined
|
||||
assert "done" in joined
|
||||
assert Message.objects.filter(conversation=conversation, role=Message.Role.ASSISTANT).exists()
|
||||
|
||||
|
||||
def test_stream_message_uses_llm_router_for_attachment_reader(
|
||||
monkeypatch,
|
||||
settings,
|
||||
tmp_path,
|
||||
django_user_model,
|
||||
):
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
attachment_path = tmp_path / "uploads" / "resume.txt"
|
||||
attachment_path.parent.mkdir(parents=True)
|
||||
attachment_path.write_text("项目经历:负责审核智能体附件解析模块。", encoding="utf-8")
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="resume.txt",
|
||||
storage_path="uploads/resume.txt",
|
||||
file_size=attachment_path.stat().st_size,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"review_agent.services.route_message_intent",
|
||||
lambda conversation, content: SkillRoute(
|
||||
action="attachment_reader",
|
||||
skill_name="attachment_reader",
|
||||
confidence=0.91,
|
||||
reason="需要读取上传简历。",
|
||||
source="llm",
|
||||
),
|
||||
)
|
||||
|
||||
frames = list(stream_message(conversation, "帮我整理其中的项目经历"))
|
||||
|
||||
joined = "".join(frames)
|
||||
assert "附件解析结果" in joined
|
||||
assert "审核智能体附件解析模块" in joined
|
||||
assert "模型调用失败" not in joined
|
||||
|
||||
|
||||
def test_stream_message_uses_llm_router_for_file_summary(monkeypatch, settings, django_user_model):
|
||||
settings.FILE_SUMMARY_ASYNC = False
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="a.docx",
|
||||
storage_path="x/a.docx",
|
||||
file_size=1,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"review_agent.services.route_message_intent",
|
||||
lambda conversation, content: SkillRoute(
|
||||
action="file_summary",
|
||||
workflow_type="file_summary",
|
||||
confidence=0.93,
|
||||
reason="需要执行文件目录与页数汇总。",
|
||||
source="llm",
|
||||
),
|
||||
)
|
||||
|
||||
frames = list(stream_message(conversation, "处理一下这批资料"))
|
||||
|
||||
joined = "".join(frames)
|
||||
assert "workflow_started" in joined
|
||||
assert "\"workflow_type\": \"file_summary\"" in joined
|
||||
assert FileSummaryBatch.objects.filter(conversation=conversation).exists()
|
||||
|
||||
Reference in New Issue
Block a user