refactor(personas): move Persona dataclass from models.py to personas.py

Per spec FR4 + Phase 3.4: Persona dataclass + properties (provider/model/
temperature/top_p/max_output_tokens) + to_dict/from_dict move from
src/models.py into src/personas.py (which already has the PersonaManager
ops layer). Re-export at top of models.py preserves 'from src.models
import Persona'.
This commit is contained in:
ed
2026-06-26 07:22:18 -04:00
parent cd828e5267
commit d7872bea53
2 changed files with 101 additions and 116 deletions
+6 -88
View File
@@ -76,6 +76,8 @@ from src.mma import (
TrackState,
WorkerContext,
)
# Backward-compat re-export for Persona (Phase 3.4 -> src/personas.py).
from src.personas import Persona
# Alias the old `Metadata` dataclass name to TrackMetadata so existing
# `from src.models import Metadata` keeps resolving to the dataclass.
Metadata = TrackMetadata # noqa: F401 — legacy class name re-export
@@ -530,96 +532,12 @@ class ExternalEditorConfig:
EMPTY_TEXT_EDITOR_CONFIG: TextEditorConfig = TextEditorConfig()
#region: Persona
# Persona dataclass moved to src/personas.py in module_taxonomy_refactor_20260627 Phase 3.4.
# PersonaManager (the ops layer) is also there. Re-export at the top of this module
# preserves backward-compat 'from src.models import Persona'.
@dataclass
class Persona:
name: str
preferred_models: list[Metadata] = field(default_factory=list)
system_prompt: str = ''
tool_preset: Optional[str] = None
bias_profile: Optional[str] = None
context_preset: Optional[str] = None
aggregation_strategy: Optional[str] = None
@property
def provider(self) -> str:
if not self.preferred_models: return ""
return self.preferred_models[0].get("provider") or ""
@property
def model(self) -> str:
if not self.preferred_models: return ""
return self.preferred_models[0].get("model") or ""
@property
def temperature(self) -> float:
if not self.preferred_models: return 0.0
return float(self.preferred_models[0].get("temperature") or 0.0)
@property
def top_p(self) -> float:
if not self.preferred_models: return 1.0
return float(self.preferred_models[0].get("top_p") or 1.0)
@property
def max_output_tokens(self) -> int:
if not self.preferred_models: return 0
return int(self.preferred_models[0].get("max_output_tokens") or 0)
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]
"""
res = {"system_prompt": self.system_prompt}
if self.preferred_models:
processed = []
for m in self.preferred_models:
if isinstance(m, str):
processed.append({"model": m})
else:
processed.append(m)
res["preferred_models"] = processed
if self.tool_preset is not None: res["tool_preset"] = self.tool_preset
if self.bias_profile is not None: res["bias_profile"] = self.bias_profile
if self.context_preset is not None: res["context_preset"] = self.context_preset
if self.aggregation_strategy is not None: res["aggregation_strategy"] = self.aggregation_strategy
return res
@classmethod
def from_dict(cls, name: str, data: Metadata) -> "Persona":
"""
[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_models = data.get("preferred_models", [])
parsed_models = []
for m in raw_models:
if isinstance(m, str):
parsed_models.append({"model": m})
else:
parsed_models.append(m)
legacy = {}
for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]:
if data.get(k) is not None:
legacy[k] = data[k]
if legacy:
if not parsed_models:
parsed_models.append(legacy)
else:
for k, v in legacy.items():
if k not in parsed_models[0] or parsed_models[0][k] is None:
parsed_models[0][k] = v
return cls(
name = name,
preferred_models = parsed_models,
system_prompt = data.get("system_prompt", ""),
tool_preset = data.get("tool_preset"),
bias_profile = data.get("bias_profile"),
context_preset = data.get("context_preset"),
aggregation_strategy = data.get("aggregation_strategy"),
)
#region: Workspace
#region: Workspace
@dataclass