OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / 9cdcecb

Add minimal Keet CLI agent that can join a room, name itself, and respond to user messages using Ollama
9cdcecb87e0780930ae46f3e429f117a23ab1f8f
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-21T17:53:46.000Z

Changed files (5) +911 -0

ollamatools-new/keet_cmd_run +11 -0
……@@ -0,0 +1,11 @@
1#!/usr/bin/env python3
2"""
3Command-line script to run the Keet CLI agent.
4"""
5
6import asyncio
7import sys
8from ollamatools.plugins.integrations.keet.keet_cmd_agent import main
9
10if __name__ == "__main__":
11 sys.exit(asyncio.run(main()))
ollamatools-new/ollamatools/plugins/integrations/keet/README_KEET_CMD.md +101 -0
……@@ -0,0 +1,101 @@
1# Keet Command-line Agent
2
3A minimal CLI agent for Keet that can join a room, name itself, and respond to user messages using Ollama.
4
5## Features
6
7- Join a Keet room using a room key or invite link
8- Set a custom display name
9- Automatically respond to user messages using Ollama
10- Support for different Ollama models
11- Mock mode for testing without actual Keet dependencies
12
13## Requirements
14
15- Node.js (for the JavaScript bridge)
16- Python 3.8+
17- Ollama installed and available in the PATH
18
19## Installation
20
21The Keet CLI agent is part of the ollamatools package. If you have ollamatools installed, you already have the Keet CLI agent.
22
23## Usage
24
25### Command-line Usage
26
27You can run the Keet CLI agent from the command line using the `keet_cmd_run` script:
28
29```bash
30./keet_cmd_run --room-key <room_key> --name <bot_name> --model <ollama_model>
31```
32
33### Options
34
35- `--storage`: Path to store Keet data. If not provided, a default path will be used.
36- `--room-key`: Room key or invite link to join. If not provided, the most recent room will be used.
37- `--name`: Display name for the agent. Default is "KeetBot".
38- `--model`: Ollama model to use for generating responses. Default is "llama3".
39- `--mock`: Use the mock implementation (for testing).
40- `--verbose`: Enable verbose logging.
41
42### Python API Usage
43
44You can also use the Keet CLI agent from Python:
45
46```python
47import asyncio
48from ollamatools.plugins.integrations.keet.keet_cmd_agent import KeetCmdAgent
49
50async def main():
51 # Create the agent
52 agent = KeetCmdAgent(
53 room_key="your-room-key",
54 name="YourBot",
55 model="llama3"
56 )
57
58 # Start the agent
59 await agent.start()
60
61 # Keep the agent running until interrupted
62 try:
63 while True:
64 await asyncio.sleep(1)
65 except KeyboardInterrupt:
66 pass
67 finally:
68 # Stop the agent
69 await agent.stop()
70
71if __name__ == "__main__":
72 asyncio.run(main())
73```
74
75## Testing
76
77You can test the Keet CLI agent using the provided test script:
78
79```bash
80python tests/test_keet_cmd.py
81```
82
83This will run a simple test that verifies the agent can join a room, set its name, and respond to messages.
84
85## Architecture
86
87The Keet CLI agent consists of two main components:
88
891. **JavaScript Bridge**: A Node.js script that interfaces with the Keet API to join rooms, send messages, and receive messages.
902. **Python Wrapper**: A Python module that manages the JavaScript bridge and integrates with Ollama to generate responses.
91
92The JavaScript bridge communicates with the Keet API using the `@holepunchto/keet-backend` and `@holepunchto/keet-core-api` packages. If these packages are not available, a mock implementation is used for testing.
93
94The Python wrapper spawns the JavaScript bridge as a subprocess and communicates with it via stdout/stderr. It also spawns Ollama as a subprocess to generate responses to user messages.
95
96## Limitations
97
98- The Keet CLI agent is a minimal implementation and may not support all Keet features.
99- The agent does not support file transfers or other advanced Keet features.
100- The agent requires Ollama to be installed and available in the PATH.
101- The agent may not work with all Ollama models.
ollamatools-new/ollamatools/plugins/integrations/keet/keet_cmd_agent.py +244 -0
……@@ -0,0 +1,244 @@
1#!/usr/bin/env python3
2"""
3Keet Command-line Agent
4
5This module provides a Python interface to the Keet CLI agent that can:
6- Join a room
7- Set its name
8- Respond to user messages using Ollama
9"""
10
11import argparse
12import asyncio
13import logging
14import os
15import signal
16import subprocess
17import sys
18from pathlib import Path
19from typing import Optional, Dict, Any, List, Union
20
21logger = logging.getLogger(__name__)
22
23class KeetCmdAgent:
24 """
25 A command-line agent for Keet that can join a room, set its name,
26 and respond to user messages using Ollama.
27 """
28
29 def __init__(
30 self,
31 storage_path: Optional[Union[str, Path]] = None,
32 room_key: Optional[str] = None,
33 name: str = "KeetBot",
34 model: str = "llama3",
35 mock: bool = False,
36 verbose: bool = False
37 ):
38 """
39 Initialize the Keet CLI agent.
40
41 Args:
42 storage_path: Path to store Keet data. If None, a default path will be used.
43 room_key: The room key or invite link to join. If None, the most recent room will be used.
44 name: The display name for the agent.
45 model: The Ollama model to use for generating responses.
46 mock: Whether to use the mock implementation.
47 verbose: Whether to enable verbose logging.
48 """
49 self.storage_path = Path(storage_path) if storage_path else None
50 self.room_key = room_key
51 self.name = name
52 self.model = model
53 self.mock = mock
54 self.verbose = verbose
55
56 # Path to the JavaScript agent script
57 self._script_dir = Path(__file__).parent.parent.parent.parent.parent / "tests"
58 self._agent_script = self._script_dir / "keet_cmd.js"
59
60 # Check if the script exists
61 if not self._agent_script.exists():
62 logger.error(f"Keet CLI agent script not found at {self._agent_script}")
63 raise FileNotFoundError(f"Keet CLI agent script not found at {self._agent_script}")
64
65 # Process handle
66 self._process = None
67
68 async def start(self) -> bool:
69 """
70 Start the Keet CLI agent.
71
72 Returns:
73 bool: True if the agent was started successfully, False otherwise.
74 """
75 if self._process is not None:
76 logger.warning("Keet CLI agent is already running")
77 return True
78
79 # Build the command
80 cmd = ["node", str(self._agent_script)]
81
82 # Add the arguments
83 if self.storage_path:
84 cmd.extend(["--storage", str(self.storage_path)])
85
86 if self.room_key:
87 cmd.extend(["--room-key", self.room_key])
88
89 cmd.extend(["--name", self.name])
90 cmd.extend(["--model", self.model])
91
92 if self.mock:
93 cmd.append("--mock")
94
95 if self.verbose:
96 cmd.append("--verbose")
97
98 # Start the process
99 try:
100 logger.info(f"Starting Keet CLI agent with command: {' '.join(cmd)}")
101
102 # Create the process
103 self._process = await asyncio.create_subprocess_exec(
104 *cmd,
105 stdout=asyncio.subprocess.PIPE,
106 stderr=asyncio.subprocess.PIPE
107 )
108
109 # Start reading the output
110 asyncio.create_task(self._read_output())
111
112 logger.info("Keet CLI agent started successfully")
113 return True
114 except Exception as e:
115 logger.error(f"Failed to start Keet CLI agent: {e}")
116 return False
117
118 async def _read_output(self) -> None:
119 """
120 Read the output from the Keet CLI agent process.
121 """
122 if self._process is None:
123 return
124
125 # Read stdout
126 while True:
127 line = await self._process.stdout.readline()
128 if not line:
129 break
130
131 line_str = line.decode("utf-8").strip()
132 logger.info(f"Keet CLI agent: {line_str}")
133
134 # Read stderr
135 while True:
136 line = await self._process.stderr.readline()
137 if not line:
138 break
139
140 line_str = line.decode("utf-8").strip()
141 logger.error(f"Keet CLI agent error: {line_str}")
142
143 async def stop(self) -> bool:
144 """
145 Stop the Keet CLI agent.
146
147 Returns:
148 bool: True if the agent was stopped successfully, False otherwise.
149 """
150 if self._process is None:
151 logger.warning("Keet CLI agent is not running")
152 return True
153
154 try:
155 # Send SIGTERM to the process
156 self._process.terminate()
157
158 # Wait for the process to terminate
159 try:
160 await asyncio.wait_for(self._process.wait(), timeout=5.0)
161 logger.info("Keet CLI agent stopped successfully")
162 except asyncio.TimeoutError:
163 # If the process doesn't terminate within the timeout, kill it
164 logger.warning("Keet CLI agent did not terminate within the timeout, killing it")
165 self._process.kill()
166 await self._process.wait()
167
168 self._process = None
169 return True
170 except Exception as e:
171 logger.error(f"Failed to stop Keet CLI agent: {e}")
172 return False
173
174 async def __aenter__(self):
175 """
176 Enter the async context manager.
177 """
178 await self.start()
179 return self
180
181 async def __aexit__(self, exc_type, exc_val, exc_tb):
182 """
183 Exit the async context manager.
184 """
185 await self.stop()
186
187
188async def main():
189 """
190 Main entry point for the Keet CLI agent.
191 """
192 # Set up logging
193 logging.basicConfig(
194 level=logging.INFO,
195 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
196 )
197
198 # Parse command-line arguments
199 parser = argparse.ArgumentParser(description="Keet Command-line Agent")
200 parser.add_argument("--storage", type=str, help="Path to store Keet data")
201 parser.add_argument("--room-key", type=str, help="Room key or invite link to join")
202 parser.add_argument("--name", type=str, default="KeetBot", help="Display name for the agent")
203 parser.add_argument("--model", type=str, default="llama3", help="Ollama model to use")
204 parser.add_argument("--mock", action="store_true", help="Use the mock implementation")
205 parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
206
207 args = parser.parse_args()
208
209 # Create the agent
210 agent = KeetCmdAgent(
211 storage_path=args.storage,
212 room_key=args.room_key,
213 name=args.name,
214 model=args.model,
215 mock=args.mock,
216 verbose=args.verbose
217 )
218
219 # Handle signals
220 loop = asyncio.get_event_loop()
221
222 def signal_handler():
223 logger.info("Received signal, stopping agent...")
224 asyncio.create_task(agent.stop())
225
226 for sig in (signal.SIGINT, signal.SIGTERM):
227 loop.add_signal_handler(sig, signal_handler)
228
229 # Start the agent
230 try:
231 await agent.start()
232
233 # Keep the agent running until interrupted
234 while True:
235 await asyncio.sleep(1)
236 except KeyboardInterrupt:
237 logger.info("Interrupted by user")
238 finally:
239 # Stop the agent
240 await agent.stop()
241
242
243if __name__ == "__main__":
244 asyncio.run(main())
ollamatools-new/tests/keet_cmd.js +432 -0
……@@ -0,0 +1,432 @@
1#!/usr/bin/env node
2/**
3 * Keet Command-line Agent
4 *
5 * This script provides a minimal CLI agent for Keet that can:
6 * - Join a room
7 * - Set its name
8 * - Respond to user messages using Ollama
9 */
10
11import fs from 'fs';
12import path from 'path';
13import { fileURLToPath } from 'url';
14import { parseArgs } from 'node:util';
15import { spawn } from 'child_process';
16
17// Get the directory name of the current module
18const __filename = fileURLToPath(import.meta.url);
19const __dirname = path.dirname(__filename);
20
21// Configure logging
22const logLevels = {
23 INFO: 'INFO',
24 ERROR: 'ERROR',
25 WARNING: 'WARNING',
26 DEBUG: 'DEBUG'
27};
28
29function log(level, message) {
30 const timestamp = new Date().toISOString();
31 console.log(`[${timestamp}] [${level}] ${message}`);
32}
33
34// Import Keet dependencies or mock implementations
35let KeetBackend, KeetCoreApi;
36let KEET_AVAILABLE = false;
37let useMock = false;
38
39async function importDependencies() {
40 try {
41 // Try to import the real Keet dependencies
42 const backendModule = await import('@holepunchto/keet-backend');
43 const coreApiModule = await import('@holepunchto/keet-core-api');
44
45 // Try to get the exports
46 KeetBackend = backendModule.default || backendModule;
47 KeetCoreApi = coreApiModule.default || coreApiModule;
48
49 KEET_AVAILABLE = true;
50
51 log(logLevels.INFO, 'Successfully imported Keet dependencies');
52 } catch (error) {
53 log(logLevels.WARNING, `Failed to import Keet dependencies: ${error.message}`);
54 log(logLevels.WARNING, 'Using mock implementation');
55
56 // Create a simple mock implementation
57 class MockKeetBackend {
58 constructor(storagePath, options) {
59 this.storagePath = storagePath;
60 this.options = options;
61 this.api = {
62 core: {
63 getIdentity: async () => {
64 try {
65 const identityPath = path.join(storagePath, 'identity.json');
66 if (fs.existsSync(identityPath)) {
67 return JSON.parse(fs.readFileSync(identityPath, 'utf8'));
68 }
69 return null;
70 } catch (error) {
71 return null;
72 }
73 },
74 generateIdentity: async () => {
75 const identity = {
76 publicKey: `mock-${Date.now()}`,
77 displayName: 'Mock User'
78 };
79 const identityPath = path.join(storagePath, 'identity.json');
80 fs.writeFileSync(identityPath, JSON.stringify(identity));
81 return identity;
82 },
83 getRecentRooms: async () => {
84 try {
85 const roomsPath = path.join(storagePath, 'rooms.json');
86 if (fs.existsSync(roomsPath)) {
87 return JSON.parse(fs.readFileSync(roomsPath, 'utf8'));
88 }
89 return [];
90 } catch (error) {
91 return [];
92 }
93 },
94 pairRoom: async (roomKey) => {
95 const rooms = await this.api.core.getRecentRooms();
96 const newRoom = {
97 roomId: roomKey,
98 title: `Room ${roomKey.substring(0, 8)}`,
99 lastMessage: null,
100 timestamp: Date.now()
101 };
102 rooms.push(newRoom);
103 const roomsPath = path.join(storagePath, 'rooms.json');
104 fs.writeFileSync(roomsPath, JSON.stringify(rooms));
105 return true;
106 },
107 getChatMessages: async (roomId) => {
108 try {
109 const messagesPath = path.join(storagePath, `messages_${roomId}.json`);
110 if (fs.existsSync(messagesPath)) {
111 return JSON.parse(fs.readFileSync(messagesPath, 'utf8'));
112 }
113 return [];
114 } catch (error) {
115 return [];
116 }
117 },
118 addChatMessage: async (roomId, text) => {
119 const messages = await this.api.core.getChatMessages(roomId);
120 const identity = await this.api.core.getIdentity();
121 const newMessage = {
122 id: `msg-${Date.now()}`,
123 timestamp: Date.now(),
124 author: identity.publicKey,
125 chat: {
126 text: text
127 }
128 };
129 messages.push(newMessage);
130 const messagesPath = path.join(storagePath, `messages_${roomId}.json`);
131 fs.writeFileSync(messagesPath, JSON.stringify(messages));
132 return true;
133 },
134 setDisplayName: async (name) => {
135 const identity = await this.api.core.getIdentity();
136 if (identity) {
137 identity.displayName = name;
138 const identityPath = path.join(storagePath, 'identity.json');
139 fs.writeFileSync(identityPath, JSON.stringify(identity));
140 }
141 return true;
142 }
143 }
144 };
145 }
146
147 async ready() {
148 // Create necessary directories
149 if (!fs.existsSync(this.storagePath)) {
150 fs.mkdirSync(this.storagePath, { recursive: true });
151 }
152
153 // Initialize rooms file if it doesn't exist
154 const roomsPath = path.join(this.storagePath, 'rooms.json');
155 if (!fs.existsSync(roomsPath)) {
156 fs.writeFileSync(roomsPath, JSON.stringify([]));
157 }
158
159 return true;
160 }
161
162 async close() {
163 return true;
164 }
165 }
166
167 KeetBackend = MockKeetBackend;
168 KEET_AVAILABLE = true;
169 useMock = true;
170 }
171}
172
173/**
174 * Parse command-line arguments.
175 */
176function parseArguments() {
177 const options = {
178 storage: { type: 'string' },
179 'room-key': { type: 'string' },
180 name: { type: 'string', default: 'KeetBot' },
181 model: { type: 'string', default: 'llama3' },
182 mock: { type: 'boolean', default: false },
183 verbose: { type: 'boolean', default: false }
184 };
185
186 const { values } = parseArgs({ options });
187
188 // Force mock mode if specified
189 if (values.mock) {
190 useMock = true;
191 }
192
193 return values;
194}
195
196/**
197 * Call Ollama to generate a response.
198 */
199async function callOllama(message, model) {
200 return new Promise((resolve, reject) => {
201 const ollama = spawn('ollama', ['run', model], {
202 stdio: ['pipe', 'pipe', 'pipe']
203 });
204
205 let response = '';
206
207 ollama.stdout.on('data', (data) => {
208 response += data.toString();
209 });
210
211 ollama.stderr.on('data', (data) => {
212 log(logLevels.ERROR, `Ollama error: ${data.toString()}`);
213 });
214
215 ollama.on('close', (code) => {
216 if (code === 0) {
217 resolve(response.trim());
218 } else {
219 reject(new Error(`Ollama process exited with code ${code}`));
220 }
221 });
222
223 // Send the message to Ollama
224 ollama.stdin.write(message);
225 ollama.stdin.end();
226 });
227}
228
229/**
230 * Process new messages and respond using Ollama.
231 */
232async function processMessages(backend, roomId, lastProcessedTimestamp, model, verbose) {
233 try {
234 // Get messages from the room
235 const messages = await backend.api.core.getChatMessages(roomId);
236
237 // Filter messages that are newer than the last processed timestamp
238 const newMessages = messages.filter(msg => {
239 // Skip our own messages
240 const identity = backend.api.core.getIdentity();
241 if (msg.author === identity.publicKey) {
242 return false;
243 }
244
245 // Check if the message is newer than the last processed timestamp
246 return msg.timestamp > lastProcessedTimestamp;
247 });
248
249 if (newMessages.length > 0) {
250 // Update the last processed timestamp
251 const latestTimestamp = Math.max(...newMessages.map(msg => msg.timestamp));
252
253 // Process each new message
254 for (const message of newMessages) {
255 if (message.chat && message.chat.text) {
256 log(logLevels.INFO, `New message: ${message.chat.text}`);
257
258 try {
259 // Generate a response using Ollama
260 log(logLevels.INFO, `Generating response using Ollama (model: ${model})...`);
261 const response = await callOllama(message.chat.text, model);
262
263 // Send the response back to the room
264 log(logLevels.INFO, `Sending response: ${response}`);
265 await backend.api.core.addChatMessage(roomId, response);
266 } catch (error) {
267 log(logLevels.ERROR, `Failed to generate or send response: ${error.message}`);
268 }
269 }
270 }
271
272 return latestTimestamp;
273 }
274
275 return lastProcessedTimestamp;
276 } catch (error) {
277 log(logLevels.ERROR, `Error processing messages: ${error.message}`);
278 return lastProcessedTimestamp;
279 }
280}
281
282/**
283 * Main function.
284 */
285async function main() {
286 // Parse command-line arguments
287 const args = parseArguments();
288
289 // Set verbose logging if requested
290 if (args.verbose) {
291 console.log('Verbose logging enabled');
292 }
293
294 // Import dependencies
295 await importDependencies();
296
297 if (!KEET_AVAILABLE) {
298 log(logLevels.ERROR, 'Keet dependencies not available');
299 return 1;
300 }
301
302 // Create a storage directory if it doesn't exist
303 const storagePath = args.storage || path.join(__dirname, '..', 'test-keet-storage');
304 if (!fs.existsSync(storagePath)) {
305 fs.mkdirSync(storagePath, { recursive: true });
306 }
307
308 // Initialize Keet backend
309 log(logLevels.INFO, 'Initializing Keet backend...');
310 const backend = new KeetBackend(storagePath, {
311 experimental: false,
312 devMirrors: false
313 });
314
315 // Wait for backend to be ready
316 await backend.ready();
317
318 // Check if we have an identity
319 log(logLevels.INFO, 'Checking identity...');
320 let identity = await backend.api.core.getIdentity();
321
322 // If we don't have an identity, create one
323 if (!identity) {
324 log(logLevels.INFO, 'No identity found, generating a new one...');
325 await backend.api.core.generateIdentity();
326 identity = await backend.api.core.getIdentity();
327 }
328
329 // Set the display name if provided
330 if (args.name && identity) {
331 log(logLevels.INFO, `Setting display name to: ${args.name}`);
332 if (backend.api.core.setDisplayName) {
333 await backend.api.core.setDisplayName(args.name);
334 } else {
335 log(logLevels.WARNING, 'setDisplayName method not available');
336 }
337 }
338
339 log(logLevels.INFO, `Current identity: ${JSON.stringify(identity)}`);
340
341 // Join the room if a room key is provided
342 let roomId;
343 if (args['room-key']) {
344 const roomKey = args['room-key'];
345 log(logLevels.INFO, `Joining room with key: ${roomKey}`);
346
347 // Extract the room key from the invite link if needed
348 let normalizedRoomKey = roomKey;
349 if (roomKey.startsWith('pear://keet/')) {
350 normalizedRoomKey = roomKey.replace('pear://keet/', '');
351 }
352
353 // Check if there's a pairRoom method available
354 if (backend.api.core.pairRoom) {
355 log(logLevels.INFO, 'Using pairRoom method to join the room...');
356 try {
357 await backend.api.core.pairRoom(normalizedRoomKey);
358 log(logLevels.INFO, 'Successfully paired with the room');
359 roomId = normalizedRoomKey;
360 } catch (error) {
361 log(logLevels.ERROR, `Failed to pair with room: ${error.message}`);
362 return 1;
363 }
364 } else {
365 log(logLevels.ERROR, 'pairRoom method not available');
366 return 1;
367 }
368 } else {
369 // Get recent rooms
370 log(logLevels.INFO, 'No room key provided, checking recent rooms...');
371 const recentRooms = await backend.api.core.getRecentRooms();
372
373 if (recentRooms.length > 0) {
374 roomId = recentRooms[0].roomId;
375 log(logLevels.INFO, `Using the most recent room: ${roomId}`);
376 } else {
377 log(logLevels.ERROR, 'No room key provided and no recent rooms found');
378 return 1;
379 }
380 }
381
382 // Main loop to process messages
383 log(logLevels.INFO, `Starting message processing loop for room: ${roomId}`);
384 log(logLevels.INFO, `Using Ollama model: ${args.model}`);
385
386 // Get the current timestamp to avoid processing old messages
387 let lastProcessedTimestamp = Date.now();
388
389 try {
390 // Send an initial message to the room
391 const initialMessage = `Hello! I'm ${args.name}, a Keet bot powered by Ollama. I'm here to assist you.`;
392 await backend.api.core.addChatMessage(roomId, initialMessage);
393 log(logLevels.INFO, 'Sent initial greeting message');
394
395 // Main loop
396 while (true) {
397 // Process new messages
398 lastProcessedTimestamp = await processMessages(
399 backend,
400 roomId,
401 lastProcessedTimestamp,
402 args.model,
403 args.verbose
404 );
405
406 // Wait a bit before checking for new messages
407 await new Promise(resolve => setTimeout(resolve, 2000));
408 }
409 } catch (error) {
410 log(logLevels.ERROR, `Error in main loop: ${error.message}`);
411
412 // Close the backend
413 log(logLevels.INFO, 'Closing backend...');
414 await backend.close();
415
416 return 1;
417 }
418}
419
420// Run the main function if this file is executed directly
421if (import.meta.url === `file://${process.argv[1]}`) {
422 try {
423 const exitCode = await main();
424 process.exit(exitCode);
425 } catch (error) {
426 log(logLevels.ERROR, `Unhandled error: ${error.message}`);
427 log(logLevels.ERROR, error.stack);
428 process.exit(1);
429 }
430}
431
432export { main };
ollamatools-new/tests/test_keet_cmd.py +123 -0
……@@ -0,0 +1,123 @@
1#!/usr/bin/env python3
2"""
3Test script to verify the Keet CLI agent functionality.
4"""
5
6import asyncio
7import json
8import logging
9import os
10import sys
11import tempfile
12from pathlib import Path
13
14from ollamatools.plugins.integrations.keet.keet_cmd_agent import KeetCmdAgent
15
16# Set up logging
17logging.basicConfig(level=logging.INFO)
18logger = logging.getLogger(__name__)
19
20async def test_keet_cmd_agent():
21 """Test the Keet CLI agent functionality."""
22 try:
23 # Create a temporary directory for the test
24 with tempfile.TemporaryDirectory() as temp_dir:
25 temp_path = Path(temp_dir)
26
27 # Create a mock room
28 room_key = "test-room-key"
29 rooms_file = temp_path / "rooms.json"
30 with open(rooms_file, "w") as f:
31 json.dump([], f)
32
33 # Create a mock identity
34 identity_file = temp_path / "identity.json"
35 with open(identity_file, "w") as f:
36 json.dump({
37 "publicKey": "mock-user",
38 "displayName": "Mock User"
39 }, f)
40
41 # Create a mock messages file
42 messages_file = temp_path / f"messages_{room_key}.json"
43 with open(messages_file, "w") as f:
44 json.dump([], f)
45
46 # Start the agent with mock mode
47 logger.info("Starting Keet CLI agent in mock mode...")
48 agent = KeetCmdAgent(
49 storage_path=temp_path,
50 room_key=room_key,
51 name="TestBot",
52 model="llama3", # This won't actually be used in the test
53 mock=True,
54 verbose=True
55 )
56
57 # Start the agent
58 await agent.start()
59
60 # Wait for the agent to initialize
61 logger.info("Waiting for agent to initialize...")
62 await asyncio.sleep(2)
63
64 # Simulate a user message
65 logger.info("Simulating a user message...")
66
67 # Read the current messages
68 with open(messages_file, "r") as f:
69 messages = json.load(f)
70
71 # Add a test message from another user
72 test_message = {
73 "id": "test-message",
74 "timestamp": int(asyncio.get_event_loop().time() * 1000),
75 "author": "another-user",
76 "chat": {
77 "text": "Hello, bot! Can you help me?"
78 }
79 }
80 messages.append(test_message)
81
82 # Write the updated messages
83 with open(messages_file, "w") as f:
84 json.dump(messages, f)
85
86 # Wait for the agent to process the message
87 logger.info("Waiting for agent to process the message...")
88 await asyncio.sleep(5)
89
90 # Check if the agent responded
91 with open(messages_file, "r") as f:
92 updated_messages = json.load(f)
93
94 # Find the agent's response
95 agent_responses = [
96 msg for msg in updated_messages
97 if msg.get("author") == "mock-user" and msg.get("id") != "test-message"
98 ]
99
100 if agent_responses:
101 logger.info(f"Agent responded with: {agent_responses[-1].get('chat', {}).get('text')}")
102 logger.info("Test successful!")
103 else:
104 logger.error("Agent did not respond to the message")
105 return False
106
107 # Stop the agent
108 logger.info("Stopping the agent...")
109 await agent.stop()
110
111 return True
112 except Exception as e:
113 logger.error(f"Error in Keet CLI agent test: {e}")
114 return False
115
116if __name__ == "__main__":
117 success = asyncio.run(test_keet_cmd_agent())
118 if success:
119 logger.info("Keet CLI agent test successful!")
120 sys.exit(0)
121 else:
122 logger.error("Keet CLI agent test failed!")
123 sys.exit(1)