diff --git a/sloppy.py b/sloppy.py index 2c1380f..984eca2 100644 --- a/sloppy.py +++ b/sloppy.py @@ -17,6 +17,12 @@ os.environ["AI_SERVER_ENABLED"] = "1" from defer.sugar import install as _install_defer _install_defer() +# Route all ai_client imports to ai_client_stub to avoid loading heavy SDKs +if os.environ.get("AI_SERVER_ENABLED"): + import sys + from src import ai_client_stub + sys.modules["src.ai_client"] = ai_client_stub + from src.gui_2 import main if __name__ == "__main__": diff --git a/src/ai_client_proxy.py b/src/ai_client_proxy.py index 820b615..6f64a69 100644 --- a/src/ai_client_proxy.py +++ b/src/ai_client_proxy.py @@ -42,6 +42,9 @@ class AIProxyClient: continue try: response = json.loads(line) + if response.get("type") == "ready" and self._status == "init": + self._status = "ready" + continue rid = response.get("id") if rid in self._pending: self._pending[rid] = response diff --git a/src/ai_client_stub.py b/src/ai_client_stub.py new file mode 100644 index 0000000..c17dabd --- /dev/null +++ b/src/ai_client_stub.py @@ -0,0 +1,370 @@ +from __future__ import annotations +import threading +import datetime +import time +import os +import json +import hashlib +from typing import Optional, Callable, Any, List, cast +from collections import deque +from pathlib import Path + +from src.gemini_cli_adapter import GeminiCliAdapter + +class EventEmitter: + def __init__(self): + self._handlers: dict[str, list[Callable]] = {} + def on(self, event: str, callback: Callable) -> None: + if event not in self._handlers: + self._handlers[event] = [] + self._handlers[event].append(callback) + def emit(self, event: str, **kwargs: Any) -> None: + for cb in self._handlers.get(event, []): + cb(**kwargs) + +events = EventEmitter() + +_provider: str = "gemini" +_model: str = "gemini-2.5-flash-lite" +_temperature: float = 0.0 +_top_p: float = 1.0 +_max_tokens: int = 8192 +_history_trunc_limit: int = 8000 + +_custom_system_prompt: str = "" +_base_system_prompt_override: str = "" +_use_default_base_system_prompt: bool = True +_project_context_marker: str = "" + +_local_storage = threading.local() +_comms_log: deque[dict[str, Any]] = deque(maxlen=1000) + +_tool_approval_modes: dict[str, str] = {} +_active_tool_preset = None +_active_bias_profile = None +_agent_tools: dict[str, bool] = {} +_active_bias_profile_name: Optional[str] = None + +confirm_and_run_callback: Optional[Callable[..., Optional[str]]] = None +comms_log_callback: Optional[Callable[[dict[str, Any]], None]] = None +tool_log_callback: Optional[Callable[[str, str], None]] = None + +COMMS_CLAMP_CHARS: int = 300 +MAX_TOOL_ROUNDS: int = 10 +MAX_TOOL_OUTPUT_BYTES: int = 500_000 + +_ai_proxy = None + +def _get_proxy(): + global _ai_proxy + if _ai_proxy is None and os.environ.get("AI_SERVER_ENABLED"): + try: + from src.ai_client_proxy import AIProxyClient + _ai_proxy = AIProxyClient() + _ai_proxy.start_server() + except Exception: + _ai_proxy = None + return _ai_proxy + +class ProviderError(Exception): + def __init__(self, kind: str, provider: str, original: Exception) -> None: + self.kind = kind + self.provider = provider + self.original = original + super().__init__(str(original)) + def ui_message(self) -> str: + labels = {"quota": "QUOTA EXHAUSTED", "rate_limit": "RATE LIMITED", "auth": "AUTH / API KEY ERROR", "balance": "BALANCE / BILLING ERROR", "network": "NETWORK / CONNECTION ERROR", "unknown": "API ERROR"} + label = labels.get(self.kind, "API ERROR") + return f"[{self.provider.upper()} {label}]\n\n{self.original}" + +def get_current_tier() -> Optional[str]: + return getattr(_local_storage, "current_tier", None) + +def set_current_tier(tier: Optional[str]) -> None: + _local_storage.current_tier = tier + +def get_comms_log_callback() -> Optional[Callable[[dict[str, Any]], None]]: + tl_cb = getattr(_local_storage, "comms_log_callback", None) + if tl_cb: + return tl_cb + return comms_log_callback + +def set_comms_log_callback(cb: Optional[Callable[[dict[str, Any]], None]]) -> None: + global comms_log_callback + comms_log_callback = cb + _local_storage.comms_log_callback = cb + +_SYSTEM_PROMPT = ( + "You are a helpful coding assistant with access to a PowerShell tool (run_powershell) and MCP tools." +) + +def set_custom_system_prompt(prompt: str) -> None: + global _custom_system_prompt + _custom_system_prompt = prompt + +def set_base_system_prompt(prompt: str) -> None: + global _base_system_prompt_override + _base_system_prompt_override = prompt + +def set_use_default_base_prompt(use_default: bool) -> None: + global _use_default_base_system_prompt + _use_default_base_system_prompt = use_default + +def set_project_context_marker(marker: str) -> None: + global _project_context_marker + _project_context_marker = marker + +def _get_combined_system_prompt() -> str: + if _use_default_base_system_prompt: + base = _SYSTEM_PROMPT + else: + base = _base_system_prompt_override + if _custom_system_prompt.strip(): + base = f"{base}\n\n[USER SYSTEM PROMPT]\n{_custom_system_prompt}" + return base + +def get_combined_system_prompt() -> str: + return _get_combined_system_prompt() + +def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None: + entry: dict[str, Any] = {"ts": datetime.datetime.now().strftime("%H:%M:%S"), "direction": direction, "kind": kind, "provider": _provider, "model": _model, "payload": payload, "source_tier": get_current_tier(), "local_ts": time.time()} + _comms_log.append(entry) + _cb = get_comms_log_callback() + if _cb is not None: + _cb(entry) + +def get_comms_log() -> list[dict[str, Any]]: + return list(_comms_log) + +def clear_comms_log() -> None: + _comms_log.clear() + +def get_credentials_path() -> Path: + return Path(os.environ.get("SLOP_CREDENTIALS", str(Path(__file__).parent.parent / "credentials.toml"))) + +def _load_credentials() -> dict[str, Any]: + import tomllib + cred_path = get_credentials_path() + try: + with open(cred_path, "rb") as f: + return tomllib.load(f) + except FileNotFoundError: + raise FileNotFoundError(f"Credentials file not found: {cred_path}") + +def set_provider(provider: str, model: str) -> None: + global _provider, _model + _provider = provider + if provider == "gemini_cli": + if model != "mock" and not any(m in model for m in ["deepseek"]): + _model = model + else: + _model = "gemini-3-flash-preview" + else: + _model = model + +def get_provider() -> str: + return _provider + +def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: float = 1.0) -> None: + global _temperature, _max_tokens, _history_trunc_limit, _top_p + _temperature = temp + _max_tokens = max_tok + _history_trunc_limit = trunc_limit + _top_p = top_p + +def set_agent_tools(tools: dict[str, bool]) -> None: + global _agent_tools + _agent_tools = tools + +def set_tool_preset(preset_name: Optional[str]) -> None: + global _tool_approval_modes, _active_tool_preset + _tool_approval_modes = {} + if not preset_name or preset_name == "None": + from src import mcp_client + _agent_tools = {name: True for name in mcp_client.TOOL_NAMES} + _agent_tools["run_powershell"] = True + _active_tool_preset = None + else: + try: + from src.tool_presets import ToolPresetManager + manager = ToolPresetManager() + presets = manager.load_all() + if preset_name in presets: + preset = presets[preset_name] + _active_tool_preset = preset + from src import mcp_client + new_tools = {name: False for name in mcp_client.TOOL_NAMES} + new_tools["run_powershell"] = False + for cat in preset.categories.values(): + for tool in cat: + name = tool.name + new_tools[name] = True + _tool_approval_modes[name] = tool.approval + _agent_tools = new_tools + except Exception: + pass + +def set_bias_profile(profile_name: Optional[str]) -> None: + global _active_bias_profile, _active_bias_profile_name + if not profile_name or profile_name == "None": + _active_bias_profile = None + _active_bias_profile_name = None + else: + try: + from src.tool_presets import ToolPresetManager + manager = ToolPresetManager() + profiles = manager.load_all_bias_profiles() + if profile_name in profiles: + _active_bias_profile = profiles[profile_name] + _active_bias_profile_name = profile_name + else: + _active_bias_profile = None + _active_bias_profile_name = None + except Exception: + _active_bias_profile = None + _active_bias_profile_name = None + +def get_bias_profile() -> Optional[str]: + return _active_bias_profile_name + +_gemini_cli_adapter = None + +def cleanup() -> None: + global _gemini_cli_adapter + proxy = _get_proxy() + if proxy and proxy.status == "ready": + proxy.send_command("cleanup", {}) + if _gemini_cli_adapter: + old_path = _gemini_cli_adapter.binary_path + _gemini_cli_adapter = None + else: + old_path = "gemini" + from src.gemini_cli_adapter import GeminiCliAdapter + _gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path) + +def reset_session() -> None: + global _gemini_cli_adapter + proxy = _get_proxy() + if proxy and proxy.status == "ready": + proxy.send_command("reset_session", {}) + if _gemini_cli_adapter: + old_path = _gemini_cli_adapter.binary_path + else: + old_path = "gemini" + from src.gemini_cli_adapter import GeminiCliAdapter + _gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path) + _comms_log.clear() + +def get_gemini_cache_stats() -> dict[str, Any]: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("get_gemini_cache_stats", {}) + if "result" in result: + return result["result"] + return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []} + +def list_models(provider: str) -> list[str]: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("list_models", {"provider": provider}) + if "result" in result: + return result["result"].get("models", []) + if provider == "gemini": + try: + from google import genai + creds = _load_credentials() + client = genai.Client(api_key=creds["gemini"]["api_key"]) + models = [] + for m in client.models.list(): + name = m.name + if name and name.startswith("models/"): + name = name[len("models/"):] + if name and "gemini" in name.lower(): + models.append(name) + return sorted(models) + except Exception: + return [] + elif provider == "anthropic": + try: + import anthropic + creds = _load_credentials() + client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"]) + return sorted([m.id for m in client.models.list()]) + except Exception: + return [] + elif provider == "deepseek": + return ["deepseek-chat", "deepseek-reasoner"] + elif provider == "gemini_cli": + return ["gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-flash-lite"] + elif provider == "minimax": + try: + from openai import OpenAI + creds = _load_credentials() + client = OpenAI(api_key=creds["minimax"]["api_key"], base_url="https://api.minimax.io/v1") + return sorted([m.id for m in client.models.list()]) + except Exception: + return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] + return [] + +def send(md_content: str, user_message: str, base_dir: str, + file_items: Optional[list[dict[str, Any]]] = None, + discussion_history: str = "", + pre_tool_callback: Optional[Callable] = None, + qa_callback: Optional[Callable] = None, + enable_tools: bool = True, + stream_callback: Optional[Callable[[str], None]] = None, + patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("send", { + "md_content": md_content, + "user_message": user_message, + "base_dir": base_dir, + "file_items": file_items or [], + "discussion_history": discussion_history, + "pre_tool_callback": pre_tool_callback is not None, + "enable_tools": enable_tools, + }) + if "result" in result: + return result["result"].get("response", "") + return "ERROR: AI server not available" + +def get_token_stats(md_content: str) -> dict[str, Any]: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("get_token_stats", {"md_content": md_content}) + if "result" in result: + return result["result"] + return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "cached_tokens": 0} + +def run_tier4_analysis(error: str) -> str: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("run_tier4_analysis", {"error": error}) + if "result" in result: + return result["result"].get("analysis", "") + return "" + +def run_tier4_patch_callback(script: str, base_dir: str) -> Optional[str]: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("run_tier4_patch_callback", {"script": script, "base_dir": base_dir}) + if "result" in result: + return result["result"].get("output") + return None + +def run_tier4_patch_generation(error: str, context: str) -> str: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("run_tier4_patch_generation", {"error": error, "context": context}) + if "result" in result: + return result["result"].get("diff", "") + return "" + +def run_subagent_summarization(text: str, system_prompt: str, provider: str = "gemini") -> str: + proxy = _get_proxy() + if proxy and proxy.status == "ready": + result = proxy.send_command("run_subagent_summarization", {"text": text, "system_prompt": system_prompt, "provider": provider}) + if "result" in result: + return result["result"].get("summary", "") + return "" \ No newline at end of file diff --git a/src/ai_server.py b/src/ai_server.py index 3e6c325..df5a7e3 100644 --- a/src/ai_server.py +++ b/src/ai_server.py @@ -2,15 +2,48 @@ import json import sys import os - -_PROVIDERS = { - "gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview"], - "anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"], -} +import threading +import hashlib +import time +import datetime +from typing import Any, Optional _google_genai = None _anthropic = None +_deepseek_client = None +_minimax_client = None +_providers = { + "gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview"], + "anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"], + "deepseek": ["deepseek-chat", "deepseek-reasoner"], + "minimax": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"], + "gemini_cli": ["gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-flash-lite"], +} + +_session_state = { + "provider": "gemini", + "model": "gemini-2.5-flash-lite", + "temperature": 0.0, + "top_p": 1.0, + "max_tokens": 8192, + "custom_system_prompt": "", + "base_system_prompt_override": "", + "use_default_base_prompt": True, + "project_context_marker": "", + "agent_tools": {}, + "gemini_cache": None, + "gemini_cache_md_hash": None, + "gemini_cache_created_at": None, + "gemini_cached_file_paths": [], +} + +_history = { + "gemini": [], + "anthropic": [], + "deepseek": [], + "minimax": [], +} def _ensure_google_genai(): global _google_genai @@ -19,7 +52,6 @@ def _ensure_google_genai(): _google_genai = genai return _google_genai - def _ensure_anthropic(): global _anthropic if _anthropic is None: @@ -27,6 +59,11 @@ def _ensure_anthropic(): _anthropic = anthropic return _anthropic +def _load_credentials(): + import tomllib + cred_path = os.environ.get("SLOP_CREDENTIALS", str(os.path.join(os.path.dirname(__file__), "..", "credentials.toml"))) + with open(cred_path, "rb") as f: + return tomllib.load(f) def handle_command(cmd: dict) -> dict: method = cmd.get("method", "") @@ -35,34 +72,175 @@ def handle_command(cmd: dict) -> dict: if method == "list_models": provider = params.get("provider", "gemini") - return {"id": cmd_id, "result": {"models": _PROVIDERS.get(provider, [])}} - - if method == "send": - provider = params.get("provider", "gemini") + if provider in _providers: + return {"id": cmd_id, "result": {"models": _providers[provider]}} if provider == "gemini": - _ensure_google_genai() - elif provider == "anthropic": - _ensure_anthropic() - return {"id": cmd_id, "result": {"status": "processed"}} + try: + client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"]) + models = [] + for m in client.models.list(): + name = m.name + if name and name.startswith("models/"): + name = name[len("models/"):] + if name and "gemini" in name.lower(): + models.append(name) + return {"id": cmd_id, "result": {"models": sorted(models)}} + except Exception as e: + return {"id": cmd_id, "error": str(e)} + if provider == "anthropic": + try: + client = _ensure_anthropic().Anthropic(api_key=_load_credentials()["anthropic"]["api_key"]) + return {"id": cmd_id, "result": {"models": sorted([m.id for m in client.models.list()])}} + except Exception as e: + return {"id": cmd_id, "error": str(e)} + return {"id": cmd_id, "result": {"models": []}} + + if method == "set_provider": + _session_state["provider"] = params.get("provider", "gemini") + _session_state["model"] = params.get("model", "gemini-2.5-flash-lite") + return {"id": cmd_id, "result": {"status": "provider_set"}} + + if method == "set_model_params": + _session_state["temperature"] = params.get("temperature", 0.0) + _session_state["top_p"] = params.get("top_p", 1.0) + _session_state["max_tokens"] = params.get("max_tokens", 8192) + return {"id": cmd_id, "result": {"status": "params_set"}} if method == "cleanup": + global _google_genai + if _session_state["gemini_cache"]: + try: + _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"]).caches.delete(name=_session_state["gemini_cache"].name) + except Exception: + pass + _session_state["gemini_cache"] = None + _session_state["gemini_cached_file_paths"] = [] return {"id": cmd_id, "result": {"status": "cleaned"}} if method == "reset_session": + _history["gemini"] = [] + _history["anthropic"] = [] + _history["deepseek"] = [] + _history["minimax"] = [] + _session_state["gemini_cache"] = None + _session_state["gemini_cache_md_hash"] = None + _session_state["gemini_cache_created_at"] = None + _session_state["gemini_cached_file_paths"] = [] return {"id": cmd_id, "result": {"status": "reset"}} - if method == "set_provider": - return {"id": cmd_id, "result": {"status": "provider_set"}} + if method == "get_gemini_cache_stats": + try: + client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"]) + caches = list(client.caches.list()) + total_size = sum(getattr(c, 'size_bytes', 0) for c in caches) + return {"id": cmd_id, "result": {"cache_count": len(caches), "total_size_bytes": total_size, "cached_files": _session_state["gemini_cached_file_paths"]}} + except Exception as e: + return {"id": cmd_id, "result": {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}} - if method == "set_credentials": - return {"id": cmd_id, "result": {"status": "credentials_set"}} + if method == "send": + return _handle_send(cmd_id, params) + + if method == "get_token_stats": + md_content = params.get("md_content", "") + approx_tokens = len(md_content) // 4 + return {"id": cmd_id, "result": {"input_tokens": approx_tokens, "output_tokens": 0, "total_tokens": approx_tokens, "cached_tokens": 0}} + + if method == "run_tier4_analysis": + error = params.get("error", "") + return {"id": cmd_id, "result": {"analysis": f"Analysis: {error[:100]}..."}} + + if method == "run_tier4_patch_callback": + return {"id": cmd_id, "result": {"output": None}} + + if method == "run_tier4_patch_generation": + return {"id": cmd_id, "result": {"diff": ""}} + + if method == "run_subagent_summarization": + return {"id": cmd_id, "result": {"summary": params.get("text", "")[:100]}} return {"id": cmd_id, "error": f"Unknown method: {method}"} +def _handle_send(cmd_id: str, params: dict) -> dict: + provider = params.get("provider", _session_state.get("provider", "gemini")) + md_content = params.get("md_content", "") + user_message = params.get("user_message", "") + base_dir = params.get("base_dir", "") + enable_tools = params.get("enable_tools", True) + + try: + if provider == "gemini": + response = _send_gemini(md_content, user_message, base_dir, enable_tools) + elif provider == "anthropic": + response = _send_anthropic(md_content, user_message) + elif provider == "deepseek": + response = _send_deepseek(md_content, user_message) + elif provider == "minimax": + response = _send_minimax(md_content, user_message) + elif provider == "gemini_cli": + response = _send_gemini_cli(md_content, user_message, base_dir) + else: + response = f"ERROR: Unknown provider {provider}" + + return {"id": cmd_id, "result": {"response": response, "provider": provider}} + except Exception as e: + return {"id": cmd_id, "error": str(e)} + +def _send_gemini(md_content: str, user_message: str, base_dir: str, enable_tools: bool) -> str: + client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"]) + model = _session_state.get("model", "gemini-2.5-flash-lite") + + system_instruction = f"{_session_state.get('custom_system_prompt', '')}\n\n\n{md_content}\n" + + config = { + "temperature": _session_state.get("temperature", 0.0), + "top_p": _session_state.get("top_p", 1.0), + "max_output_tokens": _session_state.get("max_tokens", 8192), + } + + response = client.models.generate_content(model=model, contents=user_message, config=config) + return response.text + +def _send_anthropic(md_content: str, user_message: str) -> str: + client = _ensure_anthropic().Anthropic(api_key=_load_credentials()["anthropic"]["api_key"]) + + response = client.messages.create( + model=_session_state.get("model", "claude-sonnet-4-20250514"), + max_tokens=_session_state.get("max_tokens", 8192), + system=f"{_session_state.get('custom_system_prompt', '')}\n\n\n{md_content}\n", + messages=[{"role": "user", "content": user_message}] + ) + return response.content[0].text + +def _send_deepseek(md_content: str, user_message: str) -> str: + from openai import OpenAI + global _deepseek_client + if _deepseek_client is None: + _deepseek_client = OpenAI(api_key=_load_credentials()["deepseek"]["api_key"], base_url="https://api.deepseek.com") + + response = _deepseek_client.chat.completions.create( + model=_session_state.get("model", "deepseek-chat"), + messages=[{"role": "system", "content": f"{_session_state.get('custom_system_prompt', '')}\n\n\n{md_content}\n"}, {"role": "user", "content": user_message}] + ) + return response.choices[0].message.content + +def _send_minimax(md_content: str, user_message: str) -> str: + from openai import OpenAI + global _minimax_client + if _minimax_client is None: + creds = _load_credentials() + _minimax_client = OpenAI(api_key=creds["minimax"]["api_key"], base_url="https://api.minimax.io/v1") + + response = _minimax_client.chat.completions.create( + model=_session_state.get("model", "MiniMax-M2.5"), + messages=[{"role": "system", "content": f"{_session_state.get('custom_system_prompt', '')}\n\n\n{md_content}\n"}, {"role": "user", "content": user_message}] + ) + return response.choices[0].message.content + +def _send_gemini_cli(md_content: str, user_message: str, base_dir: str) -> str: + return f"[gemini_cli] {user_message[:50]}..." def main(): - print(json.dumps({"type": "ready"})) - sys.stdout.flush() + print(json.dumps({"type": "ready"}), flush=True) for line in sys.stdin: line = line.strip() @@ -71,15 +249,11 @@ def main(): try: cmd = json.loads(line) response = handle_command(cmd) - print(json.dumps(response)) - sys.stdout.flush() + print(json.dumps(response), flush=True) except json.JSONDecodeError as e: - print(json.dumps({"error": f"Invalid JSON: {e}"})) - sys.stdout.flush() + print(json.dumps({"error": f"Invalid JSON: {e}"}), flush=True) except Exception as e: - print(json.dumps({"error": str(e)})) - sys.stdout.flush() - + print(json.dumps({"error": str(e)}), flush=True) if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/app_controller.py b/src/app_controller.py index 76097da..d2fe2b2 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -18,7 +18,7 @@ from pathlib import Path from pydantic import BaseModel from typing import Any, List, Dict, Optional, Callable from src import aggregate -from src import ai_client +from src import ai_client_stub as ai_client from src import conductor_tech_lead from src import events from src import mcp_client @@ -1618,7 +1618,7 @@ class AppController: Stops background threads and cleans up resources. [C: src/gui_2.py:App.run, src/gui_2.py:App.shutdown, tests/conftest.py:app_instance, tests/conftest.py:mock_app] """ - from src import ai_client + from src import ai_client_stub as ai_client ai_client.cleanup() if hasattr(self, 'hook_server') and self.hook_server: self.hook_server.stop() @@ -3002,7 +3002,7 @@ class AppController: self._update_cached_stats() def _update_cached_stats(self) -> None: - from src import ai_client + from src import ai_client_stub as ai_client self._cached_cache_stats = ai_client.get_gemini_cache_stats() self._cached_tool_stats = dict(self._tool_stats) @@ -3010,7 +3010,7 @@ class AppController: """ [C: src/gui_2.py:App._render_cache_panel] """ - from src import ai_client + from src import ai_client_stub as ai_client ai_client.cleanup() self._update_cached_stats()