diff --git a/functions/filters/agent_hotswap/README.md b/functions/filters/agent_hotswap/README.md index 5b1a94c..962543a 100644 --- a/functions/filters/agent_hotswap/README.md +++ b/functions/filters/agent_hotswap/README.md @@ -1,8 +1,8 @@ # ๐ŸŽญ Agent Hotswap -> **Transform your OpenWebUI experience with intelligent AI persona switching** +> **Revolutionary AI persona switching with dynamic multi-persona capabilities** -[![Version](https://img.shields.io/badge/version-0.1.2-blue.svg)](https://github.com/open-webui/functions) +[![Version](https://img.shields.io/badge/version-0.2.0-blue.svg)](https://github.com/open-webui/functions) [![OpenWebUI](https://img.shields.io/badge/OpenWebUI-Compatible-green.svg)](https://github.com/open-webui/open-webui) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -10,15 +10,17 @@ ## ๐ŸŒŸ Overview -**Agent Hotswap** is a powerful OpenWebUI filter that enables seamless switching between 50+ specialized AI personas with a simple command system. Each persona comes with unique capabilities, expertise, and communication styles, all built on a **Master Controller** foundation that provides universal OpenWebUI-native features. +**Agent Hotswap** is the most advanced OpenWebUI filter for AI persona management, enabling seamless switching between 50+ specialized AI personas with breakthrough **dynamic multi-persona capabilities**. Execute complex workflows involving multiple experts in a single conversation, with automatic persona discovery and just-in-time loading. -### โœจ Key Features +### โœจ Revolutionary Features -- ๐ŸŽ›๏ธ **Master Controller System** - Transparent foundation providing OpenWebUI capabilities to all personas +- ๐ŸŽ›๏ธ **Master Controller System** - Universal OpenWebUI capabilities foundation for all personas +- ๐Ÿ”„ **Dynamic Multi-Persona Sequences** - Multiple persona switches within a single prompt +- ๐Ÿ” **Universal Persona Detection** - Automatically works with any current or future personas +- โšก **Just-In-Time Loading** - Only loads personas actually requested for optimal performance - ๐Ÿš€ **Instant Persona Switching** - Simple `!command` syntax for immediate role changes -- ๐Ÿ“ฆ **Auto-Download Collection** - Automatically fetches the complete 50+ persona collection on first run +- ๐Ÿ“ฆ **Auto-Download Collection** - Automatically fetches the complete 50+ persona collection - ๐Ÿ”„ **Auto-Updates** - Keeps persona collection current with weekly checks -- โšก **Performance Optimized** - Smart caching, pre-compiled patterns, and efficient loading - ๐ŸŽจ **Rich Rendering Support** - LaTeX math, Mermaid diagrams, HTML artifacts built-in - ๐Ÿ’พ **Automatic Backups** - Safe persona management with rollback capabilities - ๐Ÿ”ง **Cross-Platform** - Works with both Docker and native OpenWebUI installations @@ -44,15 +46,89 @@ The plugin automatically: ### 3๏ธโƒฃ Start Using Personas ```bash +# Single persona switching !list # See all available personas !coder # Become a programming expert !writer # Transform into a creative writer !analyst # Switch to data analysis mode -!reset # Return to default assistant + +# Revolutionary multi-persona sequences +!writer create a story about AI !physicist explain the science !teacher create study questions !artist design cover art + +# Reset to default +!reset # Return to standard assistant ``` --- +## ๐Ÿ”ฅ **NEW: Dynamic Multi-Persona System** + +### **Multi-Persona Sequences** +Execute complex workflows with multiple experts in a single prompt: + +```bash +# Creative collaboration +!writer start a sci-fi story !physicist verify the science !historian add historical context !artist describe the visuals !writer conclude the story + +# Educational deep-dive +!teacher introduce quantum mechanics !physicist explain the theory !engineer show applications !philosopher discuss implications + +# Business analysis +!analyst present market data !economist add economic context !consultant recommend strategy !projectmanager create implementation timeline +``` + +### **How Multi-Persona Hotswapping Works** + +#### **1. Universal Discovery Phase** +``` +User Input: "!writer create story !teacher explain techniques !physicist add science" + +โ†“ Universal Pattern Detection โ†“ + +Discovered Commands: ['writer', 'teacher', 'physicist'] +``` + +#### **2. Just-In-Time Loading** +``` +Available Personas: 50+ in collection +โ†“ Smart Loading โ†“ +Loaded: Only 3 requested personas + Master Controller +Memory Usage: Minimal (only what's needed) +``` + +#### **3. Dynamic System Construction** +``` +System Message Built: +โ”œโ”€โ”€ Master Controller (OpenWebUI capabilities) +โ”œโ”€โ”€ Writer Persona Definition +โ”œโ”€โ”€ Teacher Persona Definition +โ”œโ”€โ”€ Physicist Persona Definition +โ””โ”€โ”€ Multi-Persona Execution Framework +``` + +#### **4. Sequence Parsing & Instruction Building** +``` +Original: "!writer create story !teacher explain techniques !physicist add science" + +โ†“ Parsed Into Structured Sequence โ†“ + +Step 1 - Creative Writer: create story +Step 2 - Educator: explain techniques +Step 3 - Quantum Physicist: add science +``` + +#### **5. Intelligent Execution** +The LLM receives comprehensive instructions and executes each persona switch seamlessly, maintaining context and flow throughout the entire sequence. + +### **Universal Compatibility** +Works with **ANY** persona combination: +- **Current 50+ personas**: `!coder !analyst !economist` +- **Future personas**: Automatically detects new additions +- **Mixed combinations**: `!existing_persona !future_persona !another_new_one` +- **Unlimited sequences**: `!a !b !c !d !e !f !g !h !i !j...` + +--- + ## ๐Ÿ’ก Usage Guide ### Core Commands @@ -62,6 +138,28 @@ The plugin automatically: | `!list` | Display all available personas in a formatted table | | `!reset`, `!default`, `!normal` | Return to standard assistant mode | | `!{persona_name}` | Switch to any specific persona | +| `!{persona1} task1 !{persona2} task2` | **NEW:** Multi-persona sequences | + +### Single Persona Switching + +```bash +!coder # Switch to programming expert +!writer # Become creative writer +!analyst # Transform into data analyst +``` + +### Multi-Persona Workflows + +```bash +# Content creation pipeline +!writer draft blog post !researcher fact-check claims !editor polish prose !marketer add compelling headlines + +# Technical analysis +!analyst examine data !statistician run tests !consultant interpret results !presenter create executive summary + +# Creative projects +!novelist create plot !historian verify period details !scientist explain technology !artist design concepts +``` ### Available Personas @@ -107,9 +205,10 @@ The plugin includes 50+ specialized personas covering: ### Persona Features - **Automatic Introduction** - Each persona introduces itself on activation -- **Persistent Context** - Persona remains active across messages until changed +- **Persistent Context** - Single personas remain active across messages until changed - **Specialized Knowledge** - Tailored expertise and communication style - **OpenWebUI Integration** - Full access to LaTeX, Mermaid, artifacts, and more +- ****NEW:** Multi-Persona Transitions** - Smooth handoffs between experts in sequences --- @@ -124,13 +223,55 @@ The **Master Controller** is the invisible foundation that powers every persona: - **Transparent** - Users never see or interact with it directly - **Smart Persistence** - Only removed on reset/default commands -### Persona Architecture +### Dynamic Multi-Persona Architecture -Each persona includes: -- **Name** - Display name with emoji for visual identification -- **Prompt** - Comprehensive system prompt defining personality and expertise -- **Description** - User-facing explanation of capabilities -- **Rules** - Structured guidelines for behavior and responses +#### **Universal Detection Engine** +- **Pattern Recognition**: Automatically detects any `!{word}` pattern +- **Future-Proof**: Works with personas that don't exist yet +- **Smart Filtering**: Distinguishes between personas and special commands +- **Error Handling**: Gracefully handles unknown commands + +#### **Just-In-Time Loading System** +``` +Available: 50+ personas in collection +Requested: !writer !physicist !teacher +โ†“ Smart Loading โ†“ +Loaded: 3 personas + Master Controller +Memory: ~75% reduction vs loading all personas +Performance: Optimal regardless of collection size +``` + +#### **Dynamic System Message Construction** +Each multi-persona session gets a custom system message containing: +1. **Master Controller** - OpenWebUI capabilities foundation +2. **Requested Personas** - Only the personas actually needed +3. **Execution Framework** - Instructions for seamless switching +4. **Available Commands** - List of active personas for the session + +### Persona Transition Control + +Control how persona switches are displayed: + +#### **Visible Transitions** (Default) +``` +๐ŸŽญ **Creative Writer** +Once upon a time, in a world where artificial intelligence... + +๐ŸŽญ **Quantum Physicist** +The quantum mechanics underlying this scenario involve... + +๐ŸŽญ **Educator** +Let me explain these concepts in simpler terms... +``` + +#### **Silent Transitions** +``` +Once upon a time, in a world where artificial intelligence... + +The quantum mechanics underlying this scenario involve... + +Let me explain these concepts in simpler terms... +``` --- @@ -142,16 +283,36 @@ Each persona includes: |---------|---------|-------------| | `keyword_prefix` | `!` | Command prefix for persona switching | | `case_sensitive` | `false` | Whether commands are case-sensitive | -| `persistent_persona` | `true` | Keep persona active across messages | +| `persistent_persona` | `true` | Keep single personas active across messages | | `show_persona_info` | `true` | Display status messages for switches | ### Advanced Settings | Setting | Default | Description | |---------|---------|-------------| +| `multi_persona_transitions` | `true` | **NEW:** Show transition announcements in multi-persona responses | | `status_message_auto_close_delay_ms` | `5000` | Auto-close delay for status messages | | `debug_performance` | `false` | Enable performance debugging logs | +### **NEW: Multi-Persona Transition Control** + +The `multi_persona_transitions` valve controls how persona switches are displayed in multi-persona sequences: + +**Enabled (Default):** +- Shows `๐ŸŽญ **Persona Name**` announcements +- Clear visual indication of expert transitions +- Helpful for understanding which expert is responding + +**Disabled:** +- Silent, seamless transitions +- Clean output without transition markers +- Personas switch invisibly behind the scenes + +**When to disable transitions:** +- Creative writing where transitions would break immersion +- Professional reports requiring clean formatting +- When persona switches should be transparent to end users + --- ## ๐Ÿ—๏ธ System Architecture @@ -176,6 +337,29 @@ The plugin automatically detects your installation type: โ”œโ”€โ”€ backups/ # Automatic backups ``` +### **NEW: Universal Detection Architecture** + +#### **Pattern Compilation System** +```python +# Universal pattern matches any valid persona command +Pattern: !{word} where word = [a-zA-Z][a-zA-Z0-9_]* + +Examples Matched: +โœ… !coder, !writer, !analyst (existing) +โœ… !quantum_engineer, !bioethicist (future) +โœ… !custom_persona_123 (user-defined) +โŒ !123invalid (invalid format) +``` + +#### **Dynamic Loading Pipeline** +``` +1. Parse Input โ†’ Discover Commands โ†’ [!writer, !physicist, !teacher] +2. Load Collection โ†’ Validate Existence โ†’ [โœ…writer, โœ…physicist, โœ…teacher] +3. Build System โ†’ Include Definitions โ†’ Master + 3 Personas +4. Create Instructions โ†’ Structure Sequence โ†’ Step-by-step execution +5. Execute โ†’ LLM follows sequence โ†’ Multi-expert response +``` + ### Auto-Update System - **Weekly Checks** - Automatically checks for updates to persona collection @@ -185,32 +369,65 @@ The plugin automatically detects your installation type: --- +## ๐Ÿš€ Advanced Use Cases + +### Creative Collaboration +```bash +!writer start mystery novel !detective add investigative realism !psychologist develop character depth !editor polish prose !marketer create book blurb +``` + +### Technical Documentation +```bash +!engineer explain system architecture !coder provide implementation examples !teacher create tutorials !technical_writer polish documentation +``` + +### Business Strategy +```bash +!analyst present market data !economist add macro trends !consultant recommend strategies !projectmanager create timelines !presenter format executive summary +``` + +### Educational Content +```bash +!teacher introduce topic !researcher provide latest findings !philosopher explore implications !artist create visual aids !writer craft engaging narrative +``` + +### Problem Solving +```bash +!analyst define problem !researcher gather evidence !consultant brainstorm solutions !engineer evaluate feasibility !projectmanager plan implementation +``` + ## ๐Ÿ”ง Troubleshooting ### Common Issues -**โŒ Persona Not Loading** +**โŒ Multi-Persona Not Working** ``` Solution: -1. Use !list to verify persona exists +1. Ensure multiple !commands in single message 2. Check that filter is enabled -3. Restart OpenWebUI if needed +3. Verify personas exist with !list +4. Check transition settings in configuration ``` -**โŒ Commands Not Recognized** +**โŒ Unknown Persona Commands** ``` -Solution: -- Check keyword_prefix setting (default: "!") -- Ensure case_sensitive matches your usage -- Verify filter is enabled and active +Behavior: System gracefully ignores unknown commands +Status: Shows "โš ๏ธ Unknown: !invalid_persona" +Solution: Use !list to see available personas ``` -**โŒ Auto-Download Failed** +**โŒ Performance Issues with Large Sequences** ``` -Solution: -- Check internet connectivity -- Plugin will create minimal config with basic personas -- Auto-retry will happen on next restart +Optimization: System only loads requested personas +Memory: Scales efficiently regardless of sequence length +Tip: No performance penalty for complex workflows +``` + +**โŒ Transitions Too Verbose/Invisible** +``` +Solution: Adjust multi_persona_transitions valve +- Enable: Shows clear ๐ŸŽญ **Persona** markers +- Disable: Silent, seamless transitions ``` ### Recovery @@ -220,6 +437,13 @@ Solution: 2. Plugin will auto-download fresh persona collection 3. Use `!list` to verify restoration +**Clear Persona State:** +```bash +!reset # Clears all active personas +!default # Alternative reset command +!normal # Another reset option +``` + --- ## ๐Ÿš€ Advanced Features @@ -230,8 +454,8 @@ Add custom personas by editing the `personas.json` file: ```json { - "custom_key": { - "name": "๐ŸŽฏ Your Custom Persona", + "custom_expert": { + "name": "๐ŸŽฏ Your Custom Expert", "prompt": "You are a specialized assistant for...", "description": "Brief description of capabilities", "rules": [ @@ -244,10 +468,24 @@ Add custom personas by editing the `personas.json` file: ### Performance Optimization +- **Universal Detection** - Works with unlimited personas - **Smart Caching** - Only reloads when files change +- **Just-In-Time Loading** - Only loads requested personas - **Pattern Pre-compilation** - Regex patterns compiled once - **Change Detection** - File modification time tracking -- **Lazy Loading** - Personas loaded on-demand + +### **NEW: Multi-Persona Performance** + +``` +Traditional Approach: Load all 50+ personas +Memory Usage: High +Loading Time: Slow + +Dynamic Approach: Load only requested personas +Memory Usage: Minimal +Loading Time: Instant +Scalability: Infinite +``` --- @@ -260,6 +498,7 @@ Include the following information: - **Filter Configuration** - Relevant valve settings - **Error Messages** - Full error text and logs - **Reproduction Steps** - How to recreate the issue +- **Multi-Persona Details** - If issue involves persona sequences ### Persona Contributions @@ -267,6 +506,43 @@ Guidelines for new personas: - **Clear Purpose** - Well-defined role and expertise - **Comprehensive Prompt** - Detailed behavioral instructions - **User-Friendly Description** - Clear capability explanation +- **Multi-Persona Compatibility** - Works well with other experts + +### Feature Requests + +When requesting features: +- **Use Case** - Explain the specific workflow need +- **Multi-Persona Impact** - How it affects persona sequences +- **Performance Considerations** - Scalability requirements + +--- + +## ๐Ÿ“Š Performance Metrics + +### **Traditional vs Dynamic Architecture** + +| Metric | Traditional | **Dynamic Multi-Persona** | +|--------|-------------|---------------------------| +| **Memory Usage** | All personas loaded | Only requested personas | +| **Loading Time** | Fixed overhead | Scales with usage | +| **Flexibility** | Single persona | Unlimited combinations | +| **Future-Proofing** | Manual updates | Automatic discovery | +| **Performance** | Degrades with size | Constant performance | + +### **Scalability Examples** + +```bash +# 2 personas: ~95% memory savings +!writer !teacher + +# 5 personas: ~90% memory savings +!coder !analyst !economist !historian !artist + +# 10 personas: ~80% memory savings +!writer !coder !teacher !physicist !artist !economist !historian !philosopher !consultant !researcher + +# Performance remains optimal regardless of sequence complexity +``` --- @@ -278,8 +554,19 @@ This project is licensed under the MIT License. ## ๐Ÿ™ Acknowledgments -- **OpenWebUI Team** - For the amazing platform +- **OpenWebUI Team** - For the amazing platform and architecture - **Community Contributors** - For persona collections and feedback +- **Early Adopters** - For testing multi-persona workflows + +--- + +## ๐Ÿ”ฎ Future Roadmap + +- **Persona Chaining** - Automatic persona suggestions based on context +- **Workflow Templates** - Pre-built multi-persona sequences for common tasks +- **Performance Analytics** - Detailed metrics on persona usage patterns +- **Custom Transition Styles** - User-defined transition formatting +- **Persona Marketplace** - Community-contributed expert collections --- @@ -287,6 +574,8 @@ This project is licensed under the MIT License. **๐ŸŽญ Transform your AI interactions with Agent Hotswap!** -*Seamless persona switching โ€ข Rich OpenWebUI integration โ€ข Automatic setup* +*Revolutionary multi-persona sequences โ€ข Universal compatibility โ€ข Infinite scalability* - +### **Experience the future of AI interaction today** + + \ No newline at end of file diff --git a/functions/filters/agent_hotswap/main.py b/functions/filters/agent_hotswap/main.py index 47083c0..9945b74 100644 --- a/functions/filters/agent_hotswap/main.py +++ b/functions/filters/agent_hotswap/main.py @@ -5,8 +5,8 @@ author_url: https://github.com/pkeffect project_urls: https://github.com/pkeffect/functions/tree/main/functions/filters/agent_hotswap | https://github.com/open-webui/functions/tree/main/functions/filters/agent_hotswap | https://openwebui.com/f/pkeffect/agent_hotswap funding_url: https://github.com/open-webui date: 2025-06-15 -version: 0.1.2 -description: Switch between AI personas with optimized performance. Features: external config, pre-compiled regex patterns, smart caching, validation, and modular architecture. Commands: !list, !reset, !coder, !writer, etc. +version: 0.2.0 +description: Universal AI persona switching with dynamic multi-persona support. Features: mid-prompt persona switching, universal persona detection, smart caching, auto-download, and modular architecture. Commands: !list, !reset, !coder, !writer, plus unlimited combinations. """ from pydantic import BaseModel, Field @@ -57,7 +57,7 @@ class PersonaDownloadManager: try: req = urllib.request.Request( - download_url, headers={"User-Agent": "OpenWebUI-AgentHotswap/0.1.0"} + download_url, headers={"User-Agent": "OpenWebUI-AgentHotswap/0.2.0"} ) with urllib.request.urlopen(req, timeout=DOWNLOAD_TIMEOUT) as response: @@ -195,19 +195,20 @@ class PersonaDownloadManager: return {} -class PatternCompiler: - """Pre-compiles and manages regex patterns for efficient persona detection.""" +class UniversalPatternCompiler: + """Enhanced pattern compiler with universal persona detection capabilities.""" def __init__(self, config_valves): self.valves = config_valves self.persona_patterns = {} self.reset_pattern = None self.list_pattern = None + self.universal_persona_pattern = None self._last_compiled_config = None self._compile_patterns() def _compile_patterns(self): - """Compile all regex patterns once for reuse.""" + """Compile all regex patterns once for reuse, including universal detection.""" try: current_config = { "prefix": self.valves.keyword_prefix, @@ -222,6 +223,12 @@ class PatternCompiler: prefix_escaped = re.escape(self.valves.keyword_prefix) flags = 0 if self.valves.case_sensitive else re.IGNORECASE + # Compile universal persona detection pattern + # Matches: !{word} where word starts with letter, followed by letters/numbers/underscores + self.universal_persona_pattern = re.compile( + rf"{prefix_escaped}([a-zA-Z][a-zA-Z0-9_]*)\b", flags + ) + # Compile list command pattern list_cmd = self.valves.list_command_keyword if not self.valves.case_sensitive: @@ -252,26 +259,39 @@ class PatternCompiler: except Exception as e: print(f"[PATTERN COMPILER] Error compiling patterns: {e}") - def get_persona_pattern(self, persona_key: str): - """Get or compile a pattern for a specific persona.""" - if persona_key not in self.persona_patterns: - try: - prefix_escaped = re.escape(self.valves.keyword_prefix) - keyword_check = ( - persona_key if self.valves.case_sensitive else persona_key.lower() - ) - flags = 0 if self.valves.case_sensitive else re.IGNORECASE - pattern_str = rf"{prefix_escaped}{re.escape(keyword_check)}\b" - self.persona_patterns[persona_key] = re.compile(pattern_str, flags) - except Exception: - return None + def discover_all_persona_commands(self, message_content: str) -> List[str]: + """ + Dynamically discover ALL persona commands in content. + Works with current 50+ personas AND any future additions. + """ + if not message_content: + return [] - return self.persona_patterns[persona_key] + self._compile_patterns() - def detect_keyword( - self, message_content: str, available_personas: Dict - ) -> Optional[str]: - """Efficiently detect persona keywords using pre-compiled patterns.""" + if not self.universal_persona_pattern: + return [] + + content_to_check = ( + message_content if self.valves.case_sensitive else message_content.lower() + ) + + # Find all persona commands + matches = self.universal_persona_pattern.findall(content_to_check) + + # Remove duplicates while preserving order + seen = set() + unique_personas = [] + for persona in matches: + persona_key = persona if self.valves.case_sensitive else persona.lower() + if persona_key not in seen: + seen.add(persona_key) + unique_personas.append(persona_key) + + return unique_personas + + def detect_special_commands(self, message_content: str) -> Optional[str]: + """Detect special commands (list, reset) that take precedence.""" if not message_content: return None @@ -289,14 +309,91 @@ class PatternCompiler: if self.reset_pattern and self.reset_pattern.search(content_to_check): return "reset" - # Check persona commands - for persona_key in available_personas.keys(): - pattern = self.get_persona_pattern(persona_key) - if pattern and pattern.search(content_to_check): - return persona_key - return None + def parse_multi_persona_sequence(self, content: str) -> Dict: + """ + Parse content with multiple persona switches into structured sequence. + + Input: "!writer do X !teacher do Y !physicist do Z" + + Output: { + 'is_multi_persona': True, + 'sequence': [ + {'persona': 'writer', 'task': 'do X'}, + {'persona': 'teacher', 'task': 'do Y'}, + {'persona': 'physicist', 'task': 'do Z'} + ], + 'requested_personas': ['writer', 'teacher', 'physicist'] + } + """ + if not content: + return {"is_multi_persona": False} + + self._compile_patterns() + + if not self.universal_persona_pattern: + return {"is_multi_persona": False} + + # Find all persona commands and their positions + persona_matches = [] + for match in self.universal_persona_pattern.finditer(content): + persona_key = match.group(1) + if not self.valves.case_sensitive: + persona_key = persona_key.lower() + + persona_matches.append( + {"persona": persona_key, "start": match.start(), "end": match.end()} + ) + + if len(persona_matches) < 1: + return {"is_multi_persona": False} + + # Extract content segments between persona commands + sequence = [] + for i, match in enumerate(persona_matches): + # Get content from end of current command to start of next command + # (or end of string for last command) + task_start = match["end"] + task_end = ( + persona_matches[i + 1]["start"] + if i + 1 < len(persona_matches) + else len(content) + ) + task_content = content[task_start:task_end].strip() + + # Clean up task content by removing any leading persona commands from next segment + if i + 1 < len(persona_matches): + # Remove the next persona command if it bleeds into this task + next_persona_cmd = ( + f"{self.valves.keyword_prefix}{persona_matches[i + 1]['persona']}" + ) + if task_content.endswith(next_persona_cmd): + task_content = task_content[: -len(next_persona_cmd)].strip() + + sequence.append( + { + "persona": match["persona"], + "task": ( + task_content + if task_content + else "Please introduce yourself and explain your capabilities." + ), + } + ) + + # Get unique personas requested + requested_personas = list( + dict.fromkeys(match["persona"] for match in persona_matches) + ) + + return { + "is_multi_persona": len(sequence) > 0, + "is_single_persona": len(sequence) == 1, + "sequence": sequence, + "requested_personas": requested_personas, + } + class SmartPersonaCache: """Intelligent caching system for persona configurations.""" @@ -369,11 +466,15 @@ class Filter: default=False, description="Enable performance debugging - logs timing information", ) + multi_persona_transitions: bool = Field( + default=True, + description="Show transition announcements in multi-persona responses (๐ŸŽญ **Persona Name**)", + ) def __init__(self): self.valves = self.Valves() self.toggle = True - self.icon = """data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iY3VycmVudENvbG9yIj4KICA8cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik0xNS43NSA1QzE1Ljc1IDMuMzQzIDE0LjQwNyAyIDEyLjc1IDJTOS43NSAzLjM0MyA5Ljc1IDV2MC41QTMuNzUgMy43NSAwIDAgMCAxMy41IDkuMjVjMi4xIDAgMy44MS0xLjc2NyAzLjc1LTMuODZWNVoiLz4KICA8cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik04LjI1IDV2LjVhMy43NSAzLjc1IDAgMCAwIDMuNzUgMy43NWMuNzE0IDAgMS4zODUtLjIgMS45Ni0uNTU2QTMuNzUgMy43NSAwIDAgMCAxNy4yNSA1djAuNUMxNy4yNSAzLjM0MyAxNS45MDcgMiAxNC4yNSAzczMuNzUgMS4zNDMgMy43NSAzdjAuNUEzLjc1IDMuNzUgMCAwIDAgMjEuNzUgOWMuNzE0IDAgMS4zODUtLjIgMS45Ni0uNTU2QTMuNzUgMy43NSAwIDAgMCAyMS4yNSA1djAuNSIvPgo8L3N2Zz4=""" + self.icon = """data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iY3VycmVudENvbG9yIj4KICA8cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik0xNS43NSA1QzE1Ljc1IDMuMzQzIDE0LjQwNyAyIDEyLjc1IDJTOS43NSAzLjM0MyA5Ljc1IDV2MC41QTMuNzUgMy43NSAwIDAgMCAxMy41IDkuMjVjMi4xIDAgMy44MS0xLjc2NyAzLjc1LTMuODZWNVoiLz4KICA8cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik04LjI1IDV2LjVhMy43NSAzLjc1IDAgMCAwIDMuNzUgMy43NWMuNzE0IDAgMS4zODUtLjIgMS45Ni0uNTU2QTMuNzUgMy43NSAwIDAgMCAxNy4yNSA1djAuNUMxNy4yNSAzLjM0MyAxNS45MDcgMiAxNC4yNSAyczMuNzUgMS4zNDMgMy43NSAzdjAuNUEzLjc1IDMuNzUgMCAwIDAgMjEuNzUgOWMuNzE0IDAgMS4zODUtLjIgMS45Ni0uNTU2QTMuNzUgMy43NSAwIDAgMCAyMS4yNSA1djAuNSIvPgo8L3N2Zz4=""" # State management self.current_persona = None @@ -382,7 +483,7 @@ class Filter: self.event_emitter_for_close_task = None # Performance optimization components - self.pattern_compiler = PatternCompiler(self.valves) + self.pattern_compiler = UniversalPatternCompiler(self.valves) self.persona_cache = SmartPersonaCache() # Download system @@ -686,21 +787,37 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag print(f"[PERSONA CONFIG] Error loading personas: {e}") return self.get_master_controller_persona() - def _detect_persona_keyword(self, message_content: str) -> Optional[str]: - """Efficiently detect persona keywords using pre-compiled patterns.""" - start_time = time.time() if self.valves.debug_performance else 0 + def _load_requested_personas_only(self, requested_personas: List[str]) -> Dict: + """ + Load ONLY the personas actually requested in the prompt. + Includes validation and graceful error handling. + """ + # Load the full personas database once + all_available_personas = self._load_personas() - if not message_content: - return None + # Always include Master Controller + result = { + "_master_controller": all_available_personas.get("_master_controller", {}) + } - personas = self._load_personas() - result = self.pattern_compiler.detect_keyword(message_content, personas) + # Track what we found vs what was requested + found_personas = [] + missing_personas = [] - if self.valves.debug_performance: - elapsed = (time.time() - start_time) * 1000 - self._debug_log( - f"_detect_persona_keyword completed in {elapsed:.2f}ms (result: {result})" - ) + for persona_key in requested_personas: + if persona_key in all_available_personas: + result[persona_key] = all_available_personas[persona_key] + found_personas.append(persona_key) + else: + missing_personas.append(persona_key) + + # Add metadata about the loading process + result["_loading_info"] = { + "requested": requested_personas, + "found": found_personas, + "missing": missing_personas, + "total_loaded": len(found_personas), + } return result @@ -728,7 +845,75 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag return {"role": "system", "content": system_content} + def _create_dynamic_multi_persona_system( + self, requested_personas: List[str] + ) -> Dict: + """ + Build dynamic system message with Master Controller + requested personas. + Works with ANY persona combination, current or future. + """ + # Load only the requested personas + loaded_personas = self._load_requested_personas_only(requested_personas) + loading_info = loaded_personas.pop("_loading_info") + + # Build system message with Master Controller + requested personas + master_controller = loaded_personas.get("_master_controller", {}) + system_content = master_controller.get("prompt", "") + + # Add each successfully loaded persona + persona_definitions = [] + for persona_key in loading_info["found"]: + persona_data = loaded_personas[persona_key] + persona_name = persona_data.get("name", persona_key.title()) + persona_prompt = persona_data.get("prompt", "") + + persona_definitions.append( + f""" +=== {persona_name.upper()} PERSONA === +Activation Command: !{persona_key} +{persona_prompt} +=== END {persona_name.upper()} === +""" + ) + + # Create execution instructions + transition_instruction = "" + if self.valves.multi_persona_transitions and self.valves.show_persona_info: + transition_instruction = '3. Announce switches: "๐ŸŽญ **[Persona Name]**"' + else: + transition_instruction = ( + "3. Switch personas seamlessly without announcements" + ) + + multi_persona_instructions = f""" + +=== DYNAMIC MULTI-PERSONA MODE === +Active Personas: {len(loading_info['found'])} loaded on-demand + +{(''.join(persona_definitions))} + +EXECUTION FRAMEWORK: +1. Parse user's persona sequence from their original message +2. When you encounter !{{persona}}, switch to that persona immediately +{transition_instruction} +4. Execute the task following each !command until the next !command +5. Maintain context flow between all switches +6. Available commands in this session: {', '.join([f'!{p}' for p in loading_info['found']])} + +{f"โš ๏ธ Unrecognized commands (will be ignored): {', '.join([f'!{p}' for p in loading_info['missing']])}" if loading_info['missing'] else ""} + +Execute the user's multi-persona sequence seamlessly. +=== END DYNAMIC MULTI-PERSONA MODE === +""" + + return { + "role": "system", + "content": system_content + multi_persona_instructions, + "loading_info": loading_info, # For status messages + } + def _remove_keyword_from_message(self, content: str, keyword_found: str) -> str: + """Remove persona command keywords from message content.""" prefix = re.escape(self.valves.keyword_prefix) flags = 0 if self.valves.case_sensitive else re.IGNORECASE @@ -751,12 +936,45 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag return content.strip() + def _build_multi_persona_instructions( + self, sequence: List[Dict], personas_data: Dict + ) -> str: + """ + Convert parsed sequence into clear LLM instructions. + """ + if not sequence: + return "No valid persona sequence found." + + instructions = ["Execute this multi-persona sequence:\n"] + + for i, step in enumerate(sequence, 1): + persona_key = step["persona"] + task = step["task"] + persona_name = personas_data.get(persona_key, {}).get( + "name", persona_key.title() + ) + + instructions.append( + f""" +**Step {i} - {persona_name}:** +{task} +""" + ) + + instructions.append( + """ +\nExecute each step in sequence, following the persona switching framework provided.""" + ) + + return "\n".join(instructions) + async def _emit_and_schedule_close( self, emitter: Callable[[dict], Any], description: str, status_type: str = "in_progress", ): + """Emit status message and schedule auto-close.""" if not emitter or not self.valves.show_persona_info: return @@ -780,6 +998,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag asyncio.create_task(self._try_close_message_after_delay(message_id)) async def _try_close_message_after_delay(self, message_id_to_close: str): + """Auto-close status message after configured delay.""" await asyncio.sleep(self.valves.status_message_auto_close_delay_ms / 1000.0) if ( self.event_emitter_for_close_task @@ -820,6 +1039,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag and ( "๐ŸŽญ **Active Persona**" in msg.get("content", "") or "=== OPENWEBUI MASTER CONTROLLER ===" in msg.get("content", "") + or "=== DYNAMIC MULTI-PERSONA MODE ===" in msg.get("content", "") ) ) ] @@ -861,7 +1081,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag ] reset_cmds_str = ", ".join(reset_cmds_formatted) - # Return instructions for the LLM to present the table (like the original) + # Return instructions for the LLM to present the table return ( f"Please present the following information. First, a Markdown table of available persona commands, " f"titled '**Available Personas**'. The table should have columns for 'Command' and 'Name', " @@ -872,7 +1092,9 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag f"{table_data_str}\n\n" f"After the table, please add the following explanation on a new line:\n" f"To revert to the default assistant, use one of these commands: {reset_cmds_str}\n\n" - f"Ensure the output is only the Markdown table with its title, followed by the reset instructions, all correctly formatted." + f"**Multi-Persona Support:** You can now use multiple personas in a single message! " + f"Example: `{self.valves.keyword_prefix}writer create a story {self.valves.keyword_prefix}teacher explain the literary techniques {self.valves.keyword_prefix}artist describe visuals`\n\n" + f"Ensure the output is properly formatted Markdown." ) async def _handle_toggle_off_state( @@ -938,8 +1160,9 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag for msg_dict in messages: msg = dict(msg_dict) - if msg.get("role") == "system" and "๐ŸŽญ **Active Persona**" in msg.get( - "content", "" + if msg.get("role") == "system" and ( + "๐ŸŽญ **Active Persona**" in msg.get("content", "") + or "=== DYNAMIC MULTI-PERSONA MODE ===" in msg.get("content", "") ): continue if ( @@ -968,29 +1191,30 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag ) return body - async def _handle_persona_switch_command( + async def _handle_single_persona_command( self, - detected_keyword_key: str, + persona_key: str, body: Dict, messages: List[Dict], last_message_idx: int, original_content: str, __event_emitter__: Callable[[dict], Any], ) -> Dict: - """Handle persona switching commands like !coder, !writer, etc.""" + """Handle single persona switching commands like !coder, !writer, etc.""" personas_data = self._load_personas() - if detected_keyword_key not in personas_data: + if persona_key not in personas_data: return body - self.current_persona = detected_keyword_key - persona_config = personas_data[detected_keyword_key] + self.current_persona = persona_key + persona_config = personas_data[persona_key] temp_messages = [] user_message_modified = False for msg_dict in messages: msg = dict(msg_dict) - if msg.get("role") == "system" and "๐ŸŽญ **Active Persona**" in msg.get( - "content", "" + if msg.get("role") == "system" and ( + "๐ŸŽญ **Active Persona**" in msg.get("content", "") + or "=== DYNAMIC MULTI-PERSONA MODE ===" in msg.get("content", "") ): continue if ( @@ -999,7 +1223,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag and msg.get("content", "") == original_content ): cleaned_content = self._remove_keyword_from_message( - original_content, detected_keyword_key + original_content, persona_key ) intro_request_default = ( "Please introduce yourself and explain what you can help me with." @@ -1024,7 +1248,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag msg["content"] = intro_request_default else: persona_name_for_prompt = persona_config.get( - "name", detected_keyword_key.title() + "name", persona_key.title() ) msg["content"] = ( f"Please briefly introduce yourself as {persona_name_for_prompt}. After your introduction, please help with the following: {cleaned_content}" @@ -1032,11 +1256,11 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag user_message_modified = True temp_messages.append(msg) - persona_system_msg = self._create_persona_system_message(detected_keyword_key) + persona_system_msg = self._create_persona_system_message(persona_key) temp_messages.insert(0, persona_system_msg) body["messages"] = temp_messages - persona_display_name = persona_config.get("name", detected_keyword_key.title()) + persona_display_name = persona_config.get("name", persona_key.title()) await self._emit_and_schedule_close( __event_emitter__, f"๐ŸŽญ Switched to {persona_display_name}", @@ -1044,6 +1268,67 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag ) return body + async def _handle_multi_persona_command( + self, + sequence_data: Dict, + body: Dict, + messages: List[Dict], + last_message_idx: int, + original_content: str, + __event_emitter__: Callable[[dict], Any], + ) -> Dict: + """ + Handle complex multi-persona sequences. + """ + requested_personas = sequence_data["requested_personas"] + sequence = sequence_data["sequence"] + + # Build dynamic system message + dynamic_system_result = self._create_dynamic_multi_persona_system( + requested_personas + ) + loading_info = dynamic_system_result.pop("loading_info") + + # Remove old persona messages + temp_messages = self._remove_persona_system_messages(messages) + temp_messages.insert(0, dynamic_system_result) + + # Build instruction content from the sequence + all_personas = self._load_personas() + instruction_content = self._build_multi_persona_instructions( + sequence, all_personas + ) + + # Update user message with structured instructions + temp_messages[last_message_idx + 1][ + "content" + ] = instruction_content # +1 because we inserted system message + + body["messages"] = temp_messages + + # Update current state for multi-persona + if len(loading_info["found"]) == 1: + self.current_persona = loading_info["found"][0] + else: + self.current_persona = f"multi:{':'.join(loading_info['found'])}" + + # Status message + if loading_info["found"]: + persona_names = [] + for p in loading_info["found"]: + name = all_personas.get(p, {}).get("name", p.title()) + persona_names.append(name) + + status_msg = f"๐ŸŽญ Multi-persona sequence: {' โ†’ '.join(persona_names)}" + if loading_info["missing"]: + status_msg += f" | โš ๏ธ Unknown: {', '.join([f'!{p}' for p in loading_info['missing']])}" + + await self._emit_and_schedule_close( + __event_emitter__, status_msg, "complete" + ) + + return body + def _apply_persistent_persona(self, body: Dict, messages: List[Dict]) -> Dict: """Apply current persona to messages when no command detected (ALWAYS includes master controller).""" if not self.valves.persistent_persona: @@ -1052,6 +1337,11 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag personas = self._load_personas() target_persona = self.current_persona if self.current_persona else None + # Handle multi-persona persistent state + if target_persona and target_persona.startswith("multi:"): + # For multi-persona, we don't persist - user needs to issue new commands + return body + if not target_persona or target_persona not in personas: return body @@ -1097,7 +1387,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag __event_emitter__: Callable[[dict], Any], __user__: Optional[dict] = None, ) -> dict: - """Main entry point - orchestrates the persona switching flow.""" + """Main entry point - orchestrates the universal persona switching flow.""" messages = body.get("messages", []) if messages is None: messages = [] @@ -1123,18 +1413,17 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag if last_message_idx == -1: return self._apply_persistent_persona(body, messages) - # Detect persona command - detected_keyword_key = self._detect_persona_keyword( + # Check for special commands first (they take precedence) + special_command = self.pattern_compiler.detect_special_commands( original_content_of_last_user_msg ) - # Route to appropriate command handler - if detected_keyword_key: - if detected_keyword_key == "list_personas": + if special_command: + if special_command == "list_personas": return await self._handle_list_personas_command( body, messages, last_message_idx, __event_emitter__ ) - elif detected_keyword_key == "reset": + elif special_command == "reset": return await self._handle_reset_command( body, messages, @@ -1142,10 +1431,28 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag original_content_of_last_user_msg, __event_emitter__, ) + + # Parse for persona sequence (universal detection) + sequence_data = self.pattern_compiler.parse_multi_persona_sequence( + original_content_of_last_user_msg + ) + + if sequence_data["is_multi_persona"]: + if sequence_data["is_single_persona"]: + # Single persona command + persona_key = sequence_data["sequence"][0]["persona"] + return await self._handle_single_persona_command( + persona_key, + body, + messages, + last_message_idx, + original_content_of_last_user_msg, + __event_emitter__, + ) else: - # Handle persona switching command - return await self._handle_persona_switch_command( - detected_keyword_key, + # Multi-persona sequence + return await self._handle_multi_persona_command( + sequence_data, body, messages, last_message_idx, @@ -1153,7 +1460,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag __event_emitter__, ) else: - # No command detected, apply persistent persona if active + # No persona commands detected, apply persistent persona if active return self._apply_persistent_persona(body, messages) async def outlet( @@ -1162,6 +1469,7 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag return body def get_persona_list(self) -> str: + """Get formatted list of available personas for API/external use.""" personas = self._load_personas() # Filter out master controller and metadata from user-facing list @@ -1190,7 +1498,9 @@ Leverage these capabilities appropriately - use LaTeX for math, Mermaid for diag command_info = ( f"\n\n**System Commands:**\n" f"โ€ข {list_command_display} - Lists persona commands and names in a multi-column Markdown table.\n" - f"โ€ข {reset_keywords_display} - Reset to default assistant behavior (LLM will confirm)." + f"โ€ข {reset_keywords_display} - Reset to default assistant behavior (LLM will confirm).\n\n" + f"**Multi-Persona Support:** Use multiple personas in one message!\n" + f"Example: `{self.valves.keyword_prefix}writer story {self.valves.keyword_prefix}teacher explain {self.valves.keyword_prefix}artist visuals`" ) if not persona_list_items: