Spaces:
Sleeping
Sleeping
Sync from GitHub via hub-sync
Browse files- evals/run_eval.py +171 -712
- evals/sample_eval_set.json +229 -889
- src/rag_system.py +44 -10
evals/run_eval.py
CHANGED
|
@@ -1,23 +1,11 @@
|
|
| 1 |
-
"""Evaluation harness for Code Compass RAG system.
|
| 2 |
-
|
| 3 |
-
Computes 4 core metrics:
|
| 4 |
-
- Hit rate @ top-5 (retrieval quality)
|
| 5 |
-
- Grounded answer rate (citation accuracy)
|
| 6 |
-
- LLM-as-judge faithfulness (Claude 3.5 Sonnet via RAGAS)
|
| 7 |
-
- Query latency P95 (responsiveness)
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import asyncio
|
| 11 |
import json
|
| 12 |
import os
|
| 13 |
-
import sys
|
| 14 |
import re
|
|
|
|
| 15 |
import time
|
| 16 |
from pathlib import Path
|
| 17 |
-
from collections import Counter, defaultdict
|
| 18 |
from statistics import mean
|
| 19 |
|
| 20 |
-
import requests
|
| 21 |
from dotenv import load_dotenv
|
| 22 |
|
| 23 |
SERVER_ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -26,770 +14,241 @@ if str(SERVER_ROOT) not in sys.path:
|
|
| 26 |
|
| 27 |
load_dotenv(SERVER_ROOT / ".env")
|
| 28 |
|
| 29 |
-
from src.
|
| 30 |
-
from src.embeddings import EmbeddingGenerator
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
-
REPO_ID = int(os.getenv("CODEBASE_RAG_REPO_ID", "1"))
|
| 35 |
-
SESSION_ID = os.getenv("CODEBASE_RAG_SESSION_ID", "eval-session")
|
| 36 |
TOP_K = int(os.getenv("CODEBASE_RAG_TOP_K", "8"))
|
| 37 |
-
QUERY_TIMEOUT_SECONDS = int(os.getenv("CODEBASE_RAG_QUERY_TIMEOUT_SECONDS", "180"))
|
| 38 |
-
QUERY_MAX_RETRIES = int(os.getenv("CODEBASE_RAG_QUERY_MAX_RETRIES", "5"))
|
| 39 |
-
QUERY_RETRY_BASE_SECONDS = float(os.getenv("CODEBASE_RAG_QUERY_RETRY_BASE_SECONDS", "2"))
|
| 40 |
EVAL_SET_PATH = Path(
|
| 41 |
-
os.getenv(
|
| 42 |
-
"CODEBASE_RAG_EVAL_SET",
|
| 43 |
-
Path(__file__).with_name("sample_eval_set.json"),
|
| 44 |
-
)
|
| 45 |
)
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def log(message: str):
|
| 49 |
-
"""Log message to stderr with [eval] prefix."""
|
| 50 |
print(f"[eval] {message}", file=sys.stderr, flush=True)
|
| 51 |
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
llm_provider = os.getenv("LLM_PROVIDER", "bedrock").lower()
|
| 55 |
-
if llm_provider == "groq":
|
| 56 |
-
llm_model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
| 57 |
-
elif llm_provider == "bedrock":
|
| 58 |
-
llm_model = os.getenv(
|
| 59 |
-
"BEDROCK_LLM_MODEL",
|
| 60 |
-
"anthropic.claude-3-5-sonnet-20240620-v1:0",
|
| 61 |
-
)
|
| 62 |
-
elif llm_provider == "vertex_ai":
|
| 63 |
-
llm_model = os.getenv("VERTEX_LLM_MODEL", "claude-3-5-sonnet@20240620")
|
| 64 |
-
else:
|
| 65 |
-
llm_model = "unknown"
|
| 66 |
-
|
| 67 |
-
embedding_provider = os.getenv("EMBEDDING_PROVIDER", "auto").lower()
|
| 68 |
-
if embedding_provider == "bedrock":
|
| 69 |
-
embedding_model = os.getenv("BEDROCK_EMBEDDING_MODEL", "cohere.embed-v3:0")
|
| 70 |
-
elif embedding_provider == "vertex_ai":
|
| 71 |
-
embedding_model = os.getenv("VERTEX_EMBEDDING_MODEL", "gemini-embedding-001")
|
| 72 |
-
elif embedding_provider == "openai":
|
| 73 |
-
embedding_model = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
|
| 74 |
-
elif embedding_provider == "local":
|
| 75 |
-
embedding_model = os.getenv("EMBEDDING_MODEL") or os.getenv(
|
| 76 |
-
"LOCAL_EMBEDDING_MODEL", "nomic-ai/CodeRankEmbed"
|
| 77 |
-
)
|
| 78 |
-
else:
|
| 79 |
-
embedding_model = os.getenv("EMBEDDING_MODEL") or "auto"
|
| 80 |
-
|
| 81 |
-
eval_model = os.getenv(
|
| 82 |
-
"EVAL_MODEL",
|
| 83 |
-
os.getenv("BEDROCK_EVAL_MODEL", "anthropic.claude-3-5-sonnet-20240620-v1:0"),
|
| 84 |
-
)
|
| 85 |
-
return {
|
| 86 |
-
"llm_provider": llm_provider,
|
| 87 |
-
"llm_model": llm_model,
|
| 88 |
-
"embedding_provider": embedding_provider,
|
| 89 |
-
"embedding_model": embedding_model,
|
| 90 |
-
"eval_model": eval_model,
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def load_eval_rows():
|
| 95 |
return json.loads(EVAL_SET_PATH.read_text())
|
| 96 |
|
| 97 |
|
| 98 |
-
def
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
"
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
try:
|
| 119 |
-
parsed = response.json()
|
| 120 |
-
detail = parsed.get("detail") or parsed
|
| 121 |
-
except Exception:
|
| 122 |
-
pass
|
| 123 |
-
|
| 124 |
-
detail_text = str(detail)
|
| 125 |
-
is_retryable = response.status_code in {429, 500, 502, 503, 504} and any(
|
| 126 |
-
marker in detail_text
|
| 127 |
-
for marker in [
|
| 128 |
-
"ThrottlingException",
|
| 129 |
-
"throttled",
|
| 130 |
-
"Too many requests",
|
| 131 |
-
"timed out",
|
| 132 |
-
"timeout",
|
| 133 |
-
"ServiceUnavailable",
|
| 134 |
-
"temporarily unavailable",
|
| 135 |
-
]
|
| 136 |
-
)
|
| 137 |
-
if is_retryable and attempt < QUERY_MAX_RETRIES:
|
| 138 |
-
retry_after = response.headers.get("Retry-After")
|
| 139 |
-
try:
|
| 140 |
-
wait_seconds = (
|
| 141 |
-
float(retry_after)
|
| 142 |
-
if retry_after
|
| 143 |
-
else QUERY_RETRY_BASE_SECONDS * (2 ** (attempt - 1))
|
| 144 |
-
)
|
| 145 |
-
except ValueError:
|
| 146 |
-
wait_seconds = QUERY_RETRY_BASE_SECONDS * (2 ** (attempt - 1))
|
| 147 |
-
log(
|
| 148 |
-
f"Retrying case {case_id} after transient query failure "
|
| 149 |
-
f"(attempt {attempt}/{QUERY_MAX_RETRIES}, wait={wait_seconds:.1f}s): {detail_text}"
|
| 150 |
-
)
|
| 151 |
-
time.sleep(wait_seconds)
|
| 152 |
-
continue
|
| 153 |
-
|
| 154 |
-
raise RuntimeError(
|
| 155 |
-
f"Query failed for eval case {case_id!r} "
|
| 156 |
-
f"with status {response.status_code}: {detail}"
|
| 157 |
-
)
|
| 158 |
-
|
| 159 |
-
raise RuntimeError(f"Query failed for eval case {case_id!r}: exhausted retries")
|
| 160 |
|
| 161 |
|
| 162 |
def normalize_path(path: str) -> str:
|
| 163 |
return path.strip().lstrip("./").lower()
|
| 164 |
|
| 165 |
|
| 166 |
-
STOPWORDS = {
|
| 167 |
-
"a",
|
| 168 |
-
"an",
|
| 169 |
-
"and",
|
| 170 |
-
"are",
|
| 171 |
-
"as",
|
| 172 |
-
"at",
|
| 173 |
-
"be",
|
| 174 |
-
"by",
|
| 175 |
-
"for",
|
| 176 |
-
"from",
|
| 177 |
-
"how",
|
| 178 |
-
"in",
|
| 179 |
-
"into",
|
| 180 |
-
"is",
|
| 181 |
-
"it",
|
| 182 |
-
"its",
|
| 183 |
-
"of",
|
| 184 |
-
"on",
|
| 185 |
-
"or",
|
| 186 |
-
"that",
|
| 187 |
-
"the",
|
| 188 |
-
"their",
|
| 189 |
-
"this",
|
| 190 |
-
"to",
|
| 191 |
-
"what",
|
| 192 |
-
"when",
|
| 193 |
-
"where",
|
| 194 |
-
"which",
|
| 195 |
-
"with",
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
|
| 199 |
def tokenize_text(text: str):
|
| 200 |
-
|
| 201 |
-
for raw_token in re.findall(r"[A-Za-z0-9_./+-]+", text or ""):
|
| 202 |
-
token = raw_token.lower()
|
| 203 |
-
tokens.append(token)
|
| 204 |
|
| 205 |
-
camel_parts = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", raw_token).split()
|
| 206 |
-
split_parts = re.split(r"[._/+-]+", token)
|
| 207 |
-
for part in [*camel_parts, *split_parts]:
|
| 208 |
-
normalized = part.strip().lower()
|
| 209 |
-
if normalized and normalized != token:
|
| 210 |
-
tokens.append(normalized)
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
seen.add(phrase)
|
| 223 |
-
normalized.append(phrase)
|
| 224 |
-
return normalized
|
| 225 |
|
| 226 |
|
| 227 |
def compute_retrieval_metrics(expected_sources, actual_sources):
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
def matches_expected(actual_path: str) -> bool:
|
| 233 |
-
for expected_path in expected:
|
| 234 |
-
expected_is_directory = (
|
| 235 |
-
expected_path.endswith("/")
|
| 236 |
-
or "." not in expected_path.rsplit("/", 1)[-1]
|
| 237 |
-
)
|
| 238 |
-
normalized_expected = expected_path.rstrip("/")
|
| 239 |
-
if actual_path == expected_path:
|
| 240 |
-
return True
|
| 241 |
-
if expected_is_directory and actual_path.startswith(normalized_expected + "/"):
|
| 242 |
-
return True
|
| 243 |
-
return False
|
| 244 |
-
|
| 245 |
-
# Hit rate: was any retrieved source relevant?
|
| 246 |
-
hit = 1 if any(matches_expected(path) for path in actual) else 0
|
| 247 |
-
|
| 248 |
-
# Top-1 hit: was the first retrieved source relevant?
|
| 249 |
-
top1_hit = 1 if actual and matches_expected(actual[0]) else 0
|
| 250 |
|
| 251 |
-
return {
|
| 252 |
-
"retrieval_hit": hit,
|
| 253 |
-
"top1_hit": top1_hit,
|
| 254 |
-
}
|
| 255 |
|
| 256 |
-
|
| 257 |
-
def keyword_match_details(row, answer: str):
|
| 258 |
-
keywords = normalize_keywords(row.get("must_include_any", []))
|
| 259 |
if not keywords:
|
| 260 |
-
return
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
if not answer_tokens:
|
| 264 |
-
return {
|
| 265 |
-
"coverage": 0.0,
|
| 266 |
-
"matched_count": 0,
|
| 267 |
-
"total_keywords": len(keywords),
|
| 268 |
-
"matched_keywords": [],
|
| 269 |
-
"missing_keywords": keywords,
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
matched_keywords = []
|
| 273 |
for keyword in keywords:
|
| 274 |
-
keyword_tokens =
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
matched_keywords.append(keyword)
|
| 279 |
-
continue
|
| 280 |
-
|
| 281 |
-
def answer_length_metrics(answer: str):
|
| 282 |
-
"""Check if answer has substantive content."""
|
| 283 |
-
tokens = tokenize_text(answer)
|
| 284 |
-
return {
|
| 285 |
-
"answer_word_count": len(tokens),
|
| 286 |
-
"has_substantive_answer": 1 if len(tokens) >= 40 else 0,
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def validate_eval_rows(rows):
|
| 291 |
-
errors = []
|
| 292 |
-
warnings = []
|
| 293 |
-
category_counts = Counter()
|
| 294 |
-
id_counts = Counter()
|
| 295 |
-
id_prefix_counts = Counter()
|
| 296 |
-
expected_source_counts = []
|
| 297 |
-
keyword_counts = []
|
| 298 |
-
conversation_cases = 0
|
| 299 |
-
benchmark_scope = {
|
| 300 |
-
"type": "mixed_or_unknown",
|
| 301 |
-
"dominant_prefix": None,
|
| 302 |
-
"dominant_prefix_fraction": 0.0,
|
| 303 |
-
}
|
| 304 |
|
| 305 |
-
for index, row in enumerate(rows, start=1):
|
| 306 |
-
row_id = row.get("id") or f"row-{index}"
|
| 307 |
-
id_counts[row_id] += 1
|
| 308 |
-
prefix = row_id.split("-", 1)[0].lower()
|
| 309 |
-
if prefix:
|
| 310 |
-
id_prefix_counts[prefix] += 1
|
| 311 |
-
category_counts[row.get("category", "general")] += 1
|
| 312 |
-
|
| 313 |
-
question = str(row.get("question", "")).strip()
|
| 314 |
-
ground_truth = str(row.get("ground_truth", "")).strip()
|
| 315 |
-
expected_sources = row.get("expected_sources", [])
|
| 316 |
-
must_include_any = row.get("must_include_any", [])
|
| 317 |
-
|
| 318 |
-
if not question:
|
| 319 |
-
errors.append(f"{row_id}: missing question")
|
| 320 |
-
if not ground_truth:
|
| 321 |
-
errors.append(f"{row_id}: missing ground_truth")
|
| 322 |
-
if not isinstance(expected_sources, list) or not expected_sources:
|
| 323 |
-
errors.append(f"{row_id}: expected_sources must be a non-empty list")
|
| 324 |
-
if must_include_any and not isinstance(must_include_any, list):
|
| 325 |
-
errors.append(f"{row_id}: must_include_any must be a list when present")
|
| 326 |
-
if isinstance(must_include_any, list):
|
| 327 |
-
normalized_keywords = normalize_keywords(must_include_any)
|
| 328 |
-
if len(normalized_keywords) != len([keyword for keyword in must_include_any if str(keyword).strip()]):
|
| 329 |
-
warnings.append(
|
| 330 |
-
f"{row_id}: duplicate or case-variant keywords were normalized; "
|
| 331 |
-
"resume metrics are stricter than the raw checklist wording."
|
| 332 |
-
)
|
| 333 |
-
if row.get("turns"):
|
| 334 |
-
conversation_cases += 1
|
| 335 |
-
expected_source_counts.append(len(expected_sources) if isinstance(expected_sources, list) else 0)
|
| 336 |
-
keyword_counts.append(len(must_include_any) if isinstance(must_include_any, list) else 0)
|
| 337 |
-
|
| 338 |
-
duplicate_ids = sorted(row_id for row_id, count in id_counts.items() if count > 1)
|
| 339 |
-
if duplicate_ids:
|
| 340 |
-
errors.append(f"duplicate ids found: {', '.join(duplicate_ids)}")
|
| 341 |
-
|
| 342 |
-
if len(rows) < 25:
|
| 343 |
-
warnings.append(
|
| 344 |
-
"Eval set has fewer than 25 cases. Good for iteration, but light for resume-grade benchmarking."
|
| 345 |
-
)
|
| 346 |
-
if len(category_counts) < 4:
|
| 347 |
-
warnings.append("Eval set covers fewer than 4 categories, so breadth is limited.")
|
| 348 |
-
if conversation_cases < 2:
|
| 349 |
-
warnings.append("Eval set has very little multi-turn coverage.")
|
| 350 |
-
if category_counts and min(category_counts.values()) < 2:
|
| 351 |
-
sparse = sorted(category for category, count in category_counts.items() if count < 2)
|
| 352 |
-
warnings.append(f"Some categories are underrepresented: {', '.join(sparse)}.")
|
| 353 |
-
|
| 354 |
-
if id_prefix_counts:
|
| 355 |
-
dominant_prefix, dominant_count = id_prefix_counts.most_common(1)[0]
|
| 356 |
-
dominant_prefix_fraction = dominant_count / len(rows)
|
| 357 |
-
if dominant_prefix_fraction >= 0.8:
|
| 358 |
-
benchmark_scope = {
|
| 359 |
-
"type": "single_repository",
|
| 360 |
-
"dominant_prefix": dominant_prefix,
|
| 361 |
-
"dominant_prefix_fraction": round(dominant_prefix_fraction, 4),
|
| 362 |
-
}
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
"
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
"
|
| 373 |
-
"
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
"retrieval_hit_rate": round(mean(item["retrieval_hit"] for item in details), 4),
|
| 388 |
-
"top1_hit_rate": round(mean(item["top1_hit"] for item in details), 4),
|
| 389 |
-
"grounded_answer_rate": round(sum(grounded_answer_passes) / len(details), 4) if details else 0.0,
|
| 390 |
-
"latency_p95_ms": round(latency_p95, 2) if latency_p95 is not None else None,
|
| 391 |
-
}
|
| 392 |
|
| 393 |
|
| 394 |
-
def
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
mean(
|
| 408 |
-
1
|
| 409 |
-
if item["retrieval_hit"] == 1 and item["has_substantive_answer"] == 1
|
| 410 |
-
else 0
|
| 411 |
-
for item in items
|
| 412 |
-
),
|
| 413 |
-
4,
|
| 414 |
-
),
|
| 415 |
-
}
|
| 416 |
-
return summary
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
def build_headline_metrics(custom_metrics, audit):
|
| 420 |
-
"""Build headline metrics section with only the 4 core metrics."""
|
| 421 |
-
return {
|
| 422 |
-
"sample_size": audit["case_count"],
|
| 423 |
-
"category_count": len(audit["category_counts"]),
|
| 424 |
-
"retrieval_hit_rate": custom_metrics["retrieval_hit_rate"],
|
| 425 |
-
"top1_hit_rate": custom_metrics["top1_hit_rate"],
|
| 426 |
-
"grounded_answer_rate": custom_metrics["grounded_answer_rate"],
|
| 427 |
-
"latency_p95_ms": custom_metrics["latency_p95_ms"],
|
| 428 |
-
}
|
| 429 |
|
| 430 |
|
| 431 |
-
def
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
| 441 |
|
| 442 |
return {
|
| 443 |
-
"
|
| 444 |
-
"
|
| 445 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
}
|
| 447 |
|
| 448 |
|
| 449 |
-
def
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
),
|
| 456 |
-
(
|
| 457 |
-
f"Retrieval hit rate @ top-5: {custom_metrics['retrieval_hit_rate']:.1%}, "
|
| 458 |
-
f"top-1 hit rate: {custom_metrics['top1_hit_rate']:.1%}."
|
| 459 |
-
),
|
| 460 |
-
(
|
| 461 |
-
f"Grounded answer rate: {custom_metrics['grounded_answer_rate']:.1%}."
|
| 462 |
-
),
|
| 463 |
-
]
|
| 464 |
-
|
| 465 |
-
if ragas_report and not ragas_error:
|
| 466 |
-
lines.append(
|
| 467 |
-
f"Faithfulness (Claude 3.5 Sonnet judge): {ragas_report.get('faithfulness', 0.0):.3f}."
|
| 468 |
-
)
|
| 469 |
-
else:
|
| 470 |
-
lines.append("Faithfulness metrics skipped or unavailable.")
|
| 471 |
-
|
| 472 |
-
if custom_metrics["latency_p95_ms"] is not None:
|
| 473 |
-
lines.append(f"Query latency P95: {custom_metrics['latency_p95_ms']:.0f}ms.")
|
| 474 |
-
|
| 475 |
-
scope = audit.get("benchmark_scope", {})
|
| 476 |
-
if scope.get("type") == "single_repository":
|
| 477 |
-
lines.append(
|
| 478 |
-
"Benchmark scope: single-repository benchmark "
|
| 479 |
-
f"({scope.get('dominant_prefix')}); use it to judge this target repo, not cross-repo generalization."
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
if audit["warnings"]:
|
| 483 |
-
lines.append(
|
| 484 |
-
"Benchmark caveat: "
|
| 485 |
-
+ " ".join(audit["warnings"][:2])
|
| 486 |
-
)
|
| 487 |
-
|
| 488 |
-
return " ".join(lines)
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
def benchmark_readiness(audit, ragas_error, metric_guidance=None):
|
| 492 |
-
reasons = []
|
| 493 |
-
if audit["case_count"] < 25:
|
| 494 |
-
reasons.append("small_sample")
|
| 495 |
-
if len(audit["category_counts"]) < 4:
|
| 496 |
-
reasons.append("limited_category_coverage")
|
| 497 |
-
if audit["conversation_case_count"] < 2:
|
| 498 |
-
reasons.append("limited_multi_turn_coverage")
|
| 499 |
-
if audit["warnings"]:
|
| 500 |
-
reasons.append("eval_set_warnings")
|
| 501 |
-
if ragas_error not in {None, "disabled"}:
|
| 502 |
-
reasons.append("ragas_instability")
|
| 503 |
-
if metric_guidance and metric_guidance.get("primary_gate") != "pass":
|
| 504 |
-
reasons.append("primary_gate_failed")
|
| 505 |
-
|
| 506 |
-
if reasons:
|
| 507 |
-
status = "single_repo_benchmark_needs_work"
|
| 508 |
-
if audit.get("benchmark_scope", {}).get("type") != "single_repository":
|
| 509 |
-
status = "internal_or_demo_benchmark"
|
| 510 |
-
return {
|
| 511 |
-
"status": status,
|
| 512 |
-
"reasons": reasons,
|
| 513 |
-
}
|
| 514 |
-
if audit.get("benchmark_scope", {}).get("type") == "single_repository":
|
| 515 |
-
return {
|
| 516 |
-
"status": "single_repo_benchmark_ready",
|
| 517 |
-
"reasons": [],
|
| 518 |
-
}
|
| 519 |
return {
|
| 520 |
-
"
|
| 521 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
}
|
| 523 |
|
| 524 |
|
| 525 |
-
def
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
target.parent.mkdir(parents=True, exist_ok=True)
|
| 531 |
-
target.write_text(json.dumps(report, indent=2))
|
| 532 |
-
return str(target)
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
def build_bedrock_ragas_llm(run_config):
|
| 536 |
-
from langchain_core.outputs import Generation, LLMResult
|
| 537 |
-
from ragas.llms.base import BaseRagasLLM
|
| 538 |
-
|
| 539 |
-
class BedrockRagasLLM(BaseRagasLLM):
|
| 540 |
-
def __init__(self, model: str, run_config):
|
| 541 |
-
self.client = create_bedrock_runtime_client()
|
| 542 |
-
self.model = model
|
| 543 |
-
self.set_run_config(run_config)
|
| 544 |
-
|
| 545 |
-
def _prompt_to_text(self, prompt):
|
| 546 |
-
prefix = (
|
| 547 |
-
"Return only valid JSON or the exact structured output requested by the prompt. "
|
| 548 |
-
"Do not add markdown fences, explanations, or extra prose.\n\n"
|
| 549 |
-
)
|
| 550 |
-
if hasattr(prompt, "to_string"):
|
| 551 |
-
return prefix + prompt.to_string()
|
| 552 |
-
return prefix + str(prompt)
|
| 553 |
-
|
| 554 |
-
def _generate_once(self, prompt, n=1, temperature=1e-8, stop=None, callbacks=None):
|
| 555 |
-
prompt_text = self._prompt_to_text(prompt)
|
| 556 |
-
text, _ = generate_bedrock_claude_text(
|
| 557 |
-
self.client,
|
| 558 |
-
self.model,
|
| 559 |
-
"Return only valid JSON or the exact structured output requested.",
|
| 560 |
-
prompt_text,
|
| 561 |
-
max_tokens=int(os.getenv("EVAL_MAX_OUTPUT_TOKENS", "2048")),
|
| 562 |
-
temperature=0.0,
|
| 563 |
-
)
|
| 564 |
-
|
| 565 |
-
generations = [Generation(text=text)] if text else []
|
| 566 |
-
|
| 567 |
-
if not generations:
|
| 568 |
-
raise RuntimeError("Bedrock Claude judge returned an empty response.")
|
| 569 |
-
|
| 570 |
-
return LLMResult(generations=[generations])
|
| 571 |
-
|
| 572 |
-
def generate_text(self, prompt, n=1, temperature=1e-8, stop=None, callbacks=None):
|
| 573 |
-
return self._generate_once(
|
| 574 |
-
prompt=prompt,
|
| 575 |
-
n=n,
|
| 576 |
-
temperature=temperature,
|
| 577 |
-
stop=stop,
|
| 578 |
-
callbacks=callbacks,
|
| 579 |
-
)
|
| 580 |
-
|
| 581 |
-
async def agenerate_text(self, prompt, n=1, temperature=1e-8, stop=None, callbacks=None):
|
| 582 |
-
return await asyncio.to_thread(
|
| 583 |
-
self._generate_once,
|
| 584 |
-
prompt,
|
| 585 |
-
n,
|
| 586 |
-
temperature,
|
| 587 |
-
stop,
|
| 588 |
-
callbacks,
|
| 589 |
-
)
|
| 590 |
-
|
| 591 |
-
model = os.getenv(
|
| 592 |
-
"EVAL_MODEL",
|
| 593 |
-
os.getenv("BEDROCK_EVAL_MODEL", "anthropic.claude-3-5-sonnet-20240620-v1:0"),
|
| 594 |
-
)
|
| 595 |
-
return BedrockRagasLLM(model=model, run_config=run_config)
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
def build_ragas_embeddings(run_config):
|
| 599 |
-
from ragas.embeddings.base import BaseRagasEmbeddings
|
| 600 |
-
|
| 601 |
-
class AppEmbeddingWrapper(BaseRagasEmbeddings):
|
| 602 |
-
def __init__(self, generator, run_config):
|
| 603 |
-
self.generator = generator
|
| 604 |
-
self.set_run_config(run_config)
|
| 605 |
-
|
| 606 |
-
def embed_query(self, text):
|
| 607 |
-
return self.generator.embed_text(text).tolist()
|
| 608 |
-
|
| 609 |
-
def embed_documents(self, texts):
|
| 610 |
-
vectors = self.generator.embed_batch(list(texts))
|
| 611 |
-
return vectors.tolist()
|
| 612 |
|
| 613 |
-
async def aembed_query(self, text):
|
| 614 |
-
return await asyncio.to_thread(self.embed_query, text)
|
| 615 |
|
| 616 |
-
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
| 618 |
|
| 619 |
-
return AppEmbeddingWrapper(EmbeddingGenerator(), run_config=run_config)
|
| 620 |
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
-
|
| 623 |
-
if
|
| 624 |
-
|
| 625 |
-
return None, "disabled"
|
| 626 |
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
from ragas import evaluate
|
| 630 |
-
from ragas.metrics import faithfulness
|
| 631 |
-
from ragas.run_config import RunConfig
|
| 632 |
-
except Exception as exc:
|
| 633 |
-
log(f"Skipping RAGAS because the evaluation dependencies could not be loaded: {exc}")
|
| 634 |
-
return None, f"import_error: {exc}"
|
| 635 |
-
|
| 636 |
-
def build_ragas_dataset():
|
| 637 |
-
samples = []
|
| 638 |
-
for row, result in zip(rows, outputs):
|
| 639 |
-
samples.append(
|
| 640 |
-
{
|
| 641 |
-
"question": row["question"],
|
| 642 |
-
"answer": result["answer"],
|
| 643 |
-
"contexts": [source["snippet"] for source in result.get("sources", [])],
|
| 644 |
-
"ground_truth": row["ground_truth"],
|
| 645 |
-
}
|
| 646 |
-
)
|
| 647 |
-
return Dataset.from_list(samples)
|
| 648 |
-
|
| 649 |
-
log("Running RAGAS metrics. This can take a while.")
|
| 650 |
-
try:
|
| 651 |
-
timeout_seconds = int(os.getenv("EVAL_TIMEOUT_SECONDS", "180"))
|
| 652 |
-
thread_timeout_seconds = float(os.getenv("EVAL_THREAD_TIMEOUT_SECONDS", str(max(timeout_seconds, 240))))
|
| 653 |
-
max_workers = int(os.getenv("EVAL_MAX_WORKERS", "2"))
|
| 654 |
-
run_config = RunConfig(
|
| 655 |
-
timeout=timeout_seconds,
|
| 656 |
-
thread_timeout=thread_timeout_seconds,
|
| 657 |
-
max_workers=max_workers,
|
| 658 |
-
max_retries=int(os.getenv("EVAL_MAX_RETRIES", "3")),
|
| 659 |
-
max_wait=int(os.getenv("EVAL_MAX_WAIT_SECONDS", "60")),
|
| 660 |
-
)
|
| 661 |
-
log(
|
| 662 |
-
"Using Bedrock for RAGAS judge model "
|
| 663 |
-
f"({os.getenv('EVAL_MODEL', os.getenv('BEDROCK_EVAL_MODEL', 'anthropic.claude-3-5-sonnet-20240620-v1:0'))})"
|
| 664 |
-
)
|
| 665 |
-
log(
|
| 666 |
-
f"RAGAS runtime: async={RAGAS_ASYNC}, raise_exceptions={RAGAS_RAISE_EXCEPTIONS}, "
|
| 667 |
-
f"timeout={timeout_seconds}s, thread_timeout={thread_timeout_seconds}s, max_workers={max_workers}"
|
| 668 |
-
)
|
| 669 |
-
llm = build_bedrock_ragas_llm(run_config)
|
| 670 |
-
embeddings = build_ragas_embeddings(run_config)
|
| 671 |
-
# Only use faithfulness as the RAGAS metric (simplified to 4-core metrics)
|
| 672 |
-
ragas_report = evaluate(
|
| 673 |
-
build_ragas_dataset(),
|
| 674 |
-
metrics=[faithfulness],
|
| 675 |
-
llm=llm,
|
| 676 |
-
embeddings=embeddings,
|
| 677 |
-
run_config=run_config,
|
| 678 |
-
is_async=RAGAS_ASYNC,
|
| 679 |
-
raise_exceptions=RAGAS_RAISE_EXCEPTIONS,
|
| 680 |
-
)
|
| 681 |
-
return {key: float(value) for key, value in ragas_report.items()}, None
|
| 682 |
-
except Exception as exc:
|
| 683 |
-
log(f"RAGAS evaluation failed: {exc}")
|
| 684 |
-
return None, str(exc)
|
| 685 |
|
|
|
|
|
|
|
| 686 |
|
| 687 |
-
def run():
|
| 688 |
-
log(f"Loading eval set from {EVAL_SET_PATH}")
|
| 689 |
-
rows = load_eval_rows()
|
| 690 |
-
audit = validate_eval_rows(rows)
|
| 691 |
-
model_config = get_app_model_config()
|
| 692 |
-
if audit["errors"]:
|
| 693 |
-
raise RuntimeError("Eval set validation failed: " + "; ".join(audit["errors"]))
|
| 694 |
-
for warning in audit["warnings"]:
|
| 695 |
-
log(f"Eval set warning: {warning}")
|
| 696 |
-
log(
|
| 697 |
-
"Eval model config: "
|
| 698 |
-
f"qna_provider={model_config['llm_provider']}, "
|
| 699 |
-
f"qna_model={model_config['llm_model']}, "
|
| 700 |
-
f"embedding_provider={model_config['embedding_provider']}, "
|
| 701 |
-
f"embedding_model={model_config['embedding_model']}, "
|
| 702 |
-
f"judge_model={model_config['eval_model']}"
|
| 703 |
-
)
|
| 704 |
-
log(
|
| 705 |
-
f"Starting eval with api_url={API_URL}, repo_id={REPO_ID}, "
|
| 706 |
-
f"session_id={SESSION_ID}, top_k={TOP_K}, cases={len(rows)}"
|
| 707 |
-
)
|
| 708 |
-
outputs = []
|
| 709 |
details = []
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
outputs.append(result)
|
| 720 |
-
log(
|
| 721 |
-
f"[{index}/{len(rows)}] Received answer for {case_id} "
|
| 722 |
-
f"with {len(result.get('sources', []))} sources in {elapsed_ms:.0f}ms"
|
| 723 |
-
)
|
| 724 |
-
|
| 725 |
-
cited_paths = [source["file_path"] for source in result.get("sources", [])]
|
| 726 |
-
metrics = compute_retrieval_metrics(row.get("expected_sources", []), cited_paths)
|
| 727 |
-
length_metrics = answer_length_metrics(result.get("answer", ""))
|
| 728 |
-
|
| 729 |
-
details.append(
|
| 730 |
-
{
|
| 731 |
-
"id": row.get("id", row["question"]),
|
| 732 |
-
"category": row.get("category", "general"),
|
| 733 |
-
"question": row["question"],
|
| 734 |
-
"answer": result.get("answer", ""),
|
| 735 |
-
"expected_sources": row.get("expected_sources", []),
|
| 736 |
-
"retrieved_sources": cited_paths,
|
| 737 |
-
"retrieval_hit": metrics["retrieval_hit"],
|
| 738 |
-
"top1_hit": metrics["top1_hit"],
|
| 739 |
-
**length_metrics,
|
| 740 |
-
}
|
| 741 |
-
)
|
| 742 |
-
|
| 743 |
-
# Compute P95 latency
|
| 744 |
-
latencies.sort()
|
| 745 |
-
p95_index = int(len(latencies) * 0.95)
|
| 746 |
-
latency_p95 = latencies[p95_index] if latencies else None
|
| 747 |
-
|
| 748 |
-
log("Finished query loop. Computing aggregate metrics.")
|
| 749 |
-
custom_metrics = summarize_custom_metrics(details, latency_p95)
|
| 750 |
-
category_breakdown = summarize_by_category(details)
|
| 751 |
-
ragas_report, ragas_error = run_ragas(rows, outputs)
|
| 752 |
-
headline_metrics = build_headline_metrics(custom_metrics, audit)
|
| 753 |
-
metric_guidance = build_metric_guidance(custom_metrics, ragas_report)
|
| 754 |
-
resume_summary = build_resume_summary(custom_metrics, audit, ragas_report, ragas_error)
|
| 755 |
-
readiness = benchmark_readiness(audit, ragas_error, metric_guidance)
|
| 756 |
|
| 757 |
report = {
|
| 758 |
"config": {
|
| 759 |
-
"
|
| 760 |
-
"
|
| 761 |
-
"session_id": SESSION_ID,
|
| 762 |
"top_k": TOP_K,
|
| 763 |
-
"qna_provider": model_config["llm_provider"],
|
| 764 |
-
"qna_model": model_config["llm_model"],
|
| 765 |
-
"embedding_provider": model_config["embedding_provider"],
|
| 766 |
-
"embedding_model": model_config["embedding_model"],
|
| 767 |
-
"eval_model": model_config["eval_model"],
|
| 768 |
-
"query_timeout_seconds": QUERY_TIMEOUT_SECONDS,
|
| 769 |
-
"query_max_retries": QUERY_MAX_RETRIES,
|
| 770 |
-
"query_retry_base_seconds": QUERY_RETRY_BASE_SECONDS,
|
| 771 |
"eval_set": str(EVAL_SET_PATH),
|
| 772 |
-
"
|
| 773 |
-
|
|
|
|
|
|
|
| 774 |
},
|
| 775 |
-
"
|
| 776 |
-
"
|
| 777 |
-
"
|
| 778 |
-
"metric_guidance": metric_guidance,
|
| 779 |
-
"ragas": ragas_report,
|
| 780 |
-
"ragas_error": ragas_error,
|
| 781 |
-
"custom_metrics": custom_metrics,
|
| 782 |
-
"category_breakdown": category_breakdown,
|
| 783 |
-
"resume_summary": resume_summary,
|
| 784 |
"cases": details,
|
| 785 |
}
|
| 786 |
-
|
| 787 |
-
if
|
| 788 |
-
|
|
|
|
|
|
|
|
|
|
| 789 |
|
| 790 |
log("Eval complete. Printing JSON report.")
|
| 791 |
print(json.dumps(report, indent=2))
|
| 792 |
|
| 793 |
|
| 794 |
if __name__ == "__main__":
|
| 795 |
-
run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
import os
|
|
|
|
| 3 |
import re
|
| 4 |
+
import sys
|
| 5 |
import time
|
| 6 |
from pathlib import Path
|
|
|
|
| 7 |
from statistics import mean
|
| 8 |
|
|
|
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
|
| 11 |
SERVER_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 14 |
|
| 15 |
load_dotenv(SERVER_ROOT / ".env")
|
| 16 |
|
| 17 |
+
from src.rag_system import CodebaseRAGSystem
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
EVAL_SESSION_KEY = "eval-session"
|
|
|
|
|
|
|
| 20 |
TOP_K = int(os.getenv("CODEBASE_RAG_TOP_K", "8"))
|
|
|
|
|
|
|
|
|
|
| 21 |
EVAL_SET_PATH = Path(
|
| 22 |
+
os.getenv("CODEBASE_RAG_EVAL_SET", Path(__file__).with_name("sample_eval_set.json"))
|
|
|
|
|
|
|
|
|
|
| 23 |
)
|
| 24 |
+
EVAL_OUTPUT_PATH = os.getenv("CODEBASE_RAG_EVAL_OUTPUT")
|
| 25 |
+
ENABLE_FAITHFULNESS = os.getenv("CODEBASE_RAG_ENABLE_FAITHFULNESS", "1") == "1"
|
| 26 |
|
| 27 |
|
| 28 |
def log(message: str):
|
|
|
|
| 29 |
print(f"[eval] {message}", file=sys.stderr, flush=True)
|
| 30 |
|
| 31 |
|
| 32 |
+
def load_eval_set():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
return json.loads(EVAL_SET_PATH.read_text())
|
| 34 |
|
| 35 |
|
| 36 |
+
def validate_eval_set(repositories):
|
| 37 |
+
errors = []
|
| 38 |
+
seen_ids = set()
|
| 39 |
+
for repo in repositories:
|
| 40 |
+
if not repo.get("github_url"):
|
| 41 |
+
errors.append(f"repo {repo.get('id', '?')}: missing github_url")
|
| 42 |
+
if not repo.get("cases"):
|
| 43 |
+
errors.append(f"repo {repo.get('id', '?')}: has no cases")
|
| 44 |
+
for case in repo.get("cases", []):
|
| 45 |
+
case_id = case.get("id") or case.get("question", "?")
|
| 46 |
+
if case_id in seen_ids:
|
| 47 |
+
errors.append(f"duplicate case id: {case_id}")
|
| 48 |
+
seen_ids.add(case_id)
|
| 49 |
+
if not case.get("question", "").strip():
|
| 50 |
+
errors.append(f"{case_id}: missing question")
|
| 51 |
+
if not case.get("ground_truth", "").strip():
|
| 52 |
+
errors.append(f"{case_id}: missing ground_truth")
|
| 53 |
+
if not case.get("expected_sources"):
|
| 54 |
+
errors.append(f"{case_id}: expected_sources must be a non-empty list")
|
| 55 |
+
return errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def normalize_path(path: str) -> str:
|
| 59 |
return path.strip().lstrip("./").lower()
|
| 60 |
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def tokenize_text(text: str):
|
| 63 |
+
return re.findall(r"[a-z0-9_]+", (text or "").lower())
|
|
|
|
|
|
|
|
|
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
def matches_expected(actual_path: str, expected_sources) -> bool:
|
| 67 |
+
actual = normalize_path(actual_path)
|
| 68 |
+
for expected in expected_sources:
|
| 69 |
+
expected_norm = normalize_path(expected).rstrip("/")
|
| 70 |
+
is_dir = "." not in expected_norm.rsplit("/", 1)[-1]
|
| 71 |
+
if actual == expected_norm:
|
| 72 |
+
return True
|
| 73 |
+
if is_dir and actual.startswith(expected_norm + "/"):
|
| 74 |
+
return True
|
| 75 |
+
return False
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def compute_retrieval_metrics(expected_sources, actual_sources):
|
| 79 |
+
hit = any(matches_expected(path, expected_sources) for path in actual_sources)
|
| 80 |
+
top1 = bool(actual_sources) and matches_expected(actual_sources[0], expected_sources)
|
| 81 |
+
return {"retrieval_hit": int(hit), "top1_hit": int(top1)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
def keyword_hits(answer: str, keywords):
|
|
|
|
|
|
|
| 85 |
if not keywords:
|
| 86 |
+
return 0, 0
|
| 87 |
+
tokens = set(tokenize_text(answer))
|
| 88 |
+
matched = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
for keyword in keywords:
|
| 90 |
+
keyword_tokens = tokenize_text(keyword)
|
| 91 |
+
if keyword_tokens and all(token in tokens for token in keyword_tokens):
|
| 92 |
+
matched += 1
|
| 93 |
+
return matched, len(keywords)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
def judge_faithfulness(rag_system, question: str, answer: str, sources: list):
|
| 97 |
+
if not ENABLE_FAITHFULNESS or not answer.strip() or not sources:
|
| 98 |
+
return None
|
| 99 |
+
context = "\n\n".join(
|
| 100 |
+
f"[{i}] {source['file_path']}\n{source['snippet'][:800]}"
|
| 101 |
+
for i, source in enumerate(sources, start=1)
|
| 102 |
+
)
|
| 103 |
+
system_prompt = (
|
| 104 |
+
"You are a strict grading assistant. Given a question, retrieved code context, and a "
|
| 105 |
+
"generated answer, output ONLY a single number between 0 and 1 for how faithful the "
|
| 106 |
+
"answer is to the context (1.0 = every claim is supported, 0.0 = the answer invents or "
|
| 107 |
+
"contradicts facts not in the context). Output just the number."
|
| 108 |
+
)
|
| 109 |
+
user_prompt = f"Question: {question}\n\nContext:\n{context}\n\nAnswer:\n{answer}\n\nFaithfulness score:"
|
| 110 |
+
try:
|
| 111 |
+
text, _ = rag_system._generate_markdown_response(system_prompt, user_prompt)
|
| 112 |
+
match = re.search(r"(\d(?:\.\d+)?)", text)
|
| 113 |
+
if not match:
|
| 114 |
+
return None
|
| 115 |
+
return max(0.0, min(1.0, float(match.group(1))))
|
| 116 |
+
except Exception as exc:
|
| 117 |
+
log(f"Faithfulness judge failed: {exc}")
|
| 118 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
|
| 121 |
+
def index_repo(rag_system, github_url: str, name: str):
|
| 122 |
+
repo = rag_system.create_or_reset_repository(github_url, EVAL_SESSION_KEY)
|
| 123 |
+
log(f"Indexing {name} ({github_url}), repo_id={repo.id}")
|
| 124 |
+
rag_system.index_repository(repo.id)
|
| 125 |
+
repo_state = rag_system.get_repository_for_session(repo.id, EVAL_SESSION_KEY)
|
| 126 |
+
if not repo_state or repo_state["status"] != "indexed":
|
| 127 |
+
detail = repo_state.get("error_message") if repo_state else "repository disappeared"
|
| 128 |
+
raise RuntimeError(f"Failed to index {name}: {detail}")
|
| 129 |
+
log(
|
| 130 |
+
f"Indexed {name}: {repo_state['file_count']} files, "
|
| 131 |
+
f"{repo_state['chunk_count']} chunks"
|
| 132 |
+
)
|
| 133 |
+
return repo.id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
+
def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
|
| 137 |
+
start = time.time()
|
| 138 |
+
result = rag_system.answer_question(
|
| 139 |
+
repo_id=repo_id,
|
| 140 |
+
session_key=EVAL_SESSION_KEY,
|
| 141 |
+
question=case["question"],
|
| 142 |
+
top_k=TOP_K,
|
| 143 |
+
history=case.get("turns", []),
|
| 144 |
+
)
|
| 145 |
+
elapsed_ms = (time.time() - start) * 1000
|
| 146 |
|
| 147 |
+
sources = result.get("sources", [])
|
| 148 |
+
cited_paths = [source["file_path"] for source in sources]
|
| 149 |
+
retrieval = compute_retrieval_metrics(case.get("expected_sources", []), cited_paths)
|
| 150 |
+
matched, total_keywords = keyword_hits(result.get("answer", ""), case.get("must_include_any", []))
|
| 151 |
+
has_citations = bool(result.get("citations"))
|
| 152 |
+
grounded = retrieval["retrieval_hit"] == 1 and has_citations and (total_keywords == 0 or matched > 0)
|
| 153 |
|
| 154 |
return {
|
| 155 |
+
"id": case.get("id", case["question"]),
|
| 156 |
+
"repo": repo_name,
|
| 157 |
+
"category": case.get("category", "general"),
|
| 158 |
+
"question": case["question"],
|
| 159 |
+
"answer": result.get("answer", ""),
|
| 160 |
+
"citations": result.get("citations", []),
|
| 161 |
+
"expected_sources": case.get("expected_sources", []),
|
| 162 |
+
"retrieved_sources": cited_paths,
|
| 163 |
+
"retrieval_hit": retrieval["retrieval_hit"],
|
| 164 |
+
"top1_hit": retrieval["top1_hit"],
|
| 165 |
+
"grounded": int(grounded),
|
| 166 |
+
"faithfulness": judge_faithfulness(rag_system, case["question"], result.get("answer", ""), sources),
|
| 167 |
+
"latency_ms": round(elapsed_ms, 1),
|
| 168 |
}
|
| 169 |
|
| 170 |
|
| 171 |
+
def summarize(details):
|
| 172 |
+
if not details:
|
| 173 |
+
return {}
|
| 174 |
+
latencies = sorted(item["latency_ms"] for item in details)
|
| 175 |
+
p95_index = min(len(latencies) - 1, int(len(latencies) * 0.95))
|
| 176 |
+
faith_scores = [item["faithfulness"] for item in details if item["faithfulness"] is not None]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
return {
|
| 178 |
+
"case_count": len(details),
|
| 179 |
+
"retrieval_hit_rate": round(mean(item["retrieval_hit"] for item in details), 4),
|
| 180 |
+
"top1_hit_rate": round(mean(item["top1_hit"] for item in details), 4),
|
| 181 |
+
"grounded_answer_rate": round(mean(item["grounded"] for item in details), 4),
|
| 182 |
+
"faithfulness": round(mean(faith_scores), 4) if faith_scores else None,
|
| 183 |
+
"latency_p95_ms": round(latencies[p95_index], 1),
|
| 184 |
}
|
| 185 |
|
| 186 |
|
| 187 |
+
def summarize_by_repo(details):
|
| 188 |
+
grouped = {}
|
| 189 |
+
for item in details:
|
| 190 |
+
grouped.setdefault(item["repo"], []).append(item)
|
| 191 |
+
return {repo: summarize(items) for repo, items in grouped.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
def summarize_by_category(details):
|
| 195 |
+
grouped = {}
|
| 196 |
+
for item in details:
|
| 197 |
+
grouped.setdefault(item["category"], []).append(item)
|
| 198 |
+
return {category: summarize(items) for category, items in sorted(grouped.items())}
|
| 199 |
|
|
|
|
| 200 |
|
| 201 |
+
def run():
|
| 202 |
+
eval_set = load_eval_set()
|
| 203 |
+
repositories = eval_set["repositories"]
|
| 204 |
|
| 205 |
+
errors = validate_eval_set(repositories)
|
| 206 |
+
if errors:
|
| 207 |
+
raise RuntimeError("Eval set validation failed: " + "; ".join(errors))
|
|
|
|
| 208 |
|
| 209 |
+
total_cases = sum(len(repo["cases"]) for repo in repositories)
|
| 210 |
+
log(f"Loaded eval set: {len(repositories)} repositories, {total_cases} cases")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
+
rag_system = CodebaseRAGSystem()
|
| 213 |
+
log(f"LLM provider={rag_system.llm_provider} model={rag_system.llm_model}")
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
details = []
|
| 216 |
+
try:
|
| 217 |
+
for repo_config in repositories:
|
| 218 |
+
repo_id = index_repo(rag_system, repo_config["github_url"], repo_config["name"])
|
| 219 |
+
cases = repo_config["cases"]
|
| 220 |
+
for index, case in enumerate(cases, start=1):
|
| 221 |
+
log(f"[{repo_config['id']} {index}/{len(cases)}] {case['id']}")
|
| 222 |
+
details.append(run_case(rag_system, repo_id, repo_config["name"], case))
|
| 223 |
+
finally:
|
| 224 |
+
rag_system.end_session(EVAL_SESSION_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
report = {
|
| 227 |
"config": {
|
| 228 |
+
"llm_provider": rag_system.llm_provider,
|
| 229 |
+
"llm_model": rag_system.llm_model,
|
|
|
|
| 230 |
"top_k": TOP_K,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
"eval_set": str(EVAL_SET_PATH),
|
| 232 |
+
"repositories": [
|
| 233 |
+
{"id": repo["id"], "name": repo["name"], "github_url": repo["github_url"]}
|
| 234 |
+
for repo in repositories
|
| 235 |
+
],
|
| 236 |
},
|
| 237 |
+
"headline_metrics": summarize(details),
|
| 238 |
+
"repo_breakdown": summarize_by_repo(details),
|
| 239 |
+
"category_breakdown": summarize_by_category(details),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
"cases": details,
|
| 241 |
}
|
| 242 |
+
|
| 243 |
+
if EVAL_OUTPUT_PATH:
|
| 244 |
+
target = Path(EVAL_OUTPUT_PATH)
|
| 245 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 246 |
+
target.write_text(json.dumps(report, indent=2))
|
| 247 |
+
log(f"Wrote JSON report to {target}")
|
| 248 |
|
| 249 |
log("Eval complete. Printing JSON report.")
|
| 250 |
print(json.dumps(report, indent=2))
|
| 251 |
|
| 252 |
|
| 253 |
if __name__ == "__main__":
|
| 254 |
+
run()
|
evals/sample_eval_set.json
CHANGED
|
@@ -1,889 +1,229 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
"
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
"
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
"must_include_any": [
|
| 231 |
-
"PostgreSQL",
|
| 232 |
-
"Prisma",
|
| 233 |
-
"Kysely",
|
| 234 |
-
"DATABASE_URL",
|
| 235 |
-
"migration"
|
| 236 |
-
],
|
| 237 |
-
"min_keyword_matches": 3
|
| 238 |
-
},
|
| 239 |
-
{
|
| 240 |
-
"id": "documenso-signing-config",
|
| 241 |
-
"category": "config-setup",
|
| 242 |
-
"question": "How is document signing configured for local and cloud-backed signing?",
|
| 243 |
-
"ground_truth": "Signing is configured with NEXT_PRIVATE_SIGNING_TRANSPORT. The env example and self-hosting docs document local signing as the default and also support gcloud-hsm with related key, certificate, credential, certificate chain, and timestamp authority configuration.",
|
| 244 |
-
"expected_sources": [
|
| 245 |
-
".env.example",
|
| 246 |
-
"apps/docs/content/docs/self-hosting/configuration/signing-certificate",
|
| 247 |
-
"packages/signing",
|
| 248 |
-
"packages/signing/transports"
|
| 249 |
-
],
|
| 250 |
-
"must_include_any": [
|
| 251 |
-
"NEXT_PRIVATE_SIGNING_TRANSPORT",
|
| 252 |
-
"local",
|
| 253 |
-
"gcloud-hsm",
|
| 254 |
-
"certificate",
|
| 255 |
-
"timestamp"
|
| 256 |
-
],
|
| 257 |
-
"min_keyword_matches": 3
|
| 258 |
-
},
|
| 259 |
-
{
|
| 260 |
-
"id": "documenso-workspace-build-config",
|
| 261 |
-
"category": "config-setup",
|
| 262 |
-
"question": "Where should you look to understand package workspaces and build orchestration in Documenso?",
|
| 263 |
-
"ground_truth": "The root package.json defines npm workspaces and scripts, while turbo.json defines Turborepo task orchestration. ARCHITECTURE.md explains the monorepo layout and how apps and packages fit together.",
|
| 264 |
-
"expected_sources": [
|
| 265 |
-
"package.json",
|
| 266 |
-
"turbo.json",
|
| 267 |
-
"ARCHITECTURE.md"
|
| 268 |
-
],
|
| 269 |
-
"must_include_any": [
|
| 270 |
-
"package.json",
|
| 271 |
-
"workspaces",
|
| 272 |
-
"turbo",
|
| 273 |
-
"Turborepo"
|
| 274 |
-
],
|
| 275 |
-
"min_keyword_matches": 3
|
| 276 |
-
},
|
| 277 |
-
{
|
| 278 |
-
"id": "documenso-api-v2-document-router",
|
| 279 |
-
"category": "api",
|
| 280 |
-
"question": "Where is the current document API implemented and how is it exposed?",
|
| 281 |
-
"ground_truth": "The current API V2 is implemented under packages/trpc/server, with document operations organized under a document router. It is exposed through /api/v2 and /api/v2-beta with tRPC and OpenAPI support, and it accepts API-token or session-cookie authentication depending on the route.",
|
| 282 |
-
"expected_sources": [
|
| 283 |
-
"ARCHITECTURE.md",
|
| 284 |
-
"packages/trpc/server",
|
| 285 |
-
"packages/trpc/server/document-router",
|
| 286 |
-
"apps/remix/server"
|
| 287 |
-
],
|
| 288 |
-
"must_include_any": [
|
| 289 |
-
"packages/trpc/server",
|
| 290 |
-
"document-router",
|
| 291 |
-
"API V2",
|
| 292 |
-
"OpenAPI",
|
| 293 |
-
"tRPC"
|
| 294 |
-
],
|
| 295 |
-
"min_keyword_matches": 3
|
| 296 |
-
},
|
| 297 |
-
{
|
| 298 |
-
"id": "documenso-api-v1-deprecated",
|
| 299 |
-
"category": "api",
|
| 300 |
-
"question": "What is the older API V1 layer in Documenso and what is its status?",
|
| 301 |
-
"ground_truth": "API V1 lives under packages/api/v1, uses ts-rest for contract-based REST, is mounted under /api/v1, and is described as deprecated but maintained.",
|
| 302 |
-
"expected_sources": [
|
| 303 |
-
"ARCHITECTURE.md",
|
| 304 |
-
"packages/api",
|
| 305 |
-
"packages/api/v1"
|
| 306 |
-
],
|
| 307 |
-
"must_include_any": [
|
| 308 |
-
"API V1",
|
| 309 |
-
"packages/api/v1",
|
| 310 |
-
"ts-rest",
|
| 311 |
-
"deprecated"
|
| 312 |
-
],
|
| 313 |
-
"min_keyword_matches": 3
|
| 314 |
-
},
|
| 315 |
-
{
|
| 316 |
-
"id": "documenso-internal-trpc",
|
| 317 |
-
"category": "api",
|
| 318 |
-
"question": "How does the frontend talk to the backend internally in Documenso?",
|
| 319 |
-
"ground_truth": "The frontend uses an internal tRPC API mounted under /api/trpc for frontend-to-backend communication. This internal API is separate from the public API V1 and V2 routes and uses session-based authentication.",
|
| 320 |
-
"expected_sources": [
|
| 321 |
-
"ARCHITECTURE.md",
|
| 322 |
-
"apps/remix/server/trpc",
|
| 323 |
-
"apps/remix/server/router.ts",
|
| 324 |
-
"packages/trpc"
|
| 325 |
-
],
|
| 326 |
-
"must_include_any": [
|
| 327 |
-
"/api/trpc",
|
| 328 |
-
"tRPC",
|
| 329 |
-
"frontend",
|
| 330 |
-
"session"
|
| 331 |
-
],
|
| 332 |
-
"min_keyword_matches": 3
|
| 333 |
-
},
|
| 334 |
-
{
|
| 335 |
-
"id": "documenso-public-api-auth",
|
| 336 |
-
"category": "api",
|
| 337 |
-
"question": "How are public API requests authenticated in Documenso?",
|
| 338 |
-
"ground_truth": "Public API routes use API-token style authentication, typically through bearer or API-key headers. API V2 can also use session cookies where appropriate, while internal tRPC uses session-based auth.",
|
| 339 |
-
"expected_sources": [
|
| 340 |
-
"ARCHITECTURE.md",
|
| 341 |
-
"packages/lib/server-only/public-api",
|
| 342 |
-
"packages/trpc/server",
|
| 343 |
-
"packages/api"
|
| 344 |
-
],
|
| 345 |
-
"must_include_any": [
|
| 346 |
-
"API Token",
|
| 347 |
-
"Bearer",
|
| 348 |
-
"session",
|
| 349 |
-
"public API"
|
| 350 |
-
],
|
| 351 |
-
"min_keyword_matches": 2
|
| 352 |
-
},
|
| 353 |
-
{
|
| 354 |
-
"id": "documenso-openapi-support",
|
| 355 |
-
"category": "api",
|
| 356 |
-
"question": "Where does OpenAPI support fit into Documenso's API architecture?",
|
| 357 |
-
"ground_truth": "OpenAPI support appears in both API layers. API V2 generates an OpenAPI document from the tRPC app router in packages/trpc/server/open-api.ts and mounts it from the Remix/Hono server. API V1 is the older ts-rest layer under packages/api/v1 and also exposes an OpenAPI document through packages/api/hono.ts.",
|
| 358 |
-
"expected_sources": [
|
| 359 |
-
"ARCHITECTURE.md",
|
| 360 |
-
"apps/remix/server/router.ts",
|
| 361 |
-
"packages/trpc/server/open-api.ts",
|
| 362 |
-
"packages/api/hono.ts",
|
| 363 |
-
"packages/api/v1/openapi.ts"
|
| 364 |
-
],
|
| 365 |
-
"must_include_any": [
|
| 366 |
-
"OpenAPI",
|
| 367 |
-
"trpc-to-openapi",
|
| 368 |
-
"tRPC",
|
| 369 |
-
"API V2"
|
| 370 |
-
],
|
| 371 |
-
"min_keyword_matches": 3
|
| 372 |
-
},
|
| 373 |
-
{
|
| 374 |
-
"id": "documenso-document-business-logic",
|
| 375 |
-
"category": "specific-function",
|
| 376 |
-
"question": "Where is document business logic likely implemented in Documenso?",
|
| 377 |
-
"ground_truth": "Document business logic belongs primarily in packages/lib/server-only/document, with API-facing operations routed through packages/trpc/server/document-router or the legacy packages/api layer. Database persistence is supported by packages/prisma.",
|
| 378 |
-
"expected_sources": [
|
| 379 |
-
"ARCHITECTURE.md",
|
| 380 |
-
"packages/lib/server-only/document",
|
| 381 |
-
"packages/trpc/server/document-router",
|
| 382 |
-
"packages/prisma"
|
| 383 |
-
],
|
| 384 |
-
"must_include_any": [
|
| 385 |
-
"server-only",
|
| 386 |
-
"document",
|
| 387 |
-
"document-router",
|
| 388 |
-
"prisma"
|
| 389 |
-
],
|
| 390 |
-
"min_keyword_matches": 3
|
| 391 |
-
},
|
| 392 |
-
{
|
| 393 |
-
"id": "documenso-recipient-field-logic",
|
| 394 |
-
"category": "specific-function",
|
| 395 |
-
"question": "Where should you look for recipient and field behavior in Documenso?",
|
| 396 |
-
"ground_truth": "Recipient and field behavior is split between packages/lib/server-only/recipient and packages/lib/server-only/field for core logic, with packages/trpc/server/recipient-router and packages/trpc/server/field-router exposing API operations.",
|
| 397 |
-
"expected_sources": [
|
| 398 |
-
"packages/lib/server-only/recipient",
|
| 399 |
-
"packages/lib/server-only/field",
|
| 400 |
-
"packages/trpc/server/recipient-router",
|
| 401 |
-
"packages/trpc/server/field-router",
|
| 402 |
-
"ARCHITECTURE.md"
|
| 403 |
-
],
|
| 404 |
-
"must_include_any": [
|
| 405 |
-
"recipient",
|
| 406 |
-
"field",
|
| 407 |
-
"server-only",
|
| 408 |
-
"router"
|
| 409 |
-
],
|
| 410 |
-
"min_keyword_matches": 3
|
| 411 |
-
},
|
| 412 |
-
{
|
| 413 |
-
"id": "documenso-template-envelope-logic",
|
| 414 |
-
"category": "specific-function",
|
| 415 |
-
"question": "How are templates and envelopes represented in the codebase?",
|
| 416 |
-
"ground_truth": "Templates and envelopes have their own server-only domain areas under packages/lib/server-only/template and packages/lib/server-only/envelope. API V2 also organizes routes into template-router and envelope-router directories under packages/trpc/server.",
|
| 417 |
-
"expected_sources": [
|
| 418 |
-
"ARCHITECTURE.md",
|
| 419 |
-
"packages/lib/server-only/template",
|
| 420 |
-
"packages/lib/server-only/envelope",
|
| 421 |
-
"packages/trpc/server/template-router",
|
| 422 |
-
"packages/trpc/server/envelope-router"
|
| 423 |
-
],
|
| 424 |
-
"must_include_any": [
|
| 425 |
-
"template",
|
| 426 |
-
"envelope",
|
| 427 |
-
"server-only",
|
| 428 |
-
"router"
|
| 429 |
-
],
|
| 430 |
-
"min_keyword_matches": 3
|
| 431 |
-
},
|
| 432 |
-
{
|
| 433 |
-
"id": "documenso-email-templates",
|
| 434 |
-
"category": "specific-function",
|
| 435 |
-
"question": "How are lifecycle emails represented in Documenso?",
|
| 436 |
-
"ground_truth": "Lifecycle emails live in the @documenso/email package, with React Email templates under packages/email/templates. The package also includes mailer, transports, providers, render helpers, and reusable template components.",
|
| 437 |
-
"expected_sources": [
|
| 438 |
-
"ARCHITECTURE.md",
|
| 439 |
-
"packages/email",
|
| 440 |
-
"packages/email/templates",
|
| 441 |
-
"packages/email/mailer.ts",
|
| 442 |
-
"packages/email/render.tsx"
|
| 443 |
-
],
|
| 444 |
-
"must_include_any": [
|
| 445 |
-
"React Email",
|
| 446 |
-
"templates",
|
| 447 |
-
"mailer",
|
| 448 |
-
"transports"
|
| 449 |
-
],
|
| 450 |
-
"min_keyword_matches": 3
|
| 451 |
-
},
|
| 452 |
-
{
|
| 453 |
-
"id": "documenso-signing-package",
|
| 454 |
-
"category": "specific-function",
|
| 455 |
-
"question": "What does the signing package do in Documenso?",
|
| 456 |
-
"ground_truth": "The @documenso/signing package owns PDF signing behavior. Its signPdf entry point selects a signing transport, applies timestamp authority settings when configured, and supports local P12 signing and Google Cloud KMS/HSM-backed signing through transport implementations.",
|
| 457 |
-
"expected_sources": [
|
| 458 |
-
"ARCHITECTURE.md",
|
| 459 |
-
"packages/signing/index.ts",
|
| 460 |
-
"packages/signing/helpers",
|
| 461 |
-
"packages/signing/transports",
|
| 462 |
-
".env.example"
|
| 463 |
-
],
|
| 464 |
-
"must_include_any": [
|
| 465 |
-
"PDF signing",
|
| 466 |
-
"transports",
|
| 467 |
-
"local",
|
| 468 |
-
"Google",
|
| 469 |
-
"KMS"
|
| 470 |
-
],
|
| 471 |
-
"min_keyword_matches": 3
|
| 472 |
-
},
|
| 473 |
-
{
|
| 474 |
-
"id": "documenso-job-system",
|
| 475 |
-
"category": "specific-function",
|
| 476 |
-
"question": "What role do jobs play in Documenso?",
|
| 477 |
-
"ground_truth": "Jobs handle asynchronous operations such as email sending, document sealing, reminders, and webhooks. The architecture describes Inngest or local providers, with job definitions and clients under packages/lib/jobs.",
|
| 478 |
-
"expected_sources": [
|
| 479 |
-
"ARCHITECTURE.md",
|
| 480 |
-
"packages/lib/jobs",
|
| 481 |
-
"packages/lib/server-only/email",
|
| 482 |
-
"packages/lib/server-only/webhooks"
|
| 483 |
-
],
|
| 484 |
-
"must_include_any": [
|
| 485 |
-
"jobs",
|
| 486 |
-
"Inngest",
|
| 487 |
-
"local",
|
| 488 |
-
"email",
|
| 489 |
-
"webhooks"
|
| 490 |
-
],
|
| 491 |
-
"min_keyword_matches": 3
|
| 492 |
-
},
|
| 493 |
-
{
|
| 494 |
-
"id": "documenso-document-send-flow",
|
| 495 |
-
"category": "cross-file",
|
| 496 |
-
"question": "How does a document send operation flow across the Documenso codebase?",
|
| 497 |
-
"ground_truth": "A document send operation starts at an API or UI route, goes through the API layer such as packages/trpc/server/document-router, delegates core behavior to packages/lib/server-only/document and related recipient or field logic, persists through packages/prisma, and can trigger emails or jobs through packages/email and packages/lib/jobs.",
|
| 498 |
-
"expected_sources": [
|
| 499 |
-
"packages/trpc/server/document-router",
|
| 500 |
-
"packages/lib/server-only/document",
|
| 501 |
-
"packages/lib/server-only/recipient",
|
| 502 |
-
"packages/prisma",
|
| 503 |
-
"packages/email",
|
| 504 |
-
"packages/lib/jobs"
|
| 505 |
-
],
|
| 506 |
-
"must_include_any": [
|
| 507 |
-
"document-router",
|
| 508 |
-
"server-only/document",
|
| 509 |
-
"prisma",
|
| 510 |
-
"email",
|
| 511 |
-
"jobs"
|
| 512 |
-
],
|
| 513 |
-
"min_keyword_matches": 3
|
| 514 |
-
},
|
| 515 |
-
{
|
| 516 |
-
"id": "documenso-signing-completion-flow",
|
| 517 |
-
"category": "cross-file",
|
| 518 |
-
"question": "How do recipient fields, PDF handling, signing, and completion emails connect?",
|
| 519 |
-
"ground_truth": "Signing completion crosses several packages: recipient and field logic model who signs and which fields are completed, the seal-document job prepares the completed PDF, server-only PDF helpers insert fields/certificates/audit logs, @documenso/signing applies cryptographic signing, Prisma persists state, and document-completed email templates notify participants.",
|
| 520 |
-
"expected_sources": [
|
| 521 |
-
"packages/lib/server-only/recipient",
|
| 522 |
-
"packages/lib/server-only/field",
|
| 523 |
-
"packages/lib/server-only/pdf",
|
| 524 |
-
"packages/lib/jobs/definitions/internal/seal-document.handler.ts",
|
| 525 |
-
"packages/signing",
|
| 526 |
-
"packages/prisma",
|
| 527 |
-
"packages/email/templates/document-completed.tsx"
|
| 528 |
-
],
|
| 529 |
-
"must_include_any": [
|
| 530 |
-
"recipient",
|
| 531 |
-
"field",
|
| 532 |
-
"PDF",
|
| 533 |
-
"signing",
|
| 534 |
-
"document-completed"
|
| 535 |
-
],
|
| 536 |
-
"min_keyword_matches": 3
|
| 537 |
-
},
|
| 538 |
-
{
|
| 539 |
-
"id": "documenso-webhook-job-flow",
|
| 540 |
-
"category": "cross-file",
|
| 541 |
-
"question": "How do background jobs and webhooks complement each other in Documenso?",
|
| 542 |
-
"ground_truth": "Background jobs handle asynchronous work, and webhook logic lives under packages/lib/server-only/webhooks. Together they allow document lifecycle events to be processed outside the immediate request path and delivered to external integrations.",
|
| 543 |
-
"expected_sources": [
|
| 544 |
-
"ARCHITECTURE.md",
|
| 545 |
-
"packages/lib/jobs",
|
| 546 |
-
"packages/lib/server-only/webhooks",
|
| 547 |
-
".env.example"
|
| 548 |
-
],
|
| 549 |
-
"must_include_any": [
|
| 550 |
-
"jobs",
|
| 551 |
-
"webhooks",
|
| 552 |
-
"asynchronous",
|
| 553 |
-
"events"
|
| 554 |
-
],
|
| 555 |
-
"min_keyword_matches": 3
|
| 556 |
-
},
|
| 557 |
-
{
|
| 558 |
-
"id": "documenso-ui-to-api-flow",
|
| 559 |
-
"category": "cross-file",
|
| 560 |
-
"question": "How does the Remix UI connect to server routes and shared API packages?",
|
| 561 |
-
"ground_truth": "The Remix UI is under apps/remix/app with route definitions and root app wiring. The server side under apps/remix/server wires routers and context, then connects to the shared packages/trpc APIs and packages/lib business logic.",
|
| 562 |
-
"expected_sources": [
|
| 563 |
-
"apps/remix/app/routes.ts",
|
| 564 |
-
"apps/remix/app/root.tsx",
|
| 565 |
-
"apps/remix/server/router.ts",
|
| 566 |
-
"apps/remix/server/context.ts",
|
| 567 |
-
"packages/trpc",
|
| 568 |
-
"packages/lib"
|
| 569 |
-
],
|
| 570 |
-
"must_include_any": [
|
| 571 |
-
"apps/remix/app",
|
| 572 |
-
"apps/remix/server",
|
| 573 |
-
"router",
|
| 574 |
-
"context",
|
| 575 |
-
"tRPC"
|
| 576 |
-
],
|
| 577 |
-
"min_keyword_matches": 3
|
| 578 |
-
},
|
| 579 |
-
{
|
| 580 |
-
"id": "documenso-storage-pdf-flow",
|
| 581 |
-
"category": "cross-file",
|
| 582 |
-
"question": "How do storage, PDF processing, and database state fit together?",
|
| 583 |
-
"ground_truth": "The architecture separates storage provider concerns, PDF handling, and database state. Server-side PDF behavior lives under packages/lib/server-only/pdf, upload and download storage logic lives under packages/lib/universal/upload, document data records are created through packages/lib/server-only/document-data, and packages/prisma persists metadata and workflow state.",
|
| 584 |
-
"expected_sources": [
|
| 585 |
-
"ARCHITECTURE.md",
|
| 586 |
-
"packages/lib/server-only/pdf",
|
| 587 |
-
"packages/lib/server-only/document-data",
|
| 588 |
-
"packages/prisma",
|
| 589 |
-
"packages/lib/universal/upload"
|
| 590 |
-
],
|
| 591 |
-
"must_include_any": [
|
| 592 |
-
"storage",
|
| 593 |
-
"PDF",
|
| 594 |
-
"database",
|
| 595 |
-
"prisma",
|
| 596 |
-
"metadata"
|
| 597 |
-
],
|
| 598 |
-
"min_keyword_matches": 3
|
| 599 |
-
},
|
| 600 |
-
{
|
| 601 |
-
"id": "documenso-auth-session-flow",
|
| 602 |
-
"category": "cross-file",
|
| 603 |
-
"question": "How does authentication show up across Documenso's app and packages?",
|
| 604 |
-
"ground_truth": "Authentication spans the @documenso/auth package, server-only auth logic under packages/lib/server-only/auth, app server context in apps/remix/server/context.ts, and API layers that choose between session cookies and API tokens.",
|
| 605 |
-
"expected_sources": [
|
| 606 |
-
"ARCHITECTURE.md",
|
| 607 |
-
"packages/auth",
|
| 608 |
-
"packages/lib/server-only/auth",
|
| 609 |
-
"apps/remix/server/context.ts",
|
| 610 |
-
"packages/trpc/server"
|
| 611 |
-
],
|
| 612 |
-
"must_include_any": [
|
| 613 |
-
"auth",
|
| 614 |
-
"session",
|
| 615 |
-
"API token",
|
| 616 |
-
"context"
|
| 617 |
-
],
|
| 618 |
-
"min_keyword_matches": 3
|
| 619 |
-
},
|
| 620 |
-
{
|
| 621 |
-
"id": "documenso-public-api-errors",
|
| 622 |
-
"category": "error-handling",
|
| 623 |
-
"question": "Where would Documenso validate or reject bad public API requests?",
|
| 624 |
-
"ground_truth": "Bad public API requests are validated in the API layer and supporting server-only public API logic. The relevant code is under packages/trpc/server for API V2, packages/api for API V1, and packages/lib/server-only/public-api for shared public API behavior such as authentication, permissions, and validation.",
|
| 625 |
-
"expected_sources": [
|
| 626 |
-
"packages/trpc/server",
|
| 627 |
-
"packages/api",
|
| 628 |
-
"packages/lib/server-only/public-api"
|
| 629 |
-
],
|
| 630 |
-
"must_include_any": [
|
| 631 |
-
"validation",
|
| 632 |
-
"API",
|
| 633 |
-
"auth",
|
| 634 |
-
"public-api"
|
| 635 |
-
],
|
| 636 |
-
"min_keyword_matches": 2
|
| 637 |
-
},
|
| 638 |
-
{
|
| 639 |
-
"id": "documenso-webhook-security-errors",
|
| 640 |
-
"category": "error-handling",
|
| 641 |
-
"question": "Where should you look for webhook security or SSRF-related safeguards?",
|
| 642 |
-
"ground_truth": "Webhook safeguards belong in packages/lib/server-only/webhooks, with related configuration documented in .env.example such as NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS. These sources explain where outbound webhook behavior and security exceptions are controlled.",
|
| 643 |
-
"expected_sources": [
|
| 644 |
-
".env.example",
|
| 645 |
-
"packages/lib/server-only/webhooks"
|
| 646 |
-
],
|
| 647 |
-
"must_include_any": [
|
| 648 |
-
"webhook",
|
| 649 |
-
"SSRF",
|
| 650 |
-
"bypass",
|
| 651 |
-
"hosts"
|
| 652 |
-
],
|
| 653 |
-
"min_keyword_matches": 2
|
| 654 |
-
},
|
| 655 |
-
{
|
| 656 |
-
"id": "documenso-signing-config-errors",
|
| 657 |
-
"category": "error-handling",
|
| 658 |
-
"question": "Where would invalid signing configuration most likely be enforced?",
|
| 659 |
-
"ground_truth": "Invalid signing configuration is handled by the signing package and the code that reads signing environment variables. The key sources are .env.example for supported values, packages/signing/index.ts for transport selection and unsupported transport errors, and packages/signing/transports for local or gcloud-hsm certificate/key validation.",
|
| 660 |
-
"expected_sources": [
|
| 661 |
-
".env.example",
|
| 662 |
-
"packages/signing/index.ts",
|
| 663 |
-
"packages/signing/transports",
|
| 664 |
-
"packages/signing/helpers"
|
| 665 |
-
],
|
| 666 |
-
"must_include_any": [
|
| 667 |
-
"signing",
|
| 668 |
-
"transport",
|
| 669 |
-
"local",
|
| 670 |
-
"gcloud-hsm"
|
| 671 |
-
],
|
| 672 |
-
"min_keyword_matches": 3
|
| 673 |
-
},
|
| 674 |
-
{
|
| 675 |
-
"id": "documenso-playwright-tests",
|
| 676 |
-
"category": "tests",
|
| 677 |
-
"question": "Where does Documenso keep end-to-end app tests?",
|
| 678 |
-
"ground_truth": "The architecture identifies @documenso/app-tests as the E2E test package, and the packages/app-tests directory is intended for Playwright coverage of app behavior.",
|
| 679 |
-
"expected_sources": [
|
| 680 |
-
"ARCHITECTURE.md",
|
| 681 |
-
"packages/app-tests",
|
| 682 |
-
"packages/app-tests/package.json"
|
| 683 |
-
],
|
| 684 |
-
"must_include_any": [
|
| 685 |
-
"app-tests",
|
| 686 |
-
"E2E",
|
| 687 |
-
"Playwright"
|
| 688 |
-
],
|
| 689 |
-
"min_keyword_matches": 2
|
| 690 |
-
},
|
| 691 |
-
{
|
| 692 |
-
"id": "documenso-package-test-config",
|
| 693 |
-
"category": "tests",
|
| 694 |
-
"question": "How can you find package-level test configuration for shared logic?",
|
| 695 |
-
"ground_truth": "Package-level tests and configuration can be found near the package they cover, such as packages/lib/vitest.config.ts for shared library tests and package.json scripts in the relevant package directories.",
|
| 696 |
-
"expected_sources": [
|
| 697 |
-
"packages/lib/vitest.config.ts",
|
| 698 |
-
"packages/lib/package.json",
|
| 699 |
-
"packages"
|
| 700 |
-
],
|
| 701 |
-
"must_include_any": [
|
| 702 |
-
"vitest",
|
| 703 |
-
"package.json",
|
| 704 |
-
"packages/lib"
|
| 705 |
-
],
|
| 706 |
-
"min_keyword_matches": 2
|
| 707 |
-
},
|
| 708 |
-
{
|
| 709 |
-
"id": "documenso-email-template-tests",
|
| 710 |
-
"category": "tests",
|
| 711 |
-
"question": "What should be tested when changing document lifecycle email behavior?",
|
| 712 |
-
"ground_truth": "A good answer should point to packages/email templates and rendering/mailer code, and should mention checking template output, variables, delivery behavior, and any app or E2E tests that cover document lifecycle notifications.",
|
| 713 |
-
"expected_sources": [
|
| 714 |
-
"packages/email/templates",
|
| 715 |
-
"packages/email/render.tsx",
|
| 716 |
-
"packages/email/mailer.ts",
|
| 717 |
-
"packages/app-tests"
|
| 718 |
-
],
|
| 719 |
-
"must_include_any": [
|
| 720 |
-
"email",
|
| 721 |
-
"templates",
|
| 722 |
-
"render",
|
| 723 |
-
"mailer",
|
| 724 |
-
"tests"
|
| 725 |
-
],
|
| 726 |
-
"min_keyword_matches": 3
|
| 727 |
-
},
|
| 728 |
-
{
|
| 729 |
-
"id": "documenso-followup-api-v2-code-path",
|
| 730 |
-
"category": "conversation",
|
| 731 |
-
"turns": [
|
| 732 |
-
{
|
| 733 |
-
"role": "user",
|
| 734 |
-
"content": "How does Documenso expose document operations in the current public API?"
|
| 735 |
-
},
|
| 736 |
-
{
|
| 737 |
-
"role": "assistant",
|
| 738 |
-
"content": "The current public API is API V2, implemented through the tRPC server package and document router, with OpenAPI support."
|
| 739 |
-
}
|
| 740 |
-
],
|
| 741 |
-
"question": "show me the code path for that",
|
| 742 |
-
"ground_truth": "The follow-up should stay anchored to packages/trpc/server/document-router and the apps/remix server routing that mounts API V2. It should avoid drifting to README-only setup material.",
|
| 743 |
-
"expected_sources": [
|
| 744 |
-
"packages/trpc/server/document-router",
|
| 745 |
-
"packages/trpc/server",
|
| 746 |
-
"apps/remix/server/router.ts",
|
| 747 |
-
"ARCHITECTURE.md"
|
| 748 |
-
],
|
| 749 |
-
"must_include_any": [
|
| 750 |
-
"document-router",
|
| 751 |
-
"packages/trpc/server",
|
| 752 |
-
"API V2",
|
| 753 |
-
"router"
|
| 754 |
-
],
|
| 755 |
-
"min_keyword_matches": 3
|
| 756 |
-
},
|
| 757 |
-
{
|
| 758 |
-
"id": "documenso-followup-email-flow",
|
| 759 |
-
"category": "conversation",
|
| 760 |
-
"turns": [
|
| 761 |
-
{
|
| 762 |
-
"role": "user",
|
| 763 |
-
"content": "What happens after a document is sent for signing?"
|
| 764 |
-
},
|
| 765 |
-
{
|
| 766 |
-
"role": "assistant",
|
| 767 |
-
"content": "The send flow updates document state, recipient state, jobs, and lifecycle emails."
|
| 768 |
-
}
|
| 769 |
-
],
|
| 770 |
-
"question": "where does the email part live?",
|
| 771 |
-
"ground_truth": "The follow-up should retrieve packages/email templates, mailer/render code, and any server-only email or job code that triggers those templates.",
|
| 772 |
-
"expected_sources": [
|
| 773 |
-
"packages/email/templates",
|
| 774 |
-
"packages/email/mailer.ts",
|
| 775 |
-
"packages/email/render.tsx",
|
| 776 |
-
"packages/lib/server-only/email",
|
| 777 |
-
"packages/lib/jobs"
|
| 778 |
-
],
|
| 779 |
-
"must_include_any": [
|
| 780 |
-
"packages/email",
|
| 781 |
-
"templates",
|
| 782 |
-
"mailer",
|
| 783 |
-
"jobs"
|
| 784 |
-
],
|
| 785 |
-
"min_keyword_matches": 3
|
| 786 |
-
},
|
| 787 |
-
{
|
| 788 |
-
"id": "documenso-followup-self-hosting-config",
|
| 789 |
-
"category": "conversation",
|
| 790 |
-
"turns": [
|
| 791 |
-
{
|
| 792 |
-
"role": "user",
|
| 793 |
-
"content": "How do I run Documenso myself?"
|
| 794 |
-
},
|
| 795 |
-
{
|
| 796 |
-
"role": "assistant",
|
| 797 |
-
"content": "Self-hosting uses the README flow: copy .env.example, set required URLs, database, SMTP, and secrets, run migrations, then start apps/remix."
|
| 798 |
-
}
|
| 799 |
-
],
|
| 800 |
-
"question": "which config files should I inspect?",
|
| 801 |
-
"ground_truth": "The follow-up should point to .env.example, README self-hosting instructions, package.json or turbo config for scripts, and apps/remix for where the app starts.",
|
| 802 |
-
"expected_sources": [
|
| 803 |
-
".env.example",
|
| 804 |
-
"README.md",
|
| 805 |
-
"package.json",
|
| 806 |
-
"turbo.json",
|
| 807 |
-
"apps/remix"
|
| 808 |
-
],
|
| 809 |
-
"must_include_any": [
|
| 810 |
-
".env.example",
|
| 811 |
-
"README",
|
| 812 |
-
"package.json",
|
| 813 |
-
"apps/remix"
|
| 814 |
-
],
|
| 815 |
-
"min_keyword_matches": 3
|
| 816 |
-
},
|
| 817 |
-
{
|
| 818 |
-
"id": "documenso-followup-signing-bridge",
|
| 819 |
-
"category": "conversation",
|
| 820 |
-
"turns": [
|
| 821 |
-
{
|
| 822 |
-
"role": "user",
|
| 823 |
-
"content": "How does Documenso seal or sign completed documents?"
|
| 824 |
-
},
|
| 825 |
-
{
|
| 826 |
-
"role": "assistant",
|
| 827 |
-
"content": "PDF completion crosses server-only PDF/document logic and the @documenso/signing package, which supports local and Google-backed signing transports."
|
| 828 |
-
}
|
| 829 |
-
],
|
| 830 |
-
"question": "show me where signing plugs in",
|
| 831 |
-
"ground_truth": "The follow-up should retrieve packages/signing, especially the signPdf entry point, transports, and helpers, plus the seal-document job where completed PDFs are decorated and passed into signing, and signing-related env configuration.",
|
| 832 |
-
"expected_sources": [
|
| 833 |
-
"packages/signing/index.ts",
|
| 834 |
-
"packages/signing/transports",
|
| 835 |
-
"packages/signing/helpers",
|
| 836 |
-
"packages/lib/jobs/definitions/internal/seal-document.handler.ts",
|
| 837 |
-
".env.example"
|
| 838 |
-
],
|
| 839 |
-
"must_include_any": [
|
| 840 |
-
"packages/signing",
|
| 841 |
-
"transports",
|
| 842 |
-
"PDF",
|
| 843 |
-
"signing"
|
| 844 |
-
],
|
| 845 |
-
"min_keyword_matches": 3
|
| 846 |
-
},
|
| 847 |
-
{
|
| 848 |
-
"id": "documenso-codegen-email-template-checklist",
|
| 849 |
-
"category": "code-generation",
|
| 850 |
-
"question": "Write a short implementation checklist for adding a new document lifecycle email template in Documenso",
|
| 851 |
-
"ground_truth": "A good checklist should mention adding a React Email template under packages/email/templates, wiring rendering or mailer usage if needed, passing required variables from server-side document or job logic, and covering the change with focused tests or previews.",
|
| 852 |
-
"expected_sources": [
|
| 853 |
-
"packages/email/templates",
|
| 854 |
-
"packages/email/mailer.ts",
|
| 855 |
-
"packages/email/render.tsx",
|
| 856 |
-
"packages/lib/jobs",
|
| 857 |
-
"packages/lib/server-only/document"
|
| 858 |
-
],
|
| 859 |
-
"must_include_any": [
|
| 860 |
-
"packages/email/templates",
|
| 861 |
-
"React Email",
|
| 862 |
-
"mailer",
|
| 863 |
-
"jobs",
|
| 864 |
-
"tests"
|
| 865 |
-
],
|
| 866 |
-
"min_keyword_matches": 3
|
| 867 |
-
},
|
| 868 |
-
{
|
| 869 |
-
"id": "documenso-codegen-api-route-checklist",
|
| 870 |
-
"category": "code-generation",
|
| 871 |
-
"question": "Write a short implementation checklist for adding a new API V2 document operation in Documenso",
|
| 872 |
-
"ground_truth": "A good checklist should point to packages/trpc/server and the document-router, define request and response types or schemas, delegate business logic to packages/lib/server-only/document, update OpenAPI exposure if applicable, persist with Prisma when needed, and add tests.",
|
| 873 |
-
"expected_sources": [
|
| 874 |
-
"packages/trpc/server/document-router",
|
| 875 |
-
"packages/trpc/server",
|
| 876 |
-
"packages/lib/server-only/document",
|
| 877 |
-
"packages/prisma",
|
| 878 |
-
"ARCHITECTURE.md"
|
| 879 |
-
],
|
| 880 |
-
"must_include_any": [
|
| 881 |
-
"document-router",
|
| 882 |
-
"tRPC",
|
| 883 |
-
"OpenAPI",
|
| 884 |
-
"server-only/document",
|
| 885 |
-
"Prisma"
|
| 886 |
-
],
|
| 887 |
-
"min_keyword_matches": 3
|
| 888 |
-
}
|
| 889 |
-
]
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repositories": [
|
| 3 |
+
{
|
| 4 |
+
"id": "documenso",
|
| 5 |
+
"name": "Documenso",
|
| 6 |
+
"github_url": "https://github.com/documenso/documenso.git",
|
| 7 |
+
"cases": [
|
| 8 |
+
{
|
| 9 |
+
"id": "documenso-purpose",
|
| 10 |
+
"category": "architecture",
|
| 11 |
+
"question": "What is Documenso and what product problem is it trying to solve?",
|
| 12 |
+
"ground_truth": "Documenso is an open-source document signing platform and DocuSign alternative. It lets users create, send, and sign documents electronically while emphasizing self-hosting, trust, and the ability to inspect how the signing system works under the hood.",
|
| 13 |
+
"expected_sources": ["README.md", "MANIFEST.md", "ARCHITECTURE.md"],
|
| 14 |
+
"must_include_any": ["document signing", "self-host", "DocuSign", "open trust"]
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": "documenso-api-v2-document-router",
|
| 18 |
+
"category": "api",
|
| 19 |
+
"question": "Where is the current document API implemented and how is it exposed?",
|
| 20 |
+
"ground_truth": "The current API V2 is implemented under packages/trpc/server, with document operations organized under a document router. It is exposed through /api/v2 and /api/v2-beta with tRPC and OpenAPI support, and it accepts API-token or session-cookie authentication depending on the route.",
|
| 21 |
+
"expected_sources": ["ARCHITECTURE.md", "packages/trpc/server", "packages/trpc/server/document-router", "apps/remix/server"],
|
| 22 |
+
"must_include_any": ["packages/trpc/server", "document-router", "API V2", "OpenAPI", "tRPC"]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": "documenso-signing-package",
|
| 26 |
+
"category": "specific-function",
|
| 27 |
+
"question": "What does the signing package do in Documenso?",
|
| 28 |
+
"ground_truth": "The @documenso/signing package owns PDF signing behavior. Its signPdf entry point selects a signing transport, applies timestamp authority settings when configured, and supports local P12 signing and Google Cloud KMS/HSM-backed signing through transport implementations.",
|
| 29 |
+
"expected_sources": ["ARCHITECTURE.md", "packages/signing/index.ts", "packages/signing/helpers", "packages/signing/transports", ".env.example"],
|
| 30 |
+
"must_include_any": ["PDF signing", "transports", "local", "Google", "KMS"]
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": "documenso-document-send-flow",
|
| 34 |
+
"category": "cross-file",
|
| 35 |
+
"question": "How does a document send operation flow across the Documenso codebase?",
|
| 36 |
+
"ground_truth": "A document send operation starts at an API or UI route, goes through the API layer such as packages/trpc/server/document-router, delegates core behavior to packages/lib/server-only/document and related recipient or field logic, persists through packages/prisma, and can trigger emails or jobs through packages/email and packages/lib/jobs.",
|
| 37 |
+
"expected_sources": ["packages/trpc/server/document-router", "packages/lib/server-only/document", "packages/lib/server-only/recipient", "packages/prisma", "packages/email", "packages/lib/jobs"],
|
| 38 |
+
"must_include_any": ["document-router", "server-only/document", "prisma", "email", "jobs"]
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "documenso-required-env",
|
| 42 |
+
"category": "config-setup",
|
| 43 |
+
"question": "Which environment variables are central to running a self-hosted Documenso instance?",
|
| 44 |
+
"ground_truth": "The setup expects values such as NEXTAUTH_SECRET, NEXT_PRIVATE_ENCRYPTION_KEY, NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_DATABASE_URL, NEXT_PRIVATE_DIRECT_DATABASE_URL, NEXT_PRIVATE_SMTP_FROM_NAME, and NEXT_PRIVATE_SMTP_FROM_ADDRESS. The env example and self-hosting docs also cover internal URLs, signing transport, storage transport, and optional OAuth and webhook configuration.",
|
| 45 |
+
"expected_sources": ["README.md", ".env.example", "apps/docs/content/docs/self-hosting/configuration/environment.mdx"],
|
| 46 |
+
"must_include_any": ["NEXTAUTH_SECRET", "NEXT_PUBLIC_WEBAPP_URL", "DATABASE_URL", "SMTP", "encryption"]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": "documenso-playwright-tests",
|
| 50 |
+
"category": "tests",
|
| 51 |
+
"question": "Where does Documenso keep end-to-end app tests?",
|
| 52 |
+
"ground_truth": "The architecture identifies @documenso/app-tests as the E2E test package, and the packages/app-tests directory is intended for Playwright coverage of app behavior.",
|
| 53 |
+
"expected_sources": ["ARCHITECTURE.md", "packages/app-tests", "packages/app-tests/package.json"],
|
| 54 |
+
"must_include_any": ["app-tests", "E2E", "Playwright"]
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "documenso-webhook-security-errors",
|
| 58 |
+
"category": "error-handling",
|
| 59 |
+
"question": "Where should you look for webhook security or SSRF-related safeguards?",
|
| 60 |
+
"ground_truth": "Webhook safeguards belong in packages/lib/server-only/webhooks, with related configuration documented in .env.example such as NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS. These sources explain where outbound webhook behavior and security exceptions are controlled.",
|
| 61 |
+
"expected_sources": [".env.example", "packages/lib/server-only/webhooks"],
|
| 62 |
+
"must_include_any": ["webhook", "SSRF", "bypass", "hosts"]
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": "documenso-followup-signing-bridge",
|
| 66 |
+
"category": "conversation",
|
| 67 |
+
"turns": [
|
| 68 |
+
{"role": "user", "content": "How does Documenso seal or sign completed documents?"},
|
| 69 |
+
{"role": "assistant", "content": "PDF completion crosses server-only PDF/document logic and the @documenso/signing package, which supports local and Google-backed signing transports."}
|
| 70 |
+
],
|
| 71 |
+
"question": "show me where signing plugs in",
|
| 72 |
+
"ground_truth": "The follow-up should retrieve packages/signing, especially the signPdf entry point, transports, and helpers, plus the seal-document job where completed PDFs are decorated and passed into signing, and signing-related env configuration.",
|
| 73 |
+
"expected_sources": ["packages/signing/index.ts", "packages/signing/transports", "packages/signing/helpers", "packages/lib/jobs/definitions/internal/seal-document.handler.ts", ".env.example"],
|
| 74 |
+
"must_include_any": ["packages/signing", "transports", "PDF", "signing"]
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"id": "sqlite",
|
| 80 |
+
"name": "SQLite",
|
| 81 |
+
"github_url": "https://github.com/sqlite/sqlite.git",
|
| 82 |
+
"cases": [
|
| 83 |
+
{
|
| 84 |
+
"id": "sqlite-purpose",
|
| 85 |
+
"category": "architecture",
|
| 86 |
+
"question": "What is SQLite and how is the project organized at a high level?",
|
| 87 |
+
"ground_truth": "SQLite is a self-contained, serverless, zero-configuration embedded SQL database engine. Its core sources live under src/, tests are TCL scripts, the build produces both a library and a single-file amalgamation, and the project's own AGENTS.md documents the execution pipeline for contributors.",
|
| 88 |
+
"expected_sources": ["README.md", "src", "AGENTS.md"],
|
| 89 |
+
"must_include_any": ["embedded", "serverless", "amalgamation", "SQL database engine"]
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "sqlite-query-pipeline",
|
| 93 |
+
"category": "cross-file",
|
| 94 |
+
"question": "How does a SQL statement flow through SQLite's execution pipeline?",
|
| 95 |
+
"ground_truth": "SQL text is tokenized in tokenize.c, parsed by the Lemon-generated parser from parse.y, turned into VDBE bytecode by the code generator files (build.c, select.c, insert.c, update.c, delete.c, expr.c) after being optimized by the where*.c query optimizer, then executed by the VDBE in vdbe.c against the B-tree layer in btree.c, the pager in pager.c, the WAL in wal.c, and finally the OS-level VFS such as os_unix.c.",
|
| 96 |
+
"expected_sources": ["src/tokenize.c", "src/parse.y", "src/vdbe.c", "src/btree.c", "src/pager.c", "src/wal.c", "AGENTS.md"],
|
| 97 |
+
"must_include_any": ["tokenizer", "parser", "VDBE", "B-Tree", "pager", "WAL"]
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"id": "sqlite-btree-role",
|
| 101 |
+
"category": "specific-function",
|
| 102 |
+
"question": "What role does btree.c play in SQLite?",
|
| 103 |
+
"ground_truth": "btree.c implements the B-tree storage engine SQLite uses to organize table and index data on disk. Its public interface is declared in btree.h, while btreeInt.h defines the data structures used only internally by the module.",
|
| 104 |
+
"expected_sources": ["src/btree.c", "src/btree.h", "src/btreeInt.h"],
|
| 105 |
+
"must_include_any": ["B-Tree", "storage engine", "btree.h"]
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": "sqlite-vdbe-opcodes",
|
| 109 |
+
"category": "specific-function",
|
| 110 |
+
"question": "How are VDBE opcodes generated and where do they come from?",
|
| 111 |
+
"ground_truth": "VDBE opcode numbers and names are extracted automatically by scanning src/vdbe.c with the mkopcodeh.tcl script, which generates opcodes.h; a second script, mkopcodec.tcl, then generates opcodes.c, which provides the reverse opcode-to-name mapping used for EXPLAIN output.",
|
| 112 |
+
"expected_sources": ["src/vdbe.c", "mkopcodeh.tcl", "mkopcodec.tcl"],
|
| 113 |
+
"must_include_any": ["opcodes.h", "mkopcodeh.tcl", "VDBE", "EXPLAIN"]
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"id": "sqlite-parser-generation",
|
| 117 |
+
"category": "config-setup",
|
| 118 |
+
"question": "How is the SQL grammar parser built for SQLite?",
|
| 119 |
+
"ground_truth": "The grammar is defined in src/parse.y and compiled into parse.c by the Lemon LALR(1) parser generator in tool/lemon.c, which uses tool/lempar.c as a template and also emits the parse.h header as a side effect.",
|
| 120 |
+
"expected_sources": ["src/parse.y", "tool/lemon.c", "tool/lempar.c"],
|
| 121 |
+
"must_include_any": ["parse.y", "Lemon", "LALR", "parse.c"]
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"id": "sqlite-testing",
|
| 125 |
+
"category": "tests",
|
| 126 |
+
"question": "How does SQLite run its test suite and what kind of tests does it use?",
|
| 127 |
+
"ground_truth": "SQLite's tests are TCL scripts executed through the testfixture binary, which is built with make testfixture. AGENTS.md instructs contributors to run at least make devtest after any change under src/, and make sqlite3.c builds the amalgamation used for distribution.",
|
| 128 |
+
"expected_sources": ["test", "AGENTS.md", "Makefile.in"],
|
| 129 |
+
"must_include_any": ["testfixture", "TCL", "devtest", "test suite"]
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"id": "sqlite-build-amalgamation",
|
| 133 |
+
"category": "config-setup",
|
| 134 |
+
"question": "What is the SQLite amalgamation and how is it produced?",
|
| 135 |
+
"ground_truth": "The amalgamation is the single-file distribution form of SQLite, sqlite3.c, assembled from the individual sources under src/ during the build. The public C API is declared in the src/sqlite.h.in template, which is expanded into the sqlite3.h header shipped with the amalgamation.",
|
| 136 |
+
"expected_sources": ["src/sqlite.h.in", "Makefile.in", "AGENTS.md"],
|
| 137 |
+
"must_include_any": ["amalgamation", "sqlite3.c", "sqlite.h.in"]
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"id": "sqlite-followup-wal",
|
| 141 |
+
"category": "conversation",
|
| 142 |
+
"turns": [
|
| 143 |
+
{"role": "user", "content": "How does SQLite guarantee that transactions survive a crash?"},
|
| 144 |
+
{"role": "assistant", "content": "Durability is enforced through the pager and, depending on journal mode, the write-ahead log."}
|
| 145 |
+
],
|
| 146 |
+
"question": "which files implement that WAL behavior?",
|
| 147 |
+
"ground_truth": "WAL-mode durability is implemented mainly in src/wal.c, working together with src/pager.c, which coordinates pager and journal behavior, and the OS-level VFS layer such as src/os_unix.c, which performs the actual fsync/durability calls.",
|
| 148 |
+
"expected_sources": ["src/wal.c", "src/pager.c", "src/os_unix.c"],
|
| 149 |
+
"must_include_any": ["wal.c", "pager.c", "fsync", "VFS"]
|
| 150 |
+
}
|
| 151 |
+
]
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "fastapi",
|
| 155 |
+
"name": "FastAPI",
|
| 156 |
+
"github_url": "https://github.com/fastapi/fastapi.git",
|
| 157 |
+
"cases": [
|
| 158 |
+
{
|
| 159 |
+
"id": "fastapi-purpose",
|
| 160 |
+
"category": "architecture",
|
| 161 |
+
"question": "What is FastAPI and what is it built on top of?",
|
| 162 |
+
"ground_truth": "FastAPI is a Python web framework for building APIs. It is built on top of Starlette for the web-facing parts and Pydantic for data validation and serialization, and it automatically generates an OpenAPI schema along with interactive Swagger UI and ReDoc documentation.",
|
| 163 |
+
"expected_sources": ["README.md", "fastapi/applications.py", "pyproject.toml"],
|
| 164 |
+
"must_include_any": ["Starlette", "Pydantic", "OpenAPI"]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"id": "fastapi-app-class",
|
| 168 |
+
"category": "specific-function",
|
| 169 |
+
"question": "What does the FastAPI application class do and where is it defined?",
|
| 170 |
+
"ground_truth": "The central FastAPI class is defined in fastapi/applications.py. It ties together routing, dependency injection, middleware, exception handling, and OpenAPI schema generation for the whole application.",
|
| 171 |
+
"expected_sources": ["fastapi/applications.py"],
|
| 172 |
+
"must_include_any": ["applications.py", "routing", "OpenAPI"]
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "fastapi-routing",
|
| 176 |
+
"category": "implementation",
|
| 177 |
+
"question": "Where is path operation routing implemented in FastAPI?",
|
| 178 |
+
"ground_truth": "Routing is implemented in fastapi/routing.py, which defines APIRoute and APIRouter. It handles path matching, resolves dependencies for each incoming request, and serializes the response for every registered endpoint.",
|
| 179 |
+
"expected_sources": ["fastapi/routing.py"],
|
| 180 |
+
"must_include_any": ["APIRoute", "APIRouter", "routing.py"]
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": "fastapi-dependency-injection",
|
| 184 |
+
"category": "cross-file",
|
| 185 |
+
"question": "How does FastAPI resolve dependencies declared with Depends()?",
|
| 186 |
+
"ground_truth": "Depends() and related parameter markers are defined in fastapi/params.py and fastapi/param_functions.py. The actual dependency tree resolution for each request happens in fastapi/dependencies/utils.py, which fastapi/routing.py calls while handling a request.",
|
| 187 |
+
"expected_sources": ["fastapi/dependencies/utils.py", "fastapi/params.py", "fastapi/param_functions.py", "fastapi/routing.py"],
|
| 188 |
+
"must_include_any": ["Depends", "dependencies/utils.py", "dependency"]
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "fastapi-openapi-generation",
|
| 192 |
+
"category": "api",
|
| 193 |
+
"question": "How does FastAPI generate the OpenAPI schema and interactive docs?",
|
| 194 |
+
"ground_truth": "The OpenAPI JSON schema is generated in fastapi/openapi/utils.py from the app's routes and Pydantic models. The Swagger UI and ReDoc HTML pages are served through helper functions such as get_swagger_ui_html and get_redoc_html in fastapi/openapi/docs.py.",
|
| 195 |
+
"expected_sources": ["fastapi/openapi/utils.py", "fastapi/openapi/docs.py"],
|
| 196 |
+
"must_include_any": ["OpenAPI", "Swagger", "ReDoc", "openapi/utils.py"]
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"id": "fastapi-error-handling",
|
| 200 |
+
"category": "error-handling",
|
| 201 |
+
"question": "How does FastAPI turn validation failures and raised exceptions into HTTP responses?",
|
| 202 |
+
"ground_truth": "FastAPI defines HTTPException and RequestValidationError in fastapi/exceptions.py. The default handlers that convert those exceptions into JSON error responses are registered in fastapi/exception_handlers.py.",
|
| 203 |
+
"expected_sources": ["fastapi/exceptions.py", "fastapi/exception_handlers.py"],
|
| 204 |
+
"must_include_any": ["HTTPException", "RequestValidationError", "exception_handlers.py"]
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "fastapi-security",
|
| 208 |
+
"category": "specific-function",
|
| 209 |
+
"question": "How does FastAPI support authentication schemes like OAuth2 and HTTP Bearer tokens?",
|
| 210 |
+
"ground_truth": "Authentication helpers live under fastapi/security, which provides classes such as OAuth2PasswordBearer and HTTPBearer. They integrate with the dependency injection system and are automatically reflected in the generated OpenAPI security schema.",
|
| 211 |
+
"expected_sources": ["fastapi/security", "fastapi/openapi/utils.py"],
|
| 212 |
+
"must_include_any": ["OAuth2", "HTTPBearer", "security", "dependency"]
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "fastapi-followup-encoder",
|
| 216 |
+
"category": "conversation",
|
| 217 |
+
"turns": [
|
| 218 |
+
{"role": "user", "content": "How does FastAPI encode response data before it goes back to the client?"},
|
| 219 |
+
{"role": "assistant", "content": "Response bodies are converted with jsonable_encoder before being serialized, factoring in the declared response_model."}
|
| 220 |
+
],
|
| 221 |
+
"question": "where is that encoder implemented and how is it tested?",
|
| 222 |
+
"ground_truth": "jsonable_encoder is implemented in fastapi/encoders.py, converting Pydantic models and other Python objects into JSON-compatible structures. Its behavior is covered by the pytest suite under the tests directory.",
|
| 223 |
+
"expected_sources": ["fastapi/encoders.py", "tests"],
|
| 224 |
+
"must_include_any": ["jsonable_encoder", "encoders.py", "tests"]
|
| 225 |
+
}
|
| 226 |
+
]
|
| 227 |
+
}
|
| 228 |
+
]
|
| 229 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/rag_system.py
CHANGED
|
@@ -497,7 +497,7 @@ Rules:
|
|
| 497 |
7. Keep the answer complete. Do not stop mid-sentence.
|
| 498 |
8. Use short sections or bullets only when they genuinely help readability.
|
| 499 |
9. Do not leave unfinished headings, dangling bullets, or trailing markdown markers like #, ##, or ###.
|
| 500 |
-
10.
|
| 501 |
11. If you cannot answer the question using the provided context, say: "I cannot find sufficient evidence in the codebase to answer this question."
|
| 502 |
12. Prefer the most canonical source files for API and implementation questions, such as package exports, core modules, and session/query code, over tutorial prose when they disagree in specificity.
|
| 503 |
13. Keep the answer tight. Lead with the direct answer, then add only the most important supporting detail.
|
|
@@ -560,15 +560,9 @@ Do not leave the answer unfinished.
|
|
| 560 |
)
|
| 561 |
|
| 562 |
answer_text = self._finalize_answer(answer_text)
|
|
|
|
| 563 |
confidence = self._estimate_confidence(sources)
|
| 564 |
summary = " ".join(answer_text.split())[:160] if answer_text else ""
|
| 565 |
-
citations = [
|
| 566 |
-
{
|
| 567 |
-
"source": index,
|
| 568 |
-
"reason": f"Relevant context from {source['file_path']}",
|
| 569 |
-
}
|
| 570 |
-
for index, source in enumerate(sources[: min(len(sources), 4)], start=1)
|
| 571 |
-
]
|
| 572 |
|
| 573 |
return {
|
| 574 |
"answer": answer_text,
|
|
@@ -699,7 +693,9 @@ Do not leave the answer unfinished.
|
|
| 699 |
def _normalize_markdown_answer(raw_text: str) -> str:
|
| 700 |
cleaned = (raw_text or "").strip()
|
| 701 |
cleaned = re.sub(r"^```(?:markdown|md)?\s*|\s*```$", "", cleaned, flags=re.IGNORECASE)
|
| 702 |
-
cleaned = re.sub(
|
|
|
|
|
|
|
| 703 |
cleaned = re.sub(
|
| 704 |
r"^(?:based on the provided context[,:\s-]*|from the provided context[,:\s-]*)",
|
| 705 |
"",
|
|
@@ -768,6 +764,44 @@ Do not leave the answer unfinished.
|
|
| 768 |
return True
|
| 769 |
return False
|
| 770 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
@staticmethod
|
| 772 |
def _estimate_confidence(sources: List[dict]) -> str:
|
| 773 |
if not sources:
|
|
@@ -1693,4 +1727,4 @@ Do not leave the answer unfinished.
|
|
| 1693 |
"content": chunk["content"],
|
| 1694 |
"searchable_text": chunk["searchable_text"],
|
| 1695 |
"metadata_json": chunk.get("metadata_json") or {},
|
| 1696 |
-
}
|
|
|
|
| 497 |
7. Keep the answer complete. Do not stop mid-sentence.
|
| 498 |
8. Use short sections or bullets only when they genuinely help readability.
|
| 499 |
9. Do not leave unfinished headings, dangling bullets, or trailing markdown markers like #, ##, or ###.
|
| 500 |
+
10. Back up factual claims with inline citations. After a sentence or clause that relies on a specific source, add its number in brackets, e.g. [1] or [2][3] if multiple sources support it. Use only the source numbers given above (Source 1, Source 2, ...) and never invent a number.
|
| 501 |
11. If you cannot answer the question using the provided context, say: "I cannot find sufficient evidence in the codebase to answer this question."
|
| 502 |
12. Prefer the most canonical source files for API and implementation questions, such as package exports, core modules, and session/query code, over tutorial prose when they disagree in specificity.
|
| 503 |
13. Keep the answer tight. Lead with the direct answer, then add only the most important supporting detail.
|
|
|
|
| 560 |
)
|
| 561 |
|
| 562 |
answer_text = self._finalize_answer(answer_text)
|
| 563 |
+
answer_text, citations = self._attach_citations(answer_text, sources)
|
| 564 |
confidence = self._estimate_confidence(sources)
|
| 565 |
summary = " ".join(answer_text.split())[:160] if answer_text else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
return {
|
| 568 |
"answer": answer_text,
|
|
|
|
| 693 |
def _normalize_markdown_answer(raw_text: str) -> str:
|
| 694 |
cleaned = (raw_text or "").strip()
|
| 695 |
cleaned = re.sub(r"^```(?:markdown|md)?\s*|\s*```$", "", cleaned, flags=re.IGNORECASE)
|
| 696 |
+
cleaned = re.sub(
|
| 697 |
+
r"\[Source\s+(\d+)\]", r"[\1]", cleaned, flags=re.IGNORECASE
|
| 698 |
+
)
|
| 699 |
cleaned = re.sub(
|
| 700 |
r"^(?:based on the provided context[,:\s-]*|from the provided context[,:\s-]*)",
|
| 701 |
"",
|
|
|
|
| 764 |
return True
|
| 765 |
return False
|
| 766 |
|
| 767 |
+
@staticmethod
|
| 768 |
+
def _attach_citations(answer_text: str, sources: List[dict]) -> tuple[str, List[dict]]:
|
| 769 |
+
max_source = len(sources)
|
| 770 |
+
if max_source == 0 or not answer_text:
|
| 771 |
+
return answer_text, []
|
| 772 |
+
|
| 773 |
+
cited_numbers = set()
|
| 774 |
+
|
| 775 |
+
def _keep_or_drop(match: "re.Match") -> str:
|
| 776 |
+
number = int(match.group(1))
|
| 777 |
+
if 1 <= number <= max_source:
|
| 778 |
+
cited_numbers.add(number)
|
| 779 |
+
return match.group(0)
|
| 780 |
+
# Drop citation markers that don't correspond to a real source.
|
| 781 |
+
return ""
|
| 782 |
+
|
| 783 |
+
cleaned_text = re.sub(r"\[(\d+)\]", _keep_or_drop, answer_text)
|
| 784 |
+
cleaned_text = re.sub(r"[ \t]+([.,;:!?])", r"\1", cleaned_text).strip()
|
| 785 |
+
|
| 786 |
+
# If the model didn't cite anything inline, fall back to listing every
|
| 787 |
+
# retrieved source so the response is still grounded in a traceable way.
|
| 788 |
+
numbers_to_cite = cited_numbers if cited_numbers else set(range(1, max_source + 1))
|
| 789 |
+
|
| 790 |
+
citations = []
|
| 791 |
+
for index in sorted(numbers_to_cite):
|
| 792 |
+
source = sources[index - 1]
|
| 793 |
+
citations.append(
|
| 794 |
+
{
|
| 795 |
+
"source": index,
|
| 796 |
+
"file_path": source["file_path"],
|
| 797 |
+
"symbol_name": source.get("symbol_name"),
|
| 798 |
+
"line_start": source.get("line_start"),
|
| 799 |
+
"line_end": source.get("line_end"),
|
| 800 |
+
"location": f"{source['file_path']}:{source.get('line_start')}-{source.get('line_end')}",
|
| 801 |
+
}
|
| 802 |
+
)
|
| 803 |
+
return cleaned_text, citations
|
| 804 |
+
|
| 805 |
@staticmethod
|
| 806 |
def _estimate_confidence(sources: List[dict]) -> str:
|
| 807 |
if not sources:
|
|
|
|
| 1727 |
"content": chunk["content"],
|
| 1728 |
"searchable_text": chunk["searchable_text"],
|
| 1729 |
"metadata_json": chunk.get("metadata_json") or {},
|
| 1730 |
+
}
|