codebuff-agent 2.1.74__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- jupyter_ext/tools/user_tools.py +132 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"""RAG Benchmark Handler — 테스트 케이스 관리, 벤치마크 실행, 결과 조회."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from jupyter_server.base.handlers import APIHandler
|
|
13
|
+
|
|
14
|
+
from codebuff_core.legacy_paths import resolve_home
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# 벤치마크 데이터 저장 경로
|
|
19
|
+
_BENCHMARK_DIR = resolve_home("benchmark")
|
|
20
|
+
_TEST_CASES_FILE = _BENCHMARK_DIR / "test_cases.json"
|
|
21
|
+
_RESULTS_DIR = _BENCHMARK_DIR / "results"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_dirs():
|
|
25
|
+
_BENCHMARK_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_test_cases() -> List[Dict[str, Any]]:
|
|
30
|
+
_ensure_dirs()
|
|
31
|
+
if _TEST_CASES_FILE.exists():
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(_TEST_CASES_FILE.read_text(encoding="utf-8"))
|
|
34
|
+
except Exception:
|
|
35
|
+
return []
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _save_test_cases(cases: List[Dict[str, Any]]):
|
|
40
|
+
_ensure_dirs()
|
|
41
|
+
_TEST_CASES_FILE.write_text(
|
|
42
|
+
json.dumps(cases, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_results() -> List[Dict[str, Any]]:
|
|
47
|
+
"""실행 결과 목록 (요약만)."""
|
|
48
|
+
_ensure_dirs()
|
|
49
|
+
results = []
|
|
50
|
+
for f in sorted(_RESULTS_DIR.glob("*.json"), reverse=True):
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
53
|
+
results.append(
|
|
54
|
+
{
|
|
55
|
+
"id": f.stem,
|
|
56
|
+
"date": data.get("date", ""),
|
|
57
|
+
"memo": data.get("memo", ""),
|
|
58
|
+
"total_cases": data.get("total_cases", 0),
|
|
59
|
+
"summary": data.get("summary", {}),
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
return results
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_result_detail(result_id: str) -> Optional[Dict[str, Any]]:
|
|
68
|
+
path = _RESULTS_DIR / f"{result_id}.json"
|
|
69
|
+
if path.exists():
|
|
70
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _save_result(result: Dict[str, Any]) -> str:
|
|
75
|
+
_ensure_dirs()
|
|
76
|
+
result_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
|
77
|
+
path = _RESULTS_DIR / f"{result_id}.json"
|
|
78
|
+
result["id"] = result_id
|
|
79
|
+
path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
80
|
+
return result_id
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ─── Test Cases CRUD ───
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BenchmarkTestCasesHandler(APIHandler):
|
|
87
|
+
"""GET/POST /codebuff/benchmark/test-cases"""
|
|
88
|
+
|
|
89
|
+
async def get(self):
|
|
90
|
+
self.write(json.dumps(_load_test_cases(), ensure_ascii=False))
|
|
91
|
+
|
|
92
|
+
async def post(self):
|
|
93
|
+
body = (
|
|
94
|
+
json.loads(self.request.body.decode("utf-8")) if self.request.body else {}
|
|
95
|
+
)
|
|
96
|
+
action = body.get("action", "save_all")
|
|
97
|
+
|
|
98
|
+
if action == "save_all":
|
|
99
|
+
cases = body.get("test_cases", [])
|
|
100
|
+
_save_test_cases(cases)
|
|
101
|
+
self.write({"status": "ok", "count": len(cases)})
|
|
102
|
+
|
|
103
|
+
elif action == "add":
|
|
104
|
+
cases = _load_test_cases()
|
|
105
|
+
new_case = body.get("test_case", {})
|
|
106
|
+
if not new_case.get("id"):
|
|
107
|
+
new_case["id"] = f"tc_{uuid.uuid4().hex[:8]}"
|
|
108
|
+
cases.append(new_case)
|
|
109
|
+
_save_test_cases(cases)
|
|
110
|
+
self.write({"status": "ok", "id": new_case["id"]})
|
|
111
|
+
|
|
112
|
+
elif action == "delete":
|
|
113
|
+
tc_id = body.get("id")
|
|
114
|
+
cases = [c for c in _load_test_cases() if c.get("id") != tc_id]
|
|
115
|
+
_save_test_cases(cases)
|
|
116
|
+
self.write({"status": "ok"})
|
|
117
|
+
|
|
118
|
+
elif action == "update":
|
|
119
|
+
tc_id = body.get("id")
|
|
120
|
+
updated = body.get("test_case", {})
|
|
121
|
+
cases = _load_test_cases()
|
|
122
|
+
for i, c in enumerate(cases):
|
|
123
|
+
if c.get("id") == tc_id:
|
|
124
|
+
cases[i] = {**c, **updated}
|
|
125
|
+
break
|
|
126
|
+
_save_test_cases(cases)
|
|
127
|
+
self.write({"status": "ok"})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ─── Auto-generate test cases ───
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BenchmarkGenerateHandler(APIHandler):
|
|
134
|
+
"""POST /codebuff/benchmark/generate — Qdrant 문서에서 QA 쌍 자동 생성."""
|
|
135
|
+
|
|
136
|
+
async def post(self):
|
|
137
|
+
body = (
|
|
138
|
+
json.loads(self.request.body.decode("utf-8")) if self.request.body else {}
|
|
139
|
+
)
|
|
140
|
+
collection = body.get("collection", "guide_docs")
|
|
141
|
+
sources = body.get("sources", []) # 선택된 문서 목록
|
|
142
|
+
per_source = body.get("per_source", 1)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
from .rag_admin_handler import _ensure_manager
|
|
146
|
+
|
|
147
|
+
mgr = _ensure_manager()
|
|
148
|
+
if mgr is None:
|
|
149
|
+
self.set_status(500)
|
|
150
|
+
self.write({"error": "RAG 매니저 미초기화"})
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
client = mgr._client
|
|
154
|
+
|
|
155
|
+
# 선택된 문서의 chunk 가져오기
|
|
156
|
+
chunks_by_source: Dict[str, List[Dict]] = {}
|
|
157
|
+
scroll_offset = None
|
|
158
|
+
while True:
|
|
159
|
+
pts, scroll_offset = client.scroll(
|
|
160
|
+
collection_name=collection,
|
|
161
|
+
limit=100,
|
|
162
|
+
with_payload=True,
|
|
163
|
+
with_vectors=False,
|
|
164
|
+
offset=scroll_offset,
|
|
165
|
+
)
|
|
166
|
+
for pt in pts:
|
|
167
|
+
p = pt.payload or {}
|
|
168
|
+
src = p.get("source", p.get("title", "unknown"))
|
|
169
|
+
if sources and src not in sources:
|
|
170
|
+
continue
|
|
171
|
+
content = p.get("content", "")
|
|
172
|
+
if len(content) < 100:
|
|
173
|
+
continue
|
|
174
|
+
chunks_by_source.setdefault(src, []).append(
|
|
175
|
+
{
|
|
176
|
+
"source": src,
|
|
177
|
+
"title": p.get("title", ""),
|
|
178
|
+
"section": p.get("section", ""),
|
|
179
|
+
"content": content[:3000],
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
if scroll_offset is None:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if not chunks_by_source:
|
|
186
|
+
self.write(
|
|
187
|
+
json.dumps(
|
|
188
|
+
{"test_cases": [], "message": "해당 문서를 찾을 수 없습니다."}
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# SSE 헤더
|
|
194
|
+
self.set_header("Content-Type", "text/event-stream")
|
|
195
|
+
self.set_header("Cache-Control", "no-cache")
|
|
196
|
+
self.set_header("Connection", "keep-alive")
|
|
197
|
+
|
|
198
|
+
# LLM으로 QA 생성
|
|
199
|
+
from codebuff_core.llm.service import LLMService
|
|
200
|
+
from codebuff_core.managers.config_manager import get_config_manager
|
|
201
|
+
|
|
202
|
+
cfg = get_config_manager().get_config()
|
|
203
|
+
llm_config = cfg.get("vllm", {})
|
|
204
|
+
if not llm_config:
|
|
205
|
+
self.write(
|
|
206
|
+
f"data: {json.dumps({'type': 'error', 'error': 'LLM 설정 없음'})}\n\n"
|
|
207
|
+
)
|
|
208
|
+
self.finish()
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
llm = LLMService({"provider": "vllm", "vllm": llm_config})
|
|
212
|
+
generated: List[Dict] = []
|
|
213
|
+
|
|
214
|
+
# 전체 chunk 목록 생성
|
|
215
|
+
all_tasks: List[Dict] = []
|
|
216
|
+
for src, chunks in chunks_by_source.items():
|
|
217
|
+
selected = sorted(
|
|
218
|
+
chunks, key=lambda c: len(c["content"]), reverse=True
|
|
219
|
+
)[:per_source]
|
|
220
|
+
for chunk in selected:
|
|
221
|
+
all_tasks.append(chunk)
|
|
222
|
+
|
|
223
|
+
total_tasks = len(all_tasks)
|
|
224
|
+
self.write(
|
|
225
|
+
f"data: {json.dumps({'type': 'progress', 'current': 0, 'total': total_tasks, 'source': '병렬 생성 시작'}, ensure_ascii=False)}\n\n"
|
|
226
|
+
)
|
|
227
|
+
await self.flush()
|
|
228
|
+
|
|
229
|
+
# 병렬 LLM 호출 (최대 5개 동시)
|
|
230
|
+
_PARALLEL = 5
|
|
231
|
+
_completed = 0
|
|
232
|
+
|
|
233
|
+
async def _generate_one(chunk: Dict) -> Optional[Dict]:
|
|
234
|
+
prompt = (
|
|
235
|
+
"아래 문서 내용을 읽고, 이 문서로만 답할 수 있는 질문과 기대 답변을 만들어주세요.\n\n"
|
|
236
|
+
"규칙:\n"
|
|
237
|
+
"- 실제 사용자가 할 법한 자연어 질문으로 만드세요.\n"
|
|
238
|
+
"- 기대 답변(ground_truth)은 문서 내용을 충실히 반영하여 **구체적이고 상세하게** 작성하세요.\n"
|
|
239
|
+
"- 핵심 함수명, 파라미터, 절차, 설정값 등을 포함하세요.\n"
|
|
240
|
+
"- must_not_contain에는 문서에 없지만 LLM이 만들어낼 수 있는 그럴듯한 키워드를 넣으세요.\n\n"
|
|
241
|
+
"반드시 아래 JSON 형식으로만 응답하세요:\n"
|
|
242
|
+
'{"question": "질문", "ground_truth": "상세한 기대 답변", '
|
|
243
|
+
'"must_not_contain": ["할루시네이션 가능 키워드"]}\n\n'
|
|
244
|
+
f"[문서: {chunk['source']}]\n"
|
|
245
|
+
f"[섹션: {chunk['section']}]\n\n"
|
|
246
|
+
f"{chunk['content']}"
|
|
247
|
+
)
|
|
248
|
+
try:
|
|
249
|
+
resp = await llm.generate_response(prompt)
|
|
250
|
+
resp = resp.strip()
|
|
251
|
+
if resp.startswith("```"):
|
|
252
|
+
resp = resp.split("```")[1]
|
|
253
|
+
if resp.startswith("json"):
|
|
254
|
+
resp = resp[4:]
|
|
255
|
+
parsed = json.loads(resp)
|
|
256
|
+
return {
|
|
257
|
+
"id": f"auto_{uuid.uuid4().hex[:8]}",
|
|
258
|
+
"category": collection,
|
|
259
|
+
"question": parsed.get("question", ""),
|
|
260
|
+
"ground_truth": parsed.get("ground_truth", ""),
|
|
261
|
+
"must_not_contain": parsed.get("must_not_contain", []),
|
|
262
|
+
"source": chunk["source"],
|
|
263
|
+
"difficulty": "medium",
|
|
264
|
+
"auto_generated": True,
|
|
265
|
+
}
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.warning(f"QA generation failed for {chunk['source']}: {e}")
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
import asyncio as _aio
|
|
271
|
+
|
|
272
|
+
sem = _aio.Semaphore(_PARALLEL)
|
|
273
|
+
|
|
274
|
+
async def _gen_with_sem(chunk: Dict):
|
|
275
|
+
nonlocal _completed
|
|
276
|
+
async with sem:
|
|
277
|
+
r = await _generate_one(chunk)
|
|
278
|
+
_completed += 1
|
|
279
|
+
if isinstance(r, dict) and r is not None:
|
|
280
|
+
generated.append(r)
|
|
281
|
+
self.write(
|
|
282
|
+
f"data: {json.dumps({'type': 'case', 'test_case': r}, ensure_ascii=False)}\n\n"
|
|
283
|
+
)
|
|
284
|
+
self.write(
|
|
285
|
+
f"data: {json.dumps({'type': 'progress', 'current': _completed, 'total': total_tasks, 'source': chunk['source']}, ensure_ascii=False)}\n\n"
|
|
286
|
+
)
|
|
287
|
+
await self.flush()
|
|
288
|
+
return r
|
|
289
|
+
|
|
290
|
+
await _aio.gather(*[_gen_with_sem(c) for c in all_tasks])
|
|
291
|
+
|
|
292
|
+
# 완료
|
|
293
|
+
self.write(
|
|
294
|
+
f"data: {json.dumps({'type': 'complete', 'total': len(generated)})}\n\n"
|
|
295
|
+
)
|
|
296
|
+
await self.flush()
|
|
297
|
+
self.finish()
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Benchmark generate failed: {e}", exc_info=True)
|
|
301
|
+
self.set_status(500)
|
|
302
|
+
self.write({"error": str(e)})
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ─── Available sources (for UI) ───
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class BenchmarkSourcesHandler(APIHandler):
|
|
309
|
+
"""GET /codebuff/benchmark/sources?collection=guide_docs"""
|
|
310
|
+
|
|
311
|
+
async def get(self):
|
|
312
|
+
collection = self.get_argument("collection", "guide_docs")
|
|
313
|
+
try:
|
|
314
|
+
from .rag_admin_handler import _ensure_manager
|
|
315
|
+
|
|
316
|
+
mgr = _ensure_manager()
|
|
317
|
+
if mgr is None:
|
|
318
|
+
self.write(json.dumps({"sources": []}))
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
client = mgr._client
|
|
322
|
+
sources: Dict[str, int] = {}
|
|
323
|
+
scroll_offset = None
|
|
324
|
+
while True:
|
|
325
|
+
pts, scroll_offset = client.scroll(
|
|
326
|
+
collection_name=collection,
|
|
327
|
+
limit=100,
|
|
328
|
+
with_payload=["source", "title"],
|
|
329
|
+
with_vectors=False,
|
|
330
|
+
offset=scroll_offset,
|
|
331
|
+
)
|
|
332
|
+
for pt in pts:
|
|
333
|
+
p = pt.payload or {}
|
|
334
|
+
src = p.get("source", p.get("title", "unknown"))
|
|
335
|
+
sources[src] = sources.get(src, 0) + 1
|
|
336
|
+
if scroll_offset is None:
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
result = [{"source": s, "chunks": c} for s, c in sorted(sources.items())]
|
|
340
|
+
self.write(json.dumps({"sources": result}, ensure_ascii=False))
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
self.write(json.dumps({"sources": [], "error": str(e)}))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ─── Run benchmark ───
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class BenchmarkRunHandler(APIHandler):
|
|
350
|
+
"""POST /codebuff/benchmark/run — SSE stream으로 벤치마크 실행."""
|
|
351
|
+
|
|
352
|
+
async def post(self):
|
|
353
|
+
body = (
|
|
354
|
+
json.loads(self.request.body.decode("utf-8")) if self.request.body else {}
|
|
355
|
+
)
|
|
356
|
+
test_case_ids = body.get("test_case_ids") # None이면 전체
|
|
357
|
+
memo = body.get("memo", "")
|
|
358
|
+
|
|
359
|
+
all_cases = _load_test_cases()
|
|
360
|
+
if test_case_ids:
|
|
361
|
+
cases = [c for c in all_cases if c.get("id") in test_case_ids]
|
|
362
|
+
else:
|
|
363
|
+
cases = all_cases
|
|
364
|
+
|
|
365
|
+
if not cases:
|
|
366
|
+
self.set_status(400)
|
|
367
|
+
self.write({"error": "테스트 케이스가 없습니다."})
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# SSE 헤더
|
|
371
|
+
self.set_header("Content-Type", "text/event-stream")
|
|
372
|
+
self.set_header("Cache-Control", "no-cache")
|
|
373
|
+
self.set_header("Connection", "keep-alive")
|
|
374
|
+
|
|
375
|
+
from codebuff_core.managers.config_manager import get_config_manager
|
|
376
|
+
|
|
377
|
+
cfg = get_config_manager().get_config()
|
|
378
|
+
|
|
379
|
+
results: List[Dict] = []
|
|
380
|
+
total = len(cases)
|
|
381
|
+
_completed = 0
|
|
382
|
+
_cancelled = False
|
|
383
|
+
_PARALLEL = 5
|
|
384
|
+
|
|
385
|
+
import asyncio as _aio
|
|
386
|
+
|
|
387
|
+
sem = _aio.Semaphore(_PARALLEL)
|
|
388
|
+
|
|
389
|
+
async def _run_one(tc):
|
|
390
|
+
nonlocal _completed, _cancelled
|
|
391
|
+
if _cancelled:
|
|
392
|
+
return None
|
|
393
|
+
async with sem:
|
|
394
|
+
if _cancelled:
|
|
395
|
+
return None
|
|
396
|
+
try:
|
|
397
|
+
r = await self._run_single_case(tc, cfg)
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f"Benchmark case {tc['id']} failed: {e}")
|
|
400
|
+
r = {"case_id": tc["id"], "error": str(e), "scores": {}}
|
|
401
|
+
_completed += 1
|
|
402
|
+
results.append(r)
|
|
403
|
+
try:
|
|
404
|
+
self.write(
|
|
405
|
+
f"data: {json.dumps({'type': 'case_result', 'result': r}, ensure_ascii=False, default=str)}\n\n"
|
|
406
|
+
)
|
|
407
|
+
self.write(
|
|
408
|
+
f"data: {json.dumps({'type': 'progress', 'current': _completed, 'total': total})}\n\n"
|
|
409
|
+
)
|
|
410
|
+
await self.flush()
|
|
411
|
+
except Exception:
|
|
412
|
+
_cancelled = True
|
|
413
|
+
return r
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
await _aio.gather(*[_run_one(tc) for tc in cases])
|
|
417
|
+
except Exception:
|
|
418
|
+
_cancelled = True
|
|
419
|
+
|
|
420
|
+
# 완료된 결과만 저장 (취소 시에도)
|
|
421
|
+
valid_results = [r for r in results if r is not None]
|
|
422
|
+
summary = self._calc_summary(valid_results)
|
|
423
|
+
|
|
424
|
+
full_result = {
|
|
425
|
+
"date": datetime.now().isoformat(),
|
|
426
|
+
"memo": memo + (" (중단됨)" if _cancelled else ""),
|
|
427
|
+
"total_cases": len(valid_results),
|
|
428
|
+
"summary": summary,
|
|
429
|
+
"results": valid_results,
|
|
430
|
+
}
|
|
431
|
+
result_id = _save_result(full_result)
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
self.write(
|
|
435
|
+
f"data: {json.dumps({'type': 'complete', 'result_id': result_id, 'summary': summary, 'cancelled': _cancelled}, ensure_ascii=False, default=str)}\n\n"
|
|
436
|
+
)
|
|
437
|
+
await self.flush()
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
self.finish()
|
|
441
|
+
|
|
442
|
+
async def _run_single_case(
|
|
443
|
+
self, tc: Dict[str, Any], cfg: Dict[str, Any]
|
|
444
|
+
) -> Dict[str, Any]:
|
|
445
|
+
"""단일 테스트 케이스 실행 + 평가."""
|
|
446
|
+
from codebuff_core.services.rag_chat_agent import RAGChatAgent
|
|
447
|
+
|
|
448
|
+
llm_cfg = {"provider": "vllm", "vllm": cfg.get("vllm", {})}
|
|
449
|
+
|
|
450
|
+
# knowledge chat과 동일한 프롬프트 사용 — 에이전트가 knowledge 빌더와 centri
|
|
451
|
+
# `prompts.chat_rag_prompt`(grounding)를 직접 읽는다. `chat_system_prompt` 는
|
|
452
|
+
# simple 모드 키라 여기에 싣지 않는다 (이슈 #178)
|
|
453
|
+
# 1. Knowledge Chat으로 질문 실행
|
|
454
|
+
thread_id = f"benchmark-{uuid.uuid4().hex[:8]}"
|
|
455
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
456
|
+
|
|
457
|
+
agent = RAGChatAgent(
|
|
458
|
+
llm_cfg,
|
|
459
|
+
checkpointer=MemorySaver(),
|
|
460
|
+
)
|
|
461
|
+
response_text = ""
|
|
462
|
+
search_count = 0
|
|
463
|
+
start_time = time.time()
|
|
464
|
+
|
|
465
|
+
async for chunk in agent.stream_response(
|
|
466
|
+
user_message=tc["question"],
|
|
467
|
+
thread_id=thread_id,
|
|
468
|
+
):
|
|
469
|
+
if isinstance(chunk, dict):
|
|
470
|
+
if chunk.get("content"):
|
|
471
|
+
response_text += chunk["content"]
|
|
472
|
+
if chunk.get("status") and "검색 완료" in chunk.get("status", ""):
|
|
473
|
+
search_count += 1
|
|
474
|
+
|
|
475
|
+
# agent state에서 ToolMessage 추출 (compression 적용 — LLM이 실제로 본 것)
|
|
476
|
+
rag_results_text = ""
|
|
477
|
+
_REFLECTION_MARKER = "[검색 결과 분석 지시]"
|
|
478
|
+
try:
|
|
479
|
+
from langchain_core.messages import ToolMessage as _TM
|
|
480
|
+
from codebuff_core.services.rag_chat_agent import (
|
|
481
|
+
_create_context_compression_middleware_class,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if agent._agent is not None:
|
|
485
|
+
_config = {"configurable": {"thread_id": thread_id}}
|
|
486
|
+
_state = await agent._agent.aget_state(_config)
|
|
487
|
+
_raw_msgs = list(_state.values.get("messages", []))
|
|
488
|
+
|
|
489
|
+
# compression 적용 (LLM이 실제로 본 compressed 버전)
|
|
490
|
+
_compressor = _create_context_compression_middleware_class()()
|
|
491
|
+
_compressed = _compressor.before_model({"messages": _raw_msgs})
|
|
492
|
+
_msgs = _compressed["messages"] if _compressed else _raw_msgs
|
|
493
|
+
|
|
494
|
+
for _msg in _msgs:
|
|
495
|
+
if isinstance(_msg, _TM) and _msg.content:
|
|
496
|
+
_content = _msg.content
|
|
497
|
+
if _REFLECTION_MARKER in _content:
|
|
498
|
+
_content = _content[: _content.index(_REFLECTION_MARKER)]
|
|
499
|
+
if _content.strip():
|
|
500
|
+
rag_results_text += _content[:8000] + "\n"
|
|
501
|
+
search_count += 1
|
|
502
|
+
except Exception as _state_err:
|
|
503
|
+
logger.warning(f"[Benchmark] {tc['id']}: aget_state failed: {_state_err}")
|
|
504
|
+
|
|
505
|
+
# fallback: 검색 결과를 못 가져왔으면 응답 자체를 근거로 사용 (평가 정확도 저하 방지)
|
|
506
|
+
if not rag_results_text and response_text:
|
|
507
|
+
rag_results_text = (
|
|
508
|
+
"[검색 결과를 직접 추출하지 못했습니다. "
|
|
509
|
+
"아래 응답 내용에서 출처 표기를 참고하여 faithfulness를 평가하세요.]\n"
|
|
510
|
+
+ response_text[:4000]
|
|
511
|
+
)
|
|
512
|
+
logger.warning(
|
|
513
|
+
f"[Benchmark] {tc['id']}: using response as fallback evidence"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# search_count fallback: ToolMessage 수로 추정
|
|
517
|
+
if search_count == 0 and rag_results_text:
|
|
518
|
+
search_count = rag_results_text.count('"success"')
|
|
519
|
+
|
|
520
|
+
elapsed = round(time.time() - start_time, 1)
|
|
521
|
+
re_search_count = max(search_count - 1, 0)
|
|
522
|
+
|
|
523
|
+
# 2. LLM 기반 평가 (검색 결과 포함)
|
|
524
|
+
scores = await self._evaluate(tc, response_text, rag_results_text, cfg)
|
|
525
|
+
scores["response_time"] = elapsed
|
|
526
|
+
scores["search_count"] = search_count
|
|
527
|
+
scores["re_search_count"] = re_search_count
|
|
528
|
+
|
|
529
|
+
# effort bonus: 재검색 시도에 대한 가산 (max +5)
|
|
530
|
+
effort_bonus = min(re_search_count, 2) * 2.5
|
|
531
|
+
scores["effort_bonus"] = effort_bonus
|
|
532
|
+
scores["overall"] = min(round(scores.get("overall", 0) + effort_bonus, 1), 100)
|
|
533
|
+
|
|
534
|
+
# Bad test case auto-detection: 검색 5회 이상 + 점수 30 미만 → 데이터 불일치
|
|
535
|
+
data_mismatch = search_count >= 5 and scores.get("overall", 0) < 30
|
|
536
|
+
if data_mismatch:
|
|
537
|
+
logger.info(
|
|
538
|
+
"[Benchmark] %s: data_mismatch detected "
|
|
539
|
+
"(search_count=%d, overall=%.1f)",
|
|
540
|
+
tc["id"],
|
|
541
|
+
search_count,
|
|
542
|
+
scores.get("overall", 0),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
result = {
|
|
546
|
+
"case_id": tc["id"],
|
|
547
|
+
"question": tc["question"],
|
|
548
|
+
"response": response_text,
|
|
549
|
+
"scores": scores,
|
|
550
|
+
}
|
|
551
|
+
if data_mismatch:
|
|
552
|
+
result["data_mismatch"] = True
|
|
553
|
+
result["data_mismatch_reason"] = (
|
|
554
|
+
f"검색 {search_count}회 시도했으나 점수 {scores.get('overall', 0):.1f}/100. "
|
|
555
|
+
"테스트 케이스의 질문이 현재 RAG 데이터와 맞지 않을 수 있습니다."
|
|
556
|
+
)
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
async def _fetch_source_content(self, source: str, collection: str) -> str:
|
|
560
|
+
"""테스트 케이스의 source 문서 원문을 Qdrant에서 직접 가져옴."""
|
|
561
|
+
try:
|
|
562
|
+
from .rag_admin_handler import _ensure_manager
|
|
563
|
+
|
|
564
|
+
mgr = _ensure_manager()
|
|
565
|
+
if mgr is None:
|
|
566
|
+
return ""
|
|
567
|
+
|
|
568
|
+
client = mgr._client
|
|
569
|
+
chunks: List[str] = []
|
|
570
|
+
scroll_offset = None
|
|
571
|
+
while True:
|
|
572
|
+
pts, scroll_offset = client.scroll(
|
|
573
|
+
collection_name=collection,
|
|
574
|
+
limit=100,
|
|
575
|
+
with_payload=True,
|
|
576
|
+
with_vectors=False,
|
|
577
|
+
offset=scroll_offset,
|
|
578
|
+
)
|
|
579
|
+
for pt in pts:
|
|
580
|
+
p = pt.payload or {}
|
|
581
|
+
src = p.get("source", p.get("title", ""))
|
|
582
|
+
# 정확 매칭 또는 부분 매칭 (source 필드에 파일명 포함)
|
|
583
|
+
if src == source or source in src or src in source:
|
|
584
|
+
content = p.get("content", "")
|
|
585
|
+
section = p.get("section", "")
|
|
586
|
+
if content:
|
|
587
|
+
header = f"[{section}] " if section else ""
|
|
588
|
+
chunks.append(f"{header}{content}")
|
|
589
|
+
if scroll_offset is None:
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
print(
|
|
593
|
+
f"[Benchmark] _fetch_source_content: "
|
|
594
|
+
f"source={source!r} collection={collection!r} "
|
|
595
|
+
f"chunks={len(chunks)}",
|
|
596
|
+
flush=True,
|
|
597
|
+
)
|
|
598
|
+
return "\n\n---\n\n".join(chunks)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
print(
|
|
601
|
+
f"[Benchmark] _fetch_source_content FAILED: {e}",
|
|
602
|
+
flush=True,
|
|
603
|
+
)
|
|
604
|
+
return ""
|
|
605
|
+
|
|
606
|
+
async def _evaluate(
|
|
607
|
+
self,
|
|
608
|
+
tc: Dict[str, Any],
|
|
609
|
+
response: str,
|
|
610
|
+
rag_results: str,
|
|
611
|
+
cfg: Dict[str, Any],
|
|
612
|
+
) -> Dict[str, Any]:
|
|
613
|
+
"""LLM 기반 평가: Faithfulness + Quality.
|
|
614
|
+
|
|
615
|
+
평가 근거로 에이전트 검색 결과 + 원본 문서 내용을 모두 사용.
|
|
616
|
+
원본 문서를 직접 참조하여 faithfulness 오판을 방지.
|
|
617
|
+
"""
|
|
618
|
+
from codebuff_core.llm.service import LLMService
|
|
619
|
+
|
|
620
|
+
llm_cfg = cfg.get("vllm", {})
|
|
621
|
+
llm = LLMService({"provider": "vllm", "vllm": llm_cfg})
|
|
622
|
+
|
|
623
|
+
# 원본 문서 내용 가져오기 (테스트 케이스의 source 필드 활용)
|
|
624
|
+
source_content = ""
|
|
625
|
+
tc_source = tc.get("source", "")
|
|
626
|
+
tc_collection = tc.get("category", "guide_docs")
|
|
627
|
+
if tc_source:
|
|
628
|
+
source_content = await self._fetch_source_content(tc_source, tc_collection)
|
|
629
|
+
|
|
630
|
+
# 평가 근거: 원본 문서 + 에이전트 검색 결과 결합
|
|
631
|
+
print(
|
|
632
|
+
f"[Benchmark] _evaluate: tc_source={tc_source!r} "
|
|
633
|
+
f"source_content_len={len(source_content)} "
|
|
634
|
+
f"rag_results_len={len(rag_results)}",
|
|
635
|
+
flush=True,
|
|
636
|
+
)
|
|
637
|
+
if source_content and rag_results:
|
|
638
|
+
evidence = (
|
|
639
|
+
f"[원본 문서 ({tc_source})]\n{source_content[:8000]}\n\n"
|
|
640
|
+
f"[에이전트 검색 결과]\n{rag_results[:12000]}"
|
|
641
|
+
)
|
|
642
|
+
elif source_content:
|
|
643
|
+
evidence = f"[원본 문서 ({tc_source})]\n{source_content[:12000]}"
|
|
644
|
+
else:
|
|
645
|
+
evidence = f"[에이전트 검색 결과]\n{rag_results[:16000]}"
|
|
646
|
+
|
|
647
|
+
eval_prompt = (
|
|
648
|
+
"아래 [질문], [근거 자료], [기대 답변], [실제 응답]을 비교 평가하세요.\n\n"
|
|
649
|
+
f"[질문]\n{tc['question']}\n\n"
|
|
650
|
+
f"[근거 자료]\n{evidence}\n\n"
|
|
651
|
+
f"[기대 답변]\n{tc.get('ground_truth', '없음')}\n\n"
|
|
652
|
+
f"[실제 응답]\n{response[:3000]}\n\n"
|
|
653
|
+
"평가 항목 (각 1~10점):\n"
|
|
654
|
+
"- correctness: 기대 답변과의 일치도\n"
|
|
655
|
+
"- completeness: 답변 완성도\n"
|
|
656
|
+
"- faithfulness: 실제 응답이 [근거 자료]에 있는 내용을 바탕으로 답변했는지. "
|
|
657
|
+
"[근거 자료]에 없는 내용을 만들어냈으면 감점\n"
|
|
658
|
+
"- conciseness: 핵심만 간결하게 답했는지\n\n"
|
|
659
|
+
"할루시네이션 판정: 실제 응답의 각 주장이 [근거 자료]에 근거가 있는지 확인하세요. "
|
|
660
|
+
"[근거 자료]에 근거 없는 주장을 hallucinated_claims에 나열하세요.\n\n"
|
|
661
|
+
"반드시 아래 JSON만 출력:\n"
|
|
662
|
+
'{"correctness": N, "completeness": N, "faithfulness": N, '
|
|
663
|
+
'"conciseness": N, "hallucinated_claims": ["근거 자료에 없는 주장"], '
|
|
664
|
+
'"comment": "평가 의견"}'
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
import re as _re
|
|
668
|
+
|
|
669
|
+
for attempt in range(2):
|
|
670
|
+
try:
|
|
671
|
+
resp = await llm.generate_response(eval_prompt)
|
|
672
|
+
resp = resp.strip()
|
|
673
|
+
# JSON 추출: 코드블록 또는 raw JSON
|
|
674
|
+
if resp.startswith("```"):
|
|
675
|
+
resp = resp.split("```")[1]
|
|
676
|
+
if resp.startswith("json"):
|
|
677
|
+
resp = resp[4:]
|
|
678
|
+
# 텍스트 내 JSON 객체 추출 (fallback)
|
|
679
|
+
json_match = _re.search(r"\{[^{}]*(?:\[[^\[\]]*\])?[^{}]*\}", resp)
|
|
680
|
+
if json_match:
|
|
681
|
+
resp = json_match.group(0)
|
|
682
|
+
parsed = json.loads(resp)
|
|
683
|
+
overall = round(
|
|
684
|
+
(
|
|
685
|
+
parsed.get("correctness", 5)
|
|
686
|
+
+ parsed.get("completeness", 5)
|
|
687
|
+
+ parsed.get("faithfulness", 5)
|
|
688
|
+
+ parsed.get("conciseness", 5)
|
|
689
|
+
)
|
|
690
|
+
/ 4
|
|
691
|
+
* 10,
|
|
692
|
+
1,
|
|
693
|
+
)
|
|
694
|
+
parsed["overall"] = overall
|
|
695
|
+
|
|
696
|
+
# 전항목 0점 감지 → 평가 실패로 간주, 재시도
|
|
697
|
+
all_zero = all(
|
|
698
|
+
parsed.get(k, 0) == 0
|
|
699
|
+
for k in (
|
|
700
|
+
"correctness",
|
|
701
|
+
"completeness",
|
|
702
|
+
"faithfulness",
|
|
703
|
+
"conciseness",
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
if all_zero and attempt == 0:
|
|
707
|
+
logger.warning("Evaluation returned all zeros, retrying")
|
|
708
|
+
continue
|
|
709
|
+
if all_zero:
|
|
710
|
+
parsed["parse_error"] = True
|
|
711
|
+
parsed["comment"] = (
|
|
712
|
+
parsed.get("comment", "") + " [전항목 0점 — 평가 실패 가능성]"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
return parsed
|
|
716
|
+
except Exception as e:
|
|
717
|
+
if attempt == 0:
|
|
718
|
+
logger.warning(f"Evaluation parse failed, retrying: {e}")
|
|
719
|
+
continue
|
|
720
|
+
logger.warning(f"Evaluation failed after retry: {e}")
|
|
721
|
+
return {
|
|
722
|
+
"correctness": 5,
|
|
723
|
+
"completeness": 5,
|
|
724
|
+
"faithfulness": 5,
|
|
725
|
+
"conciseness": 5,
|
|
726
|
+
"overall": 50.0,
|
|
727
|
+
"hallucinated_claims": [],
|
|
728
|
+
"comment": f"평가 파싱 실패 (기본값 적용): {e}",
|
|
729
|
+
"parse_error": True,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
def _calc_summary(self, results: List[Dict]) -> Dict[str, Any]:
|
|
733
|
+
"""전체 결과 요약."""
|
|
734
|
+
valid = [r for r in results if r.get("scores") and not r["scores"].get("error")]
|
|
735
|
+
if not valid:
|
|
736
|
+
return {}
|
|
737
|
+
|
|
738
|
+
def avg(key):
|
|
739
|
+
vals = [r["scores"].get(key, 0) for r in valid if r["scores"].get(key)]
|
|
740
|
+
return round(sum(vals) / len(vals), 2) if vals else 0
|
|
741
|
+
|
|
742
|
+
halluc_counts = []
|
|
743
|
+
for r in valid:
|
|
744
|
+
h = r["scores"].get("hallucinated_claims", [])
|
|
745
|
+
halluc_counts.append(len(h) if isinstance(h, list) else 0)
|
|
746
|
+
|
|
747
|
+
data_mismatch_cases = sum(1 for r in results if r.get("data_mismatch"))
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
"overall": avg("overall"),
|
|
751
|
+
"correctness": avg("correctness"),
|
|
752
|
+
"completeness": avg("completeness"),
|
|
753
|
+
"faithfulness": avg("faithfulness"),
|
|
754
|
+
"conciseness": avg("conciseness"),
|
|
755
|
+
"avg_response_time": avg("response_time"),
|
|
756
|
+
"total_hallucinated_claims": sum(halluc_counts),
|
|
757
|
+
"cases_with_hallucination": sum(1 for c in halluc_counts if c > 0),
|
|
758
|
+
"total_evaluated": len(valid),
|
|
759
|
+
"data_mismatch_cases": data_mismatch_cases,
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
# ─── Results ───
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class BenchmarkResultsHandler(APIHandler):
|
|
767
|
+
"""GET/POST /codebuff/benchmark/results — 실행 결과 목록, 상세, 삭제."""
|
|
768
|
+
|
|
769
|
+
async def get(self):
|
|
770
|
+
result_id = self.get_argument("id", None)
|
|
771
|
+
if result_id:
|
|
772
|
+
detail = _load_result_detail(result_id)
|
|
773
|
+
if detail:
|
|
774
|
+
self.write(json.dumps(detail, ensure_ascii=False, default=str))
|
|
775
|
+
else:
|
|
776
|
+
self.set_status(404)
|
|
777
|
+
self.write({"error": "결과를 찾을 수 없습니다."})
|
|
778
|
+
else:
|
|
779
|
+
self.write(json.dumps(_load_results(), ensure_ascii=False, default=str))
|
|
780
|
+
|
|
781
|
+
async def post(self):
|
|
782
|
+
body = (
|
|
783
|
+
json.loads(self.request.body.decode("utf-8")) if self.request.body else {}
|
|
784
|
+
)
|
|
785
|
+
action = body.get("action")
|
|
786
|
+
if action == "delete":
|
|
787
|
+
ids = body.get("ids", [])
|
|
788
|
+
deleted = 0
|
|
789
|
+
for rid in ids:
|
|
790
|
+
path = _RESULTS_DIR / f"{rid}.json"
|
|
791
|
+
if path.exists():
|
|
792
|
+
path.unlink()
|
|
793
|
+
deleted += 1
|
|
794
|
+
self.write({"status": "ok", "deleted": deleted})
|