diff --git a/src/ai_client.py b/src/ai_client.py index 3bef88f..ea89c33 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2450,3 +2450,52 @@ def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]: "percentage": 0, }) +def run_subagent_summarization(file_path: str, content: str, is_code: bool, outline: str) -> str: + """Performs a stateless summarization request using a sub-agent prompt.""" + prompt_tmpl = mma_prompts.TIER4_SUMMARIZE_CODE_PROMPT if is_code else mma_prompts.TIER4_SUMMARIZE_TEXT_PROMPT + prompt = prompt_tmpl.format(file_path=file_path, outline=outline, content=content) + if _provider == "gemini": + _ensure_gemini_client() + if _gemini_client: + resp = _gemini_client.models.generate_content( + model=_model, + contents=prompt, + config=types.GenerateContentConfig( + temperature=0.0, + max_output_tokens=1024, + ) + ) + return resp.text or "" + elif _provider == "anthropic": + _ensure_anthropic_client() + if _anthropic_client: + resp = _anthropic_client.messages.create( + model=_model, + max_tokens=1024, + messages=[{"role": "user", "content": prompt}] + ) + return "".join([b.text for b in resp.content if hasattr(b, "text") and b.text]) + elif _provider == "deepseek": + creds = _load_credentials() + api_key = creds.get("deepseek", {}).get("api_key") + if not api_key: return "ERROR: DeepSeek API key missing" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + payload = { + "model": _model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.0, + } + try: + r = requests.post("https://api.deepseek.com/chat/completions", headers=headers, json=payload, timeout=60) + r.raise_for_status() + return r.json()["choices"][0]["message"]["content"] + except Exception as e: + return f"ERROR: DeepSeek summarization failed: {e}" + elif _provider == "gemini_cli": + # Using the adapter for a one-off call + from src.gemini_cli_adapter import GeminiCliAdapter + adapter = GeminiCliAdapter(binary_path="gemini") + resp_data = adapter.send(prompt, model=_model) + return resp_data.get("text", "") + return "ERROR: Unsupported provider for sub-agent summarization" + diff --git a/src/mma_prompts.py b/src/mma_prompts.py index 345dd77..4a6cb44 100644 --- a/src/mma_prompts.py +++ b/src/mma_prompts.py @@ -179,3 +179,32 @@ RULES: Analyze this error and generate the patch: """ +TIER4_SUMMARIZE_CODE_PROMPT: str = """You are a Tier 4 QA Agent specializing in code summarization. +Your goal is to provide a concise, high-signal summary of the provided code file. +Focus on the primary responsibility of the module and its key architectural components. + +INPUT: +- File Path: {file_path} +- Heuristic Outline: {outline} +- Raw Content: +{content} + +OUTPUT REQUIREMENT: +Provide a 1-2 sentence high-level summary followed by a brief bulleted list of key features or responsibilities. +Keep it extremely concise. Do NOT repeat the outline. +""" + +TIER4_SUMMARIZE_TEXT_PROMPT: str = """You are a Tier 4 QA Agent specializing in document summarization. +Your goal is to provide a concise, high-signal summary of the provided text/markdown file. + +INPUT: +- File Path: {file_path} +- Heuristic Outline: {outline} +- Raw Content: +{content} + +OUTPUT REQUIREMENT: +Provide a 1-2 sentence high-level summary of the document's purpose and key takeaways. +Keep it extremely concise. +""" + diff --git a/src/summarize.py b/src/summarize.py index 058d4fd..ec98770 100644 --- a/src/summarize.py +++ b/src/summarize.py @@ -153,9 +153,9 @@ _SUMMARISERS: dict[str, Callable[[Path, str], str]] = { def summarise_file(path: Path, content: str) -> str: """ - Return a compact markdown summary string for a single file. - `content` is the already-read file text (or an error string). - """ + Return a compact markdown summary string for a single file. + `content` is the already-read file text (or an error string). + """ content_hash = get_file_hash(content) cached = _summary_cache.get_summary(str(path), content_hash) if cached: @@ -164,7 +164,25 @@ def summarise_file(path: Path, content: str) -> str: suffix = path.suffix.lower() if hasattr(path, "suffix") else "" fn = _SUMMARISERS.get(suffix, _summarise_generic) try: - summary = fn(path, content) + heuristic_outline = fn(path, content) + + # Smart AI Summarization + is_code = suffix in [".py", ".ps1", ".js", ".ts", ".cpp", ".c", ".h", ".cs", ".go", ".rs", ".lua"] + try: + from src import ai_client + smart_summary = ai_client.run_subagent_summarization( + file_path=str(path), + content=content[:10000], # Cap content to 10k chars for summarization + is_code=is_code, + outline=heuristic_outline + ) + if smart_summary and not smart_summary.startswith("ERROR:"): + summary = f"{smart_summary}\n\n**Outline:**\n{heuristic_outline}" + else: + summary = heuristic_outline + except Exception: + summary = heuristic_outline # Fallback + _summary_cache.set_summary(str(path), content_hash, summary) return summary except Exception as e: diff --git a/tests/test_subagent_summarization.py b/tests/test_subagent_summarization.py new file mode 100644 index 0000000..5f13de1 --- /dev/null +++ b/tests/test_subagent_summarization.py @@ -0,0 +1,47 @@ +import pytest +from unittest.mock import MagicMock, patch +from src import ai_client +from src import summarize +from pathlib import Path + +def test_run_subagent_summarization_gemini(): + with patch("src.ai_client._provider", "gemini"), \ + patch("src.ai_client._gemini_client") as mock_client, \ + patch("src.ai_client._ensure_gemini_client"): + mock_resp = MagicMock() + mock_resp.text = "Smart Summary" + mock_client.models.generate_content.return_value = mock_resp + + res = ai_client.run_subagent_summarization("test.py", "print('hello')", True, "Outline") + assert res == "Smart Summary" + mock_client.models.generate_content.assert_called_once() + +def test_run_subagent_summarization_anthropic(): + with patch("src.ai_client._provider", "anthropic"), \ + patch("src.ai_client._anthropic_client") as mock_client, \ + patch("src.ai_client._ensure_anthropic_client"): + mock_resp = MagicMock() + mock_block = MagicMock() + mock_block.text = "Anthropic Summary" + mock_resp.content = [mock_block] + mock_client.messages.create.return_value = mock_resp + + res = ai_client.run_subagent_summarization("test.py", "print('hello')", True, "Outline") + assert res == "Anthropic Summary" + mock_client.messages.create.assert_called_once() + +def test_summarise_file_integration(): + with patch("src.ai_client.run_subagent_summarization") as mock_run: + mock_run.return_value = "Smart AI Summary" + + # Ensure we don't hit the real cache for this test + with patch("src.summarize._summary_cache") as mock_cache: + mock_cache.get_summary.return_value = None + + p = Path("test_file.py") + content = "def hello():\n pass" + summary = summarize.summarise_file(p, content) + + assert "Smart AI Summary" in summary + assert "**Outline:**" in summary + assert "functions: hello" in summary