feat: add ai quality rule administration
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
from yuqei_sdk.ai_platform import (
|
||||
AiPlatformClient,
|
||||
AiQualityLabel,
|
||||
AiQualityLabelReviewUpdate,
|
||||
AiQualityRuleConfig,
|
||||
AiQualityRuleConfigUpdate,
|
||||
AiRunAudit,
|
||||
KnowledgeChunk,
|
||||
KnowledgeDocument,
|
||||
@@ -26,6 +29,9 @@ from yuqei_sdk.context import RequestContext
|
||||
__all__ = [
|
||||
"AiPlatformClient",
|
||||
"AiQualityLabel",
|
||||
"AiQualityLabelReviewUpdate",
|
||||
"AiQualityRuleConfig",
|
||||
"AiQualityRuleConfigUpdate",
|
||||
"AiRunAudit",
|
||||
"KnowledgeChunk",
|
||||
"KnowledgeDocument",
|
||||
|
||||
@@ -66,6 +66,21 @@ class PromptTemplate(PromptTemplateCreate):
|
||||
updated_at: str
|
||||
|
||||
|
||||
class AiQualityRuleConfigUpdate(BaseModel):
|
||||
min_citation_count: int = 1
|
||||
min_source_count: int = 2
|
||||
high_token_threshold: int = 8000
|
||||
high_latency_ms: int = 15000
|
||||
critical_latency_ms: int = 30000
|
||||
warning_penalty: int = 10
|
||||
error_penalty: int = 30
|
||||
|
||||
|
||||
class AiQualityRuleConfig(AiQualityRuleConfigUpdate):
|
||||
id: str = "default"
|
||||
updated_at: str
|
||||
|
||||
|
||||
class KnowledgeDocumentCreate(BaseModel):
|
||||
import_batch_id: str | None = None
|
||||
knowledge_base_id: str = "default"
|
||||
@@ -193,6 +208,16 @@ class AiQualityLabel(BaseModel):
|
||||
severity: str = "info"
|
||||
title: str
|
||||
detail: str
|
||||
review_status: str = "pending"
|
||||
review_note: str | None = None
|
||||
reviewed_by: str | None = None
|
||||
reviewed_at: str | None = None
|
||||
|
||||
|
||||
class AiQualityLabelReviewUpdate(BaseModel):
|
||||
review_status: str
|
||||
review_note: str | None = None
|
||||
reviewed_by: str | None = None
|
||||
|
||||
|
||||
class AiRunAudit(BaseModel):
|
||||
@@ -309,6 +334,32 @@ class AiPlatformClient:
|
||||
response.raise_for_status()
|
||||
return PromptTemplate.model_validate(response.json())
|
||||
|
||||
def get_quality_rule_config(
|
||||
self,
|
||||
*,
|
||||
context: RequestContext | None = None,
|
||||
) -> AiQualityRuleConfig:
|
||||
response = self._client.get(
|
||||
f"{self._api_prefix}/quality-rules/default",
|
||||
headers=(context or RequestContext()).to_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return AiQualityRuleConfig.model_validate(response.json())
|
||||
|
||||
def upsert_quality_rule_config(
|
||||
self,
|
||||
config: AiQualityRuleConfigUpdate,
|
||||
*,
|
||||
context: RequestContext | None = None,
|
||||
) -> AiQualityRuleConfig:
|
||||
response = self._client.put(
|
||||
f"{self._api_prefix}/quality-rules/default",
|
||||
json=config.model_dump(exclude_none=True),
|
||||
headers=(context or RequestContext()).to_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return AiQualityRuleConfig.model_validate(response.json())
|
||||
|
||||
def add_knowledge_document(
|
||||
self,
|
||||
document: KnowledgeDocumentCreate,
|
||||
@@ -460,3 +511,19 @@ class AiPlatformClient:
|
||||
)
|
||||
response.raise_for_status()
|
||||
return [AiRunAudit.model_validate(item) for item in response.json()]
|
||||
|
||||
def update_ai_run_quality_label(
|
||||
self,
|
||||
run_id: str,
|
||||
label_code: str,
|
||||
update: AiQualityLabelReviewUpdate,
|
||||
*,
|
||||
context: RequestContext | None = None,
|
||||
) -> AiRunAudit:
|
||||
response = self._client.patch(
|
||||
f"{self._api_prefix}/audit/ai-runs/{run_id}/quality-labels/{label_code}",
|
||||
json=update.model_dump(exclude_none=True),
|
||||
headers=(context or RequestContext()).to_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return AiRunAudit.model_validate(response.json())
|
||||
|
||||
@@ -5,6 +5,8 @@ import httpx
|
||||
|
||||
from yuqei_sdk import (
|
||||
AiPlatformClient,
|
||||
AiQualityLabelReviewUpdate,
|
||||
AiQualityRuleConfigUpdate,
|
||||
KnowledgeDocumentCreate,
|
||||
KnowledgeImportBatchCreate,
|
||||
KnowledgeReindexRequest,
|
||||
@@ -151,6 +153,38 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
}
|
||||
],
|
||||
)
|
||||
if request.url.path == "/api/v1/quality-rules/default" and request.method == "PUT":
|
||||
payload = json.loads(request.read().decode("utf-8"))
|
||||
assert payload["min_source_count"] == 1
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "default",
|
||||
"min_citation_count": 1,
|
||||
"min_source_count": 1,
|
||||
"high_token_threshold": 9000,
|
||||
"high_latency_ms": 12000,
|
||||
"critical_latency_ms": 24000,
|
||||
"warning_penalty": 7,
|
||||
"error_penalty": 21,
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
},
|
||||
)
|
||||
if request.url.path == "/api/v1/quality-rules/default":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "default",
|
||||
"min_citation_count": 1,
|
||||
"min_source_count": 2,
|
||||
"high_token_threshold": 8000,
|
||||
"high_latency_ms": 15000,
|
||||
"critical_latency_ms": 30000,
|
||||
"warning_penalty": 10,
|
||||
"error_penalty": 30,
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
},
|
||||
)
|
||||
if request.url.path == "/api/v1/knowledge-import-batches" and request.method == "POST":
|
||||
return httpx.Response(
|
||||
200,
|
||||
@@ -390,6 +424,10 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
"severity": "info",
|
||||
"title": "Citation present",
|
||||
"detail": "The answer returned 1 citation.",
|
||||
"review_status": "pending",
|
||||
"review_note": None,
|
||||
"reviewed_by": None,
|
||||
"reviewed_at": None,
|
||||
}
|
||||
],
|
||||
"quality_summary": "passed: 0 error(s), 0 warning(s), score 100.",
|
||||
@@ -398,6 +436,53 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
}
|
||||
],
|
||||
)
|
||||
if request.url.path == "/api/v1/audit/ai-runs/run-1/quality-labels/citation_present":
|
||||
payload = json.loads(request.read().decode("utf-8"))
|
||||
assert request.method == "PATCH"
|
||||
assert payload["review_status"] == "resolved"
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "run-1",
|
||||
"operation": "legal_qa",
|
||||
"provider_config_id": "provider-1",
|
||||
"prompt_template_id": "prompt-1",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"provider_status": "succeeded",
|
||||
"provider_error": None,
|
||||
"prompt_preview": "Search evidence first, then answer. Question: How should lease deposit refund be handled?",
|
||||
"question": "How should lease deposit refund be handled?",
|
||||
"answer": "The contract should clearly state deposit refund conditions.",
|
||||
"citations_count": 1,
|
||||
"retrieval_query": "How should lease deposit refund be handled?",
|
||||
"retrieval_strategy": "hybrid",
|
||||
"retrieved_chunk_ids": ["chunk-1"],
|
||||
"retrieved_sources": [],
|
||||
"prompt_tokens": 12,
|
||||
"completion_tokens": 8,
|
||||
"total_tokens": 20,
|
||||
"estimated_cost": 0.00028,
|
||||
"latency_ms": 12,
|
||||
"quality_status": "passed",
|
||||
"quality_score": 100,
|
||||
"quality_labels": [
|
||||
{
|
||||
"code": "citation_present",
|
||||
"severity": "info",
|
||||
"title": "Citation present",
|
||||
"detail": "The answer returned 1 citation.",
|
||||
"review_status": "resolved",
|
||||
"review_note": "已确认依据有效。",
|
||||
"reviewed_by": "admin",
|
||||
"reviewed_at": "2026-06-22T00:01:00Z",
|
||||
}
|
||||
],
|
||||
"quality_summary": "passed: 0 error(s), 0 warning(s), score 100.",
|
||||
"status": "succeeded",
|
||||
"created_at": "2026-06-22T00:00:00Z",
|
||||
},
|
||||
)
|
||||
return httpx.Response(404)
|
||||
|
||||
client = AiPlatformClient("http://ai-platform.test", transport=httpx.MockTransport(handler))
|
||||
@@ -416,6 +501,18 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
PromptTemplateCreate(name="legal_qa.default", version="v2", content="Search evidence first.")
|
||||
).version == "v2"
|
||||
assert client.list_prompt_templates()[0].name == "legal_qa.default"
|
||||
assert client.get_quality_rule_config().min_source_count == 2
|
||||
assert client.upsert_quality_rule_config(
|
||||
AiQualityRuleConfigUpdate(
|
||||
min_citation_count=1,
|
||||
min_source_count=1,
|
||||
high_token_threshold=9000,
|
||||
high_latency_ms=12000,
|
||||
critical_latency_ms=24000,
|
||||
warning_penalty=7,
|
||||
error_penalty=21,
|
||||
)
|
||||
).warning_penalty == 7
|
||||
assert client.create_knowledge_import_batch(
|
||||
KnowledgeImportBatchCreate(
|
||||
knowledge_base_id="laws-cn",
|
||||
@@ -466,5 +563,18 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
assert audit.quality_status == "passed"
|
||||
assert audit.quality_score == 100
|
||||
assert audit.quality_labels[0].code == "citation_present"
|
||||
assert audit.quality_labels[0].review_status == "pending"
|
||||
assert audit.quality_summary == "passed: 0 error(s), 0 warning(s), score 100."
|
||||
updated_audit = client.update_ai_run_quality_label(
|
||||
"run-1",
|
||||
"citation_present",
|
||||
AiQualityLabelReviewUpdate(
|
||||
review_status="resolved",
|
||||
review_note="已确认依据有效。",
|
||||
reviewed_by="admin",
|
||||
),
|
||||
)
|
||||
assert updated_audit.quality_labels[0].review_status == "resolved"
|
||||
assert updated_audit.quality_labels[0].reviewed_at == "2026-06-22T00:01:00Z"
|
||||
assert "/api/v1/audit/ai-runs" in seen_paths
|
||||
assert "/api/v1/quality-rules/default" in seen_paths
|
||||
|
||||
Reference in New Issue
Block a user