refactor(gui): redesign persona editor UI and replace popup modals with standard windows

This commit is contained in:
2026-03-10 23:21:14 -04:00
parent 6ae8737c1a
commit 6da3d95c0e
5 changed files with 654 additions and 537 deletions

View File

@@ -300,7 +300,9 @@ class AppController:
self._inject_mode: str = "skeleton" self._inject_mode: str = "skeleton"
self._inject_preview: str = "" self._inject_preview: str = ""
self._show_inject_modal: bool = False self._show_inject_modal: bool = False
self.show_preset_manager_modal: bool = False self.show_preset_manager_window: bool = False
self.show_tool_preset_manager_window: bool = False
self.show_persona_editor_window: bool = False
self._editing_preset_name: str = "" self._editing_preset_name: str = ""
self._editing_preset_content: str = "" self._editing_preset_content: str = ""
self._editing_preset_temperature: float = 0.0 self._editing_preset_temperature: float = 0.0
@@ -342,7 +344,9 @@ class AppController:
'ui_active_tool_preset': 'ui_active_tool_preset', 'ui_active_tool_preset': 'ui_active_tool_preset',
'temperature': 'temperature', 'temperature': 'temperature',
'max_tokens': 'max_tokens', 'max_tokens': 'max_tokens',
'show_preset_manager_modal': 'show_preset_manager_modal', 'show_preset_manager_window': 'show_preset_manager_window',
'show_tool_preset_manager_window': 'show_tool_preset_manager_window',
'show_persona_editor_window': 'show_persona_editor_window',
'_editing_preset_name': '_editing_preset_name', '_editing_preset_name': '_editing_preset_name',
'_editing_preset_content': '_editing_preset_content', '_editing_preset_content': '_editing_preset_content',
'_editing_preset_temperature': '_editing_preset_temperature', '_editing_preset_temperature': '_editing_preset_temperature',
@@ -390,7 +394,9 @@ class AppController:
'ui_active_tool_preset': 'ui_active_tool_preset', 'ui_active_tool_preset': 'ui_active_tool_preset',
'temperature': 'temperature', 'temperature': 'temperature',
'max_tokens': 'max_tokens', 'max_tokens': 'max_tokens',
'show_preset_manager_modal': 'show_preset_manager_modal', 'show_preset_manager_window': 'show_preset_manager_window',
'show_tool_preset_manager_window': 'show_tool_preset_manager_window',
'show_persona_editor_window': 'show_persona_editor_window',
'_editing_preset_name': '_editing_preset_name', '_editing_preset_name': '_editing_preset_name',
'_editing_preset_content': '_editing_preset_content', '_editing_preset_content': '_editing_preset_content',
'_editing_preset_temperature': '_editing_preset_temperature', '_editing_preset_temperature': '_editing_preset_temperature',
@@ -2567,3 +2573,4 @@ class AppController:
tasks=self.active_track.tickets tasks=self.active_track.tickets
) )
project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir) project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir)

View File

@@ -96,9 +96,9 @@ class App:
self.controller.init_state() self.controller.init_state()
self.show_windows.setdefault("Diagnostics", False) self.show_windows.setdefault("Diagnostics", False)
self.controller.start_services(self) self.controller.start_services(self)
self.show_preset_manager_modal = False self.show_preset_manager_window = False
self.show_tool_preset_manager_modal = False self.show_tool_preset_manager_window = False
self.show_persona_editor_modal = False self.show_persona_editor_window = False
self.ui_active_tool_preset = "" self.ui_active_tool_preset = ""
self.ui_active_bias_profile = "" self.ui_active_bias_profile = ""
self.ui_active_persona = "" self.ui_active_persona = ""
@@ -111,7 +111,7 @@ class App:
self._editing_persona_max_tokens = 4096 self._editing_persona_max_tokens = 4096
self._editing_persona_tool_preset_id = "" self._editing_persona_tool_preset_id = ""
self._editing_persona_bias_profile_id = "" self._editing_persona_bias_profile_id = ""
self._editing_persona_preferred_models_list: list[str] = [] self._editing_persona_preferred_models_list: list[dict] = []
self._editing_persona_scope = "project" self._editing_persona_scope = "project"
self._editing_persona_is_new = True self._editing_persona_is_new = True
self._persona_editor_opened = False self._persona_editor_opened = False
@@ -384,9 +384,9 @@ class App:
self._render_track_proposal_modal() self._render_track_proposal_modal()
self._render_patch_modal() self._render_patch_modal()
self._render_save_preset_modal() self._render_save_preset_modal()
self._render_preset_manager_modal() self._render_preset_manager_window()
self._render_tool_preset_manager_modal() self._render_tool_preset_manager_window()
self._render_persona_editor_modal() self._render_persona_editor_window()
# Auto-save (every 60s) # Auto-save (every 60s)
now = time.time() now = time.time()
if now - self._last_autosave >= self._autosave_interval: if now - self._last_autosave >= self._autosave_interval:
@@ -923,11 +923,16 @@ class App:
imgui.close_current_popup() imgui.close_current_popup()
imgui.end_popup() imgui.end_popup()
def _render_preset_manager_modal(self) -> None: def _render_preset_manager_window(self, is_embedded: bool = False) -> None:
if not self.show_preset_manager_modal: return if not self.show_preset_manager_window and not is_embedded: return
imgui.open_popup("Preset Manager")
opened, self.show_preset_manager_modal = imgui.begin_popup_modal("Preset Manager", self.show_preset_manager_modal) if not is_embedded:
if opened: imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever)
opened, self.show_preset_manager_window = imgui.begin("Preset Manager", self.show_preset_manager_window)
if not opened:
imgui.end()
return
try: try:
avail = imgui.get_content_region_avail() avail = imgui.get_content_region_avail()
imgui.begin_child("preset_list_area", imgui.ImVec2(250, avail.y), True) imgui.begin_child("preset_list_area", imgui.ImVec2(250, avail.y), True)
@@ -985,20 +990,26 @@ class App:
except Exception as e: except Exception as e:
self.ai_status = f"Error deleting: {e}" self.ai_status = f"Error deleting: {e}"
imgui.set_item_tooltip("Delete the selected preset") imgui.set_item_tooltip("Delete the selected preset")
if not is_embedded:
imgui.same_line() imgui.same_line()
if imgui.button("Close", imgui.ImVec2(120, 0)): if imgui.button("Close", imgui.ImVec2(120, 0)):
self.show_preset_manager_modal = False self.show_preset_manager_window = False
imgui.close_current_popup()
finally: finally:
imgui.end_child() imgui.end_child()
finally: finally:
imgui.end_popup() if not is_embedded:
imgui.end()
def _render_tool_preset_manager_window(self, is_embedded: bool = False) -> None:
if not self.show_tool_preset_manager_window and not is_embedded: return
if not is_embedded:
imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever)
opened, self.show_tool_preset_manager_window = imgui.begin("Tool Preset Manager", self.show_tool_preset_manager_window)
if not opened:
imgui.end()
return
def _render_tool_preset_manager_modal(self) -> None:
if not self.show_tool_preset_manager_modal: return
imgui.open_popup("Tool Preset Manager")
opened, self.show_tool_preset_manager_modal = imgui.begin_popup_modal("Tool Preset Manager", self.show_tool_preset_manager_modal)
if opened:
try: try:
avail = imgui.get_content_region_avail() avail = imgui.get_content_region_avail()
# Left Column: Listbox # Left Column: Listbox
@@ -1221,21 +1232,25 @@ class App:
if imgui.is_item_hovered(): if imgui.is_item_hovered():
imgui.set_tooltip("Delete this tool preset permanently.") imgui.set_tooltip("Delete this tool preset permanently.")
if not is_embedded:
imgui.same_line() imgui.same_line()
if imgui.button("Close", imgui.ImVec2(100, 0)): if imgui.button("Close", imgui.ImVec2(100, 0)):
self.show_tool_preset_manager_modal = False self.show_tool_preset_manager_window = False
imgui.close_current_popup()
finally: finally:
imgui.end_child() imgui.end_child()
finally: finally:
imgui.end_popup() if not is_embedded:
imgui.end()
def _render_persona_editor_window(self, is_embedded: bool = False) -> None:
if not self.show_persona_editor_window and not is_embedded: return
if not is_embedded:
imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever)
opened, self.show_persona_editor_window = imgui.begin("Persona Editor", self.show_persona_editor_window)
if not opened:
imgui.end()
return
def _render_persona_editor_modal(self) -> None:
if not self.show_persona_editor_modal: return
imgui.open_popup("Persona Editor")
imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever)
opened, self.show_persona_editor_modal = imgui.begin_popup_modal("Persona Editor", self.show_persona_editor_modal)
if opened:
try: try:
avail = imgui.get_content_region_avail() avail = imgui.get_content_region_avail()
# Left Pane: List of Personas # Left Pane: List of Personas
@@ -1267,7 +1282,7 @@ class App:
self._editing_persona_max_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096 self._editing_persona_max_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096
self._editing_persona_tool_preset_id = p.tool_preset or "" self._editing_persona_tool_preset_id = p.tool_preset or ""
self._editing_persona_bias_profile_id = p.bias_profile or "" self._editing_persona_bias_profile_id = p.bias_profile or ""
self._editing_persona_preferred_models_list = list(p.preferred_models) if p.preferred_models else [] self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name) self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name)
self._editing_persona_is_new = False self._editing_persona_is_new = False
finally: finally:
@@ -1295,6 +1310,7 @@ class App:
imgui.separator() imgui.separator()
imgui.text("Default Provider/Model (used if Preferred Models list is empty):")
imgui.text("Provider:") imgui.text("Provider:")
imgui.same_line() imgui.same_line()
providers = self.controller.PROVIDERS providers = self.controller.PROVIDERS
@@ -1304,6 +1320,7 @@ class App:
self._editing_persona_provider = providers[p_idx - 1] if p_idx > 0 else "" self._editing_persona_provider = providers[p_idx - 1] if p_idx > 0 else ""
imgui.pop_item_width() imgui.pop_item_width()
imgui.same_line()
imgui.text("Model:") imgui.text("Model:")
imgui.same_line() imgui.same_line()
all_models = self.controller.all_available_models.get(self._editing_persona_provider, []) all_models = self.controller.all_available_models.get(self._editing_persona_provider, [])
@@ -1317,12 +1334,16 @@ class App:
imgui.text("Temp:") imgui.text("Temp:")
imgui.same_line() imgui.same_line()
imgui.set_next_item_width(100)
_, self._editing_persona_temperature = imgui.slider_float("##ptemp", self._editing_persona_temperature, 0.0, 2.0) _, self._editing_persona_temperature = imgui.slider_float("##ptemp", self._editing_persona_temperature, 0.0, 2.0)
imgui.text("Max Output Tokens:")
imgui.same_line() imgui.same_line()
imgui.text("Max Tok:")
imgui.same_line()
imgui.set_next_item_width(100)
_, self._editing_persona_max_tokens = imgui.input_int("##pmaxt", self._editing_persona_max_tokens) _, self._editing_persona_max_tokens = imgui.input_int("##pmaxt", self._editing_persona_max_tokens)
imgui.separator()
imgui.text("Tool Preset:") imgui.text("Tool Preset:")
imgui.same_line() imgui.same_line()
t_preset_names = ["None"] + sorted(self.controller.tool_presets.keys()) t_preset_names = ["None"] + sorted(self.controller.tool_presets.keys())
@@ -1332,6 +1353,7 @@ class App:
self._editing_persona_tool_preset_id = t_preset_names[t_idx] if t_idx > 0 else "" self._editing_persona_tool_preset_id = t_preset_names[t_idx] if t_idx > 0 else ""
imgui.pop_item_width() imgui.pop_item_width()
imgui.same_line()
imgui.text("Bias Profile:") imgui.text("Bias Profile:")
imgui.same_line() imgui.same_line()
bias_names = ["None"] + sorted(self.controller.bias_profiles.keys()) bias_names = ["None"] + sorted(self.controller.bias_profiles.keys())
@@ -1342,31 +1364,62 @@ class App:
imgui.pop_item_width() imgui.pop_item_width()
imgui.separator() imgui.separator()
imgui.text("Preferred Models:") imgui.text("Preferred Models List:")
imgui.begin_child("pref_models_list", imgui.ImVec2(0, 200), True)
to_remove = [] to_remove = []
for i, model in enumerate(self._editing_persona_preferred_models_list): for i, entry in enumerate(self._editing_persona_preferred_models_list):
imgui.text(f"- {model}") imgui.push_id(f"pref_model_{i}")
imgui.text(f"{i+1}.")
imgui.same_line() imgui.same_line()
if imgui.button(f"x##pref_rem_{i}"):
# Provider
imgui.set_next_item_width(120)
prov = entry.get("provider", "")
p_idx = providers.index(prov) + 1 if prov in providers else 0
changed_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers)
if changed_p:
entry["provider"] = providers[p_idx-1] if p_idx > 0 else ""
imgui.same_line()
# Model
imgui.set_next_item_width(200)
curr_prov = entry.get("provider", "")
m_list = self.controller.all_available_models.get(curr_prov, [])
model = entry.get("model", "")
m_idx = m_list.index(model) + 1 if model in m_list else 0
changed_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list)
if changed_m:
entry["model"] = m_list[m_idx-1] if m_idx > 0 else ""
imgui.same_line()
imgui.text("T:")
imgui.same_line()
# Temp
imgui.set_next_item_width(60)
_, entry["temperature"] = imgui.input_float("##temp", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f")
imgui.same_line()
imgui.text("M:")
imgui.same_line()
# MaxTok
imgui.set_next_item_width(80)
_, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096))
imgui.same_line()
if imgui.button("x"):
to_remove.append(i) to_remove.append(i)
imgui.pop_id()
for i in reversed(to_remove): for i in reversed(to_remove):
self._editing_persona_preferred_models_list.pop(i) self._editing_persona_preferred_models_list.pop(i)
imgui.end_child()
# Add Preferred Model if imgui.button("Add Preferred Model"):
all_possible_models = [] self._editing_persona_preferred_models_list.append({
for prov_models in self.controller.all_available_models.values(): "provider": self._editing_persona_provider or self.current_provider,
all_possible_models.extend(prov_models) "model": self._editing_persona_model or self.current_model,
all_possible_models = sorted(list(set(all_possible_models))) "temperature": self._editing_persona_temperature,
"max_output_tokens": self._editing_persona_max_tokens
if not hasattr(self, "_add_pref_model_idx"): self._add_pref_model_idx = 0 })
imgui.push_item_width(200)
_, self._add_pref_model_idx = imgui.combo("Add Preferred Model##add_pref", self._add_pref_model_idx, ["Select Model..."] + all_possible_models)
imgui.pop_item_width()
if self._add_pref_model_idx > 0:
new_m = all_possible_models[self._add_pref_model_idx - 1]
if new_m not in self._editing_persona_preferred_models_list:
self._editing_persona_preferred_models_list.append(new_m)
self._add_pref_model_idx = 0
imgui.separator() imgui.separator()
imgui.text("System Prompt:") imgui.text("System Prompt:")
@@ -1396,21 +1449,31 @@ class App:
if imgui.button("Save Persona", imgui.ImVec2(120, 0)): if imgui.button("Save Persona", imgui.ImVec2(120, 0)):
if self._editing_persona_name.strip(): if self._editing_persona_name.strip():
try: try:
# If preferred list is empty, maybe we should add the default one?
# The models.Persona properties already handle returning None/default if list is empty.
# But for saving, we should probably ensure the list isn't empty if we have values.
save_models = copy.deepcopy(self._editing_persona_preferred_models_list)
if not save_models and self._editing_persona_model:
save_models.append({
"provider": self._editing_persona_provider,
"model": self._editing_persona_model,
"temperature": self._editing_persona_temperature,
"max_output_tokens": self._editing_persona_max_tokens
})
persona = models.Persona( persona = models.Persona(
name=self._editing_persona_name.strip(), name=self._editing_persona_name.strip(),
provider=self._editing_persona_provider or None,
model=self._editing_persona_model or None,
system_prompt=self._editing_persona_system_prompt, system_prompt=self._editing_persona_system_prompt,
temperature=self._editing_persona_temperature,
max_output_tokens=self._editing_persona_max_tokens,
tool_preset=self._editing_persona_tool_preset_id or None, tool_preset=self._editing_persona_tool_preset_id or None,
bias_profile=self._editing_persona_bias_profile_id or None, bias_profile=self._editing_persona_bias_profile_id or None,
preferred_models=self._editing_persona_preferred_models_list, preferred_models=save_models,
) )
self.controller._cb_save_persona(persona, self._editing_persona_scope) self.controller._cb_save_persona(persona, self._editing_persona_scope)
self.ai_status = f"Saved Persona: {persona.name}" self.ai_status = f"Saved Persona: {persona.name}"
except Exception as e: except Exception as e:
self.ai_status = f"Error saving persona: {e}" self.ai_status = f"Error saving persona: {e}"
import traceback
traceback.print_exc()
else: else:
self.ai_status = "Name required" self.ai_status = "Name required"
@@ -1422,14 +1485,15 @@ class App:
self._editing_persona_name = "" self._editing_persona_name = ""
self._editing_persona_is_new = True self._editing_persona_is_new = True
if not is_embedded:
imgui.same_line() imgui.same_line()
if imgui.button("Close", imgui.ImVec2(100, 0)): if imgui.button("Close", imgui.ImVec2(100, 0)):
self.show_persona_editor_modal = False self.show_persona_editor_window = False
imgui.close_current_popup()
finally: finally:
imgui.end_child() imgui.end_child()
finally: finally:
imgui.end_popup() if not is_embedded:
imgui.end()
def _render_projects_panel(self) -> None: def _render_projects_panel(self) -> None:
@@ -2274,7 +2338,7 @@ def hello():
imgui.end_combo() imgui.end_combo()
imgui.same_line() imgui.same_line()
if imgui.button("Manage Personas"): if imgui.button("Manage Personas"):
self.show_persona_editor_modal = True self.show_persona_editor_window = True
if self.ui_active_persona and self.ui_active_persona in personas: if self.ui_active_persona and self.ui_active_persona in personas:
persona = personas[self.ui_active_persona] persona = personas[self.ui_active_persona]
self._editing_persona_name = persona.name self._editing_persona_name = persona.name
@@ -3589,7 +3653,7 @@ def hello():
imgui.end_combo() imgui.end_combo()
imgui.same_line(0, 8) imgui.same_line(0, 8)
if imgui.button("Manage Presets##global"): if imgui.button("Manage Presets##global"):
self.show_preset_manager_modal = True self.show_preset_manager_window = True
imgui.set_item_tooltip("Open preset management modal") imgui.set_item_tooltip("Open preset management modal")
ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100))
imgui.separator() imgui.separator()
@@ -3606,7 +3670,7 @@ def hello():
imgui.end_combo() imgui.end_combo()
imgui.same_line(0, 8) imgui.same_line(0, 8)
if imgui.button("Manage Presets##project"): if imgui.button("Manage Presets##project"):
self.show_preset_manager_modal = True self.show_preset_manager_window = True
imgui.set_item_tooltip("Open preset management modal") imgui.set_item_tooltip("Open preset management modal")
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
def _render_agent_tools_panel(self) -> None: def _render_agent_tools_panel(self) -> None:
@@ -3628,7 +3692,7 @@ def hello():
imgui.same_line() imgui.same_line()
if imgui.button("Manage Presets##tools"): if imgui.button("Manage Presets##tools"):
self.show_tool_preset_manager_modal = True self.show_tool_preset_manager_window = True
if imgui.is_item_hovered(): if imgui.is_item_hovered():
imgui.set_tooltip("Configure tool availability and default modes.") imgui.set_tooltip("Configure tool availability and default modes.")
@@ -3847,3 +3911,4 @@ def main() -> None:
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -434,32 +434,48 @@ class BiasProfile:
@dataclass @dataclass
class Persona: class Persona:
name: str name: str
provider: Optional[str] = None preferred_models: List[Dict[str, Any]] = field(default_factory=list)
model: Optional[str] = None
preferred_models: List[str] = field(default_factory=list)
system_prompt: str = '' system_prompt: str = ''
temperature: Optional[float] = None
top_p: Optional[float] = None
max_output_tokens: Optional[int] = None
tool_preset: Optional[str] = None tool_preset: Optional[str] = None
bias_profile: Optional[str] = None bias_profile: Optional[str] = None
@property
def provider(self) -> Optional[str]:
if not self.preferred_models: return None
return self.preferred_models[0].get("provider")
@property
def model(self) -> Optional[str]:
if not self.preferred_models: return None
return self.preferred_models[0].get("model")
@property
def temperature(self) -> Optional[float]:
if not self.preferred_models: return None
return self.preferred_models[0].get("temperature")
@property
def top_p(self) -> Optional[float]:
if not self.preferred_models: return None
return self.preferred_models[0].get("top_p")
@property
def max_output_tokens(self) -> Optional[int]:
if not self.preferred_models: return None
return self.preferred_models[0].get("max_output_tokens")
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
res = { res = {
"system_prompt": self.system_prompt, "system_prompt": self.system_prompt,
} }
if self.provider is not None:
res["provider"] = self.provider
if self.model is not None:
res["model"] = self.model
if self.preferred_models: if self.preferred_models:
res["preferred_models"] = self.preferred_models processed = []
if self.temperature is not None: for m in self.preferred_models:
res["temperature"] = self.temperature if isinstance(m, str):
if self.top_p is not None: processed.append({"model": m})
res["top_p"] = self.top_p else:
if self.max_output_tokens is not None: processed.append(m)
res["max_output_tokens"] = self.max_output_tokens res["preferred_models"] = processed
if self.tool_preset is not None: if self.tool_preset is not None:
res["tool_preset"] = self.tool_preset res["tool_preset"] = self.tool_preset
if self.bias_profile is not None: if self.bias_profile is not None:
@@ -468,15 +484,34 @@ class Persona:
@classmethod @classmethod
def from_dict(cls, name: str, data: Dict[str, Any]) -> "Persona": def from_dict(cls, name: str, data: Dict[str, Any]) -> "Persona":
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)
# Migration logic: merge legacy fields if they exist
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:
# Merge into first item if it's missing these specific legacy fields
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( return cls(
name=name, name=name,
provider=data.get("provider"), preferred_models=parsed_models,
model=data.get("model"),
preferred_models=data.get("preferred_models", []),
system_prompt=data.get("system_prompt", ""), system_prompt=data.get("system_prompt", ""),
temperature=data.get("temperature"),
top_p=data.get("top_p"),
max_output_tokens=data.get("max_output_tokens"),
tool_preset=data.get("tool_preset"), tool_preset=data.get("tool_preset"),
bias_profile=data.get("bias_profile"), bias_profile=data.get("bias_profile"),
) )

View File

@@ -59,7 +59,7 @@ def test_load_all_merged(temp_paths):
def test_save_persona(temp_paths): def test_save_persona(temp_paths):
manager = PersonaManager(project_root=temp_paths["project_root"]) manager = PersonaManager(project_root=temp_paths["project_root"])
persona = Persona(name="New", provider="gemini", system_prompt="Test") persona = Persona(name="New", preferred_models=[{"provider": "gemini"}], system_prompt="Test")
manager.save_persona(persona, scope="project") manager.save_persona(persona, scope="project")
loaded = manager.load_all() loaded = manager.load_all()

View File

@@ -4,30 +4,38 @@ from src.models import Persona
def test_persona_serialization(): def test_persona_serialization():
persona = Persona( persona = Persona(
name="SecuritySpecialist", name="SecuritySpecialist",
provider="anthropic", preferred_models=[
model="claude-3-7-sonnet-20250219", {"provider": "anthropic", "model": "claude-3-7-sonnet-20250219", "temperature": 0.2, "top_p": 0.9, "max_output_tokens": 4000},
preferred_models=["claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022"], "claude-3-5-sonnet-20241022"
],
system_prompt="You are a security expert.", system_prompt="You are a security expert.",
temperature=0.2,
top_p=0.9,
max_output_tokens=4000,
tool_preset="SecurityTools", tool_preset="SecurityTools",
bias_profile="Execution-Focused" bias_profile="Execution-Focused"
) )
assert persona.provider == "anthropic"
assert persona.model == "claude-3-7-sonnet-20250219"
assert persona.temperature == 0.2
assert persona.top_p == 0.9
assert persona.max_output_tokens == 4000
data = persona.to_dict() data = persona.to_dict()
assert data["provider"] == "anthropic" # data should NOT have top-level provider/model anymore, it's in preferred_models
assert data["model"] == "claude-3-7-sonnet-20250219" assert "provider" not in data
assert "claude-3-5-sonnet-20241022" in data["preferred_models"] assert "model" not in data
assert data["preferred_models"][0]["provider"] == "anthropic"
assert data["preferred_models"][0]["model"] == "claude-3-7-sonnet-20250219"
assert data["preferred_models"][1] == {"model": "claude-3-5-sonnet-20241022"}
assert data["system_prompt"] == "You are a security expert." assert data["system_prompt"] == "You are a security expert."
assert data["temperature"] == 0.2 assert data["preferred_models"][0]["temperature"] == 0.2
assert data["top_p"] == 0.9 assert data["preferred_models"][0]["top_p"] == 0.9
assert data["max_output_tokens"] == 4000 assert data["preferred_models"][0]["max_output_tokens"] == 4000
assert data["tool_preset"] == "SecurityTools" assert data["tool_preset"] == "SecurityTools"
assert data["bias_profile"] == "Execution-Focused" assert data["bias_profile"] == "Execution-Focused"
def test_persona_deserialization(): def test_persona_deserialization():
# Old config format (legacy)
data = { data = {
"provider": "gemini", "provider": "gemini",
"model": "gemini-2.5-flash", "model": "gemini-2.5-flash",
@@ -45,7 +53,9 @@ def test_persona_deserialization():
assert persona.name == "Assistant" assert persona.name == "Assistant"
assert persona.provider == "gemini" assert persona.provider == "gemini"
assert persona.model == "gemini-2.5-flash" assert persona.model == "gemini-2.5-flash"
assert persona.preferred_models == ["gemini-2.5-flash"] # Migration logic should have put legacy fields into preferred_models since it only had a string
assert persona.preferred_models[0]["provider"] == "gemini"
assert persona.preferred_models[0]["model"] == "gemini-2.5-flash"
assert persona.system_prompt == "You are a helpful assistant." assert persona.system_prompt == "You are a helpful assistant."
assert persona.temperature == 0.5 assert persona.temperature == 0.5
assert persona.top_p == 1.0 assert persona.top_p == 1.0