From 9e07fac1db32e9dbe4180e7ffb54eb49579a2f4d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 26 Jun 2026 14:06:03 -0400 Subject: [PATCH] refactor(consumers): replace 'models.' with direct imports Per post_module_taxonomy_de_cruft_20260627 Phase 2 (FR7 continued). The previous migration commit (8f11340b) handled the 'from src.models import X' pattern (85 sites). This commit handles the 'models.' attribute access pattern (44 sites in 20 files), which the __getattr__ shim previously supported. The migration was performed by the one-time script scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py which: 1. For each 'models.' reference, replaces it with the bare class name (e.g., 'models.MCPConfiguration' -> 'MCPConfiguration') 2. Adds the import 'from src. import ' at the top of the file (deduplicated if the import already exists) 3. Skips moved classes that the file already imports directly The migration script inserts the import after the 'from __future__ import annotations' line if present; otherwise it adds the import to the destination module's existing import block. Two files required manual fixes because the script's regex didn't handle them: - src/rag_engine.py: uses 'from src import models' (not 'from src.models import X'); the class is accessed via 'models.RAGConfig'. Replaced with a direct 'from src.mcp_client import RAGConfig' import and removed the 'from src import models'. - tests/test_project_context_20260627.py: uses the parens-style multi-line 'from src.models import (X, Y, Z)'. Replaced with the parens-style direct import. After this commit: - 'models.MCPConfiguration', 'models.FileItem', 'models.Ticket', etc. no longer work in src/ and tests/ (the AttributeError raises because models.py no longer has the __getattr__ entries for moved classes) - All consumer files have direct imports of the moved classes Total: 44 'models.' references rewritten across 20 files. --- .../bulk_move.py | 103 + .../migrate_models_attr.py | 120 + .../resolved_ai_client.py | 3553 +++++++++++++++++ .../resolved_personas.py | 171 + .../resolved_spec.md | 224 ++ .../resolved_tool_bias.py | 93 + .../resolved_tool_presets.py | 186 + .../resolved_workspace_manager.py | 109 + src/app_controller.py | 112 +- src/gui_2.py | 60 +- src/project_manager.py | 2 +- src/rag_engine.py | 9 +- src/type_aliases.py | 2 +- tests/test_ast_inspector_extended.py | 2 +- tests/test_auto_slices.py | 2 +- tests/test_external_mcp.py | 4 +- tests/test_files_and_media_tree.py | 2 +- tests/test_gui_2_result.py | 2 +- tests/test_gui_kill_button.py | 2 +- tests/test_gui_progress.py | 2 +- tests/test_mcp_config.py | 8 +- tests/test_metadata_promotion_phase1.py | 16 +- tests/test_project_context_20260627.py | 2 +- tests/test_project_serialization.py | 18 +- tests/test_rag_engine.py | 16 +- tests/test_rag_engine_ready_status_bug.py | 8 +- tests/test_rag_integration.py | 4 +- tests/test_ui_summary_only_removal.py | 6 +- tests/test_view_presets.py | 8 +- 29 files changed, 4706 insertions(+), 140 deletions(-) create mode 100644 scripts/tier2/artifacts/module_taxonomy_refactor_20260627/bulk_move.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_ai_client.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_personas.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_spec.md create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_tool_bias.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_tool_presets.py create mode 100644 scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/resolved_workspace_manager.py 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