feat: init commint

This commit is contained in:
2026-09-18 15:26:19 +08:00
parent 3b61a767a0
commit 9f47dbb5cc
95 changed files with 7712 additions and 1137 deletions

View File

@@ -74,6 +74,7 @@ def create_skill(request, payload: SkillCreateIn):
prompt_template=payload.prompt_template,
input_schema=payload.input_schema,
output_schema=payload.output_schema,
output_enforcement=payload.output_enforcement,
example_input=payload.example_input,
created_by=request.user,
)

View File

@@ -0,0 +1,50 @@
from django.db import migrations, models
TRUSTED_SKILL_CODES = {
"ticket-fact-extraction",
"ticket-draft",
"support-analysis",
"knowledge-distillation",
}
def classify_existing_skills(apps, schema_editor):
SkillVersion = apps.get_model("skills", "SkillVersion")
SkillVersion.objects.filter(
definition__code__in=TRUSTED_SKILL_CODES,
).update(output_enforcement="native_json_schema")
def unclassify_existing_skills(apps, schema_editor):
SkillVersion = apps.get_model("skills", "SkillVersion")
SkillVersion.objects.filter(
definition__code__in=TRUSTED_SKILL_CODES,
).update(output_enforcement="validated_json")
class Migration(migrations.Migration):
dependencies = [
("skills", "0001_initial"),
("modelhub", "0003_structured_output_governance"),
]
operations = [
migrations.AddField(
model_name="skillversion",
name="output_enforcement",
field=models.CharField(
choices=[
("native_json_schema", "原生 JSON Schema"),
("json_object", "JSON Object"),
("validated_json", "校验型 JSON"),
],
default="validated_json",
max_length=32,
),
),
migrations.RunPython(
classify_existing_skills,
unclassify_existing_skills,
),
]

View File

@@ -29,6 +29,11 @@ class SkillDefinition(models.Model):
class SkillVersion(models.Model):
class OutputEnforcement(models.TextChoices):
NATIVE_JSON_SCHEMA = "native_json_schema", "原生 JSON Schema"
JSON_OBJECT = "json_object", "JSON Object"
VALIDATED_JSON = "validated_json", "校验型 JSON"
definition = models.ForeignKey(
SkillDefinition,
on_delete=models.CASCADE,
@@ -39,6 +44,11 @@ class SkillVersion(models.Model):
prompt_template = models.TextField()
input_schema = models.JSONField(default=dict)
output_schema = models.JSONField(default=dict)
output_enforcement = models.CharField(
max_length=32,
choices=OutputEnforcement.choices,
default=OutputEnforcement.VALIDATED_JSON,
)
example_input = models.JSONField(default=dict, blank=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -71,3 +81,9 @@ class SkillVersion(models.Model):
name="unique_skill_draft",
),
]
@property
def structured_output_metrics(self) -> dict:
from apps.agents.structured_metrics import build_structured_output_metrics
return build_structured_output_metrics(skill_version_id=self.id)

View File

@@ -11,6 +11,7 @@ class SkillCreateIn(Schema):
prompt_template: str = ""
input_schema: dict[str, Any] = {"type": "object"}
output_schema: dict[str, Any] = {"type": "object"}
output_enforcement: str = "validated_json"
example_input: dict[str, Any] = {}
@@ -23,6 +24,7 @@ class SkillDraftUpdateIn(Schema):
prompt_template: str | None = None
input_schema: dict[str, Any] | None = None
output_schema: dict[str, Any] | None = None
output_enforcement: str | None = None
example_input: dict[str, Any] | None = None
@@ -33,6 +35,8 @@ class SkillVersionOut(Schema):
prompt_template: str
input_schema: dict[str, Any]
output_schema: dict[str, Any]
output_enforcement: str
structured_output_metrics: dict[str, Any]
example_input: dict[str, Any]
created_at: datetime
updated_at: datetime

View File

@@ -5,6 +5,10 @@ from typing import Any
from jsonschema import Draft202012Validator, ValidationError
from apps.modelhub.structured_output import (
StructuredOutputError,
validate_native_schema_subset,
)
from apps.skills.models import SkillDefinition, SkillVersion
VARIABLE_PATTERN = re.compile(r"{{\s*([^{}]+?)\s*}}")
@@ -32,10 +36,48 @@ def validate_prompt_template(template: str) -> None:
raise SkillValidationError(f"不允许的模板变量: {variable.strip()}")
def _validate_output_object_shapes(schema: dict[str, Any], path: str = "$") -> None:
schema_type = schema.get("type")
if schema_type == "object":
properties = schema.get("properties")
if not isinstance(properties, dict):
raise SkillValidationError(f"输出 Schema 对象必须声明 properties: {path}")
if schema.get("additionalProperties") is not False:
raise SkillValidationError(
f"输出 Schema 对象必须设置 additionalProperties=false: {path}"
)
required = schema.get("required")
if not isinstance(required, list) or set(required) != set(properties):
raise SkillValidationError(
f"输出 Schema 对象的全部字段都必须列入 required: {path}"
)
for code, child in properties.items():
if isinstance(child, dict):
_validate_output_object_shapes(child, f"{path}.{code}")
elif schema_type == "array":
items = schema.get("items")
if isinstance(items, dict):
_validate_output_object_shapes(items, f"{path}[]")
def validate_skill_version(version: SkillVersion) -> None:
validate_prompt_template(version.prompt_template)
validate_json_schema(version.input_schema, "输入 Schema")
validate_json_schema(version.output_schema, "输出 Schema")
if version.output_schema.get("type") != "object":
raise SkillValidationError("输出 Schema 根节点必须是 object")
if version.output_enforcement not in {
"native_json_schema",
"json_object",
"validated_json",
}:
raise SkillValidationError("未知的结构化输出保障要求")
_validate_output_object_shapes(version.output_schema)
if version.output_enforcement == "native_json_schema":
try:
validate_native_schema_subset(version.output_schema)
except StructuredOutputError as error:
raise SkillValidationError(str(error)) from error
if version.example_input:
try:
Draft202012Validator(version.input_schema).validate(version.example_input)
@@ -91,6 +133,7 @@ def ensure_draft(definition: SkillDefinition, user=None) -> SkillVersion:
prompt_template=source.prompt_template if source else "",
input_schema=deepcopy(source.input_schema) if source else {"type": "object"},
output_schema=deepcopy(source.output_schema) if source else {"type": "object"},
output_enforcement=source.output_enforcement if source else "validated_json",
example_input=deepcopy(source.example_input) if source else {},
created_by=user,
)

View File

@@ -16,6 +16,7 @@ VALID_OUTPUT_SCHEMA = {
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
"additionalProperties": False,
}
@@ -56,3 +57,76 @@ def test_rejects_example_that_does_not_match_schema(db):
with pytest.raises(SkillValidationError, match="示例输入不符合"):
validate_skill_version(version)
@pytest.mark.parametrize(
"output_schema",
[
{"type": "array", "items": {"type": "string"}},
{
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
},
{
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": [],
"additionalProperties": False,
},
],
)
def test_rejects_incomplete_output_contracts_for_every_enforcement(db, output_schema):
version = make_version(
output_schema=output_schema,
output_enforcement="validated_json",
)
with pytest.raises(SkillValidationError):
validate_skill_version(version)
def test_native_enforcement_rejects_unsupported_schema_keyword_at_publish(db):
output_schema = {
"type": "object",
"properties": {
"missing_fields": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
}
},
"required": ["missing_fields"],
"additionalProperties": False,
}
version = make_version(
output_schema=output_schema,
output_enforcement="native_json_schema",
)
with pytest.raises(
SkillValidationError,
match=r"\$\.properties\.missing_fields\.uniqueItems",
):
validate_skill_version(version)
def test_validated_json_keeps_business_only_schema_constraints(db):
output_schema = {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
}
},
"required": ["values"],
"additionalProperties": False,
}
version = make_version(
output_schema=output_schema,
output_enforcement="validated_json",
)
validate_skill_version(version)

View File

@@ -15,6 +15,7 @@ VALID_SKILL = {
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
"additionalProperties": False,
},
"example_input": {"problem": "采集任务没有进度"},
}