feat(agent): 增加 LLM 路由与诊断日志

This commit is contained in:
2026-06-06 17:56:41 +08:00
parent 47b5ad1054
commit fa77c68d77
21 changed files with 832 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from pathlib import Path
from review_agent.models import FileSummaryBatchAttachment
@@ -9,6 +10,9 @@ from ..services.archive import ARCHIVE_EXTENSIONS, extract_archive
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.archive_extract")
class ArchiveExtractSkill(BaseSkill):
name = "archive_extract"
@@ -16,11 +20,27 @@ class ArchiveExtractSkill(BaseSkill):
extracted_count = 0
target_dir = Path(context.batch.work_dir or "")
if not target_dir:
logger.info(
"Archive extract skipped without work dir",
extra={"batch_id": context.batch.pk, "batch_no": context.batch.batch_no},
)
return SkillResult(success=True, data={"extracted_count": 0})
for binding in FileSummaryBatchAttachment.objects.filter(batch=context.batch):
path = resolve_storage_path(binding.attachment.storage_path)
if path.suffix.lower().lstrip(".") not in ARCHIVE_EXTENSIONS:
continue
logger.info(
"Archive extract started",
extra={
"batch_id": context.batch.pk,
"attachment_id": binding.attachment_id,
"path": str(path),
},
)
extracted_count += len(extract_archive(path, target_dir))
logger.info(
"Archive extract finished",
extra={"batch_id": context.batch.pk, "extracted_count": extracted_count},
)
return SkillResult(success=True, data={"extracted_count": extracted_count})

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from collections.abc import Iterable
from review_agent.models import FileAttachment
@@ -8,6 +9,9 @@ from ..services.attachment_reader import read_attachment_details
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.attachment_reader")
class AttachmentReaderSkill(BaseSkill):
name = "attachment_reader"
@@ -19,11 +23,28 @@ class AttachmentReaderSkill(BaseSkill):
return self.run_for_attachments(attachments)
def run_for_attachments(self, attachments: Iterable[FileAttachment]) -> SkillResult:
results = [read_attachment_details(attachment).to_dict() for attachment in attachments]
attachment_list = list(attachments)
logger.info(
"Attachment reader skill started",
extra={
"attachment_count": len(attachment_list),
"attachment_ids": [attachment.pk for attachment in attachment_list],
},
)
results = [read_attachment_details(attachment).to_dict() for attachment in attachment_list]
if not results:
logger.warning("Attachment reader skill found no attachments")
return SkillResult(success=False, message="当前对话没有可读取的附件。")
has_success = any(item["status"] == "success" for item in results)
logger.info(
"Attachment reader skill finished",
extra={
"success": has_success,
"success_count": sum(1 for item in results if item["status"] == "success"),
"failed_count": sum(1 for item in results if item["status"] != "success"),
},
)
return SkillResult(
success=has_success,
data={"attachments": results},

View File

@@ -1,25 +1,49 @@
from __future__ import annotations
import logging
from review_agent.models import FileSummaryItem
from ..services.page_count import SUPPORTED_EXTENSIONS, count_document_pages
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.document_page_count")
class DocumentPageCountSkill(BaseSkill):
name = "document_page_count"
def run(self, context: WorkflowContext) -> SkillResult:
success_files = failed_files = unsupported_files = uncertain_files = total_pages = 0
logger.info("Document page count started", extra={"batch_id": context.batch.pk})
for item in context.batch.items.order_by("file_index"):
if item.file_type not in SUPPORTED_EXTENSIONS:
item.statistics_status = FileSummaryItem.StatisticsStatus.UNSUPPORTED
unsupported_files += 1
item.save(update_fields=["statistics_status", "updated_at"])
logger.info(
"Document page count unsupported",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"file_type": item.file_type,
"file_name": item.file_name,
},
)
continue
result = None
for attempt in range(1, 4):
logger.info(
"Document page count attempt",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"attempt": attempt,
"storage_path": item.storage_path,
},
)
result = count_document_pages(item.storage_path)
item.retry_count = attempt - 1
if result.status != "failed":
@@ -46,6 +70,15 @@ class DocumentPageCountSkill(BaseSkill):
unsupported_files += 1
else:
failed_files += 1
logger.warning(
"Document page count failed",
extra={
"batch_id": context.batch.pk,
"item_id": item.pk,
"file_name": item.file_name,
"error": result.error_message,
},
)
context.batch.success_files = success_files
context.batch.failed_files = failed_files
@@ -61,4 +94,15 @@ class DocumentPageCountSkill(BaseSkill):
"total_pages",
]
)
logger.info(
"Document page count finished",
extra={
"batch_id": context.batch.pk,
"success_files": success_files,
"failed_files": failed_files,
"unsupported_files": unsupported_files,
"uncertain_files": uncertain_files,
"total_pages": total_pages,
},
)
return SkillResult(success=True)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from pathlib import Path
from review_agent.models import FileSummaryBatchAttachment
@@ -9,6 +10,9 @@ from ..services.inventory import scan_files_to_items
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.file_inventory")
class FileInventorySkill(BaseSkill):
name = "file_inventory"
@@ -17,5 +21,17 @@ class FileInventorySkill(BaseSkill):
resolve_storage_path(binding.attachment.storage_path)
for binding in FileSummaryBatchAttachment.objects.filter(batch=context.batch)
]
logger.info(
"File inventory started",
extra={
"batch_id": context.batch.pk,
"root_count": len(roots),
"roots": [str(root) for root in roots],
},
)
items = scan_files_to_items(batch=context.batch, roots=roots)
logger.info(
"File inventory finished",
extra={"batch_id": context.batch.pk, "total_files": len(items)},
)
return SkillResult(success=True, data={"total_files": len(items)})

View File

@@ -1,12 +1,22 @@
from __future__ import annotations
import logging
from ..services.product_detect import detect_product_name
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.product_detect")
class ProductDetectSkill(BaseSkill):
name = "product_detect"
def run(self, context: WorkflowContext) -> SkillResult:
logger.info("Product detect started", extra={"batch_id": context.batch.pk})
product_name = detect_product_name(context.batch)
logger.info(
"Product detect finished",
extra={"batch_id": context.batch.pk, "product_name": product_name},
)
return SkillResult(success=True, data={"product_name": product_name})

View File

@@ -1,8 +1,13 @@
from __future__ import annotations
import logging
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills")
class SkillRegistry:
def __init__(self):
self._skills: dict[str, BaseSkill] = {}
@@ -11,6 +16,7 @@ class SkillRegistry:
if not skill.name:
raise ValueError("Skill 必须声明 name。")
self._skills[skill.name] = skill
logger.info("Skill registered: %s", skill.name, extra={"skill_name": skill.name})
def get(self, name: str) -> BaseSkill:
try:
@@ -19,4 +25,20 @@ class SkillRegistry:
raise KeyError(f"Skill 未注册:{name}") from exc
def execute(self, name: str, context: WorkflowContext) -> SkillResult:
return self.get(name).run(context)
logger.info("Skill started: %s", name, extra={"skill_name": name, "batch_id": context.batch.pk})
try:
result = self.get(name).run(context)
except Exception:
logger.exception("Skill crashed: %s", name, extra={"skill_name": name, "batch_id": context.batch.pk})
raise
logger.info(
"Skill finished: %s",
name,
extra={
"skill_name": name,
"batch_id": context.batch.pk,
"success": result.success,
"result_message": result.message,
},
)
return result

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import logging
from django.urls import reverse
from review_agent.models import Message
@@ -9,10 +11,14 @@ from ..services.report import generate_markdown_report
from .base import BaseSkill, SkillResult, WorkflowContext
logger = logging.getLogger("review_agent.file_summary.skills.summary_report")
class SummaryReportSkill(BaseSkill):
name = "summary_report"
def run(self, context: WorkflowContext) -> SkillResult:
logger.info("Summary report started", extra={"batch_id": context.batch.pk})
markdown_export, summary_table = generate_markdown_report(context.batch)
excel_export = generate_excel_export(context.batch)
markdown_url = reverse("file_summary_export_download", args=[markdown_export.pk])
@@ -27,6 +33,14 @@ class SummaryReportSkill(BaseSkill):
role=Message.Role.ASSISTANT,
content=content,
)
logger.info(
"Summary report finished",
extra={
"batch_id": context.batch.pk,
"markdown_export_id": markdown_export.pk,
"excel_export_id": excel_export.pk,
},
)
return SkillResult(
success=True,
data={"markdown_export_id": markdown_export.pk, "excel_export_id": excel_export.pk},