Skip to content

Commit 4e2ce03

Browse files
committed
fixes
1 parent 45481ed commit 4e2ce03

File tree

2 files changed

+147
-14
lines changed

2 files changed

+147
-14
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@ make ctx Q="Explain the caching logic to me in detail"
203203
make ctx Q="Hybrid search details" ARGS="--language python --under scripts/ --limit 2 --rewrite-max-tokens 200"
204204
````
205205

206+
207+
### Detail mode (short snippets)
208+
209+
Include compact code snippets in the retrieved context for richer rewrites (trades a bit of speed for quality):
210+
211+
````bash
212+
# Enable detail mode (adds short snippets)
213+
scripts/ctx.py "Explain the caching logic" --detail
214+
215+
# Adjust snippet size if needed (default is 1 line when --detail is used)
216+
make ctx Q="Explain hybrid search" ARGS="--detail --context-lines 2"
217+
````
218+
219+
Notes:
220+
- Default behavior is header-only (fastest). `--detail` adds short snippets.
221+
- If `--detail` is set and `--context-lines` remains at its default (0), ctx.py automatically uses 1 line to keep snippets concise. Override with `--context-lines N`.
222+
206223
GPU Acceleration (Apple Silicon):
207224
For faster prompt rewriting, use the native Metal-accelerated decoder:
208225
````bash

scripts/ctx.py

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
#!/usr/bin/env python3
24
"""
35
Context-aware prompt enhancer CLI.
@@ -24,7 +26,7 @@
2426
Environment:
2527
MCP_INDEXER_URL - MCP indexer endpoint (default: http://localhost:8003/mcp)
2628
CTX_LIMIT - Default result limit (default: 5)
27-
CTX_CONTEXT_LINES - Context lines for snippets (default: 3)
29+
CTX_CONTEXT_LINES - Context lines for snippets (default: 0)
2830
"""
2931

3032
import sys
@@ -183,6 +185,28 @@ def parse_mcp_response(result: Dict[str, Any]) -> Optional[Dict[str, Any]]:
183185
return {"raw": text}
184186

185187

188+
def _compress_snippet(snippet: str, max_lines: int = 6) -> str:
189+
"""Compact, high-signal subset of a code snippet.
190+
191+
Heuristics: prefer signatures, guards, returns/raises, asserts; fall back to head/tail.
192+
"""
193+
try:
194+
raw_lines = [ln.rstrip() for ln in snippet.splitlines() if ln.strip()]
195+
if not raw_lines:
196+
return ""
197+
keys = ("def ", "class ", "return", "raise", "assert", "if ", "except", "try:")
198+
scored = [(sum(k in ln for k in keys), idx, ln) for idx, ln in enumerate(raw_lines)]
199+
keep_idx = sorted({idx for _, idx, _ in sorted(scored, key=lambda t: (-t[0], t[1]))[:max_lines]})
200+
kept = [raw_lines[i] for i in keep_idx]
201+
if not kept:
202+
head = raw_lines[: max(1, max_lines // 2)]
203+
tail = raw_lines[-(max_lines - len(head)) :]
204+
kept = head + tail
205+
return "\n".join(kept[:max_lines])
206+
except Exception:
207+
return (snippet or "").splitlines()[0][:160]
208+
209+
186210
def format_search_results(results: List[Dict[str, Any]], include_snippets: bool = False) -> str:
187211
"""Format search results succinctly for LLM rewrite.
188212
@@ -213,10 +237,71 @@ def format_search_results(results: List[Dict[str, Any]], include_snippets: bool
213237
lines.append(header)
214238

215239
if include_snippets and snippet:
216-
lang_tag = language.lower() if language else ""
217-
lines.append(f"```{lang_tag}\n{snippet}\n```")
240+
compact = _compress_snippet(snippet, max_lines=6)
241+
if compact:
242+
for ln in compact.splitlines():
243+
# Inline compact snippet (no fences to keep token count small)
244+
lines.append(f" {ln}")
245+
246+
247+
248+
def _ensure_two_paragraph_questions(text: str) -> str:
249+
"""Normalize to at least two paragraphs.
250+
251+
- Collapse excessive whitespace
252+
- For questions: ensure each paragraph ends with '?'
253+
- For commands/instructions: ensure proper punctuation
254+
- If only one paragraph, split heuristically or add a generic follow-up
255+
"""
256+
if not text:
257+
return ""
258+
# Normalize whitespace/newlines
259+
t = text.replace("\r\n", "\n").replace("\r", "\n").strip()
260+
# Collapse triple+ newlines to double
261+
while "\n\n\n" in t:
262+
t = t.replace("\n\n\n", "\n\n")
263+
paras = [p.strip() for p in t.split("\n\n") if p.strip()]
264+
265+
def normalize_paragraph(s: str) -> str:
266+
"""Ensure proper punctuation - keep questions as questions, commands as commands."""
267+
s = s.strip()
268+
if not s:
269+
return s
270+
# If already ends with proper punctuation, keep as-is
271+
if s[-1] in "?!.":
272+
return s
273+
# Check if it looks like a question (starts with question words or contains '?')
274+
question_starters = ("what", "how", "why", "when", "where", "who", "which", "can", "could", "would", "should", "is", "are", "does", "do")
275+
first_word = s.split()[0].lower() if s.split() else ""
276+
if first_word in question_starters or "?" in s:
277+
# It's a question - ensure it ends with '?'
278+
if s[-1] in ".!:":
279+
return s[:-1].rstrip() + "?"
280+
return s + "?"
281+
# It's a command/statement - ensure it ends with '.'
282+
if s[-1] in ":":
283+
return s[:-1].rstrip() + "."
284+
return s + "."
285+
286+
if len(paras) >= 2:
287+
p1, p2 = normalize_paragraph(paras[0]), normalize_paragraph(paras[1])
288+
return p1 + "\n\n" + p2
289+
290+
# Single paragraph: try to split by sentence boundary
291+
p = paras[0] if paras else t
292+
# Naive sentence split
293+
sentences = [s.strip() for s in p.replace("?", ". ").replace("!", ". ").split(". ") if s.strip()]
294+
if len(sentences) > 1:
295+
half = max(1, len(sentences) // 2)
296+
p1 = ". ".join(sentences[:half]).strip()
297+
p2 = ". ".join(sentences[half:]).strip()
298+
else:
299+
p1 = p.strip()
300+
p2 = (
301+
"Additionally, clarify algorithmic steps, inputs/outputs, configuration parameters, performance considerations, error handling behavior, tests, and edge cases relevant to the referenced components"
302+
)
303+
return normalize_paragraph(p1) + "\n\n" + normalize_paragraph(p2)
218304

219-
return "\n".join(lines).strip()
220305

221306

222307
def enhance_prompt(query: str, **filters) -> str:
@@ -270,21 +355,34 @@ def rewrite_prompt(original_prompt: str, context: str, note: str, max_tokens: Op
270355
271356
Returns ONLY the improved prompt text. Raises exception if decoder fails.
272357
"""
273-
effective_context = context.strip() if context.strip() else (note or "No context available.")
358+
ctx = (context or "").strip()
359+
nt = (note or "").strip()
360+
effective_context = ctx if ctx else (nt or "No context available.")
274361

275362
# Granite 4.0 chat template with explicit rewrite-only instruction
276363
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 when applicable; focus on concrete aspects such as algorithmic steps, inputs/outputs, parameters/configuration, performance, error handling, tests, and edge cases. "
282-
"Do not answer the question. Return only the rewritten question as plain text with no markdown or code fences."
364+
"You are a prompt rewriter. Your ONLY job is to rewrite prompts to be more specific and detailed. "
365+
"CRITICAL: You must NEVER answer questions or execute commands. You must ONLY rewrite the prompt to be better and more specific. "
366+
"ALWAYS enhance the prompt to be more detailed and actionable. "
367+
"If context is provided, use it to make the prompt more concrete by citing specific file paths, line ranges, and symbols that appear in the Context refs. "
368+
"If no relevant context is available, still enhance the prompt by expanding it to cover multiple aspects: implementation details, edge cases, error handling, performance, configuration, tests, and related components. "
369+
"Never invent references - only cite what appears verbatim in the Context refs. "
370+
"Your rewrite must be at least two short paragraphs separated by a single blank line. "
371+
"For questions: rewrite as more specific questions. For commands/instructions: rewrite as more detailed, specific instructions with concrete targets. "
372+
"Each paragraph should explore different aspects of the topic. "
373+
"Output format: plain text only, no markdown, no code fences, no answers, no explanations."
283374
)
375+
label = "with snippets" if "\n " in effective_context else "headers only"
284376
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."
377+
f"Context refs ({label}):\n{effective_context}\n\n"
378+
f"Original prompt: {(original_prompt or '').strip()}\n\n"
379+
"Rewrite this as a more specific, detailed prompt using at least two short paragraphs separated by a blank line. "
380+
"If the context above contains relevant references, cite concrete file paths, line ranges, and symbols in your rewrite. "
381+
"If the context is not relevant or empty, still enhance the prompt by expanding it to cover multiple aspects. "
382+
"For questions: make them more specific and multi-faceted (each paragraph should be a question ending with '?'). "
383+
"For commands/instructions: make them more detailed and concrete (specify exact files, functions, parameters, edge cases to handle). "
384+
"Remember: ONLY rewrite the prompt - do NOT answer questions or execute commands. "
385+
"Avoid generic phrasing. No markdown or code fences."
288386
)
289387
meta_prompt = (
290388
"<|start_of_role|>system<|end_of_role|>" + system_msg + "<|end_of_text|>\n"
@@ -326,6 +424,8 @@ def rewrite_prompt(original_prompt: str, context: str, note: str, max_tokens: Op
326424
if not enhanced:
327425
raise ValueError(f"Decoder returned empty response (stop_type={data.get('stop_type')}, tokens={data.get('tokens_predicted')})")
328426

427+
# Enforce at least two question paragraphs
428+
enhanced = _ensure_two_paragraph_questions(enhanced)
329429
return enhanced
330430

331431

@@ -390,6 +490,10 @@ def main():
390490
parser.add_argument("--rewrite-max-tokens", type=int, default=DEFAULT_REWRITE_TOKENS,
391491
help=f"Max tokens for LLM rewrite (default: {DEFAULT_REWRITE_TOKENS})")
392492

493+
# Detail mode
494+
parser.add_argument("--detail", action="store_true",
495+
help="Include short code snippets in the retrieved context for richer rewrites (slower)")
496+
393497
args = parser.parse_args()
394498

395499
# Build filter dict
@@ -404,11 +508,23 @@ def main():
404508
"symbol": args.symbol,
405509
"ext": args.ext,
406510
"per_path": args.per_path,
511+
"with_snippets": args.detail,
407512
"rewrite_options": {
408513
"max_tokens": args.rewrite_max_tokens,
409514
},
410515
}
411516

517+
# If detail mode is on and context_lines equals the default (0), bump to 1 for a short snippet
518+
if args.detail and args.context_lines == DEFAULT_CONTEXT_LINES:
519+
filters["context_lines"] = 1
520+
# Clamp result counts in detail mode for latency
521+
if args.detail:
522+
try:
523+
filters["limit"] = max(1, min(int(filters.get("limit", DEFAULT_LIMIT)), 4))
524+
except Exception:
525+
filters["limit"] = 4
526+
filters["per_path"] = 1
527+
412528
# Remove None values
413529
filters = {k: v for k, v in filters.items() if v is not None}
414530

0 commit comments

Comments
 (0)