| """Deterministic offline LLM stub. Drives unit tests without a GPU. |
| |
| Each call inspects the latest user / tool message and decides what to do |
| based on simple heuristics: |
| - Question contains 'grep' or 'search' β emit grep_codebase tool call |
| - Question contains 'read' or 'show' β emit read_file tool call |
| - After two tool turns β emit a final answer |
| """ |
| from __future__ import annotations |
| import json |
| from dataclasses import dataclass |
| from typing import Any, Dict, List |
|
|
| from .base import LLMClient, LLMResponse, ToolCall |
|
|
|
|
| @dataclass |
| class MockClient(LLMClient): |
| max_tool_turns: int = 2 |
|
|
| def complete(self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs: Any) -> LLMResponse: |
| tool_turns = sum(1 for m in messages if m.get("role") == "tool") |
| last_user = next((m for m in reversed(messages) if m.get("role") == "user"), None) |
| question = (last_user.get("content") if last_user else "") or "" |
|
|
| if tool_turns >= self.max_tool_turns: |
| tool_outputs = [m.get("content", "") for m in messages if m.get("role") == "tool"] |
| joined = "\n".join(tool_outputs)[:1500] |
| answer = ( |
| "Based on the inspected files, here is what I found:\n\n" |
| f"{joined or '(no tool output)'}\n\n" |
| f"Original question: {question.strip()}" |
| ) |
| return LLMResponse(content=answer, tool_calls=[], usage={"prompt": 0, "completion": 0}) |
|
|
| |
| q = question.lower() |
| if any(k in q for k in ("grep", "search", "find", "where", "occurr")): |
| call = ToolCall( |
| id=f"call_{tool_turns}", name="grep_codebase", |
| arguments={"pattern": _extract_term(question), "max_results": 10}, |
| ) |
| elif any(k in q for k in ("git", "history", "commits", "log")): |
| call = ToolCall(id=f"call_{tool_turns}", name="git_log", arguments={"limit": 10}) |
| elif any(k in q for k in ("test", "run", "pytest")): |
| call = ToolCall(id=f"call_{tool_turns}", name="run_tests", arguments={}) |
| elif any(k in q for k in ("read", "show", "open", "file")): |
| call = ToolCall( |
| id=f"call_{tool_turns}", name="read_file", |
| arguments={"path": _extract_path(question) or "README.md"}, |
| ) |
| else: |
| |
| call = ToolCall( |
| id=f"call_{tool_turns}", name="grep_codebase", |
| arguments={"pattern": _extract_term(question), "max_results": 5}, |
| ) |
| return LLMResponse(content="", tool_calls=[call], usage={"prompt": 0, "completion": 0}) |
|
|
|
|
| def _extract_term(text: str) -> str: |
| |
| import re |
| for m in re.finditer(r"[A-Za-z_][\w]{3,}", text): |
| if m.group(0).lower() not in {"what", "where", "find", "grep", "search", "show", "file", "read"}: |
| return m.group(0) |
| return text.strip()[:32] or "." |
|
|
|
|
| def _extract_path(text: str) -> str: |
| import re |
| m = re.search(r"[\w./-]+\.[A-Za-z]{1,5}", text) |
| return m.group(0) if m else "" |
|
|