Skip to content

Commit c59225c

Browse files
committed
feat: add evaluation framework with DepthAPI client, benchmark dataset generation, and analysis scripts
1 parent d0d161a commit c59225c

10 files changed

Lines changed: 761 additions & 8 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ test-results/
3030
.codex
3131
.env.local
3232
.env.cloud
33+
myenv/
3334

3435
# Local/dev artifacts
3536
.venv-ingest/

evaluation/benchmark.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import asyncio
2+
import typer
3+
import json
4+
from pathlib import Path
5+
from tqdm.asyncio import tqdm
6+
from typing import List
7+
8+
from generate_benchmark import generate_benchmark_dataset
9+
from depthapi_client import DepthAPIClient
10+
from langchain_baseline import LangChainBaseline
11+
from run_judge import CustomLLMJudge
12+
from run_deepeval import evaluate_deepeval
13+
from run_ragas import evaluate_ragas
14+
from analyze_results import analyze_and_report
15+
16+
app = typer.Typer()
17+
18+
SEMAPHORE = asyncio.Semaphore(1)
19+
20+
async def evaluate_single(item, system_name, client, judge, evals_to_run):
21+
"""Run evaluation for a single item."""
22+
async with SEMAPHORE:
23+
await asyncio.sleep(1.0)
24+
query = item["query"]
25+
prompt_spec = item.get("prompt_spec")
26+
27+
if system_name == "depthapi":
28+
res = await client.query(query, prompt_spec)
29+
else:
30+
res = await client.query(query)
31+
32+
answer = res.get("answer", "")
33+
context = res.get("context", res.get("sources", [])) # Handle different keys
34+
35+
result_entry = {
36+
"id": item["id"],
37+
"system": system_name,
38+
"query": query,
39+
"prompt_spec": prompt_spec,
40+
"metadata": item.get("metadata", {}),
41+
"answer": answer
42+
}
43+
44+
if "judge" in evals_to_run:
45+
judge_res = await judge.evaluate(query, answer, context, prompt_spec)
46+
result_entry["judge"] = judge_res
47+
48+
if "deepeval" in evals_to_run:
49+
deepeval_res = evaluate_deepeval(query, answer, context)
50+
result_entry["deepeval"] = deepeval_res
51+
52+
if "ragas" in evals_to_run:
53+
ragas_res = evaluate_ragas(query, answer, context)
54+
result_entry["ragas"] = ragas_res
55+
56+
return result_entry
57+
58+
async def run_benchmark(size: int, evals: List[str], compare_baseline: bool):
59+
"""Run the complete benchmark."""
60+
print(f"Generating dataset of size {size}...")
61+
dataset = generate_benchmark_dataset(size)
62+
63+
# Save dataset
64+
Path("evaluation").mkdir(exist_ok=True)
65+
with open("evaluation/benchmark_dataset.json", "w") as f:
66+
json.dump(dataset, f, indent=2)
67+
68+
judge = CustomLLMJudge() if "judge" in evals else None
69+
70+
results = []
71+
72+
async with DepthAPIClient() as depth_client:
73+
print("Evaluating DepthAPI...")
74+
tasks = [evaluate_single(item, "depthapi", depth_client, judge, evals) for item in dataset]
75+
depth_results = await tqdm.gather(*tasks)
76+
results.extend(depth_results)
77+
78+
if compare_baseline:
79+
print("Evaluating LangChain Baseline...")
80+
# Create some dummy docs for baseline initialization
81+
dummy_docs = [{"content": f"Dummy context for {item['query']}", "metadata": {}} for item in dataset]
82+
baseline_client = LangChainBaseline(dummy_docs)
83+
tasks = [evaluate_single(item, "langchain_baseline", baseline_client, judge, evals) for item in dataset]
84+
baseline_results = await tqdm.gather(*tasks)
85+
results.extend(baseline_results)
86+
87+
print("Analyzing results...")
88+
analyze_and_report(results, "evaluation/results/reports")
89+
90+
# Save raw to evaluation/results/raw
91+
Path("evaluation/results/raw").mkdir(parents=True, exist_ok=True)
92+
with open("evaluation/results/raw/all_results.json", "w") as f:
93+
json.dump(results, f, indent=2)
94+
95+
print("Benchmark complete!")
96+
97+
@app.command()
98+
def main(
99+
size: int = typer.Option(120, help="Number of test cases to generate"),
100+
evals: List[str] = typer.Option(["deepeval", "ragas", "judge"], help="Evaluations to run"),
101+
compare_baseline: bool = typer.Option(True, "--compare-baseline/--no-baseline", help="Run against LangChain baseline")
102+
):
103+
"""Run the DepthAPI evaluation benchmark."""
104+
asyncio.run(run_benchmark(size, evals, compare_baseline))
105+
106+
def fix_sys_argv():
107+
"""Workaround for Typer to support `--evals a b c` syntax."""
108+
import sys
109+
new_argv = []
110+
i = 0
111+
while i < len(sys.argv):
112+
if sys.argv[i] == "--evals":
113+
new_argv.append("--evals")
114+
i += 1
115+
if i < len(sys.argv) and not sys.argv[i].startswith("-"):
116+
new_argv.append(sys.argv[i])
117+
i += 1
118+
while i < len(sys.argv) and not sys.argv[i].startswith("-"):
119+
new_argv.append("--evals")
120+
new_argv.append(sys.argv[i])
121+
i += 1
122+
continue
123+
new_argv.append(sys.argv[i])
124+
i += 1
125+
sys.argv = new_argv
126+
127+
if __name__ == "__main__":
128+
fix_sys_argv()
129+
app()

evaluation/depthapi_client.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import asyncio
2+
import httpx
3+
from typing import Dict, Any, Optional
4+
5+
class DepthAPIClient:
6+
"""Async client for interacting with DepthAPI."""
7+
8+
def __init__(self, base_url: str = "http://127.0.0.1:8000"):
9+
self.base_url = base_url.rstrip("/")
10+
import os
11+
dev_key = os.getenv("DEV_API_KEYS", "sk-depth-test-key-12345").split(",")[0].strip()
12+
self.client = httpx.AsyncClient(
13+
timeout=60.0,
14+
headers={"Authorization": f"Bearer {dev_key}"}
15+
)
16+
# Support a mock mode to avoid needing a running DepthAPI server.
17+
# Set MOCK_DEPTHAPI=1 in the environment to enable.
18+
self._mock = os.getenv("MOCK_DEPTHAPI", "0") == "1"
19+
20+
async def close(self):
21+
await self.client.aclose()
22+
23+
async def __aenter__(self):
24+
return self
25+
26+
async def __aexit__(self, exc_type, exc_val, exc_tb):
27+
await self.close()
28+
29+
async def query(self, query: str, prompt_spec: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
30+
"""
31+
Query DepthAPI with an optional PromptSpec.
32+
"""
33+
mapped_spec = {
34+
"depth": "accessible",
35+
"task": "explain",
36+
"reasoning": "direct",
37+
"style": "normal",
38+
"capabilities": []
39+
}
40+
41+
if prompt_spec:
42+
depth_map = {
43+
"surface": "simple",
44+
"detailed": "accessible",
45+
"expert": "expert",
46+
"academic": "technical"
47+
}
48+
if "depth" in prompt_spec:
49+
mapped_spec["depth"] = depth_map.get(prompt_spec["depth"], "accessible")
50+
51+
tone_map = {
52+
"objective": "direct",
53+
"educational": "socratic",
54+
"critical": "debate",
55+
"concise": "guided"
56+
}
57+
if "tone" in prompt_spec:
58+
mapped_spec["reasoning"] = tone_map.get(prompt_spec["tone"], "direct")
59+
60+
format_map = {
61+
"markdown": "normal",
62+
"bullet_points": "concise",
63+
"essay": "academic",
64+
"code_heavy": "normal"
65+
}
66+
if "format" in prompt_spec:
67+
mapped_spec["style"] = format_map.get(prompt_spec["format"], "normal")
68+
69+
if prompt_spec.get("include_citations"):
70+
mapped_spec["capabilities"].append("requires_citations")
71+
72+
payload = {
73+
"topic": query,
74+
"prompt_spec": mapped_spec,
75+
"mode": "chat"
76+
}
77+
78+
# Mocked response path
79+
if self._mock:
80+
# produce a deterministic short answer and fake context
81+
answer = f"(MOCK) Explanation for: {query}"
82+
context = [f"Mock context paragraph about {query}"]
83+
return {"answer": answer, "context": context}
84+
85+
try:
86+
response = await self.client.post(
87+
f"{self.base_url}/api/query",
88+
json=payload
89+
)
90+
response.raise_for_status()
91+
res_json = response.json()
92+
explanations = res_json.get("explanations", {})
93+
answer = next(iter(explanations.values()), "") if explanations else ""
94+
return {
95+
"answer": answer,
96+
"context": []
97+
}
98+
except httpx.HTTPError as e:
99+
detail = ""
100+
try:
101+
detail = f": {response.json()}"
102+
except Exception:
103+
pass
104+
return {"error": f"{str(e)}{detail}", "answer": "Error fetching from API", "context": []}

evaluation/generate_benchmark.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import json
2+
import random
3+
import uuid
4+
from typing import List, Dict, Any
5+
6+
TECHNICAL_QUERIES = [
7+
"How does asyncio event loop work in Python?",
8+
"Explain the difference between threading and multiprocessing in Python.",
9+
"What are the best practices for structuring a large FastAPI application?",
10+
"Describe the Raft consensus algorithm and its leader election process.",
11+
"How does PostgreSQL implement MVCC?",
12+
"What is the time complexity of looking up a value in a Python dict and why?",
13+
"Explain how Redis achieves persistence.",
14+
"What are the trade-offs between GraphQL and REST?",
15+
"How does garbage collection work in CPython?",
16+
"Describe the architecture of Kubernetes control plane.",
17+
]
18+
19+
def generate_benchmark_dataset(size: int = 120) -> List[Dict[str, Any]]:
20+
"""Generate a diverse benchmark dataset with PromptSpec combinations."""
21+
dataset = []
22+
23+
# Stratified sampling parameters
24+
depths = ["surface", "detailed", "expert", "academic"]
25+
tones = ["objective", "educational", "critical", "concise"]
26+
formats = ["markdown", "bullet_points", "essay", "code_heavy"]
27+
28+
for i in range(size):
29+
query = random.choice(TECHNICAL_QUERIES) + f" (Variant {i})"
30+
prompt_spec = {
31+
"depth": random.choice(depths),
32+
"tone": random.choice(tones),
33+
"format": random.choice(formats),
34+
"include_citations": random.choice([True, False])
35+
}
36+
37+
dataset.append({
38+
"id": str(uuid.uuid4()),
39+
"query": query,
40+
"prompt_spec": prompt_spec,
41+
"metadata": {
42+
"category": "technical",
43+
"difficulty": random.choice(["easy", "medium", "hard"])
44+
}
45+
})
46+
47+
return dataset
48+
49+
if __name__ == "__main__":
50+
dataset = generate_benchmark_dataset(120)
51+
with open("benchmark_dataset.json", "w") as f:
52+
json.dump(dataset, f, indent=2)
53+
print(f"Generated {len(dataset)} test cases in benchmark_dataset.json")

0 commit comments

Comments
 (0)