refactor(tool_presets): merge Tool + ToolPreset from models.py into tool_presets.py
Per the 4-criteria decision rule: Tool + ToolPreset fail C1 (only used by
tool_presets + tool_bias), fail C2 (no state machine), fail C3 (no
dedicated test file), borderline C4 (~15 lines each). MERGE into the
existing src/tool_presets.py which already has ToolPresetManager.
This commit:
1. Adds Tool + ToolPreset class definitions to src/tool_presets.py at
the top (after the stdlib imports). Both classes are used by
ToolPresetManager and the tests.
2. Removes Tool + ToolPreset from src/models.py.
3. Adds lazy re-exports via the existing __getattr__ in src/models.py
(EAGER import would deadlock because src.tool_presets imports
BiasProfile from src.models; the lazy __getattr__ breaks the cycle).
4. Updates src/tool_presets.py import: from
'from src.models import ToolPreset, BiasProfile' to
'from src.models import BiasProfile' (ToolPreset is now local).
Verification: VC8 (Tool + ToolPreset)
from src.tool_presets import Tool, ToolPreset # OK
from src.models import Tool, ToolPreset # OK (lazy __getattr__)
Tool is Tool returns True
ToolPreset is ToolPreset returns True
Tests verified (7/7 PASS):
tests/test_tool_preset_manager.py (4 tests)
tests/test_bias_models.py (3 tests)
Consumer check:
src/ai_client.py: from src.models import FileItem, ToolPreset, BiasProfile, Tool
src/app_controller.py: (no Tool/ToolPreset import)
src/tool_bias.py: from src.models import Tool, ToolPreset, BiasProfile
All resolve via re-export/lazy __getattr__.
The lazy __getattr__ pattern is the same mechanism used for the
Pydantic proxies (GenerateRequest / ConfirmRequest) and for PROVIDERS.
Phase 5 will migrate Tool/ToolPreset to a similar lazy pattern in
the re-export block (or drop them entirely after the consumer
migration).
This commit is contained in:
+16
-62
@@ -111,6 +111,12 @@ from src.project_files import (
|
||||
NamedViewPreset,
|
||||
Preset,
|
||||
)
|
||||
# Backward-compat re-exports for Tool + ToolPreset (Phase 3d ->
|
||||
# src/tool_presets.py). These are LAZY via the __getattr__ below to avoid
|
||||
# the circular import (src.tool_presets imports BiasProfile from
|
||||
# src.models; if we eagerly imported Tool from src.tool_presets, the
|
||||
# cycle would deadlock on initial load). Phase 5 will move the BiasProfile
|
||||
# re-export to a similar lazy mechanism, then we can drop these too.
|
||||
|
||||
|
||||
#region: Constants
|
||||
@@ -265,6 +271,11 @@ def __getattr__(name: str) -> Any:
|
||||
cls = _PYDANTIC_CLASS_FACTORIES[name]()
|
||||
globals()[name] = cls
|
||||
return cls
|
||||
if name in ("Tool", "ToolPreset"):
|
||||
from src.tool_presets import Tool as _Tool, ToolPreset as _ToolPreset
|
||||
val = _Tool if name == "Tool" else _ToolPreset
|
||||
globals()[name] = val
|
||||
return val
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
# MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata)
|
||||
@@ -280,61 +291,10 @@ def __getattr__(name: str) -> Any:
|
||||
# src.project_files directly.
|
||||
|
||||
#region: Tool Models
|
||||
|
||||
@dataclass
|
||||
class Tool:
|
||||
name: str
|
||||
approval: str = 'auto'
|
||||
weight: int = 3
|
||||
parameter_bias: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Metadata:
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"approval": self.approval,
|
||||
"weight": self.weight,
|
||||
"parameter_bias": self.parameter_bias,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Metadata) -> "Tool":
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return cls(
|
||||
name=data["name"],
|
||||
approval=data.get("approval", "auto"),
|
||||
weight=data.get("weight", 3),
|
||||
parameter_bias=data.get("parameter_bias", {}),
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ToolPreset:
|
||||
name: str
|
||||
categories: Dict[str, List[Union[Tool, Any]]]
|
||||
|
||||
def to_dict(self) -> Metadata:
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
serialized_categories = {}
|
||||
for cat, tools in self.categories.items():
|
||||
serialized_categories[cat] = [t.to_dict() if isinstance(t, Tool) else t for t in tools]
|
||||
return {"categories": serialized_categories}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: Metadata) -> "ToolPreset":
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
raw_categories = data.get("categories", {})
|
||||
parsed_categories = {}
|
||||
for cat, tools in raw_categories.items():
|
||||
parsed_categories[cat] = [Tool.from_dict(t) if isinstance(t, dict) else t for t in tools]
|
||||
return cls(name=name, categories=parsed_categories)
|
||||
# Tool + ToolPreset moved to src/tool_presets.py in
|
||||
# module_taxonomy_refactor_20260627 Phase 3d. The re-exports at the top of
|
||||
# this module keep 'from src.models import Tool' (and ToolPreset) working for
|
||||
# legacy callers. BiasProfile stays here until Phase 3e.
|
||||
|
||||
@dataclass
|
||||
class BiasProfile:
|
||||
@@ -343,9 +303,6 @@ class BiasProfile:
|
||||
category_multipliers: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Metadata:
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"tool_weights": self.tool_weights,
|
||||
@@ -354,14 +311,11 @@ class BiasProfile:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Metadata) -> "BiasProfile":
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return cls(
|
||||
name = data["name"],
|
||||
tool_weights = data.get("tool_weights", {}),
|
||||
category_multipliers = data.get("category_multipliers", {}),
|
||||
)
|
||||
)
|
||||
|
||||
#region: UI/Editor
|
||||
|
||||
|
||||
+51
-4
@@ -1,11 +1,58 @@
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
|
||||
from src import paths
|
||||
from src.models import ToolPreset, BiasProfile
|
||||
from src import paths
|
||||
from src.models import BiasProfile
|
||||
from src.type_aliases import Metadata
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tool:
|
||||
name: str
|
||||
approval: str = 'auto'
|
||||
weight: int = 3
|
||||
parameter_bias: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Metadata:
|
||||
return {
|
||||
"name": self.name,
|
||||
"approval": self.approval,
|
||||
"weight": self.weight,
|
||||
"parameter_bias": self.parameter_bias,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Metadata) -> "Tool":
|
||||
return cls(
|
||||
name=data["name"],
|
||||
approval=data.get("approval", "auto"),
|
||||
weight=data.get("weight", 3),
|
||||
parameter_bias=data.get("parameter_bias", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolPreset:
|
||||
name: str
|
||||
categories: Dict[str, List[Union[Tool, Any]]] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Metadata:
|
||||
serialized_categories = {}
|
||||
for cat, tools in self.categories.items():
|
||||
serialized_categories[cat] = [t.to_dict() if isinstance(t, Tool) else t for t in tools]
|
||||
return {"categories": serialized_categories}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, data: Metadata) -> "ToolPreset":
|
||||
raw_categories = data.get("categories", {})
|
||||
parsed_categories = {}
|
||||
for cat, tools in raw_categories.items():
|
||||
parsed_categories[cat] = [Tool.from_dict(t) if isinstance(t, dict) else t for t in tools]
|
||||
return cls(name=name, categories=parsed_categories)
|
||||
|
||||
|
||||
class ToolPresetManager:
|
||||
|
||||
Reference in New Issue
Block a user