diff --git a/scripts/tier2/artifacts/module_taxonomy_refactor_20260627/bulk_move.py b/scripts/tier2/artifacts/module_taxonomy_refactor_20260627/bulk_move.py new file mode 100644 index 00000000..2667e5c0 --- /dev/null +++ b/scripts/tier2/artifacts/module_taxonomy_refactor_20260627/bulk_move.py @@ -0,0 +1,103 @@ +"""Bulk-move remaining dataclasses from src/models.py to their target modules. + +Phase 3.5-3.9 of module_taxonomy_refactor_20260627. +""" +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(".") +MODELS = ROOT / "src" / "models.py" + +# Map: (class_name, target_file, optional region_header_for_target) +MOVES = [ + ("Tool", ROOT / "src" / "tool_presets.py", "#region: Tool + ToolPreset Dataclasses (moved from src/models.py Phase 3.5)"), + ("ToolPreset", ROOT / "src" / "tool_presets.py", None), + ("BiasProfile", ROOT / "src" / "tool_bias.py", "#region: BiasProfile Dataclass (moved from src/models.py Phase 3.6)"), + ("TextEditorConfig", ROOT / "src" / "external_editor.py","#region: Editor Config Dataclasses (moved from src/models.py Phase 3.7)"), + ("ExternalEditorConfig",ROOT / "src" / "external_editor.py", None), + ("MCPServerConfig", ROOT / "src" / "mcp_client.py", "#region: MCP Config Dataclasses (moved from src/models.py Phase 3.8)"), + ("MCPConfiguration", ROOT / "src" / "mcp_client.py", None), + ("VectorStoreConfig", ROOT / "src" / "mcp_client.py", None), + ("RAGConfig", ROOT / "src" / "mcp_client.py", None), + ("WorkspaceProfile", ROOT / "src" / "workspace_manager.py","#region: WorkspaceProfile Dataclass (moved from src/models.py Phase 3.9)"), +] + + +def find_class_block(lines: list[str], class_name: str) -> tuple[int, int]: + """Return (start_line, end_line) 0-indexed, [start, end) for the class block. + + Includes the @dataclass decorator line(s) if present. + """ + start = None + for i, line in enumerate(lines): + if line.startswith(f"class {class_name}:"): + start = i + break + if start is None: + raise ValueError(f"Class {class_name} not found") + # Look backwards for @dataclass + decorator_start = start + for i in range(start - 1, -1, -1): + line = lines[i].strip() + if line.startswith("@dataclass"): + decorator_start = i + break + if line.startswith("class ") or line.startswith("#region:") or line.startswith("#endregion:"): + break + if line == "": + continue + break # non-decorator line + # Find end: next class/def at column 0 (excluding inner methods) + end = len(lines) + for i in range(decorator_start + 1, len(lines)): + line = lines[i] + if line and not line.startswith(" ") and not line.startswith("\t"): + stripped = line.lstrip() + if re.match(r"^(class |def |@dataclass|#region:|#endregion:)", stripped): + end = i + break + return decorator_start, end + + +def main() -> None: + source = MODELS.read_text(encoding="utf-8") + lines = source.splitlines(keepends=True) + + # Verify each class exists first + ranges = [] + for class_name, target_file, region_header in MOVES: + s, e = find_class_block(lines, class_name) + ranges.append((class_name, target_file, region_header, s, e)) + print(f"Found {class_name}: lines {s+1}-{e} ({e-s} lines)") + + # Write each target file (append) + by_target: dict[Path, list] = {} + for class_name, target_file, region_header, s, e in ranges: + by_target.setdefault(target_file, []).append((class_name, region_header, s, e)) + + for target_file, items in by_target.items(): + with target_file.open("a", encoding="utf-8") as f: + for class_name, region_header, _, _ in items: + s, e = find_class_block(lines, class_name) + block = "".join(lines[s:e]) + if region_header: + f.write(f"\n\n{region_header}\n{block}") + else: + f.write(f"\n\n{block}") + print(f"Appended {len(items)} classes to {target_file}") + + # Remove from models.py in reverse line order + sorted_ranges = sorted(ranges, key=lambda r: r[3], reverse=True) + new_lines = list(lines) + for class_name, _, _, s, e in sorted_ranges: + del new_lines[s:e] + print(f"Removed {class_name} from models.py") + + MODELS.write_text("".join(new_lines), encoding="utf-8") + print("models.py updated") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py b/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py new file mode 100644 index 00000000..8b32a9d8 --- /dev/null +++ b/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py @@ -0,0 +1,120 @@ +"""Fix script: replace 'models.' with '' and add imports. + +After the migration of 'from src.models import X' to direct imports, +the 'models.' attribute access pattern still exists in +many files. The shim previously supported this via __getattr__, but +Phase 2.3 removed the shim. This script: + 1. Finds all 'models.' references + 2. For each file, adds 'from src. import ' at + the top (if not already present) + 3. Replaces 'models.' with '' in the body + +NOT touched: + - models.GenerateRequest, models.ConfirmRequest (Phase 4) + - models.DEFAULT_TOOL_CATEGORIES (Phase 3) + - models.PROVIDERS, models.Metadata (kept on models) +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +CLASS_TO_MODULE: dict[str, str] = { + "Ticket": "mma", + "Track": "mma", + "WorkerContext": "mma", + "TrackState": "mma", + "TrackMetadata": "mma", + "ThinkingSegment": "mma", + "EMPTY_TRACK_STATE": "mma", + "ProjectContext": "project", + "ProjectMeta": "project", + "ProjectOutput": "project", + "ProjectFiles": "project", + "ProjectScreenshots": "project", + "ProjectDiscussion": "project", + "EMPTY_PROJECT_CONTEXT": "project", + "FileItem": "project_files", + "Preset": "project_files", + "ContextPreset": "project_files", + "ContextFileEntry": "project_files", + "NamedViewPreset": "project_files", + "Tool": "tool_presets", + "ToolPreset": "tool_presets", + "BiasProfile": "tool_bias", + "TextEditorConfig": "external_editor", + "ExternalEditorConfig": "external_editor", + "EMPTY_TEXT_EDITOR_CONFIG": "external_editor", + "Persona": "personas", + "WorkspaceProfile": "workspace_manager", + "MCPServerConfig": "mcp_client", + "MCPConfiguration": "mcp_client", + "VectorStoreConfig": "mcp_client", + "RAGConfig": "mcp_client", + "load_mcp_config": "mcp_client", +} + + +def migrate_file(path: Path) -> int: + """Rewrite 'models.' references in path. Returns count of changed lines.""" + try: + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return 0 + original = content + used_classes: set[str] = set() + + for cls in CLASS_TO_MODULE: + pattern = re.compile(rf"\bmodels\.{re.escape(cls)}\b") + if pattern.search(content): + content = pattern.sub(cls, content) + used_classes.add(cls) + if content == original: + return 0 + + for cls in sorted(used_classes): + mod = CLASS_TO_MODULE[cls] + import_line = f"from src.{mod} import {cls}" + if re.search(rf"^from\s+src\.{re.escape(mod)}\s+import\s+.*\b{re.escape(cls)}\b", content, re.MULTILINE): + continue + if not re.search(rf"^from\s+src\.{mod}\s+import\s", content, re.MULTILINE): + content = re.sub( + r"^(from __future__ import annotations\n)", + rf"\1{import_line}\n", + content, + count=1, + ) + else: + content = re.sub( + rf"^(from\s+src\.{re.escape(mod)}\s+import\s+[^\n]+)$", + rf"\1, {cls}", + content, + count=1, + flags=re.MULTILINE, + ) + try: + path.write_text(content, encoding="utf-8", newline="") + except OSError: + return 0 + return len(used_classes) + + +def main() -> int: + root = Path(".") + src_files = sorted(root.glob("src/*.py")) + sorted(root.glob("tests/*.py")) + total_files = 0 + total_classes = 0 + for path in src_files: + count = migrate_file(path) + if count > 0: + total_files += 1 + total_classes += count + print(f" {path}: {count} class ref(s) updated") + print(f"\nTotal: {total_classes} class ref(s) updated in {total_files} file(s)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_ai_client.py b/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_ai_client.py new file mode 100644 index 00000000..8e8f5a3e --- /dev/null +++ b/scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_ai_client.py @@ -0,0 +1,3553 @@ +# ai_client.py +from __future__ import annotations +""" +Note(Gemini): +Acts as the unified interface for multiple LLM providers (Anthropic, Gemini). +Abstracts away the differences in how they handle tool schemas, history, and caching. + +For Anthropic: aggressively manages the ~200k token limit by manually culling +stale [FILES UPDATED] entries and dropping the oldest message pairs. + +For Gemini: injects the initial context directly into system_instruction +during chat creation to avoid massive history bloat. + +HEAVY IMPORTS (startup_speedup_20260606): The heavy SDKs (anthropic, +google.genai, openai, google.genai.types, requests) are NOT imported +at module level. They are warmed on AppController's _io_pool at +startup and accessed via _require_warmed() below. This keeps the +main thread's import chain lean and the GUI responsive on startup. +""" + +import importlib +import asyncio +import datetime +import difflib +import hashlib +import json +import os +import sys +import threading +import time +import tomllib +from dataclasses import dataclass + +# TODO(Ed): Eliminate These? +from collections import deque +from pathlib import Path as _P +from pathlib import Path +from typing import Optional, Callable, Any, List, Union, cast, Iterable + +from src import project_manager +from src import file_cache +from src import mcp_client +from src import mcp_tool_specs +from src import mma_prompts +from src import performance_monitor +from src import project_manager +from src import provider_state +from src.events import EventEmitter +from src.gemini_cli_adapter import GeminiCliAdapter +from src.models import FileItem, ToolPreset, BiasProfile, Tool +from src.paths import get_credentials_path +from src.tool_bias import ToolBiasEngine +from src.tool_presets import ToolPresetManager + +# VendorCapabilities, get_capabilities, list_models_for_vendor, register +# are defined in this file (see '#region: Vendor Capabilities'). Previously +# imported from src/vendor_capabilities.py (deleted in +# module_taxonomy_refactor_20260627 Phase 2.1). + +PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] + +# _require_warmed lives +# _require_warmed lives in src/module_loader.py to avoid duplicating the +# lookup logic across files that need heavy modules. Re-exported here so +# 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.type_aliases import ( + CommsLog, + CommsLogCallback, + CommsLogEntry, + FileItem, + FileItems, + History, + HistoryMessage, + Metadata, + ToolCall, + ToolDefinition, +) + +_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 + +# Global event emitter for API lifecycle events +events: EventEmitter = EventEmitter() + +#region: Provider Configuration + +def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: float = 1.0) -> None: + """Sets global generation parameters like temperature and max tokens.""" + global _temperature, _max_tokens, _history_trunc_limit, _top_p + _temperature = temp + _max_tokens = max_tok + _history_trunc_limit = trunc_limit + _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_cache_created_at: Optional[float] = None +_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 + +_deepseek_client: Any = None + +_minimax_client: Any = None + +_qwen_client: Any = None +_qwen_region: str = "china" + +_grok_client: Any = None + +_llama_client: Any = None +_llama_base_url: str = "http://localhost:11434/v1" +_llama_api_key: str = "ollama" + +_send_lock: threading.Lock = threading.Lock() + +_BIAS_ENGINE = ToolBiasEngine() +_active_tool_preset: Optional[ToolPreset] = None +_active_bias_profile: Optional[BiasProfile] = None + +_gemini_cli_adapter: Optional[GeminiCliAdapter] = None + +# Injected by gui.py - called when AI wants to run a command. +confirm_and_run_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]], Optional[Callable[[str, str], Result[str]]]], Optional[str]]] = None + +# Injected by gui.py - called whenever a comms entry is appended. +# Use get_comms_log_callback/set_comms_log_callback for thread-safe access. +comms_log_callback: Optional[CommsLogCallback] = None + +# Injected by gui.py - called whenever a tool call completes. +tool_log_callback: Optional[Callable[[str, str], None]] = None + +_local_storage = threading.local() + +_tool_approval_modes: dict[str, str] = {} + +def get_current_tier_result() -> Result[str]: + """Returns the current tier from thread-local storage as a Result.""" + return Result(data=getattr(_local_storage, "current_tier", None)) + +def set_current_tier(tier: Optional[str]) -> None: + """Sets the current tier in thread-local storage.""" + _local_storage.current_tier = tier + +# Increased to allow thorough code exploration before forcing a summary +MAX_TOOL_ROUNDS: int = 10 + +# Maximum cumulative bytes of tool output allowed per send() call. +_MAX_TOOL_OUTPUT_BYTES: int = 500_000 + +# Maximum characters per text chunk sent to Anthropic. +_ANTHROPIC_CHUNK_SIZE: int = 120_000 + +_SYSTEM_PROMPT: str = ( + "You are a helpful coding assistant with access to a PowerShell tool (run_powershell) and MCP tools (file access: read_file, list_directory, search_files, get_file_summary, web access: web_search, fetch_url). " + "When calling file/directory tools, always use the 'path' parameter for the target path. " + "When asked to create or edit files, prefer targeted edits over full rewrites. " + "Always explain what you are doing before invoking the tool.\n\n" + "When writing or rewriting large files (especially those containing quotes, backticks, or special characters), " + "avoid python -c with inline strings. Instead: (1) write a .py helper script to disk using a PS here-string " + "(@'...'@ for literal content), (2) run it with `python