Skip to content

Commit 2bae2f3

Browse files
✨Feat:Integrate openjiuwen's intelligent agent evaluation function close#3188 (#3356)
* ✨Feat:add prompt optimization * 🐛Bugfix: dockerbuild failed when running pipefail in python3_11 * 🔨Optimize: Optimize prompt optimization display page and interaction methods * ✨Feat:add agent evaluator * 🐛Bugfix: fix fronted and report download * 🐛 Bugfix: Add xlrd dependency and improve openjiuwen SDK adapter to handle circular imports more effectively. Update import paths in frontend components for consistency. * ♻️Refactor: frontend style refact * 🔨Rename evaluator sql * ♻️ Refactor: Optimize the frontend page for agent evaluation * ♻️Refactor: update modal mask properties for consistency * ♻️ Refactor: Simplify locale handling and clean up imports in OAuthCompletePage and UserManageComp * ✨ Feature: Enhance agent evaluation with Chinese output directive and new database migrations * 🔧 Update: Change button variant from "outline" to "outlined" in EvaluationConfigCard and EvaluationReportCard components; enhance datetime handling in test_client.py to ensure correct timezone representation. * 🧪Test: Add unit tests for the jiuwen_sdk_adapter to verify module-level behavior and side effects. * 🔧 Update: Refactor the mock invocation in test_jiuwen_sdk_adapter to use an async function for better compatibility with asynchronous tests. * 🧪 Test: Add comprehensive unit tests for evaluation set functionality, including CRUD operations and Excel parsing, to ensure robust behavior and error handling. * 🧪 Test: Add extensive unit tests for Jiuwen SDK adapter and evaluation services, covering lazy imports, log envelope parsing, and case validation to enhance error handling and ensure robust functionality. * 🔧 Update: Consolidate judge_model_id addition into agent_evaluation_t migration, removing the separate migration file for improved installation simplicity and ensuring idempotency in existing environments. * 🔧 Update: Introduce a consolidated migration file for agent evaluation, combining previous separate migrations into a single idempotent script for improved installation simplicity and clarity. This includes the addition of evaluation tables and the pass_status column, ensuring all necessary structures are created in one step.
1 parent 8eae4ed commit 2bae2f3

60 files changed

Lines changed: 10693 additions & 154 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/adapters/jiuwen_sdk_adapter.py

Lines changed: 261 additions & 99 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import logging
2+
from http import HTTPStatus
3+
from typing import Optional
4+
5+
from fastapi import APIRouter, Body, Header, HTTPException, Query
6+
from fastapi.responses import JSONResponse, StreamingResponse
7+
8+
from services.agent_evaluation_service import (
9+
create_agent_evaluation_run_impl,
10+
delete_agent_evaluation_run_impl,
11+
generate_agent_evaluation_report_impl,
12+
get_agent_evaluation_run_impl,
13+
list_agent_evaluation_cases_impl,
14+
list_agent_evaluations_by_agent_impl,
15+
)
16+
from utils.auth_utils import get_current_user_id
17+
18+
logger = logging.getLogger("agent_evaluation_app")
19+
20+
router = APIRouter(prefix="/agent-evaluations")
21+
22+
23+
@router.post("")
24+
async def create_agent_evaluation_api(
25+
agent_id: int = Body(...),
26+
evaluation_set_id: int = Body(...),
27+
judge_model_id: int = Body(..., description="Model id used for judging (Jiuwen)"),
28+
authorization: Optional[str] = Header(None),
29+
):
30+
try:
31+
user_id, tenant_id = get_current_user_id(authorization)
32+
run = create_agent_evaluation_run_impl(
33+
tenant_id=tenant_id,
34+
user_id=user_id,
35+
agent_id=agent_id,
36+
evaluation_set_id=evaluation_set_id,
37+
judge_model_id=judge_model_id,
38+
)
39+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": run})
40+
except ValueError as ve:
41+
raise HTTPException(status_code=400, detail=str(ve))
42+
except Exception as exc:
43+
logger.exception("Create agent evaluation error: %r", exc)
44+
raise HTTPException(status_code=500, detail="Create agent evaluation error")
45+
46+
47+
@router.get("")
48+
async def list_agent_evaluations_by_agent_api(
49+
agent_id: int = Query(...),
50+
limit: int = Query(50, ge=1, le=200),
51+
offset: int = Query(0, ge=0),
52+
authorization: Optional[str] = Header(None),
53+
):
54+
try:
55+
_, tenant_id = get_current_user_id(authorization)
56+
data = list_agent_evaluations_by_agent_impl(
57+
agent_id=agent_id,
58+
tenant_id=tenant_id,
59+
limit=limit,
60+
offset=offset,
61+
)
62+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
63+
except Exception as exc:
64+
logger.exception("List agent evaluations error: %r", exc)
65+
raise HTTPException(status_code=500, detail="List agent evaluations error")
66+
67+
68+
@router.get("/{agent_evaluation_id}")
69+
async def get_agent_evaluation_api(
70+
agent_evaluation_id: int,
71+
authorization: Optional[str] = Header(None),
72+
):
73+
try:
74+
_, tenant_id = get_current_user_id(authorization)
75+
data = get_agent_evaluation_run_impl(agent_evaluation_id=agent_evaluation_id, tenant_id=tenant_id)
76+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
77+
except Exception as exc:
78+
logger.exception("Get agent evaluation error: %r", exc)
79+
raise HTTPException(status_code=500, detail="Get agent evaluation error")
80+
81+
82+
@router.get("/{agent_evaluation_id}/cases")
83+
async def list_agent_evaluation_cases_api(
84+
agent_evaluation_id: int,
85+
limit: int = Query(50, ge=1, le=200),
86+
offset: int = Query(0, ge=0),
87+
authorization: Optional[str] = Header(None),
88+
):
89+
try:
90+
_, tenant_id = get_current_user_id(authorization)
91+
data = list_agent_evaluation_cases_impl(
92+
agent_evaluation_id=agent_evaluation_id,
93+
tenant_id=tenant_id,
94+
limit=limit,
95+
offset=offset,
96+
)
97+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
98+
except Exception as exc:
99+
logger.exception("List agent evaluation cases error: %r", exc)
100+
raise HTTPException(status_code=500, detail="List agent evaluation cases error")
101+
102+
103+
@router.get("/{agent_evaluation_id}/report")
104+
async def download_agent_evaluation_report_api(
105+
agent_evaluation_id: int,
106+
authorization: Optional[str] = Header(None),
107+
):
108+
try:
109+
_, tenant_id = get_current_user_id(authorization)
110+
data, fail_count = generate_agent_evaluation_report_impl(
111+
agent_evaluation_id=agent_evaluation_id,
112+
tenant_id=tenant_id,
113+
)
114+
suffix = "_failed.xlsx" if fail_count > 0 else "_all.xlsx"
115+
return StreamingResponse(
116+
iter([data]),
117+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
118+
headers={
119+
"Content-Disposition": f"attachment; filename=evaluation_report_{agent_evaluation_id}{suffix}"
120+
},
121+
)
122+
except ValueError as ve:
123+
raise HTTPException(status_code=404, detail=str(ve))
124+
except Exception as exc:
125+
logger.exception("Download agent evaluation report error: %r", exc)
126+
raise HTTPException(status_code=500, detail="Download agent evaluation report error")
127+
128+
129+
@router.delete("/{agent_evaluation_id}")
130+
async def delete_agent_evaluation_api(
131+
agent_evaluation_id: int,
132+
authorization: Optional[str] = Header(None),
133+
):
134+
"""Soft-delete an evaluation run. Only the creator may delete."""
135+
try:
136+
user_id, tenant_id = get_current_user_id(authorization)
137+
delete_agent_evaluation_run_impl(
138+
agent_evaluation_id=agent_evaluation_id,
139+
tenant_id=tenant_id,
140+
user_id=user_id,
141+
)
142+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success"})
143+
except ValueError as ve:
144+
raise HTTPException(status_code=400, detail=str(ve))
145+
except Exception as exc:
146+
logger.exception("Delete agent evaluation error: %r", exc)
147+
raise HTTPException(status_code=500, detail="Delete agent evaluation error")

backend/apps/config_app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from apps.monitoring_app import router as monitoring_router
3434
from apps.a2a_server_app import router as a2a_server_router
3535
from apps.haotian_app import router as haotian_router
36+
from apps.evaluation_set_app import router as evaluation_set_router
37+
from apps.agent_evaluation_app import router as agent_evaluation_router
3638
from apps.aidp_app import router as aidp_router
3739
from apps.cas_app import router as cas_router
3840
from consts.const import IS_SPEED_MODE
@@ -93,4 +95,6 @@ async def sync_default_prompt_template_on_startup():
9395
app.include_router(a2a_client_router)
9496
app.include_router(a2a_server_router)
9597
app.include_router(haotian_router)
98+
app.include_router(evaluation_set_router)
99+
app.include_router(agent_evaluation_router)
96100
app.include_router(aidp_router)

backend/apps/evaluation_set_app.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import io
2+
import json
3+
import logging
4+
from http import HTTPStatus
5+
from typing import Any, Dict, List, Optional
6+
7+
from fastapi import APIRouter, Body, File, Form, Header, HTTPException, Query, UploadFile
8+
from fastapi.responses import JSONResponse, StreamingResponse
9+
10+
from services.evaluation_set_service import (
11+
create_evaluation_set_from_cases,
12+
create_evaluation_set_from_jsonl,
13+
delete_evaluation_set_impl,
14+
get_evaluation_set_impl,
15+
list_evaluation_set_cases_impl,
16+
list_evaluation_sets_impl,
17+
)
18+
from utils.auth_utils import get_current_user_id
19+
from utils.evaluation_set_excel_utils import build_evaluation_set_excel_template_bytes, parse_evaluation_cases_from_excel
20+
21+
logger = logging.getLogger("evaluation_set_app")
22+
23+
router = APIRouter(prefix="/evaluation-sets")
24+
25+
26+
@router.get("")
27+
async def list_evaluation_sets_api(
28+
limit: int = Query(50, ge=1, le=200),
29+
offset: int = Query(0, ge=0),
30+
authorization: Optional[str] = Header(None),
31+
):
32+
try:
33+
user_id, tenant_id = get_current_user_id(authorization)
34+
data = list_evaluation_sets_impl(tenant_id=tenant_id, limit=limit, offset=offset)
35+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
36+
except Exception as exc:
37+
logger.exception("List evaluation sets error: %r", exc)
38+
raise HTTPException(status_code=500, detail="List evaluation sets error")
39+
40+
41+
@router.post("")
42+
async def create_evaluation_set_api(
43+
name: str = Body(...),
44+
description: Optional[str] = Body(None),
45+
source_filename: Optional[str] = Body(None),
46+
jsonl_text: str = Body(..., description="Raw JSONL content"),
47+
authorization: Optional[str] = Header(None),
48+
):
49+
try:
50+
user_id, tenant_id = get_current_user_id(authorization)
51+
meta = create_evaluation_set_from_jsonl(
52+
tenant_id=tenant_id,
53+
name=name,
54+
description=description,
55+
source_filename=source_filename,
56+
jsonl_text=jsonl_text,
57+
created_by=user_id,
58+
)
59+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": meta})
60+
except ValueError as ve:
61+
raise HTTPException(status_code=400, detail=str(ve))
62+
except Exception as exc:
63+
logger.exception("Create evaluation set error: %r", exc)
64+
raise HTTPException(status_code=500, detail="Create evaluation set error")
65+
66+
67+
@router.post("/upload")
68+
async def upload_evaluation_set_api(
69+
name: str = Form(...),
70+
description: Optional[str] = Form(None),
71+
files: List[UploadFile] = File(...),
72+
authorization: Optional[str] = Header(None, alias="Authorization"),
73+
):
74+
try:
75+
user_id, tenant_id = get_current_user_id(authorization)
76+
if not files:
77+
raise ValueError("At least one file is required")
78+
79+
all_cases: List[Dict[str, Any]] = []
80+
source_filenames: List[str] = []
81+
82+
for file in files:
83+
raw = await file.read()
84+
filename = file.filename or ""
85+
source_filenames.append(filename)
86+
lower = filename.lower()
87+
88+
if lower.endswith(".xlsx") or lower.endswith(".xls"):
89+
cases = parse_evaluation_cases_from_excel(filename=filename, raw=raw)
90+
all_cases.extend(cases)
91+
else:
92+
# Backward compatible: still accept JSONL upload
93+
try:
94+
jsonl_text = raw.decode("utf-8")
95+
except Exception:
96+
jsonl_text = raw.decode("utf-8", errors="ignore")
97+
98+
# Parse JSONL into cases
99+
for line in jsonl_text.strip().splitlines():
100+
line = line.strip()
101+
if not line:
102+
continue
103+
obj = json.loads(line)
104+
all_cases.append({
105+
"query": obj.get("query", ""),
106+
"answer": obj.get("answer", ""),
107+
"context": obj.get("context"),
108+
"case_id": obj.get("case_id"),
109+
})
110+
111+
if not all_cases:
112+
raise ValueError("No valid cases found in uploaded files")
113+
114+
meta = create_evaluation_set_from_cases(
115+
tenant_id=tenant_id,
116+
name=name,
117+
description=description,
118+
source_filename=", ".join(source_filenames),
119+
cases=all_cases,
120+
created_by=user_id,
121+
)
122+
123+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": meta})
124+
except ValueError as ve:
125+
raise HTTPException(status_code=400, detail=str(ve))
126+
except Exception as exc:
127+
logger.exception("Upload evaluation set error: %r", exc)
128+
raise HTTPException(status_code=500, detail="Upload evaluation set error")
129+
130+
131+
@router.get("/template")
132+
async def download_evaluation_set_template_api():
133+
"""Download Excel template for evaluation set upload."""
134+
data = build_evaluation_set_excel_template_bytes()
135+
headers = {
136+
"Content-Disposition": 'attachment; filename="evaluation_set_template.xlsx"'
137+
}
138+
return StreamingResponse(
139+
io.BytesIO(data),
140+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
141+
headers=headers,
142+
)
143+
144+
145+
@router.get("/{evaluation_set_id}")
146+
async def get_evaluation_set_api(
147+
evaluation_set_id: int,
148+
authorization: Optional[str] = Header(None),
149+
):
150+
try:
151+
_, tenant_id = get_current_user_id(authorization)
152+
data = get_evaluation_set_impl(evaluation_set_id=evaluation_set_id, tenant_id=tenant_id)
153+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
154+
except Exception as exc:
155+
logger.exception("Get evaluation set error: %r", exc)
156+
raise HTTPException(status_code=500, detail="Get evaluation set error")
157+
158+
159+
@router.get("/{evaluation_set_id}/cases")
160+
async def list_evaluation_set_cases_api(
161+
evaluation_set_id: int,
162+
limit: int = Query(50, ge=1, le=200),
163+
offset: int = Query(0, ge=0),
164+
authorization: Optional[str] = Header(None),
165+
):
166+
try:
167+
_, tenant_id = get_current_user_id(authorization)
168+
data = list_evaluation_set_cases_impl(
169+
evaluation_set_id=evaluation_set_id,
170+
tenant_id=tenant_id,
171+
limit=limit,
172+
offset=offset,
173+
)
174+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success", "data": data})
175+
except Exception as exc:
176+
logger.exception("List evaluation set cases error: %r", exc)
177+
raise HTTPException(status_code=500, detail="List evaluation set cases error")
178+
179+
180+
@router.delete("/{evaluation_set_id}")
181+
async def delete_evaluation_set_api(
182+
evaluation_set_id: int,
183+
authorization: Optional[str] = Header(None),
184+
):
185+
"""Soft-delete an evaluation set.
186+
187+
Blocked when any active evaluation run still references the set, so
188+
historical runs never lose their context.
189+
"""
190+
try:
191+
user_id, tenant_id = get_current_user_id(authorization)
192+
delete_evaluation_set_impl(evaluation_set_id, tenant_id, user_id)
193+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "Success"})
194+
except ValueError as ve:
195+
raise HTTPException(status_code=400, detail=str(ve))
196+
except Exception as exc:
197+
logger.exception("Delete evaluation set error: %r", exc)
198+
raise HTTPException(status_code=500, detail="Delete evaluation set error")

0 commit comments

Comments
 (0)