Files
odysseus/src/preset_manager.py
T

239 lines
10 KiB
Python

import os
import json
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
class PresetManager:
DEFAULT_PRESETS = {
"code_analyze": {
"name": "Code Analyze",
"temperature": 0.2,
"max_tokens": 8000,
"system_prompt": """You are a code analyzer.
ANALYSIS FORMAT:
- Issues: [specific problems found]
- Security: [vulnerabilities if any]
- Performance: [optimization opportunities]
- Fix: [concrete solutions with code examples]
Start directly with findings. No preamble. If input isn't code, state: "Input is not code. Please provide code to analyze."
"""
},
"brainstorm": {
"name": "Brainstorm",
"temperature": 0.9,
"max_tokens": 4096,
"system_prompt": """You are a creative ideation assistant focused on divergent thinking.
Generate diverse, unexpected ideas that span from practical to experimental.
- Mix conventional and unconventional approaches
- Connect unrelated concepts to spark innovation
- Consider multiple perspectives and contexts
- Include both immediate solutions and long-term possibilities
- Challenge assumptions without being absurd for absurdity's sake
Structure ideas clearly but allow creative freedom in presentation. Aim for quantity and variety over filtering.
"""
},
"reason": {
"name": "Reason",
"temperature": 0.3,
"max_tokens": 6000,
"system_prompt": """You are a systematic reasoning assistant.
Structure all responses using clear logical progression:
1. Identify key components of the question
2. State relevant principles or facts
3. Build argument step by step
4. Address potential counterarguments
5. Conclude with justified answer
Use precise language. Show causal relationships explicitly. Quantify uncertainty where applicable.
"""
},
"custom": {
"name": "Custom",
"temperature": 1.0,
"max_tokens": 32768,
"system_prompt": "",
"inject_prefix": "",
"inject_suffix": "",
"thinking_mode": "",
"enabled": False,
}
}
def __init__(self, data_dir: str):
self.presets_file = os.path.join(data_dir, "presets.json")
self.presets = self.load()
def load(self) -> Dict[str, Any]:
"""Load presets from file, creating defaults if needed"""
if not os.path.exists(self.presets_file):
self.save(self.DEFAULT_PRESETS)
return self.DEFAULT_PRESETS.copy()
try:
with open(self.presets_file, 'r', encoding="utf-8") as f:
presets = json.load(f)
if not isinstance(presets, dict):
logger.error("Error loading presets: expected an object")
return self.DEFAULT_PRESETS.copy()
custom = presets.get("custom") if isinstance(presets, dict) else None
if isinstance(custom, dict) and "enabled" not in custom:
legacy_prompt = "You are a helpful, balanced assistant. Match your response style to the user's needs."
if (
custom.get("name") == "Custom"
and not custom.get("character_name")
and custom.get("system_prompt") == legacy_prompt
):
custom["enabled"] = False
custom["system_prompt"] = ""
custom["temperature"] = 1.0
custom["max_tokens"] = self.DEFAULT_PRESETS["custom"]["max_tokens"]
custom.setdefault("inject_prefix", "")
custom.setdefault("inject_suffix", "")
self.save(presets)
# Heal a forward-incompatible file the same way the legacy `custom`
# migration above does: fill in any built-in presets an older or
# partial presets.json is missing, so they reach existing installs
# (a missing built-in is otherwise silently absent from the picker
# served by GET /api/presets). There is no delete path for the
# built-in keys, so this never clobbers an intentional removal.
# Defaults first, loaded values win — user edits are preserved.
if isinstance(presets, dict) and any(
k not in presets for k in self.DEFAULT_PRESETS
):
presets = {**self.DEFAULT_PRESETS, **presets}
self.save(presets)
return presets
except Exception as e:
logger.error(f"Error loading presets: {e}")
return self.DEFAULT_PRESETS.copy()
def save(self, presets: Dict[str, Any]) -> bool:
"""Save presets to file"""
try:
# Atomic write (tmp file + os.replace) so a crash or serialization
# error mid-write can't truncate presets.json and lose every saved
# preset. Lazy import keeps this module free of the heavy core
# package import graph at load time.
from core.atomic_io import atomic_write_json
atomic_write_json(self.presets_file, presets, indent=2)
self.presets = presets
return True
except Exception as e:
logger.error(f"Error saving presets: {e}")
return False
def get(self, preset_id: str) -> Dict[str, Any]:
"""Get a specific preset"""
return self.presets.get(preset_id)
def update_custom(
self,
temperature: float,
max_tokens: int,
system_prompt: str,
name: str = "",
enabled: bool = True,
inject_prefix: str = "",
inject_suffix: str = "",
persona_memory: str = "",
persona_memory_schema: str = "general",
thinking_mode: str = "",
show_persona_name: bool = True,
) -> bool:
"""Update the custom preset"""
persona_memory_schema = persona_memory_schema if persona_memory_schema in {"general", "health"} else "general"
current = self.presets.get("custom") if isinstance(self.presets, dict) else {}
current_name = ""
if isinstance(current, dict):
current_name = current.get("character_name") or current.get("name") or ""
if not persona_memory and enabled and name:
if current_name == name and isinstance(current, dict):
persona_memory = current.get("persona_memory", "") or ""
persona_memory_schema = current.get("persona_memory_schema", persona_memory_schema) or persona_memory_schema
else:
for template in self.get_user_templates():
if isinstance(template, dict) and template.get("name") == name:
persona_memory = template.get("persona_memory", "") or ""
persona_memory_schema = template.get("persona_memory_schema", persona_memory_schema) or persona_memory_schema
break
self.presets["custom"] = {
"name": name or "Custom",
"character_name": name,
"temperature": temperature,
"max_tokens": max_tokens,
"system_prompt": system_prompt,
"inject_prefix": inject_prefix,
"inject_suffix": inject_suffix,
"thinking_mode": thinking_mode if thinking_mode in {"on", "off"} else "",
"show_persona_name": bool(show_persona_name),
"enabled": enabled,
"persona_memory": persona_memory if enabled and name else "",
"persona_memory_schema": persona_memory_schema if enabled and name else "general",
}
return self.save(self.presets)
def get_all(self) -> Dict[str, Any]:
"""Get all presets"""
return self.presets.copy()
def get_user_templates(self) -> list:
"""Get user-saved character templates."""
return self.presets.get("user_templates", [])
def save_user_template(self, template: dict) -> bool:
"""Save a new user template or update existing by id."""
templates = self.presets.get("user_templates", [])
# Update existing if same id
existing = next((i for i, t in enumerate(templates) if t.get("id") == template.get("id")), None)
if existing is not None:
templates[existing] = template
else:
templates.append(template)
self.presets["user_templates"] = templates
return self.save(self.presets)
def delete_user_template(self, template_id: str) -> bool:
"""Delete a user template by id."""
templates = self.presets.get("user_templates", [])
self.presets["user_templates"] = [t for t in templates if t.get("id") != template_id]
return self.save(self.presets)
def update_persona_memory(self, name: str, memory: str) -> bool:
"""Persist auto-maintained continuity notes for a saved/active persona."""
name = (name or "").strip()
memory = (memory or "").strip()
if not name:
return False
changed = False
custom = self.presets.get("custom")
if isinstance(custom, dict) and custom.get("character_name") == name:
if custom.get("persona_memory", "") != memory:
custom["persona_memory"] = memory
changed = True
templates = self.presets.get("user_templates", [])
if isinstance(templates, list):
for template in templates:
if isinstance(template, dict) and template.get("name") == name:
if template.get("persona_memory", "") != memory:
template["persona_memory"] = memory
changed = True
return self.save(self.presets) if changed else True
def get_group_presets(self) -> list:
"""Get saved group chat presets."""
return self.presets.get("group_presets", [])
def save_group_presets(self, groups: list) -> bool:
"""Save group chat presets."""
self.presets["group_presets"] = groups
return self.save(self.presets)