OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / a2dd000

Add working Keet bot implementation with basic messaging capabilities
a2dd000d84f0899afa00d32d265daa3df16dc8b4
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-21T18:26:12.000Z

Changed files (9) +1081 -50

ollamatools-new/ollamatools/plugins/integrations/keet/README.md +127 -0
……@@ -0,0 +1,127 @@
1# Keet Messaging Integration
2
3This module provides integration with Keet, a secure peer-to-peer messaging platform.
4
5## Overview
6
7The Keet integration allows you to:
8
91. Get or create an identity
102. Get recent rooms or join a new room
113. Send and receive messages
12
13## Components
14
15- `KeetClient`: A Python client for interacting with the Keet messaging platform
16- `KeetTools`: A plugin that provides tools for interacting with Keet
17- `keet_cli.py`: A command-line interface for Keet messaging
18
19## Prerequisites
20
21- Node.js (for running the Keet backend)
22- Python 3.6+ (for running the Python client)
23- Keet dependencies installed (see below)
24
25## Installing Dependencies
26
27The Keet integration requires the following Keet dependencies:
28
29```bash
30cd tests
31npm install
32```
33
34This will install the dependencies specified in `package.json`:
35
36- `@holepunchto/keet-backend`: The Keet backend module
37- `@holepunchto/keet-core-api`: The Keet Core API module
38
39## Using the Command-Line Interface
40
41The `keet_cli.py` script provides a command-line interface for interacting with Keet:
42
43```bash
44# Get the current identity
45python -m ollamatools.plugins.integrations.keet.keet_cli get-identity
46
47# Create a new identity
48python -m ollamatools.plugins.integrations.keet.keet_cli create-identity --name "My Name"
49
50# Get recent rooms
51python -m ollamatools.plugins.integrations.keet.keet_cli get-recent-rooms
52
53# Join a room
54python -m ollamatools.plugins.integrations.keet.keet_cli join-room --room-key "pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde"
55
56# Get messages from a room
57python -m ollamatools.plugins.integrations.keet.keet_cli get-messages --room-key "pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde"
58
59# Send a message to a room
60python -m ollamatools.plugins.integrations.keet.keet_cli send-message --room-key "pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde" --message "Hello, world!"
61```
62
63## Using the Python API
64
65You can also use the Python API directly:
66
67```python
68import asyncio
69from ollamatools.plugins.integrations.keet import KeetClient
70
71async def main():
72 # Initialize the client
73 client = KeetClient()
74
75 # Get the current identity
76 identity = await client.get_identity()
77 print(f"Identity: {identity}")
78
79 # Get recent rooms
80 rooms = await client.get_recent_rooms()
81 print(f"Recent rooms: {rooms}")
82
83 # Join a room
84 room_key = "pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde"
85 success = await client.join_room(room_key)
86 if success:
87 print(f"Successfully joined room: {room_key}")
88
89 # Get messages from a room
90 messages = await client.get_messages(room_key)
91 print(f"Messages: {messages}")
92
93 # Send a message to a room
94 success = await client.send_message(room_key, "Hello, world!")
95 if success:
96 print(f"Successfully sent message to room: {room_key}")
97
98if __name__ == "__main__":
99 asyncio.run(main())
100```
101
102## Using the Plugin
103
104The `KeetTools` plugin provides tools for interacting with Keet:
105
106```python
107from ollamatools.plugins.integrations.keet import KeetTools
108
109# Initialize the plugin
110keet_tools = KeetTools()
111
112# Get the tools
113tools = keet_tools.get_tools()
114
115# Use the tools
116identity = await tools["get_identity"]()
117rooms = await tools["get_recent_rooms"]()
118success = await tools["join_room"]("pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde")
119messages = await tools["get_messages"]("pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde")
120success = await tools["send_message"]("pear://keet/yrbsj9ooyqyipzghwfa6n5bouit8f4tuwtfxry41u6hdghk7ao3iuuap31z6d3n4qrqb7eerqp6f9zga5hb6ug9demezyftfarhnu3q57ttxngde", "Hello, world!")
121```
122
123## Mock Implementation
124
125For testing purposes, the Keet integration includes a mock implementation that simulates the behavior of the real Keet API. This allows you to test your code without requiring the actual Keet dependencies.
126
127The mock implementation is used automatically if the real Keet dependencies are not available.
ollamatools-new/ollamatools/plugins/integrations/keet/README_KEET_CMD.md +51 -6
……@@ -30,7 +30,9 @@ You can use either the full pear link invite or just the room key with the Keet
3030
3131- Node.js (for the JavaScript bridge)
3232- Python 3.8+
33- Ollama installed and available in the PATH
33- Ollama installed and running (accessible via API)
34- Node.js packages:
35 - node-fetch (for making HTTP requests to the Ollama API)
3436
3537## Installation
3638
……@@ -48,6 +50,9 @@ You can run the Keet CLI agent from the command line using the `keet_cmd_run` sc
4850
4951# Or specify a different room key or pear link invite
5052./keet_cmd_run --room-key <room_key_or_pear_link> --name <bot_name> --model <ollama_model>
53
54# With additional Ollama configuration
55./keet_cmd_run --room-key <room_key_or_pear_link> --name <bot_name> --model <ollama_model> --ollama-host <host:port> --prompt "Your system prompt here"
5156```
5257
5358### Options
……@@ -55,9 +60,11 @@ You can run the Keet CLI agent from the command line using the `keet_cmd_run` sc
5560- `--storage`: Path to store Keet data. If not provided, a default path will be used.
5661- `--room-key`: Room key or invite link to join. If not provided, the default hardcoded room key will be used.
5762- `--name`: Display name for the agent. Default is "KeetBot".
58- `--model`: Ollama model to use for generating responses. Default is "llama3".
63- `--model`: Ollama model to use for generating responses. Default is "llama3.1:latest".
64- `--ollama-host`: Host address for the Ollama API. Default is "localhost:11434".
65- `--prompt`: System prompt for the Ollama model. If not provided, no system prompt will be used.
5966- `--mock`: Use the mock implementation (for testing).
60- `--verbose`: Enable verbose logging.
67- `--verbose`: Enable verbose logging. This provides detailed information about room joining, message sending, and other operations, which is particularly useful for troubleshooting connection issues.
6168
6269### Python API Usage
6370
……@@ -72,7 +79,9 @@ async def main():
7279 agent = KeetCmdAgent(
7380 room_key="your-room-key",
7481 name="YourBot",
75 model="llama3"
82 model="llama3.1:latest",
83 ollama_host="localhost:11434",
84 prompt="You are a helpful assistant."
7685 )
7786
7887 # Start the agent
……@@ -111,11 +120,47 @@ The Keet CLI agent consists of two main components:
111120
112121The 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.
113122
114The 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.
123The JavaScript bridge also communicates directly with the Ollama API to generate responses to user messages, maintaining conversation context and supporting system prompts. This approach is consistent with how the Matrix Command-Line Agent interacts with Ollama.
124
125The Python wrapper spawns the JavaScript bridge as a subprocess and communicates with it via stdout/stderr, passing configuration parameters such as the Ollama host, model, and system prompt.
126
127## Verbose Output
128
129When the `--verbose` flag is enabled, the agent provides detailed logging about its operations, including:
130
131- Room joining process and verification
132- Message sending attempts and confirmations
133- Message retrieval and processing details
134- Connection status and errors with stack traces
135- Polling intervals and timing information
136
137This verbose output is particularly useful for troubleshooting issues with:
138- Room joining failures
139- Message sending problems
140- Connection interruptions
141- API compatibility issues
142
143Example verbose output:
144
145```
146[2025-04-21T18:00:00.000Z] [INFO] Joining room with key: pear://keet/yrb6mha3...
147[2025-04-21T18:00:00.000Z] [VERBOSE] Extracted room key from pear link: yrb6mha3...
148[2025-04-21T18:00:00.000Z] [INFO] Using pairRoom method to join the room...
149[2025-04-21T18:00:00.000Z] [VERBOSE] Attempting to pair with room using key: yrb6mha3...
150[2025-04-21T18:00:00.000Z] [VERBOSE] Starting room pairing process...
151[2025-04-21T18:00:00.000Z] [INFO] ✓ Successfully paired with the room
152[2025-04-21T18:00:00.000Z] [VERBOSE] Room verified in recent rooms list
153[2025-04-21T18:00:00.000Z] [INFO] Sending initial greeting message to room yrb6mha3...
154[2025-04-21T18:00:00.000Z] [VERBOSE] Full message: "Hello! I'm KeetBot, a Keet bot powered by Ollama. I'm here to assist you."
155[2025-04-21T18:00:00.000Z] [VERBOSE] Attempting to send initial message...
156[2025-04-21T18:00:00.000Z] [INFO] ✓ Initial greeting message sent successfully
157```
115158
116159## Limitations
117160
118161- The Keet CLI agent is a minimal implementation and may not support all Keet features.
119162- The agent does not support file transfers or other advanced Keet features.
120- The agent requires Ollama to be installed and available in the PATH.
163- The agent requires Ollama to be running and accessible via its API.
164- The agent maintains conversation context but has a limit on the number of messages it remembers.
121165- The agent may not work with all Ollama models.
166- In some network configurations, you may need to use the `--verbose` flag to diagnose connection issues.
ollamatools-new/ollamatools/plugins/integrations/keet/__init__.py +10 -0
……@@ -0,0 +1,10 @@
1"""
2Keet messaging integration for ollamatools.
3
4This module provides integration with Keet, a secure peer-to-peer messaging platform.
5"""
6
7from .client import KeetClient
8from .tools import KeetTools
9
10__all__ = ["KeetClient", "KeetTools"]
ollamatools-new/ollamatools/plugins/integrations/keet/client.py +254 -0
……@@ -0,0 +1,254 @@
1"""
2Keet client for ollamatools.
3
4This module provides a Python interface to the Keet messaging platform.
5"""
6
7import asyncio
8import json
9import logging
10import os
11import tempfile
12from pathlib import Path
13from typing import Dict, List, Optional, Any, Union, Tuple
14
15from .utils import (
16 is_valid_hypercore_key,
17 extract_room_key_from_invite,
18 normalize_hypercore_key,
19 run_node_script,
20 get_default_storage_path,
21)
22
23logger = logging.getLogger(__name__)
24
25class KeetClient:
26 """
27 Client for interacting with the Keet messaging platform.
28
29 This client uses the Node.js Keet API via subprocess calls.
30 """
31
32 def __init__(self, storage_path: Optional[Union[str, Path]] = None):
33 """
34 Initialize the Keet client.
35
36 Args:
37 storage_path: Path to store Keet data. If None, a default path will be used.
38 """
39 self.storage_path = Path(storage_path) if storage_path else get_default_storage_path()
40 self.storage_path.mkdir(parents=True, exist_ok=True)
41
42 # Path to the JavaScript bridge scripts
43 self._script_dir = Path(__file__).parent.parent.parent.parent.parent / "tests"
44 self._test_script = self._script_dir / "keet_messaging_test.js"
45 self._mock_script = self._script_dir / "keet_messaging_mock_test.js"
46
47 # Check if the scripts exist
48 if not self._test_script.exists():
49 logger.warning(f"Keet test script not found at {self._test_script}")
50
51 if not self._mock_script.exists():
52 logger.warning(f"Keet mock script not found at {self._mock_script}")
53
54 # Flag to indicate whether to use the mock implementation
55 self._use_mock = not self._test_script.exists()
56
57 logger.info(f"Initialized Keet client with storage path: {self.storage_path}")
58 logger.info(f"Using {'mock' if self._use_mock else 'real'} implementation")
59
60 async def get_identity(self) -> Dict[str, Any]:
61 """
62 Get the current identity or create a new one if none exists.
63
64 Returns:
65 Dict[str, Any]: The identity information
66 """
67 # Create a temporary file to store the result
68 with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
69 temp_path = temp_file.name
70
71 try:
72 # Run the script with the "get-identity" command
73 script = self._mock_script if self._use_mock else self._test_script
74 success, output = run_node_script(script, ["--command", "get-identity", "--storage", str(self.storage_path), "--output", temp_path])
75
76 if not success:
77 logger.error(f"Failed to get identity: {output}")
78 return {}
79
80 # Read the result from the temporary file
81 with open(temp_path, "r") as f:
82 result = json.load(f)
83
84 return result
85 finally:
86 # Clean up the temporary file
87 if os.path.exists(temp_path):
88 os.unlink(temp_path)
89
90 async def create_identity(self, name: Optional[str] = None) -> Dict[str, Any]:
91 """
92 Create a new identity.
93
94 Args:
95 name: The display name for the identity
96
97 Returns:
98 Dict[str, Any]: The created identity information
99 """
100 # Create a temporary file to store the result
101 with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
102 temp_path = temp_file.name
103
104 try:
105 # Run the script with the "create-identity" command
106 script = self._mock_script if self._use_mock else self._test_script
107 args = ["--command", "create-identity", "--storage", str(self.storage_path), "--output", temp_path]
108
109 if name:
110 args.extend(["--name", name])
111
112 success, output = run_node_script(script, args)
113
114 if not success:
115 logger.error(f"Failed to create identity: {output}")
116 return {}
117
118 # Read the result from the temporary file
119 with open(temp_path, "r") as f:
120 result = json.load(f)
121
122 return result
123 finally:
124 # Clean up the temporary file
125 if os.path.exists(temp_path):
126 os.unlink(temp_path)
127
128 async def get_recent_rooms(self) -> List[Dict[str, Any]]:
129 """
130 Get a list of recent rooms.
131
132 Returns:
133 List[Dict[str, Any]]: List of room information
134 """
135 # Create a temporary file to store the result
136 with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
137 temp_path = temp_file.name
138
139 try:
140 # Run the script with the "get-recent-rooms" command
141 script = self._mock_script if self._use_mock else self._test_script
142 success, output = run_node_script(script, ["--command", "get-recent-rooms", "--storage", str(self.storage_path), "--output", temp_path])
143
144 if not success:
145 logger.error(f"Failed to get recent rooms: {output}")
146 return []
147
148 # Read the result from the temporary file
149 with open(temp_path, "r") as f:
150 result = json.load(f)
151
152 return result
153 finally:
154 # Clean up the temporary file
155 if os.path.exists(temp_path):
156 os.unlink(temp_path)
157
158 async def join_room(self, room_key: str) -> bool:
159 """
160 Join a room using a room key.
161
162 Args:
163 room_key: The room key or invite link
164
165 Returns:
166 bool: True if successful, False otherwise
167 """
168 # Normalize the room key
169 normalized_key = normalize_hypercore_key(room_key)
170
171 # Validate the room key
172 if not is_valid_hypercore_key(normalized_key):
173 logger.error(f"Invalid Hypercore key: {normalized_key}")
174 return False
175
176 # Run the script with the "join-room" command
177 script = self._mock_script if self._use_mock else self._test_script
178 success, output = run_node_script(script, ["--command", "join-room", "--storage", str(self.storage_path), "--room-key", normalized_key])
179
180 if not success:
181 logger.error(f"Failed to join room: {output}")
182 return False
183
184 return True
185
186 async def get_messages(self, room_key: str) -> List[Dict[str, Any]]:
187 """
188 Get messages from a room.
189
190 Args:
191 room_key: The room key
192
193 Returns:
194 List[Dict[str, Any]]: List of messages
195 """
196 # Normalize the room key
197 normalized_key = normalize_hypercore_key(room_key)
198
199 # Validate the room key
200 if not is_valid_hypercore_key(normalized_key):
201 logger.error(f"Invalid Hypercore key: {normalized_key}")
202 return []
203
204 # Create a temporary file to store the result
205 with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
206 temp_path = temp_file.name
207
208 try:
209 # Run the script with the "get-messages" command
210 script = self._mock_script if self._use_mock else self._test_script
211 success, output = run_node_script(script, ["--command", "get-messages", "--storage", str(self.storage_path), "--room-key", normalized_key, "--output", temp_path])
212
213 if not success:
214 logger.error(f"Failed to get messages: {output}")
215 return []
216
217 # Read the result from the temporary file
218 with open(temp_path, "r") as f:
219 result = json.load(f)
220
221 return result
222 finally:
223 # Clean up the temporary file
224 if os.path.exists(temp_path):
225 os.unlink(temp_path)
226
227 async def send_message(self, room_key: str, message: str) -> bool:
228 """
229 Send a message to a room.
230
231 Args:
232 room_key: The room key
233 message: The message to send
234
235 Returns:
236 bool: True if successful, False otherwise
237 """
238 # Normalize the room key
239 normalized_key = normalize_hypercore_key(room_key)
240
241 # Validate the room key
242 if not is_valid_hypercore_key(normalized_key):
243 logger.error(f"Invalid Hypercore key: {normalized_key}")
244 return False
245
246 # Run the script with the "send-message" command
247 script = self._mock_script if self._use_mock else self._test_script
248 success, output = run_node_script(script, ["--command", "send-message", "--storage", str(self.storage_path), "--room-key", normalized_key, "--message", message])
249
250 if not success:
251 logger.error(f"Failed to send message: {output}")
252 return False
253
254 return True
ollamatools-new/ollamatools/plugins/integrations/keet/keet_cli.py +119 -0
……@@ -0,0 +1,119 @@
1#!/usr/bin/env python3
2"""
3Command-line interface for Keet messaging.
4
5This script provides a command-line interface for interacting with the Keet messaging platform.
6"""
7
8import argparse
9import asyncio
10import json
11import logging
12import os
13import sys
14from pathlib import Path
15from typing import Dict, List, Any, Optional
16
17from .client import KeetClient
18from .utils import extract_room_key_from_invite, is_valid_hypercore_key, normalize_hypercore_key
19
20# Set up logging
21logging.basicConfig(level=logging.INFO)
22logger = logging.getLogger(__name__)
23
24async def main():
25 """Main entry point for the Keet CLI."""
26 parser = argparse.ArgumentParser(description="Command-line interface for Keet messaging")
27 parser.add_argument("--storage", type=str, help="Path to store Keet data")
28
29 subparsers = parser.add_subparsers(dest="command", help="Command to execute")
30
31 # Get identity command
32 get_identity_parser = subparsers.add_parser("get-identity", help="Get the current identity")
33
34 # Create identity command
35 create_identity_parser = subparsers.add_parser("create-identity", help="Create a new identity")
36 create_identity_parser.add_argument("--name", type=str, help="Display name for the identity")
37
38 # Get recent rooms command
39 get_recent_rooms_parser = subparsers.add_parser("get-recent-rooms", help="Get a list of recent rooms")
40
41 # Join room command
42 join_room_parser = subparsers.add_parser("join-room", help="Join a room")
43 join_room_parser.add_argument("--room-key", type=str, required=True, help="Room key or invite link")
44
45 # Get messages command
46 get_messages_parser = subparsers.add_parser("get-messages", help="Get messages from a room")
47 get_messages_parser.add_argument("--room-key", type=str, required=True, help="Room key or invite link")
48
49 # Send message command
50 send_message_parser = subparsers.add_parser("send-message", help="Send a message to a room")
51 send_message_parser.add_argument("--room-key", type=str, required=True, help="Room key or invite link")
52 send_message_parser.add_argument("--message", type=str, required=True, help="Message to send")
53
54 # Output file option for commands that return data
55 for p in [get_identity_parser, create_identity_parser, get_recent_rooms_parser, get_messages_parser]:
56 p.add_argument("--output", type=str, help="Output file to write the result to (JSON format)")
57
58 args = parser.parse_args()
59
60 if not args.command:
61 parser.print_help()
62 return 1
63
64 # Initialize the client
65 client = KeetClient(args.storage)
66
67 # Execute the command
68 if args.command == "get-identity":
69 result = await client.get_identity()
70 if args.output:
71 with open(args.output, "w") as f:
72 json.dump(result, f)
73 else:
74 print(json.dumps(result, indent=2))
75
76 elif args.command == "create-identity":
77 result = await client.create_identity(args.name)
78 if args.output:
79 with open(args.output, "w") as f:
80 json.dump(result, f)
81 else:
82 print(json.dumps(result, indent=2))
83
84 elif args.command == "get-recent-rooms":
85 result = await client.get_recent_rooms()
86 if args.output:
87 with open(args.output, "w") as f:
88 json.dump(result, f)
89 else:
90 print(json.dumps(result, indent=2))
91
92 elif args.command == "join-room":
93 success = await client.join_room(args.room_key)
94 if success:
95 logger.info(f"Successfully joined room: {args.room_key}")
96 else:
97 logger.error(f"Failed to join room: {args.room_key}")
98 return 1
99
100 elif args.command == "get-messages":
101 result = await client.get_messages(args.room_key)
102 if args.output:
103 with open(args.output, "w") as f:
104 json.dump(result, f)
105 else:
106 print(json.dumps(result, indent=2))
107
108 elif args.command == "send-message":
109 success = await client.send_message(args.room_key, args.message)
110 if success:
111 logger.info(f"Successfully sent message to room: {args.room_key}")
112 else:
113 logger.error(f"Failed to send message to room: {args.room_key}")
114 return 1
115
116 return 0
117
118if __name__ == "__main__":
119 sys.exit(asyncio.run(main()))
ollamatools-new/ollamatools/plugins/integrations/keet/keet_cmd_agent.py +16 -2
……@@ -31,7 +31,9 @@ class KeetCmdAgent:
3131 storage_path: Optional[Union[str, Path]] = None,
3232 room_key: Optional[str] = None,
3333 name: str = "KeetBot",
34 model: str = "llama3",
34 model: str = "llama3.1:latest",
35 ollama_host: str = "localhost:11434",
36 prompt: Optional[str] = None,
3537 mock: bool = False,
3638 verbose: bool = False
3739 ):
……@@ -43,6 +45,8 @@ class KeetCmdAgent:
4345 room_key: The room key or invite link to join. If None, the most recent room will be used.
4446 name: The display name for the agent.
4547 model: The Ollama model to use for generating responses.
48 ollama_host: The host address for the Ollama API.
49 prompt: An optional system prompt for the Ollama model.
4650 mock: Whether to use the mock implementation.
4751 verbose: Whether to enable verbose logging.
4852 """
……@@ -50,6 +54,8 @@ class KeetCmdAgent:
5054 self.room_key = room_key
5155 self.name = name
5256 self.model = model
57 self.ollama_host = ollama_host
58 self.prompt = prompt
5359 self.mock = mock
5460 self.verbose = verbose
5561
……@@ -88,6 +94,10 @@ class KeetCmdAgent:
8894
8995 cmd.extend(["--name", self.name])
9096 cmd.extend(["--model", self.model])
97 cmd.extend(["--ollama-host", self.ollama_host])
98
99 if self.prompt:
100 cmd.extend(["--prompt", self.prompt])
91101
92102 if self.mock:
93103 cmd.append("--mock")
……@@ -200,7 +210,9 @@ async def main():
200210 parser.add_argument("--storage", type=str, help="Path to store Keet data")
201211 parser.add_argument("--room-key", type=str, help="Room key or invite link to join")
202212 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")
213 parser.add_argument("--model", type=str, default="llama3.1:latest", help="Ollama model to use")
214 parser.add_argument("--ollama-host", type=str, default="localhost:11434", help="Host address for the Ollama API")
215 parser.add_argument("--prompt", type=str, help="System prompt for the Ollama model")
204216 parser.add_argument("--mock", action="store_true", help="Use the mock implementation")
205217 parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
206218
……@@ -212,6 +224,8 @@ async def main():
212224 room_key=args.room_key,
213225 name=args.name,
214226 model=args.model,
227 ollama_host=args.ollama_host,
228 prompt=args.prompt,
215229 mock=args.mock,
216230 verbose=args.verbose
217231 )
ollamatools-new/ollamatools/plugins/integrations/keet/tools.py +147 -0
……@@ -0,0 +1,147 @@
1"""
2Tools for Keet messaging integration.
3
4This module provides tools for interacting with the Keet messaging platform.
5"""
6
7import asyncio
8import logging
9from typing import Dict, List, Any, Optional, Union
10from pathlib import Path
11
12from ollamatools.plugins.base import ToolPlugin
13from .client import KeetClient
14from .utils import extract_room_key_from_invite, is_valid_hypercore_key, normalize_hypercore_key
15
16logger = logging.getLogger(__name__)
17
18class KeetTools(ToolPlugin):
19 """
20 Tools for interacting with the Keet messaging platform.
21 """
22
23 def __init__(self, storage_path: Optional[Union[str, Path]] = None):
24 """
25 Initialize the Keet tools.
26
27 Args:
28 storage_path: Path to store Keet data. If None, a default path will be used.
29 """
30 super().__init__()
31 self.client = KeetClient(storage_path)
32 logger.info("Initialized Keet tools")
33
34 async def get_identity(self) -> Dict[str, Any]:
35 """
36 Get the current identity or create a new one if none exists.
37
38 Returns:
39 Dict[str, Any]: The identity information
40 """
41 return await self.client.get_identity()
42
43 async def create_identity(self, name: Optional[str] = None) -> Dict[str, Any]:
44 """
45 Create a new identity.
46
47 Args:
48 name: The display name for the identity
49
50 Returns:
51 Dict[str, Any]: The created identity information
52 """
53 return await self.client.create_identity(name)
54
55 async def get_recent_rooms(self) -> List[Dict[str, Any]]:
56 """
57 Get a list of recent rooms.
58
59 Returns:
60 List[Dict[str, Any]]: List of room information
61 """
62 return await self.client.get_recent_rooms()
63
64 async def join_room(self, room_key: str) -> bool:
65 """
66 Join a room using a room key or invite link.
67
68 Args:
69 room_key: The room key or invite link
70
71 Returns:
72 bool: True if successful, False otherwise
73 """
74 # Extract the room key if it's an invite link
75 if room_key.startswith("pear://keet/"):
76 room_key = extract_room_key_from_invite(room_key)
77
78 return await self.client.join_room(room_key)
79
80 async def get_messages(self, room_key: str) -> List[Dict[str, Any]]:
81 """
82 Get messages from a room.
83
84 Args:
85 room_key: The room key or invite link
86
87 Returns:
88 List[Dict[str, Any]]: List of messages
89 """
90 # Extract the room key if it's an invite link
91 if room_key.startswith("pear://keet/"):
92 room_key = extract_room_key_from_invite(room_key)
93
94 return await self.client.get_messages(room_key)
95
96 async def send_message(self, room_key: str, message: str) -> bool:
97 """
98 Send a message to a room.
99
100 Args:
101 room_key: The room key or invite link
102 message: The message to send
103
104 Returns:
105 bool: True if successful, False otherwise
106 """
107 # Extract the room key if it's an invite link
108 if room_key.startswith("pear://keet/"):
109 room_key = extract_room_key_from_invite(room_key)
110
111 return await self.client.send_message(room_key, message)
112
113 # ToolPlugin interface methods
114
115 def get_name(self) -> str:
116 """
117 Get the name of the plugin.
118
119 Returns:
120 str: The plugin name
121 """
122 return "keet"
123
124 def get_description(self) -> str:
125 """
126 Get the description of the plugin.
127
128 Returns:
129 str: The plugin description
130 """
131 return "Tools for interacting with the Keet messaging platform"
132
133 def get_tools(self) -> Dict[str, Any]:
134 """
135 Get the tools provided by this plugin.
136
137 Returns:
138 Dict[str, Any]: A dictionary of tool names to tool functions
139 """
140 return {
141 "get_identity": self.get_identity,
142 "create_identity": self.create_identity,
143 "get_recent_rooms": self.get_recent_rooms,
144 "join_room": self.join_room,
145 "get_messages": self.get_messages,
146 "send_message": self.send_message
147 }
ollamatools-new/ollamatools/plugins/integrations/keet/utils.py +143 -0
……@@ -0,0 +1,143 @@
1"""
2Utility functions for Keet messaging integration.
3"""
4
5import re
6import logging
7import subprocess
8import json
9import os
10from pathlib import Path
11from typing import Optional, Dict, Any, List, Union, Tuple
12
13logger = logging.getLogger(__name__)
14
15# Constants
16KEET_INVITE_PREFIX = "pear://keet/"
17Z_BASE32_PATTERN = re.compile(r"^[a-z2-7]+$")
18HYPERCORE_KEY_LENGTH = 52 # Standard length for z-base-32 encoded Hypercore keys
19
20def is_valid_hypercore_key(key: str) -> bool:
21 """
22 Validate if a string is a valid Hypercore key.
23
24 Hypercore keys are typically z-base-32 encoded and have a specific length.
25
26 Args:
27 key: The key to validate
28
29 Returns:
30 bool: True if the key is valid, False otherwise
31 """
32 if not key or not isinstance(key, str):
33 return False
34
35 # Check if the key matches the z-base-32 character set and has the correct length
36 return Z_BASE32_PATTERN.match(key) is not None and len(key) == HYPERCORE_KEY_LENGTH
37
38def extract_room_key_from_invite(invite_link: str) -> str:
39 """
40 Extract the room key from a Keet invite link.
41
42 Args:
43 invite_link: The Keet invite link (e.g., "pear://keet/abc123...")
44
45 Returns:
46 str: The extracted room key
47 """
48 if not invite_link:
49 raise ValueError("Invite link cannot be empty")
50
51 # Remove the prefix
52 if invite_link.startswith(KEET_INVITE_PREFIX):
53 room_key = invite_link[len(KEET_INVITE_PREFIX):]
54 else:
55 room_key = invite_link
56
57 # Remove any additional path components or query parameters
58 room_key = room_key.split("/")[0].split("?")[0]
59
60 return room_key
61
62def normalize_hypercore_key(key: str) -> str:
63 """
64 Normalize a Hypercore key to ensure it's in the correct format.
65
66 This function attempts to fix common issues with Hypercore keys.
67
68 Args:
69 key: The key to normalize
70
71 Returns:
72 str: The normalized key
73 """
74 # Remove any whitespace
75 key = key.strip()
76
77 # Remove the prefix if present
78 if key.startswith(KEET_INVITE_PREFIX):
79 key = key[len(KEET_INVITE_PREFIX):]
80
81 # Remove any additional path components or query parameters
82 key = key.split("/")[0].split("?")[0]
83
84 # Ensure the key is lowercase (z-base-32 is case-insensitive but conventionally lowercase)
85 key = key.lower()
86
87 return key
88
89def run_node_script(script_path: str, args: List[str] = None) -> Tuple[bool, str]:
90 """
91 Run a Node.js script and return the result.
92
93 Args:
94 script_path: Path to the Node.js script
95 args: Additional arguments to pass to the script
96
97 Returns:
98 Tuple[bool, str]: A tuple containing a success flag and the output
99 """
100 if args is None:
101 args = []
102
103 cmd = ["node", str(script_path)] + args
104
105 try:
106 result = subprocess.run(
107 cmd,
108 capture_output=True,
109 text=True,
110 check=False # Don't raise an exception on non-zero exit code
111 )
112
113 # Log the output
114 for line in result.stdout.splitlines():
115 logger.info(line)
116
117 for line in result.stderr.splitlines():
118 logger.error(line)
119
120 return result.returncode == 0, result.stdout
121 except Exception as e:
122 logger.error(f"Error running Node.js script: {e}")
123 return False, str(e)
124
125def get_default_storage_path() -> Path:
126 """
127 Get the default storage path for Keet data.
128
129 Returns:
130 Path: The default storage path
131 """
132 # Use the user's home directory
133 home_dir = Path.home()
134
135 # Create a .ollamatools directory if it doesn't exist
136 ollamatools_dir = home_dir / ".ollamatools"
137 ollamatools_dir.mkdir(exist_ok=True)
138
139 # Create a keet directory if it doesn't exist
140 keet_dir = ollamatools_dir / "keet"
141 keet_dir.mkdir(exist_ok=True)
142
143 return keet_dir
ollamatools-new/tests/keet_cmd.js +214 -42
……@@ -12,7 +12,7 @@ import fs from 'fs';
1212import path from 'path';
1313import { fileURLToPath } from 'url';
1414import { parseArgs } from 'node:util';
15import { spawn } from 'child_process';
15import fetch from 'node-fetch';
1616
1717// Get the directory name of the current module
1818const __filename = fileURLToPath(import.meta.url);
……@@ -31,6 +31,14 @@ function log(level, message) {
3131 console.log(`[${timestamp}] [${level}] ${message}`);
3232}
3333
34// More detailed logging for verbose mode
35function verboseLog(message, args) {
36 if (args && args.verbose) {
37 const timestamp = new Date().toISOString();
38 console.log(`[${timestamp}] [VERBOSE] ${message}`);
39 }
40}
41
3442// Import Keet dependencies or mock implementations
3543let KeetBackend, KeetCoreApi;
3644let KEET_AVAILABLE = false;
……@@ -178,7 +186,9 @@ function parseArguments() {
178186 storage: { type: 'string' },
179187 'room-key': { type: 'string' },
180188 name: { type: 'string', default: 'KeetBot' },
181 model: { type: 'string', default: 'llama3' },
189 model: { type: 'string', default: 'llama3.1:latest' },
190 'ollama-host': { type: 'string', default: 'localhost:11434' },
191 prompt: { type: 'string' },
182192 mock: { type: 'boolean', default: false },
183193 verbose: { type: 'boolean', default: false }
184194 };
……@@ -194,75 +204,197 @@ function parseArguments() {
194204}
195205
196206/**
197 * Call Ollama to generate a response.
207 * Ollama client for interacting with the Ollama API.
198208 */
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 });
209class OllamaClient {
210 constructor(host = 'localhost:11434', model = 'llama3.1:latest', systemPrompt = null) {
211 this.host = host;
212 this.model = model;
213 this.systemPrompt = systemPrompt;
214 this.baseUrl = `http://${host}`;
210215
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}`));
216 // User context tracking
217 this.userContexts = new Map();
218 this.maxContextLength = 10; // Maximum number of messages to keep in context
219 }
220
221 /**
222 * Call Ollama chat API to generate a response.
223 */
224 async chat(message, userId, roomId) {
225 try {
226 const contextKey = `${userId}:${roomId}`;
227 const messages = [];
228
229 // Add system prompt if provided
230 if (this.systemPrompt) {
231 messages.push({
232 role: 'system',
233 content: this.systemPrompt
234 });
220235 }
221 });
222
223 // Send the message to Ollama
224 ollama.stdin.write(message);
225 ollama.stdin.end();
226 });
236
237 // Add conversation context if available
238 if (this.userContexts.has(contextKey)) {
239 for (const ctxMsg of this.userContexts.get(contextKey)) {
240 messages.push(ctxMsg);
241 }
242 }
243
244 // Add user message
245 const userMessage = {
246 role: 'user',
247 content: message
248 };
249 messages.push(userMessage);
250
251 // Store user message in context
252 if (!this.userContexts.has(contextKey)) {
253 this.userContexts.set(contextKey, []);
254 }
255 this.userContexts.get(contextKey).push(userMessage);
256
257 // Trim context if it gets too long
258 if (this.userContexts.get(contextKey).length > this.maxContextLength) {
259 this.userContexts.set(
260 contextKey,
261 this.userContexts.get(contextKey).slice(-this.maxContextLength)
262 );
263 }
264
265 // Make API request to Ollama
266 const response = await fetch(`${this.baseUrl}/api/chat`, {
267 method: 'POST',
268 headers: {
269 'Content-Type': 'application/json'
270 },
271 body: JSON.stringify({
272 model: this.model,
273 messages: messages,
274 stream: false
275 })
276 });
277
278 if (!response.ok) {
279 const errorText = await response.text();
280 throw new Error(`Ollama API error (${response.status}): ${errorText}`);
281 }
282
283 const data = await response.json();
284 const messageContent = data.message.content;
285
286 // Store assistant response in context
287 this.userContexts.get(contextKey).push({
288 role: 'assistant',
289 content: messageContent
290 });
291
292 // Trim context if it gets too long
293 if (this.userContexts.get(contextKey).length > this.maxContextLength) {
294 this.userContexts.set(
295 contextKey,
296 this.userContexts.get(contextKey).slice(-this.maxContextLength)
297 );
298 }
299
300 return messageContent;
301 } catch (error) {
302 log(logLevels.ERROR, `Error calling Ollama API: ${error.message}`);
303 throw error;
304 }
305 }
306
307 /**
308 * List available models from Ollama.
309 */
310 async listModels() {
311 try {
312 const response = await fetch(`${this.baseUrl}/api/tags`);
313
314 if (!response.ok) {
315 const errorText = await response.text();
316 throw new Error(`Ollama API error (${response.status}): ${errorText}`);
317 }
318
319 const data = await response.json();
320 return data.models.map(model => model.name);
321 } catch (error) {
322 log(logLevels.ERROR, `Error listing Ollama models: ${error.message}`);
323 throw error;
324 }
325 }
326
327 /**
328 * Change the model being used.
329 */
330 changeModel(model) {
331 this.model = model;
332 log(logLevels.INFO, `Changed Ollama model to: ${model}`);
333 }
334
335 /**
336 * Clear context for a specific user/room.
337 */
338 clearContext(userId, roomId) {
339 const contextKey = `${userId}:${roomId}`;
340 this.userContexts.delete(contextKey);
341 log(logLevels.INFO, `Cleared context for ${userId} in room ${roomId}`);
342 }
227343}
228344
229345/**
230346 * Process new messages and respond using Ollama.
231347 */
232async function processMessages(backend, roomId, lastProcessedTimestamp, model, verbose) {
348async function processMessages(backend, roomId, lastProcessedTimestamp, ollamaClient, verbose) {
233349 try {
234350 // Get messages from the room
351 verboseLog(`Fetching messages from room ${roomId}...`, { verbose });
235352 const messages = await backend.api.core.getChatMessages(roomId);
353 verboseLog(`Retrieved ${messages.length} total messages from room`, { verbose });
354
355 // Get identity for filtering
356 const identity = await backend.api.core.getIdentity();
357 verboseLog(`Current identity: ${identity ? identity.publicKey : 'undefined'}`, { verbose });
236358
237359 // Filter messages that are newer than the last processed timestamp
238360 const newMessages = messages.filter(msg => {
239361 // Skip our own messages
240 const identity = backend.api.core.getIdentity();
241 if (msg.author === identity.publicKey) {
362 if (identity && identity.publicKey && msg.author === identity.publicKey) {
363 verboseLog(`Skipping own message with ID: ${typeof msg.id === 'object' ? JSON.stringify(msg.id) : msg.id}`, { verbose });
242364 return false;
243365 }
244366
245367 // Check if the message is newer than the last processed timestamp
246 return msg.timestamp > lastProcessedTimestamp;
368 const isNewer = msg.timestamp > lastProcessedTimestamp;
369 if (isNewer) {
370 verboseLog(`Found new message with ID: ${typeof msg.id === 'object' ? JSON.stringify(msg.id) : msg.id}, timestamp: ${new Date(msg.timestamp).toISOString()}`, { verbose });
371 }
372 return isNewer;
247373 });
248374
375 verboseLog(`Found ${newMessages.length} new messages to process`, { verbose });
376
249377 if (newMessages.length > 0) {
250378 // Update the last processed timestamp
251379 const latestTimestamp = Math.max(...newMessages.map(msg => msg.timestamp));
380 verboseLog(`Updating last processed timestamp to: ${new Date(latestTimestamp).toISOString()}`, { verbose });
252381
253382 // Process each new message
254383 for (const message of newMessages) {
255384 if (message.chat && message.chat.text) {
256 log(logLevels.INFO, `New message: ${message.chat.text}`);
385 const authorId = message.author ? message.author.substring(0, 8) : 'unknown';
386 log(logLevels.INFO, `New message from ${authorId}: ${message.chat.text}`);
257387
258388 try {
259389 // Generate a response using Ollama
260 log(logLevels.INFO, `Generating response using Ollama (model: ${model})...`);
261 const response = await callOllama(message.chat.text, model);
390 log(logLevels.INFO, `Generating response using Ollama (model: ${ollamaClient.model})...`);
391 const response = await ollamaClient.chat(message.chat.text, message.author || 'unknown-user', roomId);
262392
263393 // Send the response back to the room
264 log(logLevels.INFO, `Sending response: ${response}`);
394 log(logLevels.INFO, `Sending response to room ${roomId}: ${response}`);
395 verboseLog(`Attempting to send message to room ${roomId}...`, { verbose });
265396 await backend.api.core.addChatMessage(roomId, response);
397 log(logLevels.INFO, `✓ Message successfully sent to room`);
266398 } catch (error) {
267399 log(logLevels.ERROR, `Failed to generate or send response: ${error.message}`);
268400 }
……@@ -270,6 +402,8 @@ async function processMessages(backend, roomId, lastProcessedTimestamp, model, v
270402 }
271403
272404 return latestTimestamp;
405 } else {
406 verboseLog(`No new messages to process`, { verbose });
273407 }
274408
275409 return lastProcessedTimestamp;
……@@ -338,6 +472,10 @@ async function main() {
338472
339473 log(logLevels.INFO, `Current identity: ${JSON.stringify(identity)}`);
340474
475 // Initialize Ollama client
476 log(logLevels.INFO, `Initializing Ollama client with host: ${args['ollama-host']}, model: ${args.model}`);
477 const ollamaClient = new OllamaClient(args['ollama-host'], args.model, args.prompt);
478
341479 // Join the room if a room key is provided
342480 let roomId;
343481 if (args['room-key']) {
……@@ -348,31 +486,49 @@ async function main() {
348486 let normalizedRoomKey = roomKey;
349487 if (roomKey.startsWith('pear://keet/')) {
350488 normalizedRoomKey = roomKey.replace('pear://keet/', '');
489 verboseLog(`Extracted room key from pear link: ${normalizedRoomKey}`, args);
351490 }
352491
353492 // Check if there's a pairRoom method available
354493 if (backend.api.core.pairRoom) {
355494 log(logLevels.INFO, 'Using pairRoom method to join the room...');
495 verboseLog(`Attempting to pair with room using key: ${normalizedRoomKey.substring(0, 16)}...`, args);
496
356497 try {
498 verboseLog(`Starting room pairing process...`, args);
357499 await backend.api.core.pairRoom(normalizedRoomKey);
358 log(logLevels.INFO, 'Successfully paired with the room');
500 log(logLevels.INFO, '✓ Successfully paired with the room');
359501 roomId = normalizedRoomKey;
502
503 // Verify the room is in recent rooms after pairing
504 const recentRooms = await backend.api.core.getRecentRooms();
505 const roomFound = recentRooms.some(room => room.roomId === normalizedRoomKey);
506
507 if (roomFound) {
508 verboseLog(`Room verified in recent rooms list`, args);
509 } else {
510 log(logLevels.WARNING, `Room paired but not found in recent rooms list - this may cause issues`);
511 }
360512 } catch (error) {
361513 log(logLevels.ERROR, `Failed to pair with room: ${error.message}`);
514 verboseLog(`Detailed error: ${error.stack || 'No stack trace available'}`, args);
362515 return 1;
363516 }
364517 } else {
365518 log(logLevels.ERROR, 'pairRoom method not available');
519 verboseLog(`Available API methods: ${Object.keys(backend.api.core).join(', ')}`, args);
366520 return 1;
367521 }
368522 } else {
369523 // Get recent rooms
370524 log(logLevels.INFO, 'No room key provided, checking recent rooms...');
371525 const recentRooms = await backend.api.core.getRecentRooms();
526 verboseLog(`Found ${recentRooms.length} recent rooms`, args);
372527
373528 if (recentRooms.length > 0) {
374529 roomId = recentRooms[0].roomId;
375 log(logLevels.INFO, `Using the most recent room: ${roomId}`);
530 log(logLevels.INFO, `Using the most recent room: ${roomId.substring(0, 16)}...`);
531 verboseLog(`Room details: ${JSON.stringify(recentRooms[0])}`, args);
376532 } else {
377533 log(logLevels.ERROR, 'No room key provided and no recent rooms found');
378534 return 1;
……@@ -389,25 +545,41 @@ async function main() {
389545 try {
390546 // Send an initial message to the room
391547 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');
548 log(logLevels.INFO, `Sending initial greeting message to room ${roomId.substring(0, 16)}...`);
549 verboseLog(`Full message: "${initialMessage}"`, args);
550
551 try {
552 verboseLog(`Attempting to send initial message...`, args);
553 await backend.api.core.addChatMessage(roomId, initialMessage);
554 log(logLevels.INFO, '✓ Initial greeting message sent successfully');
555 } catch (sendError) {
556 log(logLevels.ERROR, `Failed to send initial message: ${sendError.message}`);
557 verboseLog(`Detailed error: ${sendError.stack || 'No stack trace available'}`, args);
558 }
394559
395560 // Main loop
561 log(logLevels.INFO, `Starting main message processing loop`);
562 verboseLog(`Message polling interval: 2000ms`, args);
563
396564 while (true) {
565 verboseLog(`Checking for new messages in room ${roomId.substring(0, 16)}...`, args);
566
397567 // Process new messages
398568 lastProcessedTimestamp = await processMessages(
399569 backend,
400570 roomId,
401571 lastProcessedTimestamp,
402 args.model,
572 ollamaClient,
403573 args.verbose
404574 );
405575
406576 // Wait a bit before checking for new messages
577 verboseLog(`Waiting 2000ms before next message check...`, args);
407578 await new Promise(resolve => setTimeout(resolve, 2000));
408579 }
409580 } catch (error) {
410581 log(logLevels.ERROR, `Error in main loop: ${error.message}`);
582 verboseLog(`Detailed error: ${error.stack || 'No stack trace available'}`, args);
411583
412584 // Close the backend
413585 log(logLevels.INFO, 'Closing backend...');