feat: init commint
This commit is contained in:
@@ -26,6 +26,7 @@ from apps.agents.models import (
|
||||
from apps.audit.models import OperationAuditLog
|
||||
from apps.modelhub.models import ModelInvocation
|
||||
from apps.modelhub.services import execute_invocation
|
||||
from apps.modelhub.structured_output import StructuredOutputError, ensure_compatible
|
||||
from apps.skills.services import (
|
||||
SkillValidationError,
|
||||
render_prompt,
|
||||
@@ -39,7 +40,20 @@ SOURCE_PATH = re.compile(
|
||||
|
||||
|
||||
class AgentValidationError(ValueError):
|
||||
pass
|
||||
def __init__(self, message: str, *, code: str = "agent_validation_error"):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
def _ensure_step_structured_output(deployment, skill_version) -> None:
|
||||
try:
|
||||
ensure_compatible(
|
||||
mode=deployment.structured_output_mode,
|
||||
test_status=deployment.structured_output_test_status,
|
||||
enforcement=skill_version.output_enforcement,
|
||||
)
|
||||
except StructuredOutputError as error:
|
||||
raise AgentValidationError(str(error), code=error.code) from error
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@@ -135,6 +149,8 @@ def validate_agent_version(version: AgentVersion, *, publishing: bool = False) -
|
||||
raise AgentValidationError(f"步骤 {step.code} 绑定的模型部署未启用")
|
||||
if publishing and step.skill_version.status != "published":
|
||||
raise AgentValidationError(f"步骤 {step.code} 必须绑定已发布 Skill")
|
||||
if publishing:
|
||||
_ensure_step_structured_output(deployment, step.skill_version)
|
||||
for target, source in step.input_mapping.items():
|
||||
if not isinstance(target, str) or not target:
|
||||
raise AgentValidationError(f"步骤 {step.code} 的映射字段无效")
|
||||
@@ -232,6 +248,8 @@ def build_config_snapshot(version: AgentVersion) -> dict:
|
||||
if version.temperature_override is not None
|
||||
else deployment.default_temperature,
|
||||
"max_output_tokens": version.max_output_tokens_override or deployment.max_output_tokens,
|
||||
"structured_output_mode": deployment.structured_output_mode,
|
||||
"structured_output_test_status": deployment.structured_output_test_status,
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
@@ -240,6 +258,8 @@ def build_config_snapshot(version: AgentVersion) -> dict:
|
||||
"skill_id": step.skill_version.definition_id,
|
||||
"skill_code": step.skill_version.definition.code,
|
||||
"skill_version": step.skill_version.version,
|
||||
"output_schema": deepcopy(step.skill_version.output_schema),
|
||||
"output_enforcement": step.skill_version.output_enforcement,
|
||||
"input_mapping": step.input_mapping,
|
||||
"model": {
|
||||
"deployment_id": (
|
||||
@@ -261,6 +281,16 @@ def build_config_snapshot(version: AgentVersion) -> dict:
|
||||
if step.model_deployment_id
|
||||
else deployment.capability
|
||||
),
|
||||
"structured_output_mode": (
|
||||
step.model_deployment.structured_output_mode
|
||||
if step.model_deployment_id
|
||||
else deployment.structured_output_mode
|
||||
),
|
||||
"structured_output_test_status": (
|
||||
step.model_deployment.structured_output_test_status
|
||||
if step.model_deployment_id
|
||||
else deployment.structured_output_test_status
|
||||
),
|
||||
},
|
||||
}
|
||||
for step in version.steps.select_related(
|
||||
@@ -348,90 +378,10 @@ def _parse_json_output(raw_output: str) -> Any:
|
||||
raise SkillValidationError(f"模型输出不是合法 JSON: {error.msg}") from error
|
||||
|
||||
|
||||
def _empty_schema_value(schema: dict) -> Any:
|
||||
value_type = schema.get("type")
|
||||
if value_type == "string" and (not schema.get("enum") or "" in schema["enum"]):
|
||||
return ""
|
||||
if value_type == "array":
|
||||
return []
|
||||
if value_type == "object":
|
||||
return {}
|
||||
return None
|
||||
|
||||
|
||||
def _complete_object_shape(value: dict, schema: dict) -> dict:
|
||||
completed = dict(value)
|
||||
properties = schema.get("properties", {})
|
||||
for code in schema.get("required", []):
|
||||
if code in completed:
|
||||
continue
|
||||
empty_value = _empty_schema_value(properties.get(code, {}))
|
||||
if empty_value is not None:
|
||||
completed[code] = empty_value
|
||||
return completed
|
||||
|
||||
|
||||
def _normalize_nested_output(output: Any, output_schema: dict) -> Any:
|
||||
"""Repair common flattened model output without another model round-trip."""
|
||||
if not isinstance(output, dict):
|
||||
return output
|
||||
properties = output_schema.get("properties", {})
|
||||
required = set(output_schema.get("required", []))
|
||||
"""保留扩展点,但不得重命名、补字段或移动模型返回的数据。"""
|
||||
|
||||
if {"facts", "evidence", "missing_fields"}.issubset(required):
|
||||
fact_schema = properties.get("facts", {})
|
||||
fact_codes = set(fact_schema.get("properties", {}))
|
||||
nested_facts = output.get("facts")
|
||||
if isinstance(nested_facts, dict):
|
||||
facts = _complete_object_shape(nested_facts, fact_schema)
|
||||
elif fact_codes.intersection(output):
|
||||
facts = _complete_object_shape(
|
||||
{code: output[code] for code in fact_codes if code in output},
|
||||
fact_schema,
|
||||
)
|
||||
else:
|
||||
facts = None
|
||||
if facts is not None:
|
||||
evidence = _complete_object_shape(
|
||||
output.get("evidence", {}) if isinstance(output.get("evidence"), dict) else {},
|
||||
properties.get("evidence", {}),
|
||||
)
|
||||
missing = list(output.get("missing_fields", []))
|
||||
for code, value in facts.items():
|
||||
if not value and code not in missing:
|
||||
missing.append(code)
|
||||
return {"facts": facts, "evidence": evidence, "missing_fields": missing}
|
||||
|
||||
if {"fields", "missing_fields", "questions"}.issubset(required):
|
||||
field_schema = properties.get("fields", {})
|
||||
field_codes = set(field_schema.get("properties", {}))
|
||||
nested_fields = output.get("fields")
|
||||
if isinstance(nested_fields, dict):
|
||||
fields = _complete_object_shape(nested_fields, field_schema)
|
||||
normalized = dict(output)
|
||||
elif field_codes.intersection(output):
|
||||
fields = _complete_object_shape(
|
||||
{code: output[code] for code in field_codes if code in output},
|
||||
field_schema,
|
||||
)
|
||||
normalized = {
|
||||
code: value
|
||||
for code, value in output.items()
|
||||
if code not in field_codes
|
||||
}
|
||||
else:
|
||||
fields = None
|
||||
if fields is not None:
|
||||
questions = output.get("questions", output.get("follow_up_questions", []))
|
||||
normalized.pop("follow_up_questions", None)
|
||||
normalized.update(
|
||||
{
|
||||
"fields": fields,
|
||||
"missing_fields": list(output.get("missing_fields", [])),
|
||||
"questions": list(questions) if isinstance(questions, list) else [],
|
||||
}
|
||||
)
|
||||
return normalized
|
||||
_ = output_schema
|
||||
return output
|
||||
|
||||
|
||||
@@ -463,6 +413,7 @@ def _invoke_for_step(
|
||||
) -> ModelInvocation:
|
||||
version = run.agent_version
|
||||
deployment = step.model_deployment or version.model_deployment
|
||||
_ensure_step_structured_output(deployment, step.skill_version)
|
||||
messages = []
|
||||
if version.system_prompt:
|
||||
messages.append({"role": "system", "content": version.system_prompt})
|
||||
@@ -509,6 +460,11 @@ def _invoke_for_step(
|
||||
messages,
|
||||
temperature=version.temperature_override,
|
||||
max_output_tokens=version.max_output_tokens_override,
|
||||
output_schema=step.skill_version.output_schema,
|
||||
schema_name=(
|
||||
f"skill_{step.skill_version.definition_id}_"
|
||||
f"v{step.skill_version.version}_{step.code}"
|
||||
),
|
||||
on_retry=mark_retry,
|
||||
total_timeout_seconds=settings.AI_AGENT_MODEL_TIMEOUT_SECONDS,
|
||||
)
|
||||
@@ -534,19 +490,19 @@ def _repair_output(
|
||||
step: AgentRunStep,
|
||||
raw_output: str,
|
||||
validation_error: str,
|
||||
output_schema: dict,
|
||||
) -> ModelInvocation:
|
||||
prompt = (
|
||||
"请只输出修正后的 JSON,不要解释。\n"
|
||||
f"输出 Schema:{json.dumps(output_schema, ensure_ascii=False)}\n"
|
||||
f"校验错误:{validation_error}\n"
|
||||
f"原始输出:{raw_output}"
|
||||
f"<invalid_output>{raw_output}</invalid_output>"
|
||||
)
|
||||
initial_attempt = (
|
||||
step.attempts.filter(attempt_type="initial").order_by("-created_at").first()
|
||||
)
|
||||
if initial_attempt:
|
||||
initial_attempt.validation_error = validation_error
|
||||
initial_attempt.save(update_fields=["validation_error"])
|
||||
invocation = _invoke_for_step(run, step, prompt, "structure_repair")
|
||||
attempt = step.attempts.order_by("-created_at").first()
|
||||
if attempt:
|
||||
attempt.validation_error = validation_error
|
||||
attempt.save(update_fields=["validation_error"])
|
||||
return invocation
|
||||
|
||||
|
||||
@@ -612,7 +568,10 @@ def execute_agent_run(run_id: int, *, image_parts: list[dict] | None = None) ->
|
||||
prompt = render_prompt(binding.skill_version.prompt_template, step_input)
|
||||
invocation = _invoke_for_step(run, step, prompt, "initial", image_parts)
|
||||
if invocation.status != "succeeded":
|
||||
raise AgentValidationError(invocation.error_message or "模型调用失败")
|
||||
raise AgentValidationError(
|
||||
invocation.error_message or "模型调用失败",
|
||||
code=invocation.error_code or "model_invocation_failed",
|
||||
)
|
||||
try:
|
||||
output = _parse_json_output(invocation.output_text)
|
||||
output = _normalize_nested_output(
|
||||
@@ -626,27 +585,47 @@ def execute_agent_run(run_id: int, *, image_parts: list[dict] | None = None) ->
|
||||
step,
|
||||
invocation.output_text,
|
||||
str(error),
|
||||
binding.skill_version.output_schema,
|
||||
)
|
||||
if repair.status != "succeeded":
|
||||
raise AgentValidationError(
|
||||
repair.error_message or "结构修复失败"
|
||||
repair.error_message or "结构修复失败",
|
||||
code=repair.error_code or "structured_output_invalid",
|
||||
) from error
|
||||
output = _parse_json_output(repair.output_text)
|
||||
output = _normalize_nested_output(
|
||||
output,
|
||||
binding.skill_version.output_schema,
|
||||
)
|
||||
validate_payload(binding.skill_version.output_schema, output, "修复输出")
|
||||
try:
|
||||
output = _parse_json_output(repair.output_text)
|
||||
output = _normalize_nested_output(
|
||||
output,
|
||||
binding.skill_version.output_schema,
|
||||
)
|
||||
validate_payload(
|
||||
binding.skill_version.output_schema,
|
||||
output,
|
||||
"修复输出",
|
||||
)
|
||||
except SkillValidationError as repair_error:
|
||||
repair_attempt = (
|
||||
step.attempts.filter(attempt_type="structure_repair")
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if repair_attempt:
|
||||
repair_attempt.validation_error = str(repair_error)
|
||||
repair_attempt.save(update_fields=["validation_error"])
|
||||
raise AgentValidationError(
|
||||
str(repair_error),
|
||||
code="structured_output_invalid",
|
||||
) from repair_error
|
||||
step.output_payload = output
|
||||
step.status = "succeeded"
|
||||
step_outputs[binding.code] = {"output": output}
|
||||
except (AgentValidationError, SkillValidationError) as error:
|
||||
step.status = "failed"
|
||||
step.error_code = "step_failed"
|
||||
step.error_code = (
|
||||
error.code if isinstance(error, AgentValidationError) else "step_failed"
|
||||
)
|
||||
step.error_message = str(error)
|
||||
run.status = "failed"
|
||||
run.error_code = "step_failed"
|
||||
run.error_code = step.error_code
|
||||
run.error_message = f"步骤 {binding.code} 失败: {error}"
|
||||
_set_run_progress(run, "failed", "Agent 运行失败")
|
||||
finally:
|
||||
@@ -732,6 +711,9 @@ def execute_agent_run(run_id: int, *, image_parts: list[dict] | None = None) ->
|
||||
sync_support_analysis_work_item(run)
|
||||
else:
|
||||
sync_support_analysis_work_item(run)
|
||||
from apps.tickets.automation import resume_automation_for_agent_run
|
||||
|
||||
resume_automation_for_agent_run(run.id)
|
||||
if run.definition.code == "knowledge-distillation":
|
||||
from apps.knowledge.models import TicketKnowledgeDistillation
|
||||
from apps.workitems.services import sync_knowledge_distillation_work_item
|
||||
|
||||
Reference in New Issue
Block a user