Spaces:
Running
Running
Sync from GitHub via hub-sync
Browse files- evals/run_eval.py +37 -6
- evals/sample_eval_set.json +98 -98
- src/code_parser.py +284 -15
- src/embeddings.py +5 -1
- src/hybrid_search.py +82 -14
- src/rag_system.py +242 -206
- src/repo_fetcher.py +22 -2
evals/run_eval.py
CHANGED
|
@@ -76,9 +76,19 @@ def matches_expected(actual_path: str, expected_sources) -> bool:
|
|
| 76 |
|
| 77 |
|
| 78 |
def compute_retrieval_metrics(expected_sources, actual_sources):
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def keyword_hits(answer: str, keywords):
|
|
@@ -97,7 +107,7 @@ 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'][:
|
| 101 |
for i, source in enumerate(sources, start=1)
|
| 102 |
)
|
| 103 |
system_prompt = (
|
|
@@ -141,6 +151,7 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
|
|
| 141 |
question=case["question"],
|
| 142 |
top_k=TOP_K,
|
| 143 |
history=case.get("turns", []),
|
|
|
|
| 144 |
)
|
| 145 |
elapsed_ms = (time.time() - start) * 1000
|
| 146 |
|
|
@@ -149,7 +160,18 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
return {
|
| 155 |
"id": case.get("id", case["question"]),
|
|
@@ -162,7 +184,11 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
|
|
| 162 |
"retrieved_sources": cited_paths,
|
| 163 |
"retrieval_hit": retrieval["retrieval_hit"],
|
| 164 |
"top1_hit": retrieval["top1_hit"],
|
| 165 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
"faithfulness": judge_faithfulness(rag_system, case["question"], result.get("answer", ""), sources),
|
| 167 |
"latency_ms": round(elapsed_ms, 1),
|
| 168 |
}
|
|
@@ -178,6 +204,11 @@ def summarize(details):
|
|
| 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),
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def compute_retrieval_metrics(expected_sources, actual_sources):
|
| 79 |
+
matching_ranks = [
|
| 80 |
+
rank
|
| 81 |
+
for rank, path in enumerate(actual_sources, start=1)
|
| 82 |
+
if matches_expected(path, expected_sources)
|
| 83 |
+
]
|
| 84 |
+
hit = bool(matching_ranks)
|
| 85 |
+
top1 = bool(matching_ranks and matching_ranks[0] == 1)
|
| 86 |
+
reciprocal_rank = 1.0 / matching_ranks[0] if matching_ranks else 0.0
|
| 87 |
+
return {
|
| 88 |
+
"retrieval_hit": int(hit),
|
| 89 |
+
"top1_hit": int(top1),
|
| 90 |
+
"reciprocal_rank": reciprocal_rank,
|
| 91 |
+
}
|
| 92 |
|
| 93 |
|
| 94 |
def keyword_hits(answer: str, keywords):
|
|
|
|
| 107 |
if not ENABLE_FAITHFULNESS or not answer.strip() or not sources:
|
| 108 |
return None
|
| 109 |
context = "\n\n".join(
|
| 110 |
+
f"[{i}] {source['file_path']}\n{source['snippet'][:1500]}"
|
| 111 |
for i, source in enumerate(sources, start=1)
|
| 112 |
)
|
| 113 |
system_prompt = (
|
|
|
|
| 151 |
question=case["question"],
|
| 152 |
top_k=TOP_K,
|
| 153 |
history=case.get("turns", []),
|
| 154 |
+
debug_retrieval=True,
|
| 155 |
)
|
| 156 |
elapsed_ms = (time.time() - start) * 1000
|
| 157 |
|
|
|
|
| 160 |
retrieval = compute_retrieval_metrics(case.get("expected_sources", []), cited_paths)
|
| 161 |
matched, total_keywords = keyword_hits(result.get("answer", ""), case.get("must_include_any", []))
|
| 162 |
has_citations = bool(result.get("citations"))
|
| 163 |
+
expected_source_grounded = (
|
| 164 |
+
retrieval["retrieval_hit"] == 1
|
| 165 |
+
and has_citations
|
| 166 |
+
and (total_keywords == 0 or matched > 0)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
retrieval_debug = result.get("retrieval_debug", [])
|
| 170 |
+
for item in retrieval_debug:
|
| 171 |
+
item["expected_source"] = matches_expected(
|
| 172 |
+
item.get("file_path", ""),
|
| 173 |
+
case.get("expected_sources", []),
|
| 174 |
+
)
|
| 175 |
|
| 176 |
return {
|
| 177 |
"id": case.get("id", case["question"]),
|
|
|
|
| 184 |
"retrieved_sources": cited_paths,
|
| 185 |
"retrieval_hit": retrieval["retrieval_hit"],
|
| 186 |
"top1_hit": retrieval["top1_hit"],
|
| 187 |
+
"reciprocal_rank": round(retrieval["reciprocal_rank"], 4),
|
| 188 |
+
"expected_source_grounded": int(expected_source_grounded),
|
| 189 |
+
# Backward-compatible alias for older report consumers.
|
| 190 |
+
"grounded": int(expected_source_grounded),
|
| 191 |
+
"retrieval_debug": retrieval_debug,
|
| 192 |
"faithfulness": judge_faithfulness(rag_system, case["question"], result.get("answer", ""), sources),
|
| 193 |
"latency_ms": round(elapsed_ms, 1),
|
| 194 |
}
|
|
|
|
| 204 |
"case_count": len(details),
|
| 205 |
"retrieval_hit_rate": round(mean(item["retrieval_hit"] for item in details), 4),
|
| 206 |
"top1_hit_rate": round(mean(item["top1_hit"] for item in details), 4),
|
| 207 |
+
"mrr": round(mean(item["reciprocal_rank"] for item in details), 4),
|
| 208 |
+
"expected_source_grounded_rate": round(
|
| 209 |
+
mean(item["expected_source_grounded"] for item in details), 4
|
| 210 |
+
),
|
| 211 |
+
# Backward-compatible alias.
|
| 212 |
"grounded_answer_rate": round(mean(item["grounded"] for item in details), 4),
|
| 213 |
"faithfulness": round(mean(faith_scores), 4) if faith_scores else None,
|
| 214 |
"latency_p95_ms": round(latencies[p95_index], 1),
|
evals/sample_eval_set.json
CHANGED
|
@@ -17,31 +17,31 @@
|
|
| 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
|
| 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
|
| 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,
|
| 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": "
|
| 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 |
},
|
|
@@ -49,7 +49,7 @@
|
|
| 49 |
"id": "documenso-playwright-tests",
|
| 50 |
"category": "tests",
|
| 51 |
"question": "Where does Documenso keep end-to-end app tests?",
|
| 52 |
-
"ground_truth": "
|
| 53 |
"expected_sources": ["ARCHITECTURE.md", "packages/app-tests", "packages/app-tests/package.json"],
|
| 54 |
"must_include_any": ["app-tests", "E2E", "Playwright"]
|
| 55 |
},
|
|
@@ -57,7 +57,7 @@
|
|
| 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
|
| 61 |
"expected_sources": [".env.example", "packages/lib/server-only/webhooks"],
|
| 62 |
"must_include_any": ["webhook", "SSRF", "bypass", "hosts"]
|
| 63 |
},
|
|
@@ -66,87 +66,12 @@
|
|
| 66 |
"category": "conversation",
|
| 67 |
"turns": [
|
| 68 |
{"role": "user", "content": "How does Documenso seal or sign completed documents?"},
|
| 69 |
-
{"role": "assistant", "content": "
|
| 70 |
],
|
| 71 |
-
"question": "show me where signing plugs in",
|
| 72 |
-
"ground_truth": "The follow-up should
|
| 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 |
},
|
|
@@ -159,7 +84,7 @@
|
|
| 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
|
| 163 |
"expected_sources": ["README.md", "fastapi/applications.py", "pyproject.toml"],
|
| 164 |
"must_include_any": ["Starlette", "Pydantic", "OpenAPI"]
|
| 165 |
},
|
|
@@ -167,7 +92,7 @@
|
|
| 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
|
| 171 |
"expected_sources": ["fastapi/applications.py"],
|
| 172 |
"must_include_any": ["applications.py", "routing", "OpenAPI"]
|
| 173 |
},
|
|
@@ -175,7 +100,7 @@
|
|
| 175 |
"id": "fastapi-routing",
|
| 176 |
"category": "implementation",
|
| 177 |
"question": "Where is path operation routing implemented in FastAPI?",
|
| 178 |
-
"ground_truth": "
|
| 179 |
"expected_sources": ["fastapi/routing.py"],
|
| 180 |
"must_include_any": ["APIRoute", "APIRouter", "routing.py"]
|
| 181 |
},
|
|
@@ -183,15 +108,15 @@
|
|
| 183 |
"id": "fastapi-dependency-injection",
|
| 184 |
"category": "cross-file",
|
| 185 |
"question": "How does FastAPI resolve dependencies declared with Depends()?",
|
| 186 |
-
"ground_truth": "Depends
|
| 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": "
|
| 195 |
"expected_sources": ["fastapi/openapi/utils.py", "fastapi/openapi/docs.py"],
|
| 196 |
"must_include_any": ["OpenAPI", "Swagger", "ReDoc", "openapi/utils.py"]
|
| 197 |
},
|
|
@@ -199,7 +124,7 @@
|
|
| 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
|
| 203 |
"expected_sources": ["fastapi/exceptions.py", "fastapi/exception_handlers.py"],
|
| 204 |
"must_include_any": ["HTTPException", "RequestValidationError", "exception_handlers.py"]
|
| 205 |
},
|
|
@@ -207,7 +132,7 @@
|
|
| 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": "
|
| 211 |
"expected_sources": ["fastapi/security", "fastapi/openapi/utils.py"],
|
| 212 |
"must_include_any": ["OAuth2", "HTTPBearer", "security", "dependency"]
|
| 213 |
},
|
|
@@ -216,14 +141,89 @@
|
|
| 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
|
| 220 |
],
|
| 221 |
-
"question": "where is
|
| 222 |
-
"ground_truth": "jsonable_encoder is implemented in fastapi/encoders.py
|
| 223 |
"expected_sources": ["fastapi/encoders.py", "tests"],
|
| 224 |
"must_include_any": ["jsonable_encoder", "encoders.py", "tests"]
|
| 225 |
}
|
| 226 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
]
|
| 229 |
}
|
|
|
|
| 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 routes with tRPC and OpenAPI support.",
|
| 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 @documenso/signing package do, and where is it implemented?",
|
| 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", "packages/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, delegates core behavior to server-only document logic and related recipient or field logic, persists state through the data layer, and can trigger emails or 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", "recipient", "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": "Self-hosting requires configuration for authentication, encryption, the public web URL, database connectivity, and SMTP/email delivery. The environment example and self-hosting documentation are the primary sources.",
|
| 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 |
},
|
|
|
|
| 49 |
"id": "documenso-playwright-tests",
|
| 50 |
"category": "tests",
|
| 51 |
"question": "Where does Documenso keep end-to-end app tests?",
|
| 52 |
+
"ground_truth": "Documenso keeps end-to-end application tests in the @documenso/app-tests package under packages/app-tests, using Playwright for app-level coverage.",
|
| 53 |
"expected_sources": ["ARCHITECTURE.md", "packages/app-tests", "packages/app-tests/package.json"],
|
| 54 |
"must_include_any": ["app-tests", "E2E", "Playwright"]
|
| 55 |
},
|
|
|
|
| 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 security safeguards are implemented in packages/lib/server-only/webhooks, with related configuration such as SSRF bypass hosts documented in environment configuration.",
|
| 61 |
"expected_sources": [".env.example", "packages/lib/server-only/webhooks"],
|
| 62 |
"must_include_any": ["webhook", "SSRF", "bypass", "hosts"]
|
| 63 |
},
|
|
|
|
| 66 |
"category": "conversation",
|
| 67 |
"turns": [
|
| 68 |
{"role": "user", "content": "How does Documenso seal or sign completed documents?"},
|
| 69 |
+
{"role": "assistant", "content": "Completed PDFs cross server-only document/job logic and the @documenso/signing package, whose signPdf entry point supports multiple signing transports."}
|
| 70 |
],
|
| 71 |
+
"question": "show me where that signing package plugs in",
|
| 72 |
+
"ground_truth": "The follow-up should connect the @documenso/signing package, especially its entry point, transports, and helpers, with the seal-document job that passes completed PDFs into signing and with relevant signing 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", "seal-document", "PDF", "signing"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
]
|
| 77 |
},
|
|
|
|
| 84 |
"id": "fastapi-purpose",
|
| 85 |
"category": "architecture",
|
| 86 |
"question": "What is FastAPI and what is it built on top of?",
|
| 87 |
+
"ground_truth": "FastAPI is a Python web framework for building APIs. It builds on Starlette for web functionality and Pydantic for data validation and serialization, and it generates OpenAPI-based documentation.",
|
| 88 |
"expected_sources": ["README.md", "fastapi/applications.py", "pyproject.toml"],
|
| 89 |
"must_include_any": ["Starlette", "Pydantic", "OpenAPI"]
|
| 90 |
},
|
|
|
|
| 92 |
"id": "fastapi-app-class",
|
| 93 |
"category": "specific-function",
|
| 94 |
"question": "What does the FastAPI application class do and where is it defined?",
|
| 95 |
+
"ground_truth": "The central FastAPI class is defined in fastapi/applications.py and coordinates application routing, middleware, exception handling, dependency-related behavior, and OpenAPI generation.",
|
| 96 |
"expected_sources": ["fastapi/applications.py"],
|
| 97 |
"must_include_any": ["applications.py", "routing", "OpenAPI"]
|
| 98 |
},
|
|
|
|
| 100 |
"id": "fastapi-routing",
|
| 101 |
"category": "implementation",
|
| 102 |
"question": "Where is path operation routing implemented in FastAPI?",
|
| 103 |
+
"ground_truth": "Path operation routing is implemented in fastapi/routing.py, including APIRoute and APIRouter and request handling that integrates dependency resolution and response serialization.",
|
| 104 |
"expected_sources": ["fastapi/routing.py"],
|
| 105 |
"must_include_any": ["APIRoute", "APIRouter", "routing.py"]
|
| 106 |
},
|
|
|
|
| 108 |
"id": "fastapi-dependency-injection",
|
| 109 |
"category": "cross-file",
|
| 110 |
"question": "How does FastAPI resolve dependencies declared with Depends()?",
|
| 111 |
+
"ground_truth": "Depends-related parameter declarations are defined in FastAPI's parameter modules, while dependency tree construction and request-time resolution live under fastapi/dependencies and are integrated into routing.",
|
| 112 |
"expected_sources": ["fastapi/dependencies/utils.py", "fastapi/params.py", "fastapi/param_functions.py", "fastapi/routing.py"],
|
| 113 |
+
"must_include_any": ["Depends", "dependencies/utils.py", "dependency", "routing.py"]
|
| 114 |
},
|
| 115 |
{
|
| 116 |
"id": "fastapi-openapi-generation",
|
| 117 |
"category": "api",
|
| 118 |
"question": "How does FastAPI generate the OpenAPI schema and interactive docs?",
|
| 119 |
+
"ground_truth": "FastAPI generates its OpenAPI schema from routes and models using its OpenAPI utilities, while Swagger UI and ReDoc HTML are produced by helpers in the OpenAPI docs module.",
|
| 120 |
"expected_sources": ["fastapi/openapi/utils.py", "fastapi/openapi/docs.py"],
|
| 121 |
"must_include_any": ["OpenAPI", "Swagger", "ReDoc", "openapi/utils.py"]
|
| 122 |
},
|
|
|
|
| 124 |
"id": "fastapi-error-handling",
|
| 125 |
"category": "error-handling",
|
| 126 |
"question": "How does FastAPI turn validation failures and raised exceptions into HTTP responses?",
|
| 127 |
+
"ground_truth": "FastAPI defines framework exceptions such as HTTPException and RequestValidationError and provides default exception handlers that convert them into HTTP responses.",
|
| 128 |
"expected_sources": ["fastapi/exceptions.py", "fastapi/exception_handlers.py"],
|
| 129 |
"must_include_any": ["HTTPException", "RequestValidationError", "exception_handlers.py"]
|
| 130 |
},
|
|
|
|
| 132 |
"id": "fastapi-security",
|
| 133 |
"category": "specific-function",
|
| 134 |
"question": "How does FastAPI support authentication schemes like OAuth2 and HTTP Bearer tokens?",
|
| 135 |
+
"ground_truth": "FastAPI provides authentication helpers under fastapi/security, including OAuth2 and HTTP bearer classes. They integrate with dependency injection and contribute security information to OpenAPI generation.",
|
| 136 |
"expected_sources": ["fastapi/security", "fastapi/openapi/utils.py"],
|
| 137 |
"must_include_any": ["OAuth2", "HTTPBearer", "security", "dependency"]
|
| 138 |
},
|
|
|
|
| 141 |
"category": "conversation",
|
| 142 |
"turns": [
|
| 143 |
{"role": "user", "content": "How does FastAPI encode response data before it goes back to the client?"},
|
| 144 |
+
{"role": "assistant", "content": "Response data can be converted with jsonable_encoder before serialization, taking declared response behavior into account."}
|
| 145 |
],
|
| 146 |
+
"question": "where is jsonable_encoder implemented and where is that behavior tested?",
|
| 147 |
+
"ground_truth": "jsonable_encoder is implemented in fastapi/encoders.py and converts supported Python and Pydantic values into JSON-compatible structures. Its behavior is exercised by the test suite.",
|
| 148 |
"expected_sources": ["fastapi/encoders.py", "tests"],
|
| 149 |
"must_include_any": ["jsonable_encoder", "encoders.py", "tests"]
|
| 150 |
}
|
| 151 |
]
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "django",
|
| 155 |
+
"name": "Django",
|
| 156 |
+
"github_url": "https://github.com/django/django.git",
|
| 157 |
+
"cases": [
|
| 158 |
+
{
|
| 159 |
+
"id": "django-purpose",
|
| 160 |
+
"category": "architecture",
|
| 161 |
+
"question": "What is Django and how is the repository organized at a high level?",
|
| 162 |
+
"ground_truth": "Django is a high-level Python web framework. Framework implementation code lives under django/, documentation under docs/, and the main test suite under tests/.",
|
| 163 |
+
"expected_sources": ["README.rst", "django", "docs", "tests"],
|
| 164 |
+
"must_include_any": ["web framework", "django", "docs", "tests"]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"id": "django-url-routing",
|
| 168 |
+
"category": "specific-function",
|
| 169 |
+
"question": "Where is Django's URL routing machinery implemented and what does it do?",
|
| 170 |
+
"ground_truth": "Django's URL routing machinery is implemented under django/urls. It provides URL configuration and resolution that maps incoming request paths to views.",
|
| 171 |
+
"expected_sources": ["django/urls", "django/urls/resolvers.py"],
|
| 172 |
+
"must_include_any": ["django/urls", "resolver", "URL", "view"]
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "django-request-response",
|
| 176 |
+
"category": "cross-file",
|
| 177 |
+
"question": "How does an incoming HTTP request reach a Django view and become a response?",
|
| 178 |
+
"ground_truth": "Django request handling crosses the core handler layer, middleware processing, URL resolution, the selected view, and HTTP response classes before the response is returned.",
|
| 179 |
+
"expected_sources": ["django/core/handlers", "django/urls", "django/http"],
|
| 180 |
+
"must_include_any": ["handler", "middleware", "URL", "view", "response"]
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": "django-queryset",
|
| 184 |
+
"category": "specific-function",
|
| 185 |
+
"question": "Where is Django's QuerySet implemented and what role does it play?",
|
| 186 |
+
"ground_truth": "QuerySet is implemented in django/db/models/query.py. It is a central ORM abstraction representing database queries and collections of model instances and supports operations such as filtering and ordering.",
|
| 187 |
+
"expected_sources": ["django/db/models/query.py"],
|
| 188 |
+
"must_include_any": ["QuerySet", "query.py", "ORM", "database"]
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "django-settings",
|
| 192 |
+
"category": "config-setup",
|
| 193 |
+
"question": "How does Django load and expose project settings?",
|
| 194 |
+
"ground_truth": "Django's settings machinery lives under django/conf. The settings proxy loads configuration from the configured project settings module and exposes it to framework and application code.",
|
| 195 |
+
"expected_sources": ["django/conf", "django/conf/__init__.py"],
|
| 196 |
+
"must_include_any": ["settings", "django/conf", "SETTINGS_MODULE", "configuration"]
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"id": "django-tests",
|
| 200 |
+
"category": "tests",
|
| 201 |
+
"question": "Where is Django's main test suite and where are contributors told how to run it?",
|
| 202 |
+
"ground_truth": "Django keeps its main test suite under tests/. The repository README points contributors to the unit-test instructions under docs/internals/contributing/writing-code/unit-tests.txt.",
|
| 203 |
+
"expected_sources": ["tests", "README.rst", "docs/internals/contributing/writing-code/unit-tests.txt"],
|
| 204 |
+
"must_include_any": ["tests", "unit-tests.txt", "test suite"]
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "django-security-csrf",
|
| 208 |
+
"category": "error-handling",
|
| 209 |
+
"question": "Where is Django's CSRF protection implemented and what does it validate?",
|
| 210 |
+
"ground_truth": "Django's CSRF protection is implemented in django/middleware/csrf.py. The middleware validates CSRF-related request state and tokens and rejects requests that fail its checks.",
|
| 211 |
+
"expected_sources": ["django/middleware/csrf.py"],
|
| 212 |
+
"must_include_any": ["CSRF", "middleware", "token", "csrf.py"]
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "django-followup-template",
|
| 216 |
+
"category": "conversation",
|
| 217 |
+
"turns": [
|
| 218 |
+
{"role": "user", "content": "How does Django render HTML templates?"},
|
| 219 |
+
{"role": "assistant", "content": "Django's built-in template system loads templates through configured engines and renders them with a context."}
|
| 220 |
+
],
|
| 221 |
+
"question": "where is that built-in template engine implemented?",
|
| 222 |
+
"ground_truth": "Django's built-in template engine is implemented under django/template, including engine configuration, parsing, loading, and rendering machinery.",
|
| 223 |
+
"expected_sources": ["django/template", "django/template/engine.py", "django/template/base.py"],
|
| 224 |
+
"must_include_any": ["django/template", "Engine", "Template", "render"]
|
| 225 |
+
}
|
| 226 |
+
]
|
| 227 |
}
|
| 228 |
]
|
| 229 |
}
|
src/code_parser.py
CHANGED
|
@@ -14,8 +14,16 @@ LANGUAGE_BY_EXTENSION = {
|
|
| 14 |
".java": "java",
|
| 15 |
".go": "go",
|
| 16 |
".rs": "rust",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
|
|
|
| 19 |
SYMBOL_NODE_TYPES = {
|
| 20 |
"python": {"function_definition", "class_definition"},
|
| 21 |
"javascript": {
|
|
@@ -62,6 +70,39 @@ SYMBOL_NODE_TYPES = {
|
|
| 62 |
"enum_item",
|
| 63 |
"trait_item",
|
| 64 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
IDENTIFIER_TYPES = {
|
|
@@ -71,6 +112,10 @@ IDENTIFIER_TYPES = {
|
|
| 71 |
"field_identifier",
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
class CodeParser:
|
| 76 |
def __init__(self):
|
|
@@ -102,58 +147,282 @@ class CodeParser:
|
|
| 102 |
lines = source.splitlines()
|
| 103 |
chunks = []
|
| 104 |
capture_types = SYMBOL_NODE_TYPES.get(language, set())
|
|
|
|
| 105 |
|
| 106 |
-
def visit(node):
|
| 107 |
if node.type in capture_types:
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if chunk:
|
| 110 |
chunks.append(chunk)
|
| 111 |
return
|
| 112 |
for child in node.children:
|
| 113 |
-
visit(child)
|
| 114 |
|
| 115 |
visit(tree.root_node)
|
| 116 |
|
| 117 |
if not chunks:
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
return chunks
|
| 121 |
|
| 122 |
-
def _build_chunk(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
start_line = node.start_point[0] + 1
|
| 124 |
end_line = node.end_point[0] + 1
|
| 125 |
snippet = "\n".join(lines[start_line - 1 : end_line]).strip()
|
| 126 |
if len(snippet.splitlines()) < 2:
|
| 127 |
return None
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
if
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
|
| 136 |
signature = lines[start_line - 1].strip() if start_line - 1 < len(lines) else ""
|
| 137 |
searchable_text = "\n".join(
|
| 138 |
-
part
|
|
|
|
|
|
|
| 139 |
)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
return {
|
| 142 |
"file_path": relative_path,
|
| 143 |
"language": language,
|
| 144 |
-
"symbol_name":
|
| 145 |
"symbol_type": node.type,
|
| 146 |
"line_start": start_line,
|
| 147 |
"line_end": end_line,
|
| 148 |
"signature": signature,
|
| 149 |
"content": snippet,
|
| 150 |
"searchable_text": searchable_text,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
"metadata_json": {
|
| 152 |
"parser": "tree-sitter",
|
|
|
|
|
|
|
| 153 |
},
|
| 154 |
}
|
| 155 |
|
| 156 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
stack = list(node.children)
|
| 158 |
while stack:
|
| 159 |
current = stack.pop(0)
|
|
@@ -220,4 +489,4 @@ class CodeParser:
|
|
| 220 |
},
|
| 221 |
}
|
| 222 |
)
|
| 223 |
-
return blocks
|
|
|
|
| 14 |
".java": "java",
|
| 15 |
".go": "go",
|
| 16 |
".rs": "rust",
|
| 17 |
+
".c": "c",
|
| 18 |
+
".h": "c",
|
| 19 |
+
".cc": "cpp",
|
| 20 |
+
".cpp": "cpp",
|
| 21 |
+
".cxx": "cpp",
|
| 22 |
+
".hpp": "cpp",
|
| 23 |
+
".hh": "cpp",
|
| 24 |
}
|
| 25 |
|
| 26 |
+
# Node types that make a good standalone retrieval unit (function/method-sized).
|
| 27 |
SYMBOL_NODE_TYPES = {
|
| 28 |
"python": {"function_definition", "class_definition"},
|
| 29 |
"javascript": {
|
|
|
|
| 70 |
"enum_item",
|
| 71 |
"trait_item",
|
| 72 |
},
|
| 73 |
+
"c": {
|
| 74 |
+
"function_definition",
|
| 75 |
+
"struct_specifier",
|
| 76 |
+
"enum_specifier",
|
| 77 |
+
"type_definition",
|
| 78 |
+
},
|
| 79 |
+
"cpp": {
|
| 80 |
+
"function_definition",
|
| 81 |
+
"class_specifier",
|
| 82 |
+
"struct_specifier",
|
| 83 |
+
"enum_specifier",
|
| 84 |
+
"namespace_definition",
|
| 85 |
+
"type_definition",
|
| 86 |
+
},
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
# Node types that are "containers": they hold nested members (methods, fields)
|
| 90 |
+
# that are themselves worth indexing as their own chunks. For these, we emit a
|
| 91 |
+
# compact *overview* chunk (signature + docstring + head-of-body + member
|
| 92 |
+
# list) instead of dumping the entire body into one chunk. Without this, a
|
| 93 |
+
# large class becomes a single multi-hundred-line chunk whose embedding is a
|
| 94 |
+
# blurry average of everything inside it (hurts hit rate for method-specific
|
| 95 |
+
# queries) and whose content is silently cut off by the ~1500 char preview
|
| 96 |
+
# used when the chunk is fed to the LLM (hurts faithfulness, since the method
|
| 97 |
+
# the user actually asked about may fall outside the truncated window).
|
| 98 |
+
CONTAINER_NODE_TYPES = {
|
| 99 |
+
"python": {"class_definition"},
|
| 100 |
+
"javascript": {"class_declaration"},
|
| 101 |
+
"typescript": {"class_declaration"},
|
| 102 |
+
"tsx": {"class_declaration"},
|
| 103 |
+
"java": {"class_declaration", "interface_declaration", "enum_declaration"},
|
| 104 |
+
"rust": {"impl_item", "trait_item"},
|
| 105 |
+
"cpp": {"class_specifier", "namespace_definition"},
|
| 106 |
}
|
| 107 |
|
| 108 |
IDENTIFIER_TYPES = {
|
|
|
|
| 112 |
"field_identifier",
|
| 113 |
}
|
| 114 |
|
| 115 |
+
MAX_OVERVIEW_BODY_LINES = 40
|
| 116 |
+
MAX_OVERVIEW_CHARS = 1200
|
| 117 |
+
MAX_MEMBERS_LISTED = 25
|
| 118 |
+
|
| 119 |
|
| 120 |
class CodeParser:
|
| 121 |
def __init__(self):
|
|
|
|
| 147 |
lines = source.splitlines()
|
| 148 |
chunks = []
|
| 149 |
capture_types = SYMBOL_NODE_TYPES.get(language, set())
|
| 150 |
+
container_types = CONTAINER_NODE_TYPES.get(language, set())
|
| 151 |
|
| 152 |
+
def visit(node, context_name: Optional[str] = None):
|
| 153 |
if node.type in capture_types:
|
| 154 |
+
if node.type in container_types:
|
| 155 |
+
overview = self._build_container_overview(
|
| 156 |
+
node, source, lines, relative_path, language, context_name, capture_types
|
| 157 |
+
)
|
| 158 |
+
if overview:
|
| 159 |
+
chunks.append(overview)
|
| 160 |
+
|
| 161 |
+
own_name = self._extract_own_name(node, source)
|
| 162 |
+
nested_context = own_name
|
| 163 |
+
if context_name and own_name:
|
| 164 |
+
nested_context = f"{context_name}.{own_name}"
|
| 165 |
+
elif context_name and not own_name:
|
| 166 |
+
nested_context = context_name
|
| 167 |
+
|
| 168 |
+
for child in node.children:
|
| 169 |
+
visit(child, nested_context)
|
| 170 |
+
return
|
| 171 |
+
|
| 172 |
+
chunk = self._build_chunk(node, source, lines, relative_path, language, context_name)
|
| 173 |
if chunk:
|
| 174 |
chunks.append(chunk)
|
| 175 |
return
|
| 176 |
for child in node.children:
|
| 177 |
+
visit(child, context_name)
|
| 178 |
|
| 179 |
visit(tree.root_node)
|
| 180 |
|
| 181 |
if not chunks:
|
| 182 |
+
chunks = self._fallback_chunks(source, relative_path, language)
|
| 183 |
+
|
| 184 |
+
file_overview = self._build_file_overview(
|
| 185 |
+
tree.root_node, source, lines, relative_path, language, chunks
|
| 186 |
+
)
|
| 187 |
+
if file_overview:
|
| 188 |
+
chunks.insert(0, file_overview)
|
| 189 |
|
| 190 |
return chunks
|
| 191 |
|
| 192 |
+
def _build_chunk(
|
| 193 |
+
self,
|
| 194 |
+
node,
|
| 195 |
+
source: str,
|
| 196 |
+
lines: List[str],
|
| 197 |
+
relative_path: str,
|
| 198 |
+
language: str,
|
| 199 |
+
context_name: Optional[str] = None,
|
| 200 |
+
) -> Optional[Dict]:
|
| 201 |
start_line = node.start_point[0] + 1
|
| 202 |
end_line = node.end_point[0] + 1
|
| 203 |
snippet = "\n".join(lines[start_line - 1 : end_line]).strip()
|
| 204 |
if len(snippet.splitlines()) < 2:
|
| 205 |
return None
|
| 206 |
|
| 207 |
+
own_name = self._extract_own_name(node, source)
|
| 208 |
+
qualified_name = own_name
|
| 209 |
+
if context_name and own_name:
|
| 210 |
+
qualified_name = f"{context_name}.{own_name}"
|
| 211 |
+
elif context_name and not own_name:
|
| 212 |
+
qualified_name = context_name
|
| 213 |
|
| 214 |
signature = lines[start_line - 1].strip() if start_line - 1 < len(lines) else ""
|
| 215 |
searchable_text = "\n".join(
|
| 216 |
+
part
|
| 217 |
+
for part in [relative_path, context_name or "", qualified_name or "", signature, snippet]
|
| 218 |
+
if part
|
| 219 |
)
|
| 220 |
|
| 221 |
+
metadata = {"parser": "tree-sitter"}
|
| 222 |
+
if context_name:
|
| 223 |
+
metadata["parent"] = context_name
|
| 224 |
+
|
| 225 |
return {
|
| 226 |
"file_path": relative_path,
|
| 227 |
"language": language,
|
| 228 |
+
"symbol_name": qualified_name or relative_path.split("/")[-1],
|
| 229 |
"symbol_type": node.type,
|
| 230 |
"line_start": start_line,
|
| 231 |
"line_end": end_line,
|
| 232 |
"signature": signature,
|
| 233 |
"content": snippet,
|
| 234 |
"searchable_text": searchable_text,
|
| 235 |
+
"metadata_json": metadata,
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
def _build_container_overview(
|
| 239 |
+
self,
|
| 240 |
+
node,
|
| 241 |
+
source: str,
|
| 242 |
+
lines: List[str],
|
| 243 |
+
relative_path: str,
|
| 244 |
+
language: str,
|
| 245 |
+
context_name: Optional[str],
|
| 246 |
+
capture_types: set,
|
| 247 |
+
) -> Optional[Dict]:
|
| 248 |
+
start_line = node.start_point[0] + 1
|
| 249 |
+
end_line = node.end_point[0] + 1
|
| 250 |
+
body_lines = lines[start_line - 1 : end_line]
|
| 251 |
+
if not body_lines:
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
own_name = self._extract_own_name(node, source)
|
| 255 |
+
qualified_name = own_name
|
| 256 |
+
if context_name and own_name:
|
| 257 |
+
qualified_name = f"{context_name}.{own_name}"
|
| 258 |
+
elif context_name and not own_name:
|
| 259 |
+
qualified_name = context_name
|
| 260 |
+
if not qualified_name:
|
| 261 |
+
qualified_name = relative_path.split("/")[-1]
|
| 262 |
+
|
| 263 |
+
# Head-of-body preview: naturally captures the signature, docstring,
|
| 264 |
+
# and field declarations that come before the first nested method,
|
| 265 |
+
# so simple data classes / structs keep their field list even though
|
| 266 |
+
# we no longer store the entire body verbatim.
|
| 267 |
+
preview_lines = body_lines[:MAX_OVERVIEW_BODY_LINES]
|
| 268 |
+
content = "\n".join(preview_lines).strip()
|
| 269 |
+
truncated_body = len(body_lines) > MAX_OVERVIEW_BODY_LINES
|
| 270 |
+
if len(content) > MAX_OVERVIEW_CHARS:
|
| 271 |
+
content = content[:MAX_OVERVIEW_CHARS].rstrip()
|
| 272 |
+
truncated_body = True
|
| 273 |
+
|
| 274 |
+
member_names = self._collect_member_names(node, source, capture_types)
|
| 275 |
+
members_line = ""
|
| 276 |
+
if member_names:
|
| 277 |
+
shown = member_names[:MAX_MEMBERS_LISTED]
|
| 278 |
+
members_line = f"Members: {', '.join(shown)}"
|
| 279 |
+
remaining = len(member_names) - len(shown)
|
| 280 |
+
if remaining > 0:
|
| 281 |
+
members_line += f" (+{remaining} more, indexed separately)"
|
| 282 |
+
elif truncated_body:
|
| 283 |
+
members_line = "(body truncated; see file for full contents)"
|
| 284 |
+
|
| 285 |
+
content_parts = [part for part in [content, members_line] if part]
|
| 286 |
+
full_content = "\n\n".join(content_parts)
|
| 287 |
+
|
| 288 |
+
signature = body_lines[0].strip() if body_lines else ""
|
| 289 |
+
searchable_text = "\n".join(
|
| 290 |
+
part
|
| 291 |
+
for part in [relative_path, context_name or "", qualified_name, signature, full_content]
|
| 292 |
+
if part
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"file_path": relative_path,
|
| 297 |
+
"language": language,
|
| 298 |
+
"symbol_name": qualified_name,
|
| 299 |
+
"symbol_type": f"{node.type}_overview",
|
| 300 |
+
"line_start": start_line,
|
| 301 |
+
"line_end": end_line,
|
| 302 |
+
"signature": signature,
|
| 303 |
+
"content": full_content,
|
| 304 |
+
"searchable_text": searchable_text,
|
| 305 |
"metadata_json": {
|
| 306 |
"parser": "tree-sitter",
|
| 307 |
+
"kind": "container_overview",
|
| 308 |
+
**({"parent": context_name} if context_name else {}),
|
| 309 |
},
|
| 310 |
}
|
| 311 |
|
| 312 |
+
def _build_file_overview(self, root_node, source: str, lines: List[str], relative_path: str, language: str, symbol_chunks: List[Dict]) -> Optional[Dict]:
|
| 313 |
+
if language == "text":
|
| 314 |
+
return None
|
| 315 |
+
path = Path(relative_path)
|
| 316 |
+
parts = list(path.parts)
|
| 317 |
+
directory = str(path.parent) if str(path.parent) != "." else "repository root"
|
| 318 |
+
filename = path.name
|
| 319 |
+
component = parts[1] if len(parts) >= 2 and parts[0] in {"packages", "apps"} else (parts[-2] if len(parts) >= 2 else "")
|
| 320 |
+
role = self._infer_file_role(relative_path)
|
| 321 |
+
imports, exports, export_targets = self._extract_module_edges(root_node, source)
|
| 322 |
+
symbols, seen = [], set()
|
| 323 |
+
for chunk in symbol_chunks:
|
| 324 |
+
name = chunk.get("symbol_name")
|
| 325 |
+
if name and name not in seen and chunk.get("symbol_type") != "fallback_chunk":
|
| 326 |
+
seen.add(name); symbols.append(name)
|
| 327 |
+
if len(symbols) >= 40:
|
| 328 |
+
break
|
| 329 |
+
overview = [f"File: {relative_path}", f"Filename: {filename}", f"Directory: {directory}", f"Language: {language}"]
|
| 330 |
+
if component: overview.append(f"Package/component: {component}")
|
| 331 |
+
overview.append(f"Role: {role}")
|
| 332 |
+
if imports: overview.append("Imports: " + ", ".join(imports[:30]))
|
| 333 |
+
if exports: overview.append("Exports: " + ", ".join(exports[:40]))
|
| 334 |
+
if export_targets: overview.append("Export targets: " + ", ".join(export_targets[:30]))
|
| 335 |
+
if symbols: overview.append("Symbols: " + ", ".join(symbols))
|
| 336 |
+
content = "\n".join(overview)
|
| 337 |
+
path_terms = " ".join(t for t in re.split(r"[\/._:-]+", relative_path) if t)
|
| 338 |
+
return {
|
| 339 |
+
"file_path": relative_path, "language": language,
|
| 340 |
+
"symbol_name": f"{filename}:module-overview", "symbol_type": "file_overview",
|
| 341 |
+
"line_start": 1, "line_end": 1, "signature": f"module {relative_path}",
|
| 342 |
+
"content": content,
|
| 343 |
+
"searchable_text": content + f"\nPath terms: {path_terms}\nModule: {path.stem}",
|
| 344 |
+
"metadata_json": {"parser": "tree-sitter", "kind": "file_overview", "directory": directory, "component": component, "role": role, "imports": imports[:30], "exports": exports[:40], "export_targets": export_targets[:30]},
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
@staticmethod
|
| 348 |
+
def _infer_file_role(relative_path: str) -> str:
|
| 349 |
+
path = relative_path.lower().replace("\\", "/")
|
| 350 |
+
name = Path(path).name
|
| 351 |
+
if name in {"index.ts", "index.tsx", "index.js", "index.jsx"}:
|
| 352 |
+
return "module/package entry point or barrel export"
|
| 353 |
+
for needle, role in [("router", "router/API composition"), ("route", "request route/endpoint"), ("handler", "handler/job execution"), ("controller", "request controller"), ("service", "service/business logic"), ("transport", "transport/integration adapter"), ("adapter", "integration adapter"), ("repository", "data repository"), ("config", "configuration"), ("schema", "schema/type definition"), ("test", "test"), ("spec", "test/specification")]:
|
| 354 |
+
if needle in name or f"/{needle}" in path:
|
| 355 |
+
return role
|
| 356 |
+
return "source module"
|
| 357 |
+
|
| 358 |
+
def _extract_module_edges(self, root_node, source: str):
|
| 359 |
+
imports, exports, targets = [], [], []
|
| 360 |
+
for child in root_node.children:
|
| 361 |
+
text = source[child.start_byte:child.end_byte].strip()
|
| 362 |
+
if not text: continue
|
| 363 |
+
if child.type == "import_statement" or text.startswith("import "):
|
| 364 |
+
target = self._module_target(text)
|
| 365 |
+
if target and target not in imports: imports.append(target)
|
| 366 |
+
if "export" in child.type or text.startswith("export ") or text.startswith("module.exports"):
|
| 367 |
+
target = self._module_target(text)
|
| 368 |
+
if target and target not in targets: targets.append(target)
|
| 369 |
+
for name in self._exported_names(text):
|
| 370 |
+
if name not in exports: exports.append(name)
|
| 371 |
+
return imports, exports, targets
|
| 372 |
+
|
| 373 |
+
@staticmethod
|
| 374 |
+
def _module_target(statement: str) -> Optional[str]:
|
| 375 |
+
for pattern in [r"from\s+[\"']([^\"']+)[\"']", r"(?:import|export)\s+[\"']([^\"']+)[\"']", r"require\(\s*[\"']([^\"']+)[\"']\s*\)"]:
|
| 376 |
+
match = re.search(pattern, statement)
|
| 377 |
+
if match: return match.group(1)
|
| 378 |
+
return None
|
| 379 |
+
|
| 380 |
+
@staticmethod
|
| 381 |
+
def _exported_names(statement: str) -> List[str]:
|
| 382 |
+
names = []
|
| 383 |
+
match = re.search(r"export\s*\{([^}]+)\}", statement, re.S)
|
| 384 |
+
if match:
|
| 385 |
+
for part in match.group(1).split(","):
|
| 386 |
+
item = part.strip()
|
| 387 |
+
if item: names.append(re.split(r"\s+as\s+", item)[-1].strip())
|
| 388 |
+
match = re.search(r"export\s+(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)", statement)
|
| 389 |
+
if match: names.append(match.group(1))
|
| 390 |
+
if statement.startswith("export default") and not match: names.append("default")
|
| 391 |
+
if statement.startswith("export *"): names.append("*")
|
| 392 |
+
return names
|
| 393 |
+
|
| 394 |
+
def _collect_member_names(self, node, source: str, capture_types: set) -> List[str]:
|
| 395 |
+
"""Collect names of direct member symbols (methods/fields) inside a
|
| 396 |
+
container without descending into nested containers, so a class's
|
| 397 |
+
member list doesn't pick up grandchildren from an inner class."""
|
| 398 |
+
names = []
|
| 399 |
+
seen = set()
|
| 400 |
+
|
| 401 |
+
def walk(current):
|
| 402 |
+
for child in current.children:
|
| 403 |
+
if child.type in capture_types:
|
| 404 |
+
name = self._extract_own_name(child, source)
|
| 405 |
+
if name and name not in seen:
|
| 406 |
+
seen.add(name)
|
| 407 |
+
names.append(name)
|
| 408 |
+
# Don't descend further into this member's own body.
|
| 409 |
+
continue
|
| 410 |
+
walk(child)
|
| 411 |
+
|
| 412 |
+
walk(node)
|
| 413 |
+
return names
|
| 414 |
+
|
| 415 |
+
@staticmethod
|
| 416 |
+
def _extract_own_name(node, source: str) -> Optional[str]:
|
| 417 |
+
name_node = node.child_by_field_name("name")
|
| 418 |
+
if name_node is not None:
|
| 419 |
+
candidate = source[name_node.start_byte : name_node.end_byte].strip()
|
| 420 |
+
if candidate:
|
| 421 |
+
return candidate
|
| 422 |
+
return CodeParser._find_identifier(node, source)
|
| 423 |
+
|
| 424 |
+
@staticmethod
|
| 425 |
+
def _find_identifier(node, source: str) -> Optional[str]:
|
| 426 |
stack = list(node.children)
|
| 427 |
while stack:
|
| 428 |
current = stack.pop(0)
|
|
|
|
| 489 |
},
|
| 490 |
}
|
| 491 |
)
|
| 492 |
+
return blocks
|
src/embeddings.py
CHANGED
|
@@ -320,13 +320,17 @@ class EmbeddingGenerator:
|
|
| 320 |
return np.array(embeddings, dtype="float32")
|
| 321 |
|
| 322 |
def _build_bedrock_embedding_request(self, texts: List[str], input_type: str) -> dict:
|
|
|
|
|
|
|
| 323 |
payload = {
|
| 324 |
-
"texts": texts,
|
| 325 |
"input_type": input_type,
|
| 326 |
"embedding_types": ["float"],
|
| 327 |
}
|
|
|
|
| 328 |
if self.bedrock_output_dimensionality:
|
| 329 |
payload["output_dimension"] = self.bedrock_output_dimensionality
|
|
|
|
| 330 |
return payload
|
| 331 |
|
| 332 |
def _encode_with_backoff(
|
|
|
|
| 320 |
return np.array(embeddings, dtype="float32")
|
| 321 |
|
| 322 |
def _build_bedrock_embedding_request(self, texts: List[str], input_type: str) -> dict:
|
| 323 |
+
max_chars = int(os.getenv("BEDROCK_EMBEDDING_MAX_CHARS", "2000"))
|
| 324 |
+
|
| 325 |
payload = {
|
| 326 |
+
"texts": [text[:max_chars] for text in texts],
|
| 327 |
"input_type": input_type,
|
| 328 |
"embedding_types": ["float"],
|
| 329 |
}
|
| 330 |
+
|
| 331 |
if self.bedrock_output_dimensionality:
|
| 332 |
payload["output_dimension"] = self.bedrock_output_dimensionality
|
| 333 |
+
|
| 334 |
return payload
|
| 335 |
|
| 336 |
def _encode_with_backoff(
|
src/hybrid_search.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import re
|
|
|
|
| 2 |
from collections import defaultdict
|
| 3 |
-
from typing import List
|
| 4 |
|
| 5 |
from rank_bm25 import BM25Okapi
|
| 6 |
from sentence_transformers import CrossEncoder
|
|
@@ -9,34 +10,95 @@ TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_./:-]*")
|
|
| 9 |
|
| 10 |
|
| 11 |
def tokenize(text: str) -> List[str]:
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
class HybridSearchEngine:
|
| 16 |
def __init__(self, reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
|
| 17 |
self.reranker = CrossEncoder(reranker_model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def build_for_repository(self, repo_id: int, chunks: List[dict]):
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def remove_repository(self, repo_id: int):
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
def bm25_search(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
if not chunks:
|
| 27 |
return []
|
| 28 |
tokens = tokenize(query)
|
| 29 |
if not tokens:
|
| 30 |
return []
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
if not bm25:
|
| 35 |
return []
|
| 36 |
|
| 37 |
scores = bm25.get_scores(tokens)
|
| 38 |
ranked = sorted(
|
| 39 |
-
zip(
|
| 40 |
key=lambda item: item[1],
|
| 41 |
reverse=True,
|
| 42 |
)[:top_k]
|
|
@@ -69,11 +131,17 @@ class HybridSearchEngine:
|
|
| 69 |
merged = sorted(fused.values(), key=lambda item: item["rrf_score"], reverse=True)
|
| 70 |
return merged[:top_k]
|
| 71 |
|
| 72 |
-
def rerank(
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
"""
|
| 78 |
if not candidates:
|
| 79 |
return []
|
|
@@ -91,7 +159,7 @@ class HybridSearchEngine:
|
|
| 91 |
reranked.append(enriched)
|
| 92 |
|
| 93 |
reranked.sort(key=lambda item: item["rerank_score"], reverse=True)
|
| 94 |
-
return reranked[:top_k]
|
| 95 |
|
| 96 |
@staticmethod
|
| 97 |
def normalize_semantic_results(results: List[dict]) -> List[dict]:
|
|
|
|
| 1 |
import re
|
| 2 |
+
import threading
|
| 3 |
from collections import defaultdict
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
|
| 6 |
from rank_bm25 import BM25Okapi
|
| 7 |
from sentence_transformers import CrossEncoder
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def tokenize(text: str) -> List[str]:
|
| 13 |
+
raw_tokens = TOKEN_RE.findall(text or "")
|
| 14 |
+
tokens = []
|
| 15 |
+
|
| 16 |
+
for raw in raw_tokens:
|
| 17 |
+
lowered = raw.lower()
|
| 18 |
+
tokens.append(lowered)
|
| 19 |
+
|
| 20 |
+
# Keep the original code/path token, but also expose its components to
|
| 21 |
+
# BM25. This makes sendDocument, seal-document.handler.ts, etc. match
|
| 22 |
+
# natural-language queries much more reliably.
|
| 23 |
+
pieces = re.split(r"[./:_-]+", raw)
|
| 24 |
+
for piece in pieces:
|
| 25 |
+
if not piece:
|
| 26 |
+
continue
|
| 27 |
+
tokens.append(piece.lower())
|
| 28 |
+
camel_parts = re.findall(
|
| 29 |
+
r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+",
|
| 30 |
+
piece,
|
| 31 |
+
)
|
| 32 |
+
tokens.extend(part.lower() for part in camel_parts if part)
|
| 33 |
+
|
| 34 |
+
return [token for token in tokens if token]
|
| 35 |
|
| 36 |
|
| 37 |
class HybridSearchEngine:
|
| 38 |
def __init__(self, reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
|
| 39 |
self.reranker = CrossEncoder(reranker_model)
|
| 40 |
+
# Per-repo cached BM25 index so a question doesn't have to
|
| 41 |
+
# re-tokenize and re-build the lexical index over every chunk in the
|
| 42 |
+
# repo on every single request. Built once when indexing finishes,
|
| 43 |
+
# evicted when the repo is reset/deleted/expired.
|
| 44 |
+
self._repo_indexes: Dict[int, dict] = {}
|
| 45 |
+
self._index_lock = threading.Lock()
|
| 46 |
|
| 47 |
def build_for_repository(self, repo_id: int, chunks: List[dict]):
|
| 48 |
+
if not chunks:
|
| 49 |
+
with self._index_lock:
|
| 50 |
+
self._repo_indexes.pop(repo_id, None)
|
| 51 |
+
return
|
| 52 |
+
|
| 53 |
+
corpus_tokens = [tokenize(chunk["searchable_text"]) for chunk in chunks]
|
| 54 |
+
bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None
|
| 55 |
+
with self._index_lock:
|
| 56 |
+
self._repo_indexes[repo_id] = {"bm25": bm25, "chunks": chunks}
|
| 57 |
|
| 58 |
def remove_repository(self, repo_id: int):
|
| 59 |
+
with self._index_lock:
|
| 60 |
+
self._repo_indexes.pop(repo_id, None)
|
| 61 |
|
| 62 |
+
def bm25_search(
|
| 63 |
+
self,
|
| 64 |
+
chunks: List[dict],
|
| 65 |
+
query: str,
|
| 66 |
+
top_k: int = 12,
|
| 67 |
+
repo_id: Optional[int] = None,
|
| 68 |
+
) -> List[dict]:
|
| 69 |
if not chunks:
|
| 70 |
return []
|
| 71 |
tokens = tokenize(query)
|
| 72 |
if not tokens:
|
| 73 |
return []
|
| 74 |
|
| 75 |
+
bm25 = None
|
| 76 |
+
source_chunks = chunks
|
| 77 |
+
|
| 78 |
+
if repo_id is not None:
|
| 79 |
+
with self._index_lock:
|
| 80 |
+
cached = self._repo_indexes.get(repo_id)
|
| 81 |
+
# Guard against a stale cache (e.g. repo was re-indexed but the
|
| 82 |
+
# cache write raced with this read) by checking the corpus size
|
| 83 |
+
# still lines up before trusting it.
|
| 84 |
+
if cached is not None and len(cached["chunks"]) == len(chunks):
|
| 85 |
+
bm25 = cached["bm25"]
|
| 86 |
+
source_chunks = cached["chunks"]
|
| 87 |
+
|
| 88 |
+
if bm25 is None:
|
| 89 |
+
# Fall back to building an ephemeral index. Keeps this method
|
| 90 |
+
# correct on its own even if build_for_repository wasn't called
|
| 91 |
+
# first (e.g. direct/test usage), just without the caching win.
|
| 92 |
+
corpus_tokens = [tokenize(chunk["searchable_text"]) for chunk in chunks]
|
| 93 |
+
bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None
|
| 94 |
+
source_chunks = chunks
|
| 95 |
+
|
| 96 |
if not bm25:
|
| 97 |
return []
|
| 98 |
|
| 99 |
scores = bm25.get_scores(tokens)
|
| 100 |
ranked = sorted(
|
| 101 |
+
zip(source_chunks, scores),
|
| 102 |
key=lambda item: item[1],
|
| 103 |
reverse=True,
|
| 104 |
)[:top_k]
|
|
|
|
| 131 |
merged = sorted(fused.values(), key=lambda item: item["rrf_score"], reverse=True)
|
| 132 |
return merged[:top_k]
|
| 133 |
|
| 134 |
+
def rerank(
|
| 135 |
+
self,
|
| 136 |
+
query: str,
|
| 137 |
+
candidates: List[dict],
|
| 138 |
+
top_k: Optional[int] = None,
|
| 139 |
+
) -> List[dict]:
|
| 140 |
+
"""Score candidates with the cross-encoder and optionally truncate.
|
| 141 |
+
|
| 142 |
+
Reranking depth is intentionally separate from answer-context depth.
|
| 143 |
+
Callers can rerank a broad candidate set and still send only a small
|
| 144 |
+
final source set to the LLM.
|
| 145 |
"""
|
| 146 |
if not candidates:
|
| 147 |
return []
|
|
|
|
| 159 |
reranked.append(enriched)
|
| 160 |
|
| 161 |
reranked.sort(key=lambda item: item["rerank_score"], reverse=True)
|
| 162 |
+
return reranked[:top_k] if top_k is not None else reranked
|
| 163 |
|
| 164 |
@staticmethod
|
| 165 |
def normalize_semantic_results(results: List[dict]) -> List[dict]:
|
src/rag_system.py
CHANGED
|
@@ -278,6 +278,9 @@ class CodebaseRAGSystem:
|
|
| 278 |
repo.session_expires_at = self._session_expiry()
|
| 279 |
self._mark_repo_updated(repo)
|
| 280 |
self.repo_chunks[repo.id] = serialized
|
|
|
|
|
|
|
|
|
|
| 281 |
self.vector_store.save()
|
| 282 |
with self.repo_lock:
|
| 283 |
self.indexing_progress.pop(repo.id, None)
|
|
@@ -343,6 +346,7 @@ class CodebaseRAGSystem:
|
|
| 343 |
question: str,
|
| 344 |
top_k: int = 8,
|
| 345 |
history=None,
|
|
|
|
| 346 |
) -> dict:
|
| 347 |
with self.repo_lock:
|
| 348 |
self._cleanup_expired_sessions()
|
|
@@ -390,9 +394,17 @@ class CodebaseRAGSystem:
|
|
| 390 |
repo_chunks,
|
| 391 |
retrieval_query,
|
| 392 |
top_k=search_depth,
|
|
|
|
| 393 |
)
|
|
|
|
| 394 |
semantic_hits = self.hybrid_search.normalize_semantic_results(semantic_hits)
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
path_hits = self._path_intent_search(
|
| 398 |
repo_chunks,
|
|
@@ -400,26 +412,67 @@ class CodebaseRAGSystem:
|
|
| 400 |
retrieval_query,
|
| 401 |
top_k=search_depth,
|
| 402 |
)
|
| 403 |
-
|
|
|
|
|
|
|
| 404 |
|
| 405 |
rerank_query = retrieval_query if question_intent in deep_search_intents else question
|
| 406 |
|
| 407 |
-
#
|
| 408 |
-
#
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
reranked = self.hybrid_search.rerank(rerank_query, fused, top_k=rerank_pool)
|
| 413 |
|
| 414 |
-
|
|
|
|
|
|
|
| 415 |
|
| 416 |
-
# FIX: cap final sources at 5 instead of top_k (8).
|
| 417 |
-
# 5 sources × 1500 chars = ~7500 chars context, which the LLM handles well.
|
| 418 |
-
# 8 sources × 2500 chars = ~20000 chars, which causes lost-in-the-middle issues.
|
| 419 |
final_top_k = min(top_k, 5)
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
-
answer = self._generate_answer(repo, question,
|
|
|
|
|
|
|
| 423 |
return answer
|
| 424 |
|
| 425 |
def end_session(self, session_key: str):
|
|
@@ -928,6 +981,29 @@ Do not leave the answer unfinished.
|
|
| 928 |
parts.append(f"Previous answer: {recent_assistant[0][:300]}")
|
| 929 |
return "\n".join(parts)
|
| 930 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
def _merge_ranked_candidates(
|
| 932 |
self,
|
| 933 |
ranked_results: List[dict],
|
|
@@ -1036,6 +1112,9 @@ Do not leave the answer unfinished.
|
|
| 1036 |
results: List[dict],
|
| 1037 |
top_k: int,
|
| 1038 |
) -> List[dict]:
|
|
|
|
|
|
|
|
|
|
| 1039 |
combined_query = f"{question} {retrieval_query}".lower()
|
| 1040 |
wants_code = any(
|
| 1041 |
token in combined_query
|
|
@@ -1046,41 +1125,47 @@ Do not leave the answer unfinished.
|
|
| 1046 |
"docs",
|
| 1047 |
"overview",
|
| 1048 |
}
|
| 1049 |
-
wants_repo_overview = self._is_repo_overview_question(
|
| 1050 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
)
|
| 1052 |
|
| 1053 |
-
|
|
|
|
|
|
|
| 1054 |
is_doc = self._is_doc_source(item)
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1066 |
)
|
|
|
|
| 1067 |
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
return ranked[:top_k]
|
| 1071 |
-
|
| 1072 |
-
selected = []
|
| 1073 |
-
doc_items = []
|
| 1074 |
-
for item in ranked:
|
| 1075 |
-
if self._is_doc_source(item):
|
| 1076 |
-
doc_items.append(item)
|
| 1077 |
-
continue
|
| 1078 |
-
selected.append(item)
|
| 1079 |
-
if len(selected) == top_k:
|
| 1080 |
-
return selected
|
| 1081 |
-
|
| 1082 |
-
selected.extend(doc_items[: max(1, top_k - len(selected))])
|
| 1083 |
-
return selected[:top_k]
|
| 1084 |
|
| 1085 |
def _select_answer_sources(
|
| 1086 |
self,
|
|
@@ -1094,14 +1179,33 @@ Do not leave the answer unfinished.
|
|
| 1094 |
intent = self._question_intent(question)
|
| 1095 |
max_per_file = 2 if intent in {"overview", "docs"} else 1
|
| 1096 |
selected = []
|
|
|
|
| 1097 |
file_counts = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
|
| 1099 |
for item in results:
|
|
|
|
|
|
|
| 1100 |
file_path = item.get("file_path", "")
|
| 1101 |
count = file_counts.get(file_path, 0)
|
| 1102 |
if count >= max_per_file:
|
| 1103 |
continue
|
| 1104 |
selected.append(item)
|
|
|
|
| 1105 |
file_counts[file_path] = count + 1
|
| 1106 |
if len(selected) == top_k:
|
| 1107 |
break
|
|
@@ -1260,27 +1364,44 @@ Do not leave the answer unfinished.
|
|
| 1260 |
@staticmethod
|
| 1261 |
def _is_repo_overview_question(question: str) -> bool:
|
| 1262 |
normalized = " ".join((question or "").lower().split())
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1283 |
)
|
|
|
|
| 1284 |
|
| 1285 |
@staticmethod
|
| 1286 |
def _is_doc_source(item: dict) -> bool:
|
|
@@ -1301,189 +1422,104 @@ Do not leave the answer unfinished.
|
|
| 1301 |
|
| 1302 |
@staticmethod
|
| 1303 |
def _domain_path_hints(query: str) -> List[str]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
normalized = " ".join((query or "").lower().split())
|
| 1305 |
hints = []
|
| 1306 |
|
| 1307 |
def has_any(terms: set[str]) -> bool:
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
continue
|
| 1313 |
-
matched = matched or bool(
|
| 1314 |
-
re.search(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", normalized)
|
| 1315 |
-
)
|
| 1316 |
-
return matched
|
| 1317 |
|
| 1318 |
rules = [
|
| 1319 |
(
|
| 1320 |
-
{"
|
| 1321 |
-
[
|
| 1322 |
-
"packages/email",
|
| 1323 |
-
"packages/lib/server-only/email",
|
| 1324 |
-
"packages/lib/jobs/definitions/emails",
|
| 1325 |
-
],
|
| 1326 |
),
|
| 1327 |
(
|
| 1328 |
-
{"
|
| 1329 |
-
[
|
| 1330 |
-
"packages/signing",
|
| 1331 |
-
"packages/signing/transports",
|
| 1332 |
-
"packages/signing/helpers",
|
| 1333 |
-
"packages/lib/jobs/definitions/internal/seal-document",
|
| 1334 |
-
],
|
| 1335 |
),
|
| 1336 |
(
|
| 1337 |
-
{"
|
| 1338 |
-
[
|
| 1339 |
-
"packages/lib/jobs",
|
| 1340 |
-
"packages/lib/jobs/client",
|
| 1341 |
-
"packages/lib/jobs/definitions",
|
| 1342 |
-
"apps/remix/server/router.ts",
|
| 1343 |
-
],
|
| 1344 |
),
|
| 1345 |
(
|
| 1346 |
-
{"
|
| 1347 |
-
[
|
| 1348 |
-
"packages/lib/server-only/webhooks",
|
| 1349 |
-
"packages/lib/jobs/definitions/internal/execute-webhook",
|
| 1350 |
-
],
|
| 1351 |
),
|
| 1352 |
(
|
| 1353 |
-
{"
|
| 1354 |
[
|
| 1355 |
-
"
|
| 1356 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1357 |
],
|
| 1358 |
),
|
| 1359 |
(
|
| 1360 |
-
{"
|
| 1361 |
-
[
|
| 1362 |
-
"packages/lib/server-only/field",
|
| 1363 |
-
"packages/trpc/server/field-router",
|
| 1364 |
-
"packages/lib/universal/field-renderer",
|
| 1365 |
-
],
|
| 1366 |
),
|
| 1367 |
(
|
| 1368 |
-
{"
|
| 1369 |
-
[
|
| 1370 |
-
"packages/lib/server-only/template",
|
| 1371 |
-
"packages/trpc/server/template-router",
|
| 1372 |
-
],
|
| 1373 |
),
|
| 1374 |
(
|
| 1375 |
-
{"
|
| 1376 |
-
[
|
| 1377 |
-
"packages/lib/server-only/envelope",
|
| 1378 |
-
"packages/lib/server-only/envelope-item",
|
| 1379 |
-
"packages/trpc/server/envelope-router",
|
| 1380 |
-
],
|
| 1381 |
),
|
| 1382 |
(
|
| 1383 |
-
{"
|
| 1384 |
-
[
|
| 1385 |
-
"packages/lib/server-only/document",
|
| 1386 |
-
"packages/lib/server-only/document-data",
|
| 1387 |
-
"packages/trpc/server/document-router",
|
| 1388 |
-
],
|
| 1389 |
),
|
| 1390 |
(
|
| 1391 |
-
{"
|
| 1392 |
-
[
|
| 1393 |
-
"packages/lib/server-only/pdf",
|
| 1394 |
-
"packages/lib/server-only/document-data",
|
| 1395 |
-
"packages/lib/universal/upload",
|
| 1396 |
-
"apps/remix/server/api/files",
|
| 1397 |
-
],
|
| 1398 |
),
|
| 1399 |
(
|
| 1400 |
-
{"
|
| 1401 |
-
[
|
| 1402 |
-
"packages/api",
|
| 1403 |
-
"packages/api/v1",
|
| 1404 |
-
"packages/api/hono.ts",
|
| 1405 |
-
],
|
| 1406 |
),
|
| 1407 |
(
|
| 1408 |
-
{"
|
| 1409 |
-
[
|
| 1410 |
-
"packages/trpc/server",
|
| 1411 |
-
"packages/trpc/server/open-api.ts",
|
| 1412 |
-
"apps/remix/server/router.ts",
|
| 1413 |
-
"apps/remix/server/trpc",
|
| 1414 |
-
],
|
| 1415 |
),
|
| 1416 |
(
|
| 1417 |
-
{"
|
| 1418 |
-
[
|
| 1419 |
-
"packages/trpc",
|
| 1420 |
-
"packages/trpc/react",
|
| 1421 |
-
"packages/trpc/client",
|
| 1422 |
-
"packages/trpc/server/context.ts",
|
| 1423 |
-
"apps/remix/server/trpc",
|
| 1424 |
-
"apps/remix/server/router.ts",
|
| 1425 |
-
],
|
| 1426 |
-
),
|
| 1427 |
-
(
|
| 1428 |
-
{"auth", "authentication", "session", "api token", "authorization", "bearer"},
|
| 1429 |
-
[
|
| 1430 |
-
"packages/auth",
|
| 1431 |
-
"packages/lib/server-only/auth",
|
| 1432 |
-
"packages/lib/server-only/public-api",
|
| 1433 |
-
"packages/trpc/server/context.ts",
|
| 1434 |
-
"packages/trpc/server/trpc.ts",
|
| 1435 |
-
"packages/api/v1/middleware/authenticated.ts",
|
| 1436 |
-
"apps/remix/server/context.ts",
|
| 1437 |
-
],
|
| 1438 |
),
|
| 1439 |
(
|
| 1440 |
-
{"
|
| 1441 |
-
[
|
| 1442 |
-
"packages/prisma",
|
| 1443 |
-
"packages/prisma/schema.prisma",
|
| 1444 |
-
"packages/prisma/migrations",
|
| 1445 |
-
".env.example",
|
| 1446 |
-
],
|
| 1447 |
),
|
| 1448 |
(
|
| 1449 |
-
{"
|
| 1450 |
-
[
|
| 1451 |
-
"apps/remix/server",
|
| 1452 |
-
"apps/remix/app/routes",
|
| 1453 |
-
"apps/remix/app/root.tsx",
|
| 1454 |
-
"apps/remix/app/routes.ts",
|
| 1455 |
-
],
|
| 1456 |
),
|
| 1457 |
(
|
| 1458 |
-
{"
|
| 1459 |
-
[
|
| 1460 |
-
"packages/app-tests",
|
| 1461 |
-
"packages/lib/vitest.config.ts",
|
| 1462 |
-
"packages/lib/package.json",
|
| 1463 |
-
],
|
| 1464 |
),
|
| 1465 |
(
|
| 1466 |
-
{
|
| 1467 |
-
|
| 1468 |
-
"configuration",
|
| 1469 |
-
"env",
|
| 1470 |
-
"environment",
|
| 1471 |
-
"local development",
|
| 1472 |
-
"self-host",
|
| 1473 |
-
"self hosting",
|
| 1474 |
-
"workspace",
|
| 1475 |
-
"workspaces",
|
| 1476 |
-
"turborepo",
|
| 1477 |
-
"turbo",
|
| 1478 |
-
},
|
| 1479 |
-
[
|
| 1480 |
-
".env.example",
|
| 1481 |
-
"README.md",
|
| 1482 |
-
"package.json",
|
| 1483 |
-
"turbo.json",
|
| 1484 |
-
"apps/docs/content/docs/developers/local-development",
|
| 1485 |
-
"apps/docs/content/docs/self-hosting/configuration",
|
| 1486 |
-
],
|
| 1487 |
),
|
| 1488 |
]
|
| 1489 |
|
|
|
|
| 278 |
repo.session_expires_at = self._session_expiry()
|
| 279 |
self._mark_repo_updated(repo)
|
| 280 |
self.repo_chunks[repo.id] = serialized
|
| 281 |
+
# Build the lexical (BM25) index once now, instead of
|
| 282 |
+
# re-tokenizing every chunk in the repo on every question.
|
| 283 |
+
self.hybrid_search.build_for_repository(repo.id, serialized)
|
| 284 |
self.vector_store.save()
|
| 285 |
with self.repo_lock:
|
| 286 |
self.indexing_progress.pop(repo.id, None)
|
|
|
|
| 346 |
question: str,
|
| 347 |
top_k: int = 8,
|
| 348 |
history=None,
|
| 349 |
+
debug_retrieval: bool = False,
|
| 350 |
) -> dict:
|
| 351 |
with self.repo_lock:
|
| 352 |
self._cleanup_expired_sessions()
|
|
|
|
| 394 |
repo_chunks,
|
| 395 |
retrieval_query,
|
| 396 |
top_k=search_depth,
|
| 397 |
+
repo_id=repo_id,
|
| 398 |
)
|
| 399 |
+
|
| 400 |
semantic_hits = self.hybrid_search.normalize_semantic_results(semantic_hits)
|
| 401 |
+
semantic_ranks = self._rank_map(semantic_hits)
|
| 402 |
+
lexical_ranks = self._rank_map(lexical_hits)
|
| 403 |
+
|
| 404 |
+
fused = self.hybrid_search.reciprocal_rank_fusion(
|
| 405 |
+
lexical_hits, semantic_hits, top_k=search_depth
|
| 406 |
+
)
|
| 407 |
+
fused_ranks = self._rank_map(fused)
|
| 408 |
|
| 409 |
path_hits = self._path_intent_search(
|
| 410 |
repo_chunks,
|
|
|
|
| 412 |
retrieval_query,
|
| 413 |
top_k=search_depth,
|
| 414 |
)
|
| 415 |
+
path_ranks = self._rank_map(path_hits)
|
| 416 |
+
|
| 417 |
+
merged = self._merge_ranked_candidates(fused, path_hits, top_k=search_depth)
|
| 418 |
|
| 419 |
rerank_query = retrieval_query if question_intent in deep_search_intents else question
|
| 420 |
|
| 421 |
+
# Rerank broadly for recall. This is independent of the final LLM
|
| 422 |
+
# context size, which remains capped below.
|
| 423 |
+
rerank_pool = min(search_depth, 50)
|
| 424 |
+
reranked = self.hybrid_search.rerank(rerank_query, merged, top_k=rerank_pool)
|
| 425 |
+
rerank_ranks = self._rank_map(reranked)
|
|
|
|
| 426 |
|
| 427 |
+
prioritized = self._prioritize_results(
|
| 428 |
+
question, retrieval_query, reranked, top_k=top_k
|
| 429 |
+
)
|
| 430 |
|
|
|
|
|
|
|
|
|
|
| 431 |
final_top_k = min(top_k, 5)
|
| 432 |
+
final_sources = self._select_answer_sources(
|
| 433 |
+
question, prioritized, top_k=final_top_k
|
| 434 |
+
)
|
| 435 |
+
final_ranks = self._rank_map(final_sources)
|
| 436 |
+
|
| 437 |
+
retrieval_debug = []
|
| 438 |
+
if debug_retrieval:
|
| 439 |
+
all_candidates = {}
|
| 440 |
+
for stage_items in (semantic_hits, lexical_hits, fused, path_hits, merged, reranked, prioritized, final_sources):
|
| 441 |
+
for item in stage_items:
|
| 442 |
+
all_candidates[item["id"]] = {**all_candidates.get(item["id"], {}), **item}
|
| 443 |
+
|
| 444 |
+
for chunk_id, item in all_candidates.items():
|
| 445 |
+
retrieval_debug.append(
|
| 446 |
+
{
|
| 447 |
+
"id": chunk_id,
|
| 448 |
+
"file_path": item.get("file_path"),
|
| 449 |
+
"symbol_name": item.get("symbol_name"),
|
| 450 |
+
"semantic_rank": semantic_ranks.get(chunk_id),
|
| 451 |
+
"bm25_rank": lexical_ranks.get(chunk_id),
|
| 452 |
+
"fused_rank": fused_ranks.get(chunk_id),
|
| 453 |
+
"path_rank": path_ranks.get(chunk_id),
|
| 454 |
+
"rerank_rank": rerank_ranks.get(chunk_id),
|
| 455 |
+
"final_rank": final_ranks.get(chunk_id),
|
| 456 |
+
"semantic_score": item.get("semantic_score"),
|
| 457 |
+
"bm25_score": item.get("bm25_score"),
|
| 458 |
+
"rrf_score": item.get("rrf_score"),
|
| 459 |
+
"path_score": item.get("path_score"),
|
| 460 |
+
"rerank_score": item.get("rerank_score"),
|
| 461 |
+
"final_score": item.get("final_score"),
|
| 462 |
+
}
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
retrieval_debug.sort(
|
| 466 |
+
key=lambda item: (
|
| 467 |
+
item["final_rank"] is None,
|
| 468 |
+
item["final_rank"] or 10**9,
|
| 469 |
+
item["rerank_rank"] or 10**9,
|
| 470 |
+
)
|
| 471 |
+
)
|
| 472 |
|
| 473 |
+
answer = self._generate_answer(repo, question, final_sources, normalized_history)
|
| 474 |
+
if debug_retrieval:
|
| 475 |
+
answer["retrieval_debug"] = retrieval_debug
|
| 476 |
return answer
|
| 477 |
|
| 478 |
def end_session(self, session_key: str):
|
|
|
|
| 981 |
parts.append(f"Previous answer: {recent_assistant[0][:300]}")
|
| 982 |
return "\n".join(parts)
|
| 983 |
|
| 984 |
+
@staticmethod
|
| 985 |
+
def _rank_map(items: List[dict]) -> Dict[str, int]:
|
| 986 |
+
return {item["id"]: rank for rank, item in enumerate(items, start=1)}
|
| 987 |
+
|
| 988 |
+
@staticmethod
|
| 989 |
+
def _minmax(values: List[float]) -> List[float]:
|
| 990 |
+
if not values:
|
| 991 |
+
return []
|
| 992 |
+
low = min(values)
|
| 993 |
+
high = max(values)
|
| 994 |
+
if high == low:
|
| 995 |
+
return [0.0 for _ in values]
|
| 996 |
+
return [(value - low) / (high - low) for value in values]
|
| 997 |
+
|
| 998 |
+
@staticmethod
|
| 999 |
+
def _source_family(file_path: str) -> str:
|
| 1000 |
+
parts = (file_path or "").strip("/").split("/")
|
| 1001 |
+
if not parts or not parts[0]:
|
| 1002 |
+
return ""
|
| 1003 |
+
if parts[0] in {"packages", "apps"} and len(parts) >= 2:
|
| 1004 |
+
return "/".join(parts[:2])
|
| 1005 |
+
return parts[0]
|
| 1006 |
+
|
| 1007 |
def _merge_ranked_candidates(
|
| 1008 |
self,
|
| 1009 |
ranked_results: List[dict],
|
|
|
|
| 1112 |
results: List[dict],
|
| 1113 |
top_k: int,
|
| 1114 |
) -> List[dict]:
|
| 1115 |
+
if not results:
|
| 1116 |
+
return []
|
| 1117 |
+
|
| 1118 |
combined_query = f"{question} {retrieval_query}".lower()
|
| 1119 |
wants_code = any(
|
| 1120 |
token in combined_query
|
|
|
|
| 1125 |
"docs",
|
| 1126 |
"overview",
|
| 1127 |
}
|
| 1128 |
+
wants_repo_overview = self._is_repo_overview_question(
|
| 1129 |
+
question
|
| 1130 |
+
) or self._is_repo_overview_question(retrieval_query)
|
| 1131 |
+
|
| 1132 |
+
rerank_norm = self._minmax([float(item.get("rerank_score", 0.0)) for item in results])
|
| 1133 |
+
rrf_norm = self._minmax([float(item.get("rrf_score", 0.0)) for item in results])
|
| 1134 |
+
path_norm = self._minmax([float(item.get("path_score", 0.0)) for item in results])
|
| 1135 |
+
canonical_norm = self._minmax(
|
| 1136 |
+
[float(self._canonical_path_priority(item, combined_query)) for item in results]
|
| 1137 |
)
|
| 1138 |
|
| 1139 |
+
scored = []
|
| 1140 |
+
for index, item in enumerate(results):
|
| 1141 |
+
enriched = dict(item)
|
| 1142 |
is_doc = self._is_doc_source(item)
|
| 1143 |
+
intent_bonus = 0.0
|
| 1144 |
+
|
| 1145 |
+
if wants_repo_overview and is_doc:
|
| 1146 |
+
intent_bonus += 1.0
|
| 1147 |
+
if wants_docs and is_doc:
|
| 1148 |
+
intent_bonus += 0.8
|
| 1149 |
+
if wants_code and not is_doc:
|
| 1150 |
+
intent_bonus += 0.5
|
| 1151 |
+
if (
|
| 1152 |
+
question_intent
|
| 1153 |
+
in {"api", "implementation", "cross_file", "error_handling", "setup"}
|
| 1154 |
+
and not is_doc
|
| 1155 |
+
):
|
| 1156 |
+
intent_bonus += 0.4
|
| 1157 |
+
|
| 1158 |
+
enriched["final_score"] = (
|
| 1159 |
+
0.50 * rerank_norm[index]
|
| 1160 |
+
+ 0.20 * rrf_norm[index]
|
| 1161 |
+
+ 0.12 * path_norm[index]
|
| 1162 |
+
+ 0.10 * canonical_norm[index]
|
| 1163 |
+
+ 0.08 * min(intent_bonus, 1.0)
|
| 1164 |
)
|
| 1165 |
+
scored.append(enriched)
|
| 1166 |
|
| 1167 |
+
scored.sort(key=lambda item: item["final_score"], reverse=True)
|
| 1168 |
+
return scored[:top_k]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
|
| 1170 |
def _select_answer_sources(
|
| 1171 |
self,
|
|
|
|
| 1179 |
intent = self._question_intent(question)
|
| 1180 |
max_per_file = 2 if intent in {"overview", "docs"} else 1
|
| 1181 |
selected = []
|
| 1182 |
+
selected_ids = set()
|
| 1183 |
file_counts = {}
|
| 1184 |
+
used_families = set()
|
| 1185 |
+
|
| 1186 |
+
if intent == "cross_file":
|
| 1187 |
+
for item in results:
|
| 1188 |
+
file_path = item.get("file_path", "")
|
| 1189 |
+
family = self._source_family(file_path)
|
| 1190 |
+
if family and family in used_families:
|
| 1191 |
+
continue
|
| 1192 |
+
selected.append(item)
|
| 1193 |
+
selected_ids.add(item["id"])
|
| 1194 |
+
if family:
|
| 1195 |
+
used_families.add(family)
|
| 1196 |
+
file_counts[file_path] = 1
|
| 1197 |
+
if len(selected) == top_k:
|
| 1198 |
+
return selected
|
| 1199 |
|
| 1200 |
for item in results:
|
| 1201 |
+
if item["id"] in selected_ids:
|
| 1202 |
+
continue
|
| 1203 |
file_path = item.get("file_path", "")
|
| 1204 |
count = file_counts.get(file_path, 0)
|
| 1205 |
if count >= max_per_file:
|
| 1206 |
continue
|
| 1207 |
selected.append(item)
|
| 1208 |
+
selected_ids.add(item["id"])
|
| 1209 |
file_counts[file_path] = count + 1
|
| 1210 |
if len(selected) == top_k:
|
| 1211 |
break
|
|
|
|
| 1364 |
@staticmethod
|
| 1365 |
def _is_repo_overview_question(question: str) -> bool:
|
| 1366 |
normalized = " ".join((question or "").lower().split())
|
| 1367 |
+
explicit_phrases = {
|
| 1368 |
+
"what is the repo about",
|
| 1369 |
+
"what is this repo about",
|
| 1370 |
+
"what does the repo do",
|
| 1371 |
+
"what does this repo do",
|
| 1372 |
+
"what is the repository about",
|
| 1373 |
+
"what does the repository do",
|
| 1374 |
+
"what is this project about",
|
| 1375 |
+
"what does this project do",
|
| 1376 |
+
"repo summary",
|
| 1377 |
+
"repository summary",
|
| 1378 |
+
"project summary",
|
| 1379 |
+
"summarize the repo",
|
| 1380 |
+
"summarize this repo",
|
| 1381 |
+
"repo overview",
|
| 1382 |
+
"repository overview",
|
| 1383 |
+
"project overview",
|
| 1384 |
+
}
|
| 1385 |
+
if any(phrase in normalized for phrase in explicit_phrases):
|
| 1386 |
+
return True
|
| 1387 |
+
|
| 1388 |
+
code_markers = {
|
| 1389 |
+
"function", "class", "method", "endpoint", "api", "router",
|
| 1390 |
+
"route", "implementation", "implemented", "file", "package",
|
| 1391 |
+
"module", "where", "tests",
|
| 1392 |
+
}
|
| 1393 |
+
if any(re.search(rf"(?<![a-z0-9]){re.escape(marker)}(?![a-z0-9])", normalized) for marker in code_markers):
|
| 1394 |
+
return False
|
| 1395 |
+
|
| 1396 |
+
purpose_patterns = (
|
| 1397 |
+
r"^what is [\w.-]+(?:\s+and\s+what\s+.+)?\??$",
|
| 1398 |
+
r"^what does [\w.-]+ do\??$",
|
| 1399 |
+
r"^what problem does [\w.-]+ solve\??$",
|
| 1400 |
+
r"^what product problem .+ solve\??$",
|
| 1401 |
+
r"^(?:describe|explain) [\w.-]+\??$",
|
| 1402 |
+
r"^(?:what is|explain|describe) the (?:purpose|project|repository)\b",
|
| 1403 |
)
|
| 1404 |
+
return any(re.search(pattern, normalized) for pattern in purpose_patterns)
|
| 1405 |
|
| 1406 |
@staticmethod
|
| 1407 |
def _is_doc_source(item: dict) -> bool:
|
|
|
|
| 1422 |
|
| 1423 |
@staticmethod
|
| 1424 |
def _domain_path_hints(query: str) -> List[str]:
|
| 1425 |
+
"""Map generic question concepts to the directory/file naming
|
| 1426 |
+
conventions real-world repos tend to use for that concept.
|
| 1427 |
+
|
| 1428 |
+
This intentionally stays convention-level (not tied to any single
|
| 1429 |
+
project's folder layout) because this system indexes arbitrary
|
| 1430 |
+
GitHub repositories: a hint list hardcoded to one repo's paths would
|
| 1431 |
+
never match anything in any other repo, silently doing nothing for
|
| 1432 |
+
almost every user while looking like it's helping. These patterns
|
| 1433 |
+
are matched as path *substrings* by the callers, so they work across
|
| 1434 |
+
Python/JS/TS/Go/Java/Rust project layouts without needing to know
|
| 1435 |
+
the specific repo's structure in advance.
|
| 1436 |
+
"""
|
| 1437 |
normalized = " ".join((query or "").lower().split())
|
| 1438 |
hints = []
|
| 1439 |
|
| 1440 |
def has_any(terms: set[str]) -> bool:
|
| 1441 |
+
return any(
|
| 1442 |
+
bool(re.search(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", normalized))
|
| 1443 |
+
for term in terms
|
| 1444 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1445 |
|
| 1446 |
rules = [
|
| 1447 |
(
|
| 1448 |
+
{"auth", "authentication", "authorization", "login", "session", "bearer", "oauth", "jwt", "token"},
|
| 1449 |
+
["auth", "authentication", "authorization", "login", "session", "middleware/auth"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1450 |
),
|
| 1451 |
(
|
| 1452 |
+
{"api", "endpoint", "route", "router", "controller", "handler", "rest", "graphql", "trpc"},
|
| 1453 |
+
["api", "routes", "routers", "controllers", "handlers", "endpoints", "resolvers"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
),
|
| 1455 |
(
|
| 1456 |
+
{"database", "db", "sql", "orm", "migration", "migrations", "schema", "model", "models"},
|
| 1457 |
+
["models", "schema", "migrations", "db", "database", "entities", "repositories", "prisma"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1458 |
),
|
| 1459 |
(
|
| 1460 |
+
{"test", "tests", "testing", "pytest", "spec", "e2e", "unit test", "integration test"},
|
| 1461 |
+
["test", "tests", "__tests__", "spec", "e2e", "testing"],
|
|
|
|
|
|
|
|
|
|
| 1462 |
),
|
| 1463 |
(
|
| 1464 |
+
{"config", "configuration", "env", "environment", "settings", "setup", "install", "installation"},
|
| 1465 |
[
|
| 1466 |
+
"config",
|
| 1467 |
+
"settings",
|
| 1468 |
+
".env.example",
|
| 1469 |
+
"readme.md",
|
| 1470 |
+
"package.json",
|
| 1471 |
+
"pyproject.toml",
|
| 1472 |
+
"docker-compose",
|
| 1473 |
+
"dockerfile",
|
| 1474 |
],
|
| 1475 |
),
|
| 1476 |
(
|
| 1477 |
+
{"job", "jobs", "background", "worker", "workers", "queue", "task", "tasks", "cron", "scheduler"},
|
| 1478 |
+
["jobs", "workers", "queue", "tasks", "scheduler"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1479 |
),
|
| 1480 |
(
|
| 1481 |
+
{"webhook", "webhooks", "callback", "callbacks"},
|
| 1482 |
+
["webhook", "webhooks", "callbacks"],
|
|
|
|
|
|
|
|
|
|
| 1483 |
),
|
| 1484 |
(
|
| 1485 |
+
{"email", "emails", "mailer", "notification", "notifications"},
|
| 1486 |
+
["email", "mailer", "notifications", "templates"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1487 |
),
|
| 1488 |
(
|
| 1489 |
+
{"upload", "storage", "s3", "file", "files", "attachment", "blob"},
|
| 1490 |
+
["storage", "upload", "uploads", "files", "attachments"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
),
|
| 1492 |
(
|
| 1493 |
+
{"cli", "command line", "command-line"},
|
| 1494 |
+
["cli", "commands", "bin"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1495 |
),
|
| 1496 |
(
|
| 1497 |
+
{"ui", "frontend", "component", "components", "page", "pages", "view", "views"},
|
| 1498 |
+
["components", "pages", "views", "ui", "frontend", "client", "app"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1499 |
),
|
| 1500 |
(
|
| 1501 |
+
{"backend", "server", "service", "services"},
|
| 1502 |
+
["server", "backend", "services", "api"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1503 |
),
|
| 1504 |
(
|
| 1505 |
+
{"middleware", "interceptor"},
|
| 1506 |
+
["middleware", "interceptors"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
),
|
| 1508 |
(
|
| 1509 |
+
{"docker", "container", "deployment", "deploy", "kubernetes", "helm", "ci", "cd", "pipeline"},
|
| 1510 |
+
[".github/workflows", "docker", "dockerfile", "docker-compose", "helm", "deploy", "deployment", "ci"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1511 |
),
|
| 1512 |
(
|
| 1513 |
+
{"docs", "documentation", "readme"},
|
| 1514 |
+
["readme.md", "docs", "documentation"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1515 |
),
|
| 1516 |
(
|
| 1517 |
+
{"util", "utils", "utility", "helper", "helpers", "common", "shared"},
|
| 1518 |
+
["utils", "util", "helpers", "common", "shared", "lib"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1519 |
),
|
| 1520 |
(
|
| 1521 |
+
{"type", "types", "interface", "schema"},
|
| 1522 |
+
["types", "type", "interfaces", "schemas"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1523 |
),
|
| 1524 |
]
|
| 1525 |
|
src/repo_fetcher.py
CHANGED
|
@@ -20,6 +20,16 @@ SUPPORTED_EXTENSIONS = {
|
|
| 20 |
".java",
|
| 21 |
".go",
|
| 22 |
".rs",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
".md",
|
| 24 |
".mdx",
|
| 25 |
".json",
|
|
@@ -32,11 +42,20 @@ SUPPORTED_EXTENSIONS = {
|
|
| 32 |
".prisma",
|
| 33 |
}
|
| 34 |
|
|
|
|
|
|
|
| 35 |
SUPPORTED_FILENAMES = {
|
| 36 |
".env.example",
|
| 37 |
"Dockerfile",
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
IGNORED_FILENAMES = {
|
| 41 |
"package-lock.json",
|
| 42 |
"yarn.lock",
|
|
@@ -68,7 +87,7 @@ IGNORED_DIRS = {
|
|
| 68 |
"__pycache__",
|
| 69 |
}
|
| 70 |
|
| 71 |
-
MAX_FILE_SIZE_BYTES =
|
| 72 |
|
| 73 |
|
| 74 |
class RepoFetcher:
|
|
@@ -181,9 +200,10 @@ class RepoFetcher:
|
|
| 181 |
continue
|
| 182 |
if (
|
| 183 |
file_path.suffix.lower() not in SUPPORTED_EXTENSIONS
|
|
|
|
| 184 |
and file_path.name not in SUPPORTED_FILENAMES
|
| 185 |
):
|
| 186 |
continue
|
| 187 |
if file_path.stat().st_size > MAX_FILE_SIZE_BYTES:
|
| 188 |
continue
|
| 189 |
-
yield file_path
|
|
|
|
| 20 |
".java",
|
| 21 |
".go",
|
| 22 |
".rs",
|
| 23 |
+
".c",
|
| 24 |
+
".h",
|
| 25 |
+
".cc",
|
| 26 |
+
".cpp",
|
| 27 |
+
".cxx",
|
| 28 |
+
".hpp",
|
| 29 |
+
".hh",
|
| 30 |
+
".y",
|
| 31 |
+
".test",
|
| 32 |
+
".tcl",
|
| 33 |
".md",
|
| 34 |
".mdx",
|
| 35 |
".json",
|
|
|
|
| 42 |
".prisma",
|
| 43 |
}
|
| 44 |
|
| 45 |
+
# Extensionless / templated files that matter for a repo even though their
|
| 46 |
+
# suffix (".in", no suffix, etc.) isn't a language extension on its own.
|
| 47 |
SUPPORTED_FILENAMES = {
|
| 48 |
".env.example",
|
| 49 |
"Dockerfile",
|
| 50 |
}
|
| 51 |
|
| 52 |
+
# Suffixes matched in addition to SUPPORTED_EXTENSIONS, for files like
|
| 53 |
+
# "sqlite.h.in" or "Makefile.in" where the *last* suffix (".in") is a
|
| 54 |
+
# build-template marker rather than the real language.
|
| 55 |
+
SUPPORTED_TEMPLATE_SUFFIXES = {
|
| 56 |
+
".in",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
IGNORED_FILENAMES = {
|
| 60 |
"package-lock.json",
|
| 61 |
"yarn.lock",
|
|
|
|
| 87 |
"__pycache__",
|
| 88 |
}
|
| 89 |
|
| 90 |
+
MAX_FILE_SIZE_BYTES = 400_000
|
| 91 |
|
| 92 |
|
| 93 |
class RepoFetcher:
|
|
|
|
| 200 |
continue
|
| 201 |
if (
|
| 202 |
file_path.suffix.lower() not in SUPPORTED_EXTENSIONS
|
| 203 |
+
and file_path.suffix.lower() not in SUPPORTED_TEMPLATE_SUFFIXES
|
| 204 |
and file_path.name not in SUPPORTED_FILENAMES
|
| 205 |
):
|
| 206 |
continue
|
| 207 |
if file_path.stat().st_size > MAX_FILE_SIZE_BYTES:
|
| 208 |
continue
|
| 209 |
+
yield file_path
|