"""Agent 运行、事件、工具调用和人工确认模型。""" import uuid from django.conf import settings from django.db import models from django.db.models import Q from common.models import TimeStampedModel, UserOwnedModel class RunStatus(models.TextChoices): PENDING = "pending", "等待" RUNNING = "running", "运行中" WAITING_APPROVAL = "waiting_approval", "等待确认" SUCCEEDED = "succeeded", "成功" FAILED = "failed", "失败" CANCELLED = "cancelled", "已取消" class ProviderConnectionStatus(models.TextChoices): """模型服务商连接状态。""" CONNECTED = "connected", "已连接" FAILED = "failed", "需要重新验证" class ModelProviderConfig(UserOwnedModel): """当前用户的模型服务商凭据与已验证模型快照。""" provider_code = models.CharField("服务商编码", max_length=64) encrypted_api_key = models.TextField("加密 API Key") key_hint = models.CharField("密钥提示", max_length=12) status = models.CharField( "连接状态", max_length=20, choices=ProviderConnectionStatus, default=ProviderConnectionStatus.CONNECTED, ) enabled = models.BooleanField("已启用", default=True) is_default = models.BooleanField("默认服务商", default=False) default_model_id = models.CharField("默认模型", max_length=160, blank=True) available_models = models.JSONField("可用模型快照", default=list) last_verified_at = models.DateTimeField("最近验证时间", null=True, blank=True) last_error_code = models.CharField("最近错误码", max_length=80, blank=True) class Meta: verbose_name = "模型服务商配置" verbose_name_plural = "模型服务商配置" ordering = ("provider_code",) constraints = [ models.UniqueConstraint( fields=("owner", "provider_code"), name="provider_owner_code_uniq" ) ] def __str__(self): return f"{self.owner} / {self.provider_code}" class AgentRun(UserOwnedModel): """一次可审计的 Agent 运行;摘要字段不得保存敏感原文。""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField("标题", max_length=160) status = models.CharField("状态", max_length=32, choices=RunStatus, default=RunStatus.PENDING) lock_version = models.PositiveBigIntegerField("锁版本", default=0) input_summary = models.JSONField("输入摘要", default=dict) output_summary = models.JSONField("输出摘要", null=True, blank=True) agent_name = models.CharField("Agent 名称", max_length=100, default="job_research") agent_version = models.CharField("Agent 版本", max_length=40, blank=True) instruction_version = models.CharField("指令版本", max_length=40, blank=True) toolset_version = models.CharField("工具集版本", max_length=40, blank=True) model_name = models.CharField("模型名称", max_length=100, blank=True) trace_id = models.CharField("追踪标识", max_length=128, blank=True) started_at = models.DateTimeField("开始时间", null=True, blank=True) finished_at = models.DateTimeField("结束时间", null=True, blank=True) duration_ms = models.PositiveBigIntegerField("耗时毫秒", null=True, blank=True) input_tokens = models.PositiveBigIntegerField("输入 Token", null=True, blank=True) output_tokens = models.PositiveBigIntegerField("输出 Token", null=True, blank=True) estimated_cost = models.DecimalField( "估算费用", max_digits=12, decimal_places=6, null=True, blank=True ) error_code = models.CharField("错误码", max_length=80, blank=True) error_summary = models.TextField("错误摘要", blank=True) class Meta: ordering = ("-created_at",) indexes = [models.Index(fields=("owner", "created_at"), name="run_owner_created_idx")] constraints = [ models.CheckConstraint(condition=Q(lock_version__gte=0), name="run_lock_nonnegative") ] def __str__(self): return self.title class AgentRunEvent(models.Model): """按序追加的不可变运行事件。""" run = models.ForeignKey(AgentRun, on_delete=models.PROTECT, related_name="events") sequence = models.PositiveBigIntegerField("事件序号") event_type = models.CharField("事件类型", max_length=64) summary = models.CharField("摘要", max_length=240) payload_summary = models.JSONField("数据摘要", default=dict) occurred_at = models.DateTimeField("发生时间") created_at = models.DateTimeField("记录时间", auto_now_add=True) class Meta: ordering = ("sequence",) constraints = [ models.UniqueConstraint(fields=("run", "sequence"), name="event_run_sequence_uniq") ] def __str__(self): """使用运行标题和序号提供稳定、可读的管理端标识。""" return f"{self.run} / 事件 {self.sequence}" class ToolCallStatus(models.TextChoices): STARTED = "started", "已开始" SUCCEEDED = "succeeded", "成功" FAILED = "failed", "失败" class ToolCall(TimeStampedModel): """记录一次工具调用的脱敏输入、结果和失败信息。""" run = models.ForeignKey(AgentRun, on_delete=models.PROTECT, related_name="tool_calls") call_id = models.CharField("调用标识", max_length=128) tool_name = models.CharField("工具名称", max_length=100) tool_version = models.CharField("工具版本", max_length=40, blank=True) idempotency_key = models.CharField("幂等键", max_length=128, blank=True) status = models.CharField( "状态", max_length=20, choices=ToolCallStatus, default=ToolCallStatus.STARTED ) arguments_summary = models.JSONField("参数摘要", default=dict) result_summary = models.JSONField("结果摘要", null=True, blank=True) error_code = models.CharField("错误码", max_length=80, blank=True) error_summary = models.TextField("错误摘要", blank=True) started_at = models.DateTimeField("开始时间") finished_at = models.DateTimeField("结束时间", null=True, blank=True) duration_ms = models.PositiveBigIntegerField("耗时毫秒", null=True, blank=True) class Meta: indexes = [models.Index(fields=("run", "started_at"), name="tool_run_started_idx")] constraints = [ models.UniqueConstraint(fields=("run", "call_id"), name="tool_run_call_uniq"), models.UniqueConstraint( fields=("run", "idempotency_key"), condition=~Q(idempotency_key=""), name="tool_run_idempotency_uniq", ), ] class ApprovalStatus(models.TextChoices): PENDING = "pending", "待处理" APPROVED = "approved", "已通过" REJECTED = "rejected", "已拒绝" EXPIRED = "expired", "已过期" CANCELLED = "cancelled", "已取消" class HumanApproval(TimeStampedModel): """保存高风险动作的人工确认请求与处理证据。""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) run = models.ForeignKey(AgentRun, on_delete=models.PROTECT, related_name="approvals") request_key = models.CharField("请求键", max_length=128) approval_type = models.CharField("确认类型", max_length=64) status = models.CharField( "状态", max_length=20, choices=ApprovalStatus, default=ApprovalStatus.PENDING ) request_summary = models.JSONField("请求摘要", default=dict) decision_summary = models.JSONField("处理摘要", null=True, blank=True) requested_at = models.DateTimeField("请求时间") resolved_at = models.DateTimeField("处理时间", null=True, blank=True) resolved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="resolved_agent_approvals", ) class Meta: indexes = [models.Index(fields=("status", "requested_at"), name="approval_status_time_idx")] constraints = [ models.UniqueConstraint(fields=("run", "request_key"), name="approval_run_key_uniq") ]