Skip to content

Commit 4583afc

Browse files
committed
Update ctx.py
1 parent 1239ef7 commit 4583afc

1 file changed

Lines changed: 60 additions & 29 deletions

File tree

scripts/ctx.py

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@
3333
import argparse
3434
import subprocess
3535
from urllib import request
36+
from urllib.parse import urlparse
3637
from urllib.error import HTTPError, URLError
3738
from typing import Dict, Any, List, Optional, Tuple
3839

3940
# Configuration from environment
4041
MCP_URL = os.environ.get("MCP_INDEXER_URL", "http://localhost:8003/mcp")
4142
DEFAULT_LIMIT = int(os.environ.get("CTX_LIMIT", "5"))
42-
DEFAULT_CONTEXT_LINES = int(os.environ.get("CTX_CONTEXT_LINES", "3"))
43-
DEFAULT_REWRITE_TOKENS = int(os.environ.get("CTX_REWRITE_MAX_TOKENS", "400"))
43+
DEFAULT_CONTEXT_LINES = int(os.environ.get("CTX_CONTEXT_LINES", "0"))
44+
DEFAULT_REWRITE_TOKENS = int(os.environ.get("CTX_REWRITE_MAX_TOKENS", "320"))
45+
DEFAULT_PER_PATH = int(os.environ.get("CTX_PER_PATH", "2"))
4446

4547
# Local decoder configuration (llama.cpp server)
4648
def resolve_decoder_url() -> str:
@@ -181,54 +183,70 @@ def parse_mcp_response(result: Dict[str, Any]) -> Optional[Dict[str, Any]]:
181183
return {"raw": text}
182184

183185

184-
def format_search_results(results: List[Dict[str, Any]]) -> str:
185-
"""Format search results with enough detail for the LLM rewrite."""
186+
def format_search_results(results: List[Dict[str, Any]], include_snippets: bool = False) -> str:
187+
"""Format search results succinctly for LLM rewrite.
188+
189+
When include_snippets is False (default), only include headers with path and line ranges.
190+
This keeps prompts small and fast for Granite via llama.cpp.
191+
"""
186192
lines: List[str] = []
187-
for idx, hit in enumerate(results, 1):
193+
for hit in results:
188194
path = hit.get("path", "unknown")
189195
start = hit.get("start_line", "?")
190196
end = hit.get("end_line", "?")
191-
score = hit.get("score", 0.0)
192197
language = hit.get("language") or ""
193198
symbol = hit.get("symbol") or ""
194199
snippet = (hit.get("snippet") or "").strip()
195200

196-
header = f"### Ref {idx}: {path}:{start}-{end} (score {score:.3f})"
201+
# Only include line ranges when both start and end are known
202+
if start in (None, "?") or end in (None, "?"):
203+
header = f"- {path}"
204+
else:
205+
header = f"- {path}:{start}-{end}"
206+
meta: List[str] = []
197207
if language:
198-
header += f" [{language}]"
208+
meta.append(language)
199209
if symbol:
200-
header += f" symbol=`{symbol}`"
210+
meta.append(f"{symbol}")
211+
if meta:
212+
header += f" ({', '.join(meta)})"
201213
lines.append(header)
202-
if snippet:
214+
215+
if include_snippets and snippet:
203216
lang_tag = language.lower() if language else ""
204217
lines.append(f"```{lang_tag}\n{snippet}\n```")
205-
lines.append("") # spacer
218+
206219
return "\n".join(lines).strip()
207220

208221

209222
def enhance_prompt(query: str, **filters) -> str:
210223
"""Retrieve context, invoke the LLM, and return a final enhanced prompt."""
211224
context_text, context_note = fetch_context(query, **filters)
212225
rewrite_opts = filters.get("rewrite_options") or {}
213-
include_context = bool(filters.get("include_context", False))
214226
rewritten = rewrite_prompt(
215227
query,
216228
context_text,
217229
context_note,
218230
max_tokens=rewrite_opts.get("max_tokens"),
219231
)
220-
return build_final_output(rewritten, context_text, context_note, include_context)
232+
# Always return only the enhanced prompt text by default (no context block)
233+
return rewritten.strip()
221234

222235

223236
def fetch_context(query: str, **filters) -> Tuple[str, str]:
224-
"""Fetch repository context text plus a note describing the status."""
237+
"""Fetch repository context text plus a note describing the status.
238+
239+
Defaults to header-only refs for speed unless with_snippets=True is provided.
240+
"""
241+
with_snippets = bool(filters.get("with_snippets", False))
225242
params = {
226243
"query": query,
227244
"limit": filters.get("limit", DEFAULT_LIMIT),
228-
"include_snippet": True,
245+
"include_snippet": with_snippets,
229246
"context_lines": filters.get("context_lines", DEFAULT_CONTEXT_LINES),
247+
"per_path": filters.get("per_path", DEFAULT_PER_PATH),
230248
}
231-
for key in ["language", "under", "path_glob", "not_glob", "kind", "symbol", "ext", "per_path"]:
249+
for key in ["language", "under", "path_glob", "not_glob", "kind", "symbol", "ext"]:
232250
if filters.get(key):
233251
params[key] = filters[key]
234252

@@ -244,7 +262,7 @@ def fetch_context(query: str, **filters) -> Tuple[str, str]:
244262
if not hits:
245263
return "", "No relevant context found for the prompt."
246264

247-
return format_search_results(hits), ""
265+
return format_search_results(hits, include_snippets=with_snippets), ""
248266

249267

250268
def rewrite_prompt(original_prompt: str, context: str, note: str, max_tokens: Optional[int]) -> str:
@@ -254,22 +272,35 @@ def rewrite_prompt(original_prompt: str, context: str, note: str, max_tokens: Op
254272
"""
255273
effective_context = context.strip() if context.strip() else (note or "No context available.")
256274

257-
meta_prompt = f"""Task: Rewrite the user's question to be more specific by adding references to relevant files, line numbers, and function names from the context.
258-
259-
DO NOT answer the question. Only rewrite it to be more precise.
260-
261-
Context from codebase:
262-
{effective_context}
263-
264-
User's original question: {original_prompt.strip()}
265-
266-
Rewritten question (do not answer, only rewrite):"""
275+
# Granite 4.0 chat template with explicit rewrite-only instruction
276+
system_msg = (
277+
"You are a prompt rewriter. "
278+
"Rewrite the user's question to be specific and actionable using only the provided context. "
279+
"Cite file paths, line ranges, and symbols only if they appear verbatim in the Context refs; never invent references. "
280+
"If line ranges are not shown for a file, cite only the file path. "
281+
"Prefer a multi-clause question that explicitly calls out what to analyze across the referenced components (e.g., algorithmic steps; inputs/outputs; configuration/parameters; performance; error handling; edge cases), when applicable. "
282+
"Do not answer the question. Return only the rewritten question as plain text with no markdown or code fences."
283+
)
284+
user_msg = (
285+
f"Context refs (headers only):\n{effective_context}\n\n"
286+
f"Original question: {original_prompt.strip()}\n\n"
287+
"Rewrite the question now. Ground it in the context above; include concrete file/symbol references only when present, avoid generic phrasing, and do not include markdown."
288+
)
289+
meta_prompt = (
290+
"<|start_of_role|>system<|end_of_role|>" + system_msg + "<|end_of_text|>\n"
291+
"<|start_of_role|>user<|end_of_role|>" + user_msg + "<|end_of_text|>\n"
292+
"<|start_of_role|>assistant<|end_of_role|>"
293+
)
267294

268295
decoder_url = DECODER_URL
296+
# Safety: only allow local decoder hosts
297+
parsed = urlparse(decoder_url)
298+
if parsed.hostname not in {"localhost", "127.0.0.1", "host.docker.internal"}:
299+
raise ValueError(f"Unsafe decoder host: {parsed.hostname}")
269300
payload = {
270301
"prompt": meta_prompt,
271302
"n_predict": int(max_tokens or DEFAULT_REWRITE_TOKENS),
272-
"temperature": 0.7,
303+
"temperature": 0.35,
273304
"stream": False,
274305
}
275306

@@ -290,7 +321,7 @@ def rewrite_prompt(original_prompt: str, context: str, note: str, max_tokens: Op
290321
or (data.get("generated_text") if isinstance(data, dict) else None)
291322
or (data.get("text") if isinstance(data, dict) else None)
292323
)
293-
enhanced = (enhanced or "").strip()
324+
enhanced = (enhanced or "").replace("```", "").replace("`", "").strip()
294325

295326
if not enhanced:
296327
raise ValueError(f"Decoder returned empty response (stop_type={data.get('stop_type')}, tokens={data.get('tokens_predicted')})")

0 commit comments

Comments
 (0)