docs(personas): new guide covering data model, scope inheritance, MMA integration, editor modal
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
# Personas (Unified Agent Profiles)
|
||||
|
||||
[Top](../README.md) | [MMA](guide_mma.md) | [Tools & IPC](guide_tools.md) | [Architecture](guide_architecture.md)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A **Persona** is a unified agent profile that consolidates the model choice, system prompt, tool preset, bias profile, context preset, and aggregation strategy into a single named, reusable entity. Personas eliminate the need to configure these settings individually per session or per Tier — the developer names a persona, and the entire configuration is applied atomically.
|
||||
|
||||
Personas are persisted in TOML files with **scope-based inheritance** (Global vs Project). The PersonaManager merges both scopes at load time, with project-level entries overriding global entries of the same name.
|
||||
|
||||
This guide covers:
|
||||
|
||||
1. **Data Model** — The `Persona` schema and its fields
|
||||
2. **Storage & Scope** — Global and project TOML files, merge rules
|
||||
3. **PersonaManager** — CRUD operations
|
||||
4. **MMA Integration** — How personas are applied to workers
|
||||
5. **Editor Modal** — The GUI for creating and editing personas
|
||||
6. **Testing** — Test areas for persona behavior
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `Persona` (`src/models.py:704`)
|
||||
|
||||
```python
|
||||
class Persona:
|
||||
name: str
|
||||
preferred_models: List[Dict[str, Any]] = 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
|
||||
```
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `name` | `str` | Unique identifier. Used as the key in TOML. |
|
||||
| `preferred_models` | `List[Dict]` | Ordered list of `{provider, model, temperature, top_p, max_output_tokens}` dicts. The first entry is the initial model; subsequent entries are escalation targets. |
|
||||
| `system_prompt` | `str` | Replaces the default system prompt when this persona is active. |
|
||||
| `tool_preset` | `Optional[str]` | Name of a `ToolPreset` to apply. See [guide_tools.md](guide_tools.md). |
|
||||
| `bias_profile` | `Optional[str]` | Name of a `BiasProfile` to apply. See [guide_tools.md](guide_tools.md). |
|
||||
| `context_preset` | `Optional[str]` | Name of a `ContextPreset` to apply for file selection. |
|
||||
| `aggregation_strategy` | `Optional[str]` | One of `auto`, `full`, `summarize`, `skeleton`. Drives how files are aggregated for this persona. |
|
||||
|
||||
**Convenience Properties**:
|
||||
|
||||
```python
|
||||
@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")
|
||||
|
||||
# Similar: top_p, max_output_tokens
|
||||
```
|
||||
|
||||
These read from `preferred_models[0]` (the primary model). The full escalation list is used by `multi_agent_conductor.run_worker_lifecycle` for retry-based model escalation.
|
||||
|
||||
### `preferred_models` Schema
|
||||
|
||||
Each entry in the list is a dict with optional keys:
|
||||
|
||||
```python
|
||||
{
|
||||
"provider": "gemini", # Required
|
||||
"model": "gemini-3.1-pro-preview", # Required
|
||||
"temperature": 0.0, # Optional, defaults to 0.0
|
||||
"top_p": 1.0, # Optional, defaults to 1.0
|
||||
"max_output_tokens": 8192, # Optional, defaults to provider default
|
||||
}
|
||||
```
|
||||
|
||||
The list is ordered: index 0 is the first attempt, index 1 is the first escalation, etc. The ConductorEngine's model escalation logic iterates this list on retry.
|
||||
|
||||
---
|
||||
|
||||
## Storage & Scope
|
||||
|
||||
### File Locations
|
||||
|
||||
| Scope | Path | Configured By |
|
||||
|---|---|---|
|
||||
| Global | `<user_config>/personas.toml` | `src/paths.py:get_global_personas_path()` |
|
||||
| Project | `<project_root>/personas.toml` | `src/paths.py:get_project_personas_path(project_root)` |
|
||||
|
||||
Both files use the same TOML schema:
|
||||
|
||||
```toml
|
||||
[personas.<name>]
|
||||
preferred_models = [
|
||||
{ provider = "gemini", model = "gemini-3.1-pro-preview", temperature = 0.0 },
|
||||
{ provider = "gemini", model = "gemini-3-flash-preview", temperature = 0.0 },
|
||||
]
|
||||
system_prompt = "You are a senior backend engineer with deep knowledge of Python asyncio."
|
||||
tool_preset = "read_only"
|
||||
bias_profile = "discovery_heavy"
|
||||
context_preset = "codebase_full"
|
||||
aggregation_strategy = "summarize"
|
||||
```
|
||||
|
||||
### Scope Inheritance
|
||||
|
||||
`PersonaManager.load_all()` merges global and project personas:
|
||||
|
||||
1. Load global personas first.
|
||||
2. If `project_root` is set, load project personas and **overwrite** entries with matching names.
|
||||
3. Return the merged dict.
|
||||
|
||||
**Example**: If `personas.toml` (global) has a `code_reviewer` persona and `personas.toml` (project) also has a `code_reviewer` persona, the project version wins. This allows projects to override global defaults without losing the global fallback.
|
||||
|
||||
### Scope Lookup
|
||||
|
||||
`get_persona_scope(name)` returns `"global"` or `"project"` based on which file contains the persona. Used by the editor modal to show the user where a persona is defined.
|
||||
|
||||
### Save / Delete
|
||||
|
||||
- `save_persona(persona, scope="project")` writes the persona to the specified scope's TOML file.
|
||||
- `delete_persona(name, scope="project")` removes the entry.
|
||||
- The default scope for both is `"project"`. Use `"global"` for application-wide personas.
|
||||
|
||||
---
|
||||
|
||||
## PersonaManager
|
||||
|
||||
`PersonaManager` is the CRUD interface.
|
||||
|
||||
```python
|
||||
from src.personas import PersonaManager
|
||||
from src.models import Persona
|
||||
|
||||
manager = PersonaManager(project_root=Path("/path/to/project"))
|
||||
all_personas = manager.load_all() # Merged global + project
|
||||
manager.save_persona(my_persona, scope="project")
|
||||
manager.delete_persona("old_persona", scope="project")
|
||||
scope = manager.get_persona_scope("code_reviewer")
|
||||
```
|
||||
|
||||
### `load_all()`
|
||||
|
||||
Returns a `Dict[str, Persona]` mapping name to Persona. Project entries override global entries of the same name.
|
||||
|
||||
### `save_persona(persona, scope)`
|
||||
|
||||
Writes a Persona to the specified scope's TOML file. If the file doesn't exist, it's created (parent directories included). If the file exists, the entry is added or updated; other entries are preserved.
|
||||
|
||||
### `delete_persona(name, scope)`
|
||||
|
||||
Removes the named entry from the specified scope's TOML file. If the entry doesn't exist, the operation is a no-op.
|
||||
|
||||
### `get_persona_scope(name)`
|
||||
|
||||
Returns `"global"` or `"project"` based on which file contains the persona. Returns `"project"` if neither (effectively saying "would be saved to project if you save").
|
||||
|
||||
### Internal Helpers
|
||||
|
||||
- `_load_file(path) -> Dict[str, Any]`: Reads a TOML file, returns `{}` on error or missing file.
|
||||
- `_save_file(path, data)`: Writes a TOML file using `tomli_w`. Creates parent directories as needed.
|
||||
|
||||
---
|
||||
|
||||
## MMA Integration
|
||||
|
||||
When a `Ticket` has `persona_id` set, or when a Tier has a default persona, the ConductorEngine applies the persona to the worker. See [guide_mma.md#persona-application](guide_mma.md#persona-application) for the full integration details.
|
||||
|
||||
**Application order** (in `run_worker_lifecycle`):
|
||||
|
||||
1. **Model selection**:
|
||||
- `ticket.model_override` (if set) — used unconditionally
|
||||
- Persona `preferred_models[0]` (if persona applied) — initial model
|
||||
- Default tier model — fallback
|
||||
2. **System prompt**: `ai_client.set_custom_system_prompt(persona.system_prompt)` replaces the default.
|
||||
3. **Bias profile**: `ai_client.set_bias_profile(persona.bias_profile)` applies semantic nudging.
|
||||
4. **Tool preset**: `ai_client.set_tool_preset(persona.tool_preset)` configures enabled tools.
|
||||
5. **Aggregation strategy**: Used by the `aggregate.py` pipeline to choose `full`/`summarize`/`skeleton`.
|
||||
|
||||
**Failure handling**: If the persona fails to load (file not found, parse error, missing fields), the worker logs a warning and falls back to the default model list. The persona is **not** a hard failure point.
|
||||
|
||||
### Tier-Scoped vs Ticket-Scoped Personas
|
||||
|
||||
- **Tier-scoped**: Set in `tier_usage[<tier>]["persona"]`. Applied to every worker for that tier when no ticket-level persona is set.
|
||||
- **Ticket-scoped**: Set in `ticket.persona_id`. Overrides tier-level for that specific ticket.
|
||||
|
||||
---
|
||||
|
||||
## Editor Modal
|
||||
|
||||
The GUI provides a Persona Editor modal (`src/gui_2.py:_render_persona_editor_modal` or similar) for creating and editing personas without manually editing TOML.
|
||||
|
||||
**Fields exposed in the modal**:
|
||||
- Name (text input, required)
|
||||
- Preferred Models (editable list, with provider/model/temperature/top_p/max_output_tokens per entry)
|
||||
- System Prompt (multi-line text input)
|
||||
- Tool Preset (dropdown of available presets)
|
||||
- Bias Profile (dropdown)
|
||||
- Context Preset (dropdown)
|
||||
- Aggregation Strategy (radio buttons: auto / full / summarize / skeleton)
|
||||
- Scope (radio: Global / Project)
|
||||
|
||||
**Actions**:
|
||||
- **Save** — Writes the persona to the selected scope.
|
||||
- **Delete** — Removes the persona (with confirmation).
|
||||
- **Duplicate** — Creates a copy with a new name.
|
||||
- **Cancel** — Discards changes.
|
||||
|
||||
The modal validates the name (must be unique, must be a valid TOML key) and the model entries (provider and model are required) before allowing save.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Personas are project-scoped (or global) configuration. There is no central `config.toml` setting for personas themselves; they're standalone TOML files.
|
||||
|
||||
**Related settings** (in `manual_slop.toml` or `config.toml`):
|
||||
|
||||
```toml
|
||||
[mma]
|
||||
default_personas = {
|
||||
"Tier 1": "orchestrator",
|
||||
"Tier 2": "tech_lead",
|
||||
"Tier 3": "code_worker",
|
||||
"Tier 4": "qa_reviewer",
|
||||
}
|
||||
```
|
||||
|
||||
This sets the default persona for each tier. Tickets without `persona_id` use the tier's default.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `tests/test_persona_manager.py` — `PersonaManager` CRUD, scope merging, file I/O
|
||||
- `tests/test_persona_models.py` — `Persona` serialization/deserialization
|
||||
- `tests/test_persona_id.py` — `Persona` ID validation and uniqueness
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `tests/test_mma_prompts.py` — Verifies persona-derived prompts are constructed correctly
|
||||
- `tests/test_bias_efficacy.py` — Verifies bias profile integration
|
||||
|
||||
### Test Pattern
|
||||
|
||||
```python
|
||||
def test_persona_scope_overrides(tmp_path):
|
||||
# Global persona
|
||||
global_path = tmp_path / "global_personas.toml"
|
||||
global_path.write_text("""
|
||||
[personas.coder]
|
||||
system_prompt = "Global: code only."
|
||||
""")
|
||||
|
||||
# Project persona (override)
|
||||
project_path = tmp_path / "personas.toml"
|
||||
project_path.write_text("""
|
||||
[personas.coder]
|
||||
system_prompt = "Project: code only, focus on our domain."
|
||||
""")
|
||||
|
||||
manager = PersonaManager(project_root=tmp_path, ...)
|
||||
# Patch paths module to return the test paths
|
||||
with patch("src.paths.get_global_personas_path", return_value=global_path), \
|
||||
patch("src.paths.get_project_personas_path", return_value=project_path):
|
||||
all_personas = manager.load_all()
|
||||
|
||||
assert "coder" in all_personas
|
||||
assert all_personas["coder"].system_prompt == "Project: code only, focus on our domain."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **No Versioning**: Personas are stored in TOML. Changes are not versioned unless the project uses git (which most do, but the persona changes are buried in diffs). Consider a "persona history" feature for future.
|
||||
|
||||
2. **No Inheritance Chains**: A persona cannot reference another persona as a base. If a project wants to share settings across multiple personas, it must duplicate them.
|
||||
|
||||
3. **No Validation of Referenced Names**: A persona can name a `tool_preset` that doesn't exist. The error surfaces only when the worker tries to apply the persona.
|
||||
|
||||
4. **No Live Reload**: Changing a persona TOML file does not take effect until the application is restarted (or `PersonaManager.load_all()` is called again).
|
||||
|
||||
5. **No Conflict Resolution UI**: If a project and global define different personas with the same name, the project wins silently. The user must check the editor modal to see the actual definition.
|
||||
|
||||
---
|
||||
|
||||
## Future Work
|
||||
|
||||
- **Persona Composition** — Allow a persona to reference another as a base, with override semantics.
|
||||
- **Live Reload** — Watch the persona TOML files and reload on change.
|
||||
- **Persona History** — Track changes over time, allow rollback.
|
||||
- **Conflict Resolution UI** — When global and project both define a persona, show both in the editor with a "use global" / "use project" / "merge" choice.
|
||||
- **Persona Templates** — Pre-built personas for common roles (code reviewer, test writer, doc writer) that users can clone.
|
||||
|
||||
See [guide_mma.md#persona-application](guide_mma.md#persona-application) for the worker-side integration.
|
||||
Reference in New Issue
Block a user