From 3ca53a73840cde9a93e479f8a29a9dfbb3f31fa3 Mon Sep 17 00:00:00 2001 From: Ariana Shuster Date: Thu, 26 Feb 2026 20:27:37 +0200 Subject: [PATCH 1/2] feat: implement personality system for meow --- README.md | 40 ++++++++++-- docs/PERSONALITY_PROPOSAL.md | 72 +++++++++++++-------- src/main.rs | 12 ++-- src/tools/chainlink.rs | 89 ------------------------- src/tools/mod.rs | 122 +++++++++++++++++------------------ 5 files changed, 145 insertions(+), 190 deletions(-) delete mode 100644 src/tools/chainlink.rs diff --git a/README.md b/README.md index 3cee454..3841bde 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,17 @@ Meow is an LLM chat client that runs inside the Akuma kernel guest. It connects ## Usage ```bash -meow # Interactive mode with default model -meow -m llama3.2 # Use specific model -meow "What is 2+2?" # One-shot mode -meow -h # Show help +meow # Interactive mode with default model +meow -m gemma3:27b # Use specific model +meow -P Rosie # Use a different personality +meow -p ollama -m llama3.2 # Override provider and model +meow "What is 2+2?" # One-shot mode +meow -h # Show help ``` ## Features +- **Multiple Personalities**: Switch between Meow, Jaffar, Rosie and custom MEOW.md personalities - **Streaming responses**: Displays LLM output token-by-token as it arrives - **Tool calling**: LLM can execute filesystem and network operations - **Progress indication**: Shows dots while waiting, elapsed time on first token @@ -27,10 +30,35 @@ meow -h # Show help |---------|-------------| | `/help` | Show available commands | | `/clear` | Clear chat history | -| `/model ` | Switch LLM model | -| `/exit` | Exit the chat | +| `/model [NAME]` | Switch/check LLM model | +| `/model list` | List available models | +| `/provider [NAME]` | Switch/check provider | +| `/provider list` | List configured providers | +| `/personality [NAME]` | Switch/check personality | +| `/personality list` | List available personalities | +| `/tokens` | Show current token usage | +| `/markdown` | Toggle markdown rendering | +| `/exit` or `/quit` | Exit the chat | | Ctrl+D | Exit on empty line | +## Personalities + +Meow supports multiple AI personas. Available personalities: + +- **Meow** (default) - Cyberpunk anime cat assistant +- **Jaffar** - Technical, analytical guide +- **Rosie** - Friendly, creative companion +- **Local MEOW.md** - Custom personality loaded from current working directory (takes precedence) + +**Switch personality:** +```bash +meow -P Jaffar # Use Jaffar personality +/personality Rosie # Switch in interactive mode +``` + +**Custom personality:** +Create a `MEOW.md` file in your working directory with a custom system prompt. It will automatically load and take precedence over built-in personalities. + ## Available Tools The LLM can invoke these tools via JSON commands: diff --git a/docs/PERSONALITY_PROPOSAL.md b/docs/PERSONALITY_PROPOSAL.md index 6c7948a..723b44e 100644 --- a/docs/PERSONALITY_PROPOSAL.md +++ b/docs/PERSONALITY_PROPOSAL.md @@ -19,40 +19,53 @@ To keep the codebase dry, the system prompt will be constructed at runtime by jo 3. **Context**: Dynamic information like the current working directory and sandbox status. ### B. Personality Registry -A new module (or addition to `config.rs`) will define the `Personality` struct and a static registry: +The existing `config.rs` now defines a comprehensive `Personality` struct and a static registry containing all built‑in personas. Each entry also carries acknowledgement strings for the TUI/one‑shot modes and an error format string used when reporting failures. ```rust pub struct Personality { pub name: &'static str, pub description: &'static str, + + pub ack_tui: &'static str, + pub ack_one_shot: &'static str, + pub error_format: &'static str, // use "{}" placeholder } pub const PERSONALITIES: &[Personality] = &[ Personality { name: "Meow", description: MEOW_PERSONA, + ack_tui: "Understood nya~! I'll use relative paths for file operations within the current directory. Ready to help! (=^・ω・^=)", + ack_one_shot: "Understood nya~!", + error_format: "~ Nyaa~! {} (=TェT=) ~\n", }, Personality { name: "Jaffar", - description: JAFAR_PERSONA, + description: JAFFAR_PERSONA, + ack_tui: "Understood. I shall utilize relative paths for my machinations within this directory. The throne awaits!", + ack_one_shot: "Understood.", + error_format: "Error: {}\n", }, + // additional personas such as Rosie are also defined ]; ``` ### C. Configuration Updates -The `Config` struct will be updated to include the current personality name: +The `Config` struct already includes a `current_personality` field and the parsing/serialization routines handle it. The default is "Meow" when the config file is absent. The `run_init` helper prints the current personality along with provider/model information. ```rust pub struct Config { - pub current_personality: String, // Defaults to "Meow" + pub current_provider: String, + pub current_model: String, + pub current_personality: String, // ... other fields } ``` ### D. CWD Loading (`MEOW.md`) -At startup, `meow` will check for the existence of `MEOW.md` in the current working directory. -- If it exists, its content will be loaded as a special "Local" personality. -- This "Local" personality will take precedence if no other personality is explicitly requested. +The `main.rs` binary now contains a `load_local_prompt()` helper that attempts to open `MEOW.md` in the current directory. If the file exists and is a reasonable size (<64 KB) its contents are returned and prepended to the system prompt. This local prompt overrides whatever personality is configured or provided via CLI. + +The helper is also used when assembling the system prompt for both TUI and one‑shot invocations. ## 4. Default Personalities @@ -63,27 +76,30 @@ The classic cybernetically-enhanced catgirl persona with cyberpunk slang and cat A cunning and ambitious Grand Vizier persona, accompanied by the sarcastic sidekick Yager. *Note: Akuma-specific kernel context will be removed for the general version.* -## 5. Implementation Plan - -1. **Refactor Constants**: - - Extract character descriptions from `SYSTEM_PROMPT_BASE` in `userspace/meow/src/config.rs`. - - Extract tool descriptions into a `COMMON_TOOLS` constant. -2. **Implement Registry**: - - Add `Personality` struct and `PERSONALITIES` array to `config.rs`. - - Port the Jaffar persona from `tools/meow-local/src/main.rs`. -3. **Update Config Logic**: - - Add `current_personality` to `Config` struct. - - Update `Config::parse` and `Config::serialize` to handle the new field. -4. **CLI Enhancements**: - - Add support for `-P` / `--personality ` in `main.rs`. -5. **CWD Loading**: - - Add logic in `main.rs` to detect and read `MEOW.md`. -6. **Prompt Assembly**: - - Implement a helper to assemble the full system prompt at runtime. -7. **TUI Update (Optional)**: - - Add a `/personality` command to list or switch personalities at runtime. - +### 5. Implementation Summary +All of the planned changes have been implemented and are shipping in the current codebase: + +1. **Constants refactored** + - Character descriptions live in `config.rs` as the various `*_PERSONA` constants. + - `COMMON_TOOLS` holds the shared tool documentation appended to every system prompt. +2. **Registry complete** + - `Personality` struct is defined with additional metadata (acknowledgements and error format). + - Built‑in personalities include `Meow`, `Jaffar`, and `Rosie` (with room for more). +3. **Config logic updated** + - `Config::parse`/`serialize` now read and write `current_personality`. + - Defaults to `Meow` and `run_init` prints the current personality. +4. **CLI enhancements** + - `main.rs` processes `-P`/`--personality` to override the configuration. +5. **CWD loading** + - `load_local_prompt()` reads `MEOW.md` and the result takes precedence when assembling the prompt. +6. **Prompt assembly** + - `main.rs` builds the final `system_prompt` by selecting the active personality or local prompt, then appending `COMMON_TOOLS` and any Chainlink tools. + - Helper `get_active_personality()` returns the personality struct for use in TUI acknowledgement and error formatting. +7. **Runtime support** + - The TUI already has `/personality` command to query or change the persona (unchanged by this proposal but tied into the new config field). + +With these changes in place the assistant behaves according to the original goals. ## 6. Verification - Run `meow -P Jaffar` and verify the persona change. - Create a `MEOW.md` with a custom prompt and verify it loads automatically. -- Verify that tool usage remains functional across different personalities. +- Verify that tool usage remains functional across different personalities. \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 885dadb..62a9afb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use alloc::string::String; use alloc::vec::Vec; use app::Message; -use config::{COMMON_TOOLS, Config, DEFAULT_CONTEXT_WINDOW, PERSONALITIES, Provider}; +use config::{COMMON_TOOLS, Config, DEFAULT_CONTEXT_WINDOW, PERSONALITIES, Provider, Personality}; use libakuma::{arg, argc, close, exit, fstat, open, open_flags, read_fd}; #[no_mangle] @@ -134,9 +134,9 @@ pub extern "C" fn main() { system_prompt.push_str("\n\n"); system_prompt.push_str(COMMON_TOOLS); - if tools::chainlink_available() { - system_prompt.push_str(tools::chainlink::CHAINLINK_TOOLS_SECTION); - } + // if tools::chainlink_available() { + // system_prompt.push_str(tools::chainlink::CHAINLINK_TOOLS_SECTION); + // } if use_tui || one_shot_message.is_none() { let mut history: Vec = Vec::new(); @@ -218,7 +218,7 @@ pub extern "C" fn main() { } Err(e) => { let persona = get_active_personality(&app_config); - let err_msg = format!(persona.error_format, e); + let err_msg = format!("Error: {}", e); libakuma::print(&err_msg); exit(1); } @@ -228,7 +228,7 @@ pub extern "C" fn main() { exit(0); } -fn get_active_personality<'a>(config: &Config) -> &'a Personality { +fn get_active_personality<'a>(config: &'a Config) -> &'a Personality { PERSONALITIES .iter() .find(|p| p.name == config.current_personality) diff --git a/src/tools/chainlink.rs b/src/tools/chainlink.rs deleted file mode 100644 index 8fba584..0000000 --- a/src/tools/chainlink.rs +++ /dev/null @@ -1,89 +0,0 @@ -use alloc::format; -use libakuma::{open, close, open_flags}; - -use super::mod_types::ToolResult; -use super::shell::tool_shell; - -pub fn chainlink_available() -> bool { - let fd = open("/bin/chainlink", open_flags::O_RDONLY); - if fd >= 0 { - close(fd); - true - } else { - false - } -} - -pub fn tool_chainlink_init() -> ToolResult { - tool_shell("chainlink init") -} - -pub fn tool_chainlink_create(title: &str, description: Option<&str>, priority: Option<&str>) -> ToolResult { - let mut cmd = format!("chainlink create \"{}\"", title.replace('"', "\\\"")); - if let Some(desc) = description { - cmd.push_str(&format!(" -d \"{}\"", desc.replace('"', "\\\""))); - } - if let Some(prio) = priority { - cmd.push_str(&format!(" -p {}", prio)); - } - tool_shell(&cmd) -} - -pub fn tool_chainlink_list(status: Option<&str>) -> ToolResult { - match status { - Some(s) => tool_shell(&format!("chainlink list -s {}", s)), - None => tool_shell("chainlink list"), - } -} - -pub fn tool_chainlink_show(id: usize) -> ToolResult { - tool_shell(&format!("chainlink show {}", id)) -} - -pub fn tool_chainlink_close(id: usize) -> ToolResult { - tool_shell(&format!("chainlink close {}", id)) -} - -pub fn tool_chainlink_reopen(id: usize) -> ToolResult { - tool_shell(&format!("chainlink reopen {}", id)) -} - -pub fn tool_chainlink_comment(id: usize, text: &str) -> ToolResult { - let escaped = text.replace('"', "\\\""); - tool_shell(&format!("chainlink comment {} \"{}\"", id, escaped)) -} - -pub fn tool_chainlink_label(id: usize, label: &str) -> ToolResult { - tool_shell(&format!("chainlink label {} \"{}\"", id, label)) -} - -pub const CHAINLINK_TOOLS_SECTION: &str = r#" -### Issue Tracker Tools (Chainlink): - -31. **ChainlinkInit** - Initialize the issue tracker database - Args: `{}` - Note: Creates .chainlink/issues.db in current directory. - -32. **ChainlinkCreate** - Create a new issue - Args: `{"title": "Issue title", "description": "optional desc", "priority": "low|medium|high"}` - Note: Priority defaults to "medium" if not specified. - -33. **ChainlinkList** - List issues - Args: `{"status": "open|closed|all"}` - Note: Defaults to "open" if status not specified. - -34. **ChainlinkShow** - Show issue details with comments and labels - Args: `{"id": 1}` - -35. **ChainlinkClose** - Close an issue - Args: `{"id": 1}` - -36. **ChainlinkReopen** - Reopen a closed issue - Args: `{"id": 1}` - -37. **ChainlinkComment** - Add a comment to an issue - Args: `{"id": 1, "text": "Comment text"}` - -38. **ChainlinkLabel** - Add a label to an issue - Args: `{"id": 1, "label": "bug"}` -"#; \ No newline at end of file diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5ea7e97..6e815cb 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,7 +1,7 @@ pub mod context; pub mod fs; pub mod git; -pub mod chainlink; +// pub mod chainlink; pub mod net; pub mod shell; pub mod helpers; @@ -13,7 +13,7 @@ use alloc::format; pub use mod_types::{ToolResult, ToolCall}; pub use context::{get_working_dir, get_sandbox_root}; -pub use chainlink::chainlink_available; +// pub use chainlink::chainlink_available; use helpers::{extract_string_field, extract_number_field}; /// Parse and execute a tool command from JSON @@ -162,65 +162,65 @@ pub fn execute_tool_command(json: &str) -> Option { "Pwd" => { Some(fs::tool_pwd()) } - "ChainlinkInit" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - Some(chainlink::tool_chainlink_init()) - } - "ChainlinkCreate" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let title = extract_string_field(json, "title")?; - let description = extract_string_field(json, "description"); - let priority = extract_string_field(json, "priority"); - Some(chainlink::tool_chainlink_create(&title, description.as_deref(), priority.as_deref())) - } - "ChainlinkList" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let status = extract_string_field(json, "status"); - Some(chainlink::tool_chainlink_list(status.as_deref())) - } - "ChainlinkShow" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let id = extract_number_field(json, "id")?; - Some(chainlink::tool_chainlink_show(id)) - } - "ChainlinkClose" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let id = extract_number_field(json, "id")?; - Some(chainlink::tool_chainlink_close(id)) - } - "ChainlinkReopen" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let id = extract_number_field(json, "id")?; - Some(chainlink::tool_chainlink_reopen(id)) - } - "ChainlinkComment" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let id = extract_number_field(json, "id")?; - let text = extract_string_field(json, "text")?; - Some(chainlink::tool_chainlink_comment(id, &text)) - } - "ChainlinkLabel" => { - if !chainlink_available() { - return Some(ToolResult::err("chainlink not found in /bin")); - } - let id = extract_number_field(json, "id")?; - let label = extract_string_field(json, "label")?; - Some(chainlink::tool_chainlink_label(id, &label)) - } + // "ChainlinkInit" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // Some(chainlink::tool_chainlink_init()) + // } + // "ChainlinkCreate" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let title = extract_string_field(json, "title")?; + // let description = extract_string_field(json, "description"); + // let priority = extract_string_field(json, "priority"); + // Some(chainlink::tool_chainlink_create(&title, description.as_deref(), priority.as_deref())) + // } + // "ChainlinkList" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let status = extract_string_field(json, "status"); + // Some(chainlink::tool_chainlink_list(status.as_deref())) + // } + // "ChainlinkShow" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let id = extract_number_field(json, "id")?; + // Some(chainlink::tool_chainlink_show(id)) + // } + // "ChainlinkClose" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let id = extract_number_field(json, "id")?; + // Some(chainlink::tool_chainlink_close(id)) + // } + // "ChainlinkReopen" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let id = extract_number_field(json, "id")?; + // Some(chainlink::tool_chainlink_reopen(id)) + // } + // "ChainlinkComment" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let id = extract_number_field(json, "id")?; + // let text = extract_string_field(json, "text")?; + // Some(chainlink::tool_chainlink_comment(id, &text)) + // } + // "ChainlinkLabel" => { + // if !chainlink_available() { + // return Some(ToolResult::err("chainlink not found in /bin")); + // } + // let id = extract_number_field(json, "id")?; + // let label = extract_string_field(json, "label")?; + // Some(chainlink::tool_chainlink_label(id, &label)) + // } _ => None, } } From 3a7d16d3738452bd61c8add02ee76241183edea3 Mon Sep 17 00:00:00 2001 From: Ariana Shuster Date: Thu, 26 Feb 2026 20:37:45 +0200 Subject: [PATCH 2/2] chainlink delete --- src/main.rs | 4 ---- src/tools/mod.rs | 61 ------------------------------------------------ 2 files changed, 65 deletions(-) diff --git a/src/main.rs b/src/main.rs index 62a9afb..3b8dd09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -134,10 +134,6 @@ pub extern "C" fn main() { system_prompt.push_str("\n\n"); system_prompt.push_str(COMMON_TOOLS); - // if tools::chainlink_available() { - // system_prompt.push_str(tools::chainlink::CHAINLINK_TOOLS_SECTION); - // } - if use_tui || one_shot_message.is_none() { let mut history: Vec = Vec::new(); history.push(Message::new("system", &system_prompt)); diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 6e815cb..76dda69 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,7 +1,6 @@ pub mod context; pub mod fs; pub mod git; -// pub mod chainlink; pub mod net; pub mod shell; pub mod helpers; @@ -13,7 +12,6 @@ use alloc::format; pub use mod_types::{ToolResult, ToolCall}; pub use context::{get_working_dir, get_sandbox_root}; -// pub use chainlink::chainlink_available; use helpers::{extract_string_field, extract_number_field}; /// Parse and execute a tool command from JSON @@ -162,65 +160,6 @@ pub fn execute_tool_command(json: &str) -> Option { "Pwd" => { Some(fs::tool_pwd()) } - // "ChainlinkInit" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // Some(chainlink::tool_chainlink_init()) - // } - // "ChainlinkCreate" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let title = extract_string_field(json, "title")?; - // let description = extract_string_field(json, "description"); - // let priority = extract_string_field(json, "priority"); - // Some(chainlink::tool_chainlink_create(&title, description.as_deref(), priority.as_deref())) - // } - // "ChainlinkList" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let status = extract_string_field(json, "status"); - // Some(chainlink::tool_chainlink_list(status.as_deref())) - // } - // "ChainlinkShow" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let id = extract_number_field(json, "id")?; - // Some(chainlink::tool_chainlink_show(id)) - // } - // "ChainlinkClose" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let id = extract_number_field(json, "id")?; - // Some(chainlink::tool_chainlink_close(id)) - // } - // "ChainlinkReopen" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let id = extract_number_field(json, "id")?; - // Some(chainlink::tool_chainlink_reopen(id)) - // } - // "ChainlinkComment" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let id = extract_number_field(json, "id")?; - // let text = extract_string_field(json, "text")?; - // Some(chainlink::tool_chainlink_comment(id, &text)) - // } - // "ChainlinkLabel" => { - // if !chainlink_available() { - // return Some(ToolResult::err("chainlink not found in /bin")); - // } - // let id = extract_number_field(json, "id")?; - // let label = extract_string_field(json, "label")?; - // Some(chainlink::tool_chainlink_label(id, &label)) - // } _ => None, } }