3333import argparse
3434import subprocess
3535from urllib import request
36+ from urllib .parse import urlparse
3637from urllib .error import HTTPError , URLError
3738from typing import Dict , Any , List , Optional , Tuple
3839
3940# Configuration from environment
4041MCP_URL = os .environ .get ("MCP_INDEXER_URL" , "http://localhost:8003/mcp" )
4142DEFAULT_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)
4648def 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
209222def 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
223236def 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
250268def 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