From 6caa6680bf01543dd868ccf01908a754a32c6c9c Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 13 Jun 2026 17:46:38 -0400 Subject: [PATCH] working on ai client --- src/ai_client.py | 126 +++++++++++++++++++++-------------------------- 1 file changed, 57 insertions(+), 69 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index 810d693d..e8a4b844 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -46,13 +46,13 @@ from src import project_manager from src.vendor_capabilities import VendorCapabilities, get_capabilities # TODO(Ed): Eliminate these? -from src.events import EventEmitter +from src.events import EventEmitter from src.gemini_cli_adapter import GeminiCliAdapter -from src.models import ToolPreset, BiasProfile, Tool -from src.paths import get_credentials_path -from src.tool_bias import ToolBiasEngine -from src.tool_presets import ToolPresetManager -from src.tool_presets import ToolPresetManager +from src.models import ToolPreset, BiasProfile, Tool +from src.paths import get_credentials_path +from src.tool_bias import ToolBiasEngine +from src.tool_presets import ToolPresetManager +from src.tool_presets import ToolPresetManager PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] @@ -62,8 +62,7 @@ PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minima # existing call sites and the T3.1 test (which asserts # hasattr(src.ai_client, '_require_warmed')) continue to work. from src.module_loader import _require_warmed # noqa: E402,F401 -from src.result_types import ErrorInfo, ErrorKind, Result # noqa: E402,F401 - +from src.result_types import ErrorInfo, ErrorKind, Result # noqa: E402,F401 _provider: str = "gemini" _model: str = "gemini-2.5-flash-lite" @@ -87,42 +86,42 @@ def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: _top_p = top_p _gemini_client: Optional[genai.Client] = None -_gemini_chat: Any = None -_gemini_cache: Any = None -_gemini_cache_md_hash: Optional[str] = None +_gemini_chat: Any = None +_gemini_cache: Any = None +_gemini_cache_md_hash: Optional[str] = None _gemini_cache_created_at: Optional[float] = None -_gemini_cached_file_paths: list[str] = [] +_gemini_cached_file_paths: list[str] = [] # Gemini cache TTL in seconds. Caches are created with this TTL and # proactively rebuilt at 90% of this value to avoid stale-reference errors. _GEMINI_CACHE_TTL: int = 3600 _anthropic_client: Optional[anthropic.Anthropic] = None -_anthropic_history: list[dict[str, Any]] = [] +_anthropic_history: list[dict[str, Any]] = [] _anthropic_history_lock: threading.Lock = threading.Lock() -_deepseek_client: Any = None +_deepseek_client: Any = None _deepseek_history: list[dict[str, Any]] = [] _deepseek_history_lock: threading.Lock = threading.Lock() -_minimax_client: Any = None +_minimax_client: Any = None _minimax_history: list[dict[str, Any]] = [] _minimax_history_lock: threading.Lock = threading.Lock() -_qwen_client: Any = None +_qwen_client: Any = None _qwen_history: list[dict[str, Any]] = [] _qwen_history_lock: threading.Lock = threading.Lock() -_qwen_region: str = "china" +_qwen_region: str = "china" -_grok_client: Any = None +_grok_client: Any = None _grok_history: list[dict[str, Any]] = [] _grok_history_lock: threading.Lock = threading.Lock() -_llama_client: Any = None -_llama_history: list[dict[str, Any]] = [] -_llama_history_lock: threading.Lock = threading.Lock() -_llama_base_url: str = "http://localhost:11434/v1" -_llama_api_key: str = "ollama" +_llama_client: Any = None +_llama_history: list[dict[str, Any]] = [] +_llama_history_lock: threading.Lock = threading.Lock() +_llama_base_url: str = "http://localhost:11434/v1" +_llama_api_key: str = "ollama" _send_lock: threading.Lock = threading.Lock() @@ -271,6 +270,7 @@ def get_credentials_path() -> Path: def _load_credentials() -> dict[str, Any]: cred_path = get_credentials_path() + #TODO(Ed): Exception(Review) try: with open(cred_path, "rb") as f: return tomllib.load(f) @@ -407,8 +407,9 @@ def get_provider() -> str: def cleanup() -> None: """Performs cleanup operations like deleting server-side Gemini caches.""" - global _gemini_client, _gemini_cache, _gemini_cached_file_paths - if _gemini_client and _gemini_cache: + global _gemini_client, _gemini_cache, _gemini_cached_file_paths + if _gemini_client and _gemini_cache: + #TODO(Ed): Exception(Review) try: _gemini_client.caches.delete(name=_gemini_cache.name) except Exception: @@ -417,15 +418,16 @@ def cleanup() -> None: def reset_session() -> None: """Clears conversation history and resets provider-specific session state.""" - global _gemini_client, _gemini_chat, _gemini_cache + global _gemini_client, _gemini_chat, _gemini_cache global _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths - global _anthropic_client, _anthropic_history - global _deepseek_client, _deepseek_history - global _minimax_client, _minimax_history - global _qwen_client, _qwen_history + global _anthropic_client, _anthropic_history + global _deepseek_client, _deepseek_history + global _minimax_client, _minimax_history + global _qwen_client, _qwen_history global _CACHED_ANTHROPIC_TOOLS, _CACHED_DEEPSEEK_TOOLS global _gemini_cli_adapter if _gemini_client and _gemini_cache: + #TODO(Ed): Review(Exception) try: _gemini_client.caches.delete(name=_gemini_cache.name) except Exception: @@ -438,7 +440,7 @@ def reset_session() -> None: _gemini_cached_file_paths = [] # Preserve binary_path if adapter exists - old_path = _gemini_cli_adapter.binary_path if _gemini_cli_adapter else "gemini" + old_path = _gemini_cli_adapter.binary_path if _gemini_cli_adapter else "gemini" _gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path) _anthropic_client = None @@ -456,28 +458,25 @@ def reset_session() -> None: _grok_client = None with _grok_history_lock: _grok_history = [] - _llama_client = None + _llama_client = None with _llama_history_lock: _llama_history = [] _llama_base_url = "http://localhost:11434/v1" - _llama_api_key = "ollama" + _llama_api_key = "ollama" _CACHED_ANTHROPIC_TOOLS = None _CACHED_DEEPSEEK_TOOLS = None file_cache.reset_client() def list_models(provider: str) -> list[str]: - """ - [C: src/app_controller.py:AppController.do_fetch, tests/test_agent_capabilities.py:test_agent_capabilities_listing, tests/test_ai_client_list_models.py:test_list_models_gemini_cli, tests/test_deepseek_infra.py:test_deepseek_model_listing, tests/test_minimax_provider.py:test_minimax_list_models] - """ creds = _load_credentials() if provider == "gemini": return _list_gemini_models(creds["gemini"]["api_key"]) elif provider == "anthropic": return _list_anthropic_models() elif provider == "deepseek": return _list_deepseek_models(creds["deepseek"]["api_key"]) elif provider == "gemini_cli": return _list_gemini_cli_models() elif provider == "minimax": return _list_minimax_models(creds["minimax"]["api_key"]) - elif provider == "qwen": return _list_qwen_models() - elif provider == "grok": return _list_grok_models() - elif provider == "llama": return _list_llama_models() + elif provider == "qwen": return _list_qwen_models() + elif provider == "grok": return _list_grok_models() + elif provider == "llama": return _list_llama_models() return [] #endregion: Comms Log @@ -489,20 +488,14 @@ _agent_tools: dict[str, bool] = {} #region: Tool Configuration def set_agent_tools(tools: dict[str, bool]) -> None: - """ - Configures which tools are enabled for the AI agent. - [C: src/app_controller.py:AppController._handle_request_event, src/app_controller.py:_api_generate, tests/test_agent_tools_wiring.py:test_build_anthropic_tools_conversion, tests/test_agent_tools_wiring.py:test_set_agent_tools, tests/test_tool_access_exclusion.py:test_build_anthropic_tools_excludes_disabled, tests/test_tool_access_exclusion.py:test_build_deepseek_tools_excludes_disabled, tests/test_tool_access_exclusion.py:test_gemini_tool_declaration_excludes_disabled, tests/test_tool_access_exclusion.py:test_set_agent_tools_clears_caches] - """ + """Configures which tools are enabled for the AI agent.""" global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _CACHED_DEEPSEEK_TOOLS _agent_tools = tools _CACHED_ANTHROPIC_TOOLS = None _CACHED_DEEPSEEK_TOOLS = None def set_tool_preset(preset_name: Optional[str]) -> None: - """ - Loads a tool preset and applies it via set_agent_tools. - [C: src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_persona_selector_panel, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_bias_integration.py:test_set_tool_preset_with_objects, tests/test_tool_preset_env.py:test_tool_preset_env_loading, tests/test_tool_preset_env.py:test_tool_preset_env_no_var, tests/test_tool_presets_execution.py:test_tool_ask_approval, tests/test_tool_presets_execution.py:test_tool_auto_approval, tests/test_tool_presets_execution.py:test_tool_rejection] - """ + """Loads a tool preset and applies it via set_agent_tools.""" global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _CACHED_DEEPSEEK_TOOLS, _tool_approval_modes, _active_tool_preset _tool_approval_modes = {} if not preset_name or preset_name == "None": @@ -532,11 +525,7 @@ def set_tool_preset(preset_name: Optional[str]) -> None: _CACHED_DEEPSEEK_TOOLS = None def set_bias_profile(profile_name: Optional[str]) -> None: - """ - - Sets the active tool bias profile for tuning model behavior. - [C: src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_agent_tools_panel, src/gui_2.py:App._render_persona_selector_panel, src/multi_agent_conductor.py:run_worker_lifecycle] - """ + """Sets the active tool bias profile for tuning model behavior.""" global _active_bias_profile if not profile_name or profile_name == "None": _active_bias_profile = None @@ -1191,7 +1180,7 @@ def _repair_anthropic_history(history: list[dict[str, Any]]) -> None: ], }) -def _send_anthropic_result(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, qa_callback: Optional[Callable[[str], str]] = None, stream_callback: Optional[Callable[[str], None]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Result[str]: +def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, qa_callback: Optional[Callable[[str], str]] = None, stream_callback: Optional[Callable[[str], None]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Result[str]: """ [C: src/ai_server.py:_handle_send] """ @@ -1434,7 +1423,7 @@ def _get_gemini_history_list(chat: Any | None) -> list[Any]: return cast(list[Any], chat.get_history()) return [] -def _send_gemini_result(md_content: str, user_message: str, base_dir: str, +def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, @@ -1670,7 +1659,7 @@ def _send_gemini_result(md_content: str, user_message: str, base_dir: str, if monitor.enabled: monitor.end_component("ai_client._send_gemini") return Result(data="", errors=[_classify_gemini_error(e, source="ai_client.gemini")]) -def _send_gemini_cli_result(md_content: str, user_message: str, base_dir: str, +def _send_gemini_cli(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, @@ -1815,7 +1804,7 @@ def _ensure_deepseek_client() -> None: _load_credentials() pass -def _send_deepseek_result(md_content: str, user_message: str, base_dir: str, +def _send_deepseek(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", stream: bool = False, @@ -2076,6 +2065,7 @@ def _send_deepseek_result(md_content: str, user_message: str, base_dir: str, #region: MiniMax Provider +#TODO(Ed): This causes a pause on gui thread, this should be cached. def _list_minimax_models(api_key: str) -> list[str]: try: openai = _require_warmed("openai") @@ -2090,17 +2080,14 @@ def _list_minimax_models(api_key: str) -> list[str]: return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] def _repair_minimax_history(history: list[dict[str, Any]]) -> None: - if not history: - return + if not history: return last = history[-1] - if last.get("role") != "assistant": - return + if last.get("role") != "assistant": return tool_calls = last.get("tool_calls", []) - if not tool_calls: - return + if not tool_calls: return call_ids = [] for tc in tool_calls: - if hasattr(tc, "id"): call_ids.append(tc.id) + if hasattr(tc, "id"): call_ids.append(tc.id) elif isinstance(tc, dict) and tc.get("id"): call_ids.append(tc["id"]) for cid in call_ids: @@ -2332,7 +2319,7 @@ def _list_qwen_models() -> list[str]: from src.vendor_capabilities import list_models_for_vendor return list_models_for_vendor("qwen") -def _send_qwen_result(md_content: str, user_message: str, base_dir: str, +def _send_qwen(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", stream: bool = False, @@ -2481,6 +2468,7 @@ def _send_llama_native(md_content: str, user_message: str, base_dir: str, return Result(data=(f"\n{thinking}\n\n" if thinking else "") + text) except Exception as exc: return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(exc), source="ai_client.llama_native", original=exc)]) + def _list_llama_models() -> list[str]: from src.vendor_capabilities import list_models_for_vendor return list_models_for_vendor("llama") @@ -2682,22 +2670,22 @@ def send_result( p = str(_provider).lower().strip() try: if p == "gemini": - res = _send_gemini_result( + res = _send_gemini( md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback, enable_tools, stream_callback, patch_callback ) elif p == "gemini_cli": - res = _send_gemini_cli_result( + res = _send_gemini_cli( md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback, stream_callback, patch_callback ) elif p == "anthropic": - res = _send_anthropic_result( + res = _send_anthropic( md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback, stream_callback=stream_callback, patch_callback=patch_callback ) elif p == "deepseek": - res = _send_deepseek_result( + res = _send_deepseek( md_content, user_message, base_dir, file_items, discussion_history, stream, pre_tool_callback, qa_callback, stream_callback, patch_callback ) @@ -2707,7 +2695,7 @@ def send_result( stream, pre_tool_callback, qa_callback, stream_callback, patch_callback ) elif p == "qwen": - res = _send_qwen_result( + res = _send_qwen( md_content, user_message, base_dir, file_items, discussion_history, stream, pre_tool_callback, qa_callback, stream_callback, patch_callback )