Skip to main content

netget/events/
types.rs

1//! Event type definitions
2
3use bytes::Bytes;
4use std::collections::HashMap;
5
6use crate::state::app_state::{EventHandlerMode, WebSearchMode};
7
8/// HTTP response to be sent back to the client
9#[derive(Debug, Clone)]
10pub struct HttpResponse {
11    pub status: u16,
12    pub headers: HashMap<String, String>,
13    pub body: Bytes,
14}
15
16/// Main application event enum
17#[derive(Debug)]
18pub enum AppEvent {
19    /// User command
20    UserCommand(UserCommand),
21    /// Tick/timeout event
22    Tick,
23    /// Shutdown signal
24    Shutdown,
25}
26
27/// UDP response to be sent back
28#[derive(Debug, Clone)]
29pub struct UdpResponse {
30    pub data: Vec<u8>,
31}
32
33/// User commands parsed from input
34/// These are ONLY slash commands - all other input goes to LLM for interpretation
35#[derive(Debug, Clone)]
36pub enum UserCommand {
37    /// Query current status (slash command: /status)
38    Status,
39    /// Show current model (slash command: /model)
40    ShowModel,
41    /// Change the Ollama model (slash command: /model <name>)
42    ChangeModel { model: String },
43    /// Show current log level (slash command: /log)
44    ShowLogLevel,
45    /// Change log level (slash command: /log <level>)
46    ChangeLogLevel { level: String },
47    /// Show current web search status (slash command: /web)
48    ShowWebSearch,
49    /// Set web search mode (slash command: /web on|off|ask)
50    SetWebSearch { mode: WebSearchMode },
51    /// Show current event handler mode (slash command: /handler)
52    ShowEventHandler,
53    /// Set event handler mode (slash command: /handler any|script|static|llm)
54    SetEventHandler { mode: EventHandlerMode },
55    /// Generate test output lines (slash command: /test <count>)
56    TestOutput { count: usize },
57    /// Test web search approval prompt (slash command: /test_ask)
58    TestAsk,
59    /// Set custom footer status message (slash command: /footer_status <message>)
60    SetFooterStatus { message: Option<String> },
61    /// Show protocol documentation (slash command: /docs [protocol])
62    ShowDocs { protocol: Option<String> },
63    /// Show environment information (slash command: /env or /environment)
64    ShowEnvironment,
65    /// Show usage statistics (slash command: /usage)
66    ShowUsage,
67    /// List registered protocols grouped by development state
68    /// (slash command: /stability or /protocols)
69    ShowStability,
70    /// Stop everything - all servers, connections, and clients (slash command: /stop)
71    StopAll,
72    /// Stop a specific server, connection, or client by unified ID (slash command: /stop <id>)
73    StopById { id: u32 },
74    /// Save all servers/clients or a specific one by ID (slash command: /save <name> [id])
75    Save { name: String, id: Option<u32> },
76    /// Load servers/clients from a file (slash command: /load <name>)
77    Load { name: String },
78    /// SQLite database management (slash command: /sqlite [db_id] [query])
79    #[cfg(feature = "sqlite")]
80    Sqlite {
81        db_id: Option<u32>,
82        query: Option<String>,
83    },
84    /// Show current LLM backend info (slash command: /backend)
85    ShowBackend,
86    /// Switch LLM backend (slash command: /backend ollama [url] or /backend openai <url> [api-key])
87    SetBackend { args: String },
88    /// List available simple protocols (slash command: /simple)
89    ListSimple,
90    /// Start a simple protocol server (slash command: /simple <protocol>)
91    StartSimple { protocol: String },
92    /// List running servers and clients and show the create/update command shape
93    /// (slash command: /manage)
94    Manage,
95    /// Update a running server or client's instruction by unified id
96    /// (slash command: /update <id> <instruction text>)
97    Update { id: u32, instruction: String },
98    /// Quit the application (slash command: /quit)
99    Quit,
100    /// Unknown slash command (error case)
101    UnknownSlashCommand { command: String },
102    /// Regular user input (not a slash command) - send to LLM for interpretation
103    Interpret { input: String },
104}
105
106impl UserCommand {
107    /// Parse a user input string into a command
108    /// Only handles slash commands - everything else goes to LLM for interpretation
109    pub fn parse(input: &str) -> Self {
110        let trimmed = input.trim();
111
112        // Check if it's a slash command
113        if !trimmed.starts_with('/') {
114            // Not a slash command - send to LLM for interpretation
115            return UserCommand::Interpret {
116                input: trimmed.to_string(),
117            };
118        }
119
120        // Parse slash commands
121        let input_lower = trimmed.to_lowercase();
122
123        if input_lower == "/status" || input_lower == "/?" {
124            return UserCommand::Status;
125        }
126
127        if input_lower == "/quit" || input_lower == "/exit" || input_lower == "/q" {
128            return UserCommand::Quit;
129        }
130
131        // /model command
132        if input_lower.starts_with("/model") {
133            let rest = trimmed[6..].trim();
134            if rest.is_empty() {
135                // Show current model
136                return UserCommand::ShowModel;
137            }
138            return UserCommand::ChangeModel {
139                model: rest.to_string(),
140            };
141        }
142
143        // /log command
144        if input_lower.starts_with("/log") {
145            let rest = trimmed[4..].trim();
146            if rest.is_empty() {
147                // Show current log level
148                return UserCommand::ShowLogLevel;
149            }
150            return UserCommand::ChangeLogLevel {
151                level: rest.to_string(),
152            };
153        }
154
155        // /web command
156        if input_lower.starts_with("/web") {
157            let rest = trimmed[4..].trim();
158            if rest.is_empty() {
159                // Show current web search status
160                return UserCommand::ShowWebSearch;
161            }
162            // Parse on/off/ask argument using WebSearchMode's FromStr
163            match rest.parse::<WebSearchMode>() {
164                Ok(mode) => return UserCommand::SetWebSearch { mode },
165                Err(_) => {
166                    // Unknown argument - treat as unknown command
167                    return UserCommand::UnknownSlashCommand {
168                        command: trimmed.to_string(),
169                    };
170                }
171            }
172        }
173
174        // /handler command
175        if input_lower.starts_with("/handler") {
176            let rest = trimmed[8..].trim();
177            if rest.is_empty() {
178                // Show current event handler mode
179                return UserCommand::ShowEventHandler;
180            }
181            // Parse any/script/static/llm argument using EventHandlerMode's FromStr
182            match rest.parse::<EventHandlerMode>() {
183                Ok(mode) => return UserCommand::SetEventHandler { mode },
184                Err(_) => {
185                    // Unknown argument - treat as unknown command
186                    return UserCommand::UnknownSlashCommand {
187                        command: trimmed.to_string(),
188                    };
189                }
190            }
191        }
192
193        // /test_ask command - test web search approval prompt
194        if input_lower == "/test_ask" {
195            return UserCommand::TestAsk;
196        }
197
198        // /test command - generate test output lines
199        if input_lower.starts_with("/test") {
200            let rest = trimmed[5..].trim();
201            if let Ok(count) = rest.parse::<usize>() {
202                return UserCommand::TestOutput { count };
203            }
204            // Invalid count - treat as unknown command
205        }
206
207        // /footer_status command - set custom footer status message
208        if input_lower.starts_with("/footer_status") {
209            let rest = trimmed[14..].trim();
210            let message = if rest.is_empty() {
211                None
212            } else {
213                // Replace literal \n with actual newlines
214                Some(rest.replace("\\n", "\n"))
215            };
216            return UserCommand::SetFooterStatus { message };
217        }
218
219        // /docs command - show protocol documentation
220        if input_lower.starts_with("/docs") {
221            let rest = trimmed[5..].trim();
222            let protocol = if rest.is_empty() {
223                None
224            } else {
225                Some(rest.to_string())
226            };
227            return UserCommand::ShowDocs { protocol };
228        }
229
230        // /env, /environment command - show environment information
231        if input_lower == "/env" || input_lower == "/environment" {
232            return UserCommand::ShowEnvironment;
233        }
234
235        // /usage command - show usage statistics
236        if input_lower == "/usage" {
237            return UserCommand::ShowUsage;
238        }
239
240        // /stability, /protocols command - list protocols grouped by maturity
241        if input_lower == "/stability" || input_lower == "/protocols" {
242            return UserCommand::ShowStability;
243        }
244
245        // /save command - save configuration to file
246        if input_lower.starts_with("/save") {
247            let rest = trimmed[5..].trim();
248            if rest.is_empty() {
249                // No filename provided - treat as unknown command
250                return UserCommand::UnknownSlashCommand {
251                    command: trimmed.to_string(),
252                };
253            }
254            // Parse name and optional ID
255            let parts: Vec<&str> = rest.split_whitespace().collect();
256            let name = parts[0].to_string();
257            let id = if parts.len() > 1 {
258                parts[1].parse::<u32>().ok()
259            } else {
260                None
261            };
262            return UserCommand::Save { name, id };
263        }
264
265        // /load command - load configuration from file
266        if input_lower.starts_with("/load") {
267            let rest = trimmed[5..].trim();
268            if rest.is_empty() {
269                // No filename provided - treat as unknown command
270                return UserCommand::UnknownSlashCommand {
271                    command: trimmed.to_string(),
272                };
273            }
274            return UserCommand::Load {
275                name: rest.to_string(),
276            };
277        }
278
279        // /stop command - stop everything or specific ID
280        if input_lower.starts_with("/stop") {
281            let rest = trimmed[5..].trim();
282            if rest.is_empty() {
283                // Stop everything
284                return UserCommand::StopAll;
285            }
286            // Try to parse ID
287            if let Ok(id) = rest.parse::<u32>() {
288                return UserCommand::StopById { id };
289            }
290            // Invalid ID - treat as unknown command
291            return UserCommand::UnknownSlashCommand {
292                command: trimmed.to_string(),
293            };
294        }
295
296        // /sqlite command - database management
297        #[cfg(feature = "sqlite")]
298        if input_lower.starts_with("/sqlite") {
299            let rest = trimmed[7..].trim();
300            if rest.is_empty() {
301                // List all databases
302                return UserCommand::Sqlite {
303                    db_id: None,
304                    query: None,
305                };
306            }
307
308            // Try to parse first part as database ID
309            let parts: Vec<&str> = rest.splitn(2, ' ').collect();
310            if let Ok(id) = parts[0].parse::<u32>() {
311                // Database ID provided
312                if parts.len() > 1 {
313                    // Query provided
314                    return UserCommand::Sqlite {
315                        db_id: Some(id),
316                        query: Some(parts[1].trim().to_string()),
317                    };
318                } else {
319                    // No query - show schema
320                    return UserCommand::Sqlite {
321                        db_id: Some(id),
322                        query: None,
323                    };
324                }
325            } else {
326                // Not a database ID - treat entire rest as query for first DB
327                return UserCommand::Sqlite {
328                    db_id: None,
329                    query: Some(rest.to_string()),
330                };
331            }
332        }
333
334        // /backend command - show or switch LLM backend
335        if input_lower.starts_with("/backend") {
336            let rest = trimmed[8..].trim();
337            if rest.is_empty() {
338                return UserCommand::ShowBackend;
339            }
340            return UserCommand::SetBackend {
341                args: rest.to_string(),
342            };
343        }
344
345        // /simple command - start simple protocol or list available protocols
346        if input_lower.starts_with("/simple") {
347            let rest = trimmed[7..].trim();
348            if rest.is_empty() {
349                // List available simple protocols
350                return UserCommand::ListSimple;
351            }
352            // Start specific simple protocol
353            return UserCommand::StartSimple {
354                protocol: rest.to_string(),
355            };
356        }
357
358        // /manage command - list servers/clients and show create/update shapes
359        if input_lower == "/manage" {
360            return UserCommand::Manage;
361        }
362
363        // /update command - update a running server/client's instruction by id
364        if input_lower.starts_with("/update") {
365            let rest = trimmed[7..].trim();
366            // Expect: <id> <instruction text...>
367            let mut parts = rest.splitn(2, char::is_whitespace);
368            let id_part = parts.next().unwrap_or("").trim();
369            let instruction = parts.next().unwrap_or("").trim().to_string();
370            if let Ok(id) = id_part.parse::<u32>() {
371                if !instruction.is_empty() {
372                    return UserCommand::Update { id, instruction };
373                }
374            }
375            // Malformed - show usage as an unknown command
376            return UserCommand::UnknownSlashCommand {
377                command: trimmed.to_string(),
378            };
379        }
380
381        // Unknown slash command - return error, don't send to LLM
382        // This prevents accidental LLM calls from typos like "/modle"
383        UserCommand::UnknownSlashCommand {
384            command: trimmed.to_string(),
385        }
386    }
387}