working on ai client

This commit is contained in:
ed
2026-06-13 17:46:38 -04:00
parent 3b6a9dd0dc
commit 6caa6680bf
+57 -69
View File
@@ -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"<thinking>\n{thinking}\n</thinking>\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
)