OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / fa6223d

Refactor matrix_client.py to use ollamatools Matrix integration
fa6223d86bc2e72bf680e94e3391fc97a094a7f0
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-19T23:37:22.000Z

Changed files (1) +261 -578

ollamatools-new/matrix_cmd/matrix_client.py +261 -578
……@@ -4,6 +4,10 @@ Matrix client for the Matrix Command-Line Agent.
44This module provides functionality to connect to Matrix homeservers,
55join rooms, send messages, and listen for incoming messages.
66It integrates with Ollama to process messages and generate responses.
7
8This implementation uses the ollamatools Matrix integration to provide
9enhanced features like search, conversation history tracking, topic analysis,
10and performance metrics.
711"""
812
913import os
……@@ -22,21 +26,61 @@ logging.basicConfig(
2226)
2327logger = logging.getLogger(__name__)
2428
29# Import from ollamatools Matrix integration
2530try:
26 from nio import (
31 from ollamatools.plugins.integrations.matrix.client import (
32 MatrixClient as OllamaToolsMatrixClient,
33 MatrixConfig as OllamaToolsMatrixConfig,
2734 AsyncClient,
2835 MatrixRoom,
2936 RoomMessageText,
3037 LoginResponse,
3138 JoinError
3239 )
40
41 from ollamatools.plugins.integrations.matrix.commands import (
42 connect_enhanced_matrix,
43 handle_history_command,
44 handle_topics_command,
45 handle_metrics_command
46 )
47
48 from ollamatools.plugins.integrations.matrix.search import (
49 handle_search_command,
50 get_default_opensearch_config
51 )
52
53 from ollamatools.plugins.integrations.matrix.history import (
54 load_conversation_history,
55 save_conversation_history,
56 record_conversation,
57 analyze_conversation_topics,
58 get_performance_metrics
59 )
60
61 OLLAMATOOLS_AVAILABLE = True
3362except ImportError:
3463 logger.error(
35 "Matrix support requires the 'matrix-nio' package. "
36 "Please install it with 'pip install matrix-nio' or "
37 "'pip install matrix-nio[e2e]' for end-to-end encryption support."
64 "Matrix integration requires the 'ollamatools' package. "
65 "Please ensure it is installed correctly."
3866 )
39 sys.exit(1)
67 # Fall back to direct imports for basic functionality
68 try:
69 from nio import (
70 AsyncClient,
71 MatrixRoom,
72 RoomMessageText,
73 LoginResponse,
74 JoinError
75 )
76 except ImportError:
77 logger.error(
78 "Matrix support requires the 'matrix-nio' package. "
79 "Please install it with 'pip install matrix-nio' or "
80 "'pip install matrix-nio[e2e]' for end-to-end encryption support."
81 )
82 sys.exit(1)
83 OLLAMATOOLS_AVAILABLE = False
4084
4185# Make ollama import optional
4286try:
……@@ -57,6 +101,10 @@ class MatrixClient:
57101 This client can connect to a Matrix homeserver, join rooms, send messages,
58102 and listen for incoming messages. It integrates with Ollama to process
59103 messages and generate responses.
104
105 This implementation uses the ollamatools Matrix integration to provide
106 enhanced features like search, conversation history tracking, topic analysis,
107 and performance metrics.
60108 """
61109
62110 # Command patterns
……@@ -102,6 +150,12 @@ class MatrixClient:
102150 available_models: List of available Ollama models
103151 admin_users: List of Matrix user IDs that can change models for all users
104152 ssl: Whether to use SSL for the Matrix connection
153 enable_search: Whether to enable search functionality
154 enable_conversation_tracking: Whether to enable conversation history tracking
155 opensearch_config: Configuration for OpenSearchNG
156 history_file: Path to the conversation history file
157 max_conversation_history: Maximum number of conversations to keep in history
158 respond_to_all_messages: Whether to respond to all messages (not just mentions)
105159 """
106160 self.homeserver = homeserver
107161 self.user_id = user_id
……@@ -115,25 +169,26 @@ class MatrixClient:
115169 # Enhanced features
116170 self.enable_search = enable_search
117171 self.enable_conversation_tracking = enable_conversation_tracking
118 self.opensearch_config = opensearch_config or self._get_default_opensearch_config()
119172 self.history_file = history_file or os.path.expanduser("~/.config/matrix-cmd/history.json")
120173 self.max_conversation_history = max_conversation_history
121174 self.respond_to_all_messages = respond_to_all_messages
122175
176 # Set OpenSearch config
177 if OLLAMATOOLS_AVAILABLE and hasattr(get_default_opensearch_config, '__call__'):
178 self.opensearch_config = opensearch_config or get_default_opensearch_config()
179 else:
180 self.opensearch_config = opensearch_config or self._get_default_opensearch_config()
181
123182 # Conversation history
124183 self.conversation_history = []
125184 if self.enable_conversation_tracking:
126 self.conversation_history = self._load_conversation_history()
185 if OLLAMATOOLS_AVAILABLE and hasattr(load_conversation_history, '__call__'):
186 self.conversation_history = load_conversation_history(self.history_file)
187 else:
188 self.conversation_history = self._load_conversation_history()
127189
128 # Initialize the Matrix client
129 self.client = AsyncClient(
130 homeserver=self.homeserver,
131 device_id=self.device_id,
132 ssl=self.ssl
133 )
134
135 # Set the user ID
136 self.client.user_id = self.user_id
190 # Initialize the client (will be set in connect())
191 self.client = None
137192
138193 # Track when the client connected to avoid processing old messages
139194 self.connection_time = None
……@@ -168,11 +223,63 @@ class MatrixClient:
168223 Returns:
169224 bool: True if the connection was successful, False otherwise
170225 """
226 if OLLAMATOOLS_AVAILABLE and hasattr(connect_enhanced_matrix, '__call__'):
227 try:
228 # Use the ollamatools enhanced Matrix client
229 enhanced_client = await connect_enhanced_matrix(
230 homeserver=self.homeserver,
231 user_id=self.user_id,
232 password=self.password,
233 device_id=self.device_id,
234 ollama_host=self.ollama_host,
235 ollama_model=self.ollama_model,
236 ollama_prompt=self.ollama_prompt,
237 available_models=self.available_models,
238 admin_users=list(self.admin_users),
239 opensearch_config=self.opensearch_config,
240 ssl=self.ssl,
241 enable_search=self.enable_search,
242 enable_conversation_tracking=self.enable_conversation_tracking,
243 max_context_length=self.max_context_length,
244 max_conversation_history=self.max_conversation_history,
245 respond_to_all_messages=self.respond_to_all_messages
246 )
247
248 if enhanced_client:
249 # Store the client
250 self.client = enhanced_client
251
252 # Set connection time
253 self.connection_time = asyncio.get_event_loop().time()
254
255 # Copy conversation history if available
256 if hasattr(enhanced_client, 'conversation_history'):
257 self.conversation_history = enhanced_client.conversation_history
258
259 logger.info(f"Successfully connected to {self.homeserver} as {self.user_id}")
260 return True
261 else:
262 logger.error(f"Failed to connect to {self.homeserver}")
263 return False
264
265 except Exception as e:
266 logger.error(f"Error connecting to Matrix server with ollamatools: {e}")
267 logger.info("Falling back to standard Matrix connection")
268 # Fall back to standard connection
269
270 # Standard connection (fallback or if ollamatools is not available)
171271 retry_count = 0
172272 current_retry_delay = initial_retry_delay
173273
174274 while retry_count <= max_retries:
175275 try:
276 # Initialize the Matrix client
277 self.client = AsyncClient(
278 homeserver=self.homeserver,
279 device_id=self.device_id,
280 ssl=self.ssl
281 )
282
176283 # Extract username from user_id (e.g., "@user:example.com" -> "user")
177284 username = self.user_id.split(':')[0].lstrip('@')
178285
……@@ -269,20 +376,41 @@ class MatrixClient:
269376 Returns:
270377 bool: True if joining was successful, False otherwise
271378 """
272 try:
273 response = await self.client.join(room_id)
379 if not self.client:
380 logger.error("Not connected to Matrix server")
381 return False
274382
275 if isinstance(response, JoinError):
276 logger.error(f"Failed to join room {room_id}: {response}")
383 # Check if we're using ollamatools client
384 if OLLAMATOOLS_AVAILABLE and hasattr(self.client, 'join_room'):
385 try:
386 success = await self.client.join_room(room_id)
387
388 if success:
389 logger.info(f"Successfully joined room {room_id}")
390 self.rooms.append(room_id)
391 return True
392 else:
393 logger.error(f"Failed to join room {room_id}")
394 return False
395 except Exception as e:
396 logger.error(f"Error joining room {room_id}: {e}")
397 return False
398 else:
399 # Standard client
400 try:
401 response = await self.client.join(room_id)
402
403 if isinstance(response, JoinError):
404 logger.error(f"Failed to join room {room_id}: {response}")
405 return False
406
407 logger.info(f"Successfully joined room {room_id}")
408 self.rooms.append(room_id)
409 return True
410
411 except Exception as e:
412 logger.error(f"Error joining room {room_id}: {e}")
277413 return False
278
279 logger.info(f"Successfully joined room {room_id}")
280 self.rooms.append(room_id)
281 return True
282
283 except Exception as e:
284 logger.error(f"Error joining room {room_id}: {e}")
285 return False
286414
287415 async def send_message(self, room_id: str, message: str) -> bool:
288416 """
……@@ -295,23 +423,44 @@ class MatrixClient:
295423 Returns:
296424 bool: True if sending was successful, False otherwise
297425 """
298 try:
299 logger.info(f"Sending message to room {room_id}: {message}")
300 response = await self.client.room_send(
301 room_id=room_id,
302 message_type="m.room.message",
303 content={
304 "msgtype": "m.text",
305 "body": message
306 }
307 )
308
309 logger.info(f"Message sent successfully to room {room_id}, response: {response}")
310 return True
311
312 except Exception as e:
313 logger.error(f"Error sending message to room {room_id}: {e}")
426 if not self.client:
427 logger.error("Not connected to Matrix server")
314428 return False
429
430 # Check if we're using ollamatools client
431 if OLLAMATOOLS_AVAILABLE and hasattr(self.client, 'send_message'):
432 try:
433 logger.info(f"Sending message to room {room_id}: {message}")
434 success = await self.client.send_message(room_id, message)
435
436 if success:
437 logger.info(f"Message sent successfully to room {room_id}")
438 return True
439 else:
440 logger.error(f"Failed to send message to room {room_id}")
441 return False
442 except Exception as e:
443 logger.error(f"Error sending message to room {room_id}: {e}")
444 return False
445 else:
446 # Standard client
447 try:
448 logger.info(f"Sending message to room {room_id}: {message}")
449 response = await self.client.room_send(
450 room_id=room_id,
451 message_type="m.room.message",
452 content={
453 "msgtype": "m.text",
454 "body": message
455 }
456 )
457
458 logger.info(f"Message sent successfully to room {room_id}")
459 return True
460
461 except Exception as e:
462 logger.error(f"Error sending message to room {room_id}: {e}")
463 return False
315464
316465 async def process_with_ollama(self, message: str, sender: str = None, room_id: str = None) -> str:
317466 """
……@@ -325,9 +474,25 @@ class MatrixClient:
325474 Returns:
326475 str: The response from Ollama
327476 """
477 if not self.client:
478 logger.error("Not connected to Matrix server")
479 return "Error: Not connected to Matrix server"
480
328481 if not OLLAMA_AVAILABLE:
329482 return "Ollama is not available. Please install the 'ollama' package with 'pip install ollama'."
330483
484 # Check if we're using ollamatools client
485 if OLLAMATOOLS_AVAILABLE and hasattr(self.client, 'process_with_ollama'):
486 try:
487 # Use the ollamatools client to process the message
488 response = await self.client.process_with_ollama(message, sender, room_id)
489 return response
490 except Exception as e:
491 logger.error(f"Error processing message with ollamatools: {e}")
492 logger.info("Falling back to standard Ollama processing")
493 # Fall back to standard processing
494
495 # Standard processing
331496 try:
332497 ollama_client = OllamaAsync(host=self.ollama_host)
333498
……@@ -388,6 +553,32 @@ class MatrixClient:
388553 if len(self.user_contexts[context_key]) > self.max_context_length:
389554 self.user_contexts[context_key] = self.user_contexts[context_key][-self.max_context_length:]
390555
556 # Record conversation if tracking is enabled
557 if self.enable_conversation_tracking and sender and room_id:
558 import time
559 start_time = time.time()
560 response_time = time.time() - start_time
561
562 if OLLAMATOOLS_AVAILABLE and hasattr(record_conversation, '__call__'):
563 record_conversation(
564 self.conversation_history,
565 message,
566 message_content,
567 sender,
568 room_id,
569 response_time,
570 self.history_file,
571 self.max_conversation_history
572 )
573 else:
574 self._record_conversation(
575 message,
576 message_content,
577 sender,
578 room_id,
579 response_time
580 )
581
391582 logger.debug(f"Received response from Ollama (model: {model}): {message_content}")
392583 return message_content
393584
……@@ -439,7 +630,7 @@ class MatrixClient:
439630 username = self.user_id.split(':')[0].lstrip('@')
440631
441632 # Check if the message contains the bot's username (case insensitive)
442 if username.lower() in message.lower():
633 if username.lower() in message.lower() or self.respond_to_all_messages:
443634 logger.info(f"Username mention detected in message: {message}")
444635 # Process with Ollama and send response
445636 response = await self.process_with_ollama(message, sender, room_id)
……@@ -486,7 +677,12 @@ class MatrixClient:
486677 await self.send_message(room_id, "Search functionality is not enabled.")
487678 return
488679 query = search_match.group(1)
489 await self._handle_search_command(query, room_id)
680
681 # Use ollamatools if available
682 if OLLAMATOOLS_AVAILABLE and hasattr(handle_search_command, '__call__'):
683 await handle_search_command(self, query, room_id)
684 else:
685 await self._handle_search_command(query, room_id)
490686 return
491687
492688 # Check if this is a history command
……@@ -496,7 +692,12 @@ class MatrixClient:
496692 await self.send_message(room_id, "Conversation tracking is not enabled.")
497693 return
498694 count = int(history_match.group(1)) if history_match.group(1) else 10
499 await self._handle_history_command(count, room_id)
695
696 # Use ollamatools if available
697 if OLLAMATOOLS_AVAILABLE and hasattr(handle_history_command, '__call__'):
698 await handle_history_command(self, count, room_id)
699 else:
700 await self._handle_history_command(count, room_id)
500701 return
501702
502703 # Check if this is a topics command
……@@ -504,7 +705,12 @@ class MatrixClient:
504705 if not self.enable_conversation_tracking:
505706 await self.send_message(room_id, "Conversation tracking is not enabled.")
506707 return
507 await self._handle_topics_command(room_id)
708
709 # Use ollamatools if available
710 if OLLAMATOOLS_AVAILABLE and hasattr(handle_topics_command, '__call__'):
711 await handle_topics_command(self, room_id)
712 else:
713 await self._handle_topics_command(room_id)
508714 return
509715
510716 # Check if this is a metrics command
……@@ -512,7 +718,12 @@ class MatrixClient:
512718 if not self.enable_conversation_tracking:
513719 await self.send_message(room_id, "Conversation tracking is not enabled.")
514720 return
515 await self._handle_metrics_command(room_id)
721
722 # Use ollamatools if available
723 if OLLAMATOOLS_AVAILABLE and hasattr(handle_metrics_command, '__call__'):
724 await handle_metrics_command(self, room_id)
725 else:
726 await self._handle_metrics_command(room_id)
516727 return
517728
518729 # Unknown command
……@@ -591,532 +802,4 @@ For example: "Hey {username}, what's the weather like today?"
591802 room_id: The ID of the room to send the list to
592803 """
593804 models_text = f"Available models: {', '.join(self.available_models)}"
594 await self.send_message(room_id, models_text)
595
596 async def get_available_models(self) -> List[str]:
597 """
598 Get a list of available Ollama models.
599
600 Returns:
601 List[str]: A list of available model names
602 """
603 if not OLLAMA_AVAILABLE:
604 return [self.ollama_model]
605
606 try:
607 ollama_client = OllamaAsync(host=self.ollama_host)
608 response = await ollama_client.list()
609 models = [model["name"] for model in response["models"]]
610 return models
611 except Exception as e:
612 logger.error(f"Error getting available models: {e}")
613 return [self.ollama_model]
614
615 async def update_available_models(self) -> None:
616 """
617 Update the list of available models from the Ollama server.
618 """
619 models = await self.get_available_models()
620 self.available_models = models
621 logger.info(f"Updated available models: {', '.join(models)}")
622
623 def add_admin_user(self, user_id: str) -> None:
624 """
625 Add a user to the list of admin users.
626
627 Args:
628 user_id: The Matrix user ID to add as an admin
629 """
630 self.admin_users.add(user_id)
631 logger.info(f"Added admin user: {user_id}")
632
633 def remove_admin_user(self, user_id: str) -> None:
634 """
635 Remove a user from the list of admin users.
636
637 Args:
638 user_id: The Matrix user ID to remove as an admin
639 """
640 if user_id in self.admin_users:
641 self.admin_users.remove(user_id)
642 logger.info(f"Removed admin user: {user_id}")
643
644 def set_message_callback(self, callback: Callable) -> None:
645 """
646 Set a custom callback for handling incoming messages.
647
648 The callback should be an async function that takes three arguments:
649 - room: The MatrixRoom object
650 - event: The RoomMessageText event
651 - client: This MatrixClient instance
652
653 Args:
654 callback: The callback function
655 """
656 self.message_callback = callback
657
658 async def listen(self) -> None:
659 """
660 Start listening for messages in joined rooms.
661
662 This method will block until the client is stopped.
663 """
664 # Set up the message callback
665 self.client.add_event_callback(self._message_callback, RoomMessageText)
666
667 # Start syncing with the server
668 await self.client.sync_forever(timeout=30000, full_state=True)
669
670 async def disconnect(self) -> None:
671 """
672 Disconnect from the Matrix homeserver.
673 """
674 await self.client.logout()
675 await self.client.close()
676 logger.info(f"Disconnected from {self.homeserver}")
677
678 def _get_default_opensearch_config(self) -> Dict[str, Any]:
679 """
680 Get the default OpenSearchNG configuration.
681
682 Returns:
683 Dict[str, Any]: The default configuration
684 """
685 return {
686 "BASE_URL": "http://localhost:8081",
687 "ENGINES": ["google", "bing", "duckduckgo"],
688 "NUM_RESULTS": 5
689 }
690
691 def _load_conversation_history(self) -> List[Dict[str, Any]]:
692 """
693 Load conversation history from file.
694
695 Returns:
696 List[Dict[str, Any]]: The conversation history
697 """
698 if os.path.exists(self.history_file):
699 try:
700 import json
701 with open(self.history_file, 'r') as f:
702 history = json.load(f)
703 logger.info(f"Loaded {len(history)} conversations from history")
704 return history
705 except Exception as e:
706 logger.error(f"Failed to load conversation history: {str(e)}")
707 return []
708 else:
709 logger.info("No conversation history file found")
710 return []
711
712 def _save_conversation_history(self) -> None:
713 """
714 Save conversation history to file.
715 """
716 if not self.enable_conversation_tracking:
717 return
718
719 try:
720 import json
721 # Create directory if it doesn't exist
722 os.makedirs(os.path.dirname(self.history_file), exist_ok=True)
723
724 with open(self.history_file, 'w') as f:
725 json.dump(self.conversation_history, f, indent=2)
726 logger.debug(f"Saved {len(self.conversation_history)} conversations to history")
727 except Exception as e:
728 logger.error(f"Failed to save conversation history: {str(e)}")
729
730 async def _handle_search_command(self, query: str, room_id: str) -> None:
731 """
732 Handle the /search command.
733
734 Args:
735 query: The search query
736 room_id: The ID of the room where the command was sent
737 """
738 if not self.enable_search:
739 await self.send_message(room_id, "Search functionality is not enabled.")
740 return
741
742 await self.send_message(room_id, f"Searching for: {query}...")
743
744 try:
745 # Simple implementation that uses Ollama to generate a response
746 # In a real implementation, this would use OpenSearchNG
747 search_prompt = f"Please provide a brief search result for the query: {query}"
748 response = await self.process_with_ollama(search_prompt, None, room_id)
749
750 # Format the response
751 formatted_response = f"Search Results for '{query}':\n\n{response}"
752
753 # Send the formatted response
754 await self.send_message(room_id, formatted_response)
755
756 except Exception as e:
757 logger.error(f"Error executing search: {e}")
758 await self.send_message(room_id, f"Error executing search: {str(e)}")
759
760 async def _handle_history_command(self, count: int, room_id: str) -> None:
761 """
762 Handle the /history command.
763
764 Args:
765 count: Number of recent conversations to show
766 room_id: The ID of the room where the command was sent
767 """
768 if not self.enable_conversation_tracking:
769 await self.send_message(room_id, "Conversation tracking is not enabled.")
770 return
771
772 if not self.conversation_history:
773 await self.send_message(room_id, "No conversation history available.")
774 return
775
776 # Get the most recent conversations for this room
777 room_history = [conv for conv in self.conversation_history if conv.get("room_id") == room_id]
778
779 if not room_history:
780 await self.send_message(room_id, "No conversation history available for this room.")
781 return
782
783 # Get the most recent conversations
784 recent = room_history[-count:] if len(room_history) >= count else room_history
785
786 # Format the history
787 formatted_history = ["Recent Conversations:"]
788
789 for i, conv in enumerate(recent, 1):
790 timestamp = conv.get("timestamp", "Unknown time")
791 user_message = conv.get("user_message", "No message")
792 sender = conv.get("sender", "Unknown sender")
793
794 # Truncate message if it's too long
795 if len(user_message) > 100:
796 user_message = user_message[:100] + "..."
797
798 formatted_history.append(f"{i}. {timestamp}")
799 formatted_history.append(f" From: {sender}")
800 formatted_history.append(f" Message: {user_message}")
801 formatted_history.append("")
802
803 # Send the formatted history
804 await self.send_message(room_id, "\n".join(formatted_history))
805
806 async def _handle_topics_command(self, room_id: str) -> None:
807 """
808 Handle the /topics command.
809
810 Args:
811 room_id: The ID of the room where the command was sent
812 """
813 if not self.enable_conversation_tracking:
814 await self.send_message(room_id, "Conversation tracking is not enabled.")
815 return
816
817 if not self.conversation_history:
818 await self.send_message(room_id, "No conversation history available.")
819 return
820
821 # Get the conversations for this room
822 room_history = [conv for conv in self.conversation_history if conv.get("room_id") == room_id]
823
824 if not room_history:
825 await self.send_message(room_id, "No conversation history available for this room.")
826 return
827
828 # Analyze topics
829 topics = self._analyze_conversation_topics(room_history)
830
831 if not topics:
832 await self.send_message(room_id, "No topics found in conversation history.")
833 return
834
835 # Format the topics
836 formatted_topics = ["Conversation Topics:"]
837
838 for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True):
839 formatted_topics.append(f"- {topic}: {count} mentions")
840
841 # Send the formatted topics
842 await self.send_message(room_id, "\n".join(formatted_topics))
843
844 async def _handle_metrics_command(self, room_id: str) -> None:
845 """
846 Handle the /metrics command.
847
848 Args:
849 room_id: The ID of the room where the command was sent
850 """
851 if not self.enable_conversation_tracking:
852 await self.send_message(room_id, "Conversation tracking is not enabled.")
853 return
854
855 if not self.conversation_history:
856 await self.send_message(room_id, "No conversation history available.")
857 return
858
859 # Get the conversations for this room
860 room_history = [conv for conv in self.conversation_history if conv.get("room_id") == room_id]
861
862 if not room_history:
863 await self.send_message(room_id, "No conversation history available for this room.")
864 return
865
866 # Calculate metrics
867 metrics = self._get_performance_metrics(room_history)
868
869 # Format the metrics
870 formatted_metrics = ["Performance Metrics:"]
871 formatted_metrics.append(f"- Total conversations: {metrics['total_conversations']}")
872 formatted_metrics.append(f"- Average response time: {metrics['avg_response_time']:.2f}s")
873 formatted_metrics.append(f"- Estimated total tokens: {metrics['total_tokens_estimated']}")
874 formatted_metrics.append(f"- Average tokens per conversation: {metrics['avg_tokens_per_conversation']:.1f}")
875
876 if metrics.get("slowest_response"):
877 formatted_metrics.append(f"- Slowest response: {metrics['slowest_response']['time']:.2f}s for '{metrics['slowest_response']['message']}'")
878
879 if metrics.get("fastest_response"):
880 formatted_metrics.append(f"- Fastest response: {metrics['fastest_response']['time']:.2f}s for '{metrics['fastest_response']['message']}'")
881
882 # Send the formatted metrics
883 await self.send_message(room_id, "\n".join(formatted_metrics))
884
885 def _analyze_conversation_topics(self, conversations: List[Dict[str, Any]]) -> Dict[str, int]:
886 """
887 Analyze common topics in conversation history.
888
889 Args:
890 conversations: List of conversation records
891
892 Returns:
893 Dict[str, int]: Dictionary of topics and their frequency
894 """
895 if not conversations:
896 return {}
897
898 # Simple keyword-based topic analysis
899 topics = {
900 "help": 0,
901 "search": 0,
902 "file": 0,
903 "command": 0,
904 "code": 0,
905 "error": 0,
906 "issue": 0,
907 "question": 0,
908 "other": 0
909 }
910
911 for conv in conversations:
912 message = conv.get("user_message", "").lower()
913
914 # Check for keywords
915 if "help" in message or "how" in message:
916 topics["help"] += 1
917 if "search" in message or "find" in message or "look" in message:
918 topics["search"] += 1
919 if "file" in message or "read" in message or "write" in message:
920 topics["file"] += 1
921 if "command" in message or "run" in message or "execute" in message:
922 topics["command"] += 1
923 if "code" in message or "program" in message or "script" in message:
924 topics["code"] += 1
925 if "error" in message or "bug" in message or "fix" in message:
926 topics["error"] += 1
927 if "issue" in message or "problem" in message:
928 topics["issue"] += 1
929 if "?" in message:
930 topics["question"] += 1
931
932 # If no keywords matched, count as other
933 if not any(count > 0 for count in topics.values()):
934 topics["other"] += 1
935
936 # Remove topics with zero count
937 return {k: v for k, v in topics.items() if v > 0}
938
939 def _get_performance_metrics(self, conversations: List[Dict[str, Any]]) -> Dict[str, Any]:
940 """
941 Get performance metrics from conversation history.
942
943 Args:
944 conversations: List of conversation records
945
946 Returns:
947 Dict[str, Any]: Performance metrics
948 """
949 if not conversations:
950 return {"error": "No conversation history available"}
951
952 # Calculate metrics
953 total_conversations = len(conversations)
954 total_response_time = sum(conv.get("response_time", 0) for conv in conversations)
955 avg_response_time = total_response_time / total_conversations if total_conversations > 0 else 0
956
957 # Calculate token usage
958 total_tokens = sum(conv.get("tokens_estimated", 0) for conv in conversations)
959 avg_tokens_per_conversation = total_tokens / total_conversations if total_conversations > 0 else 0
960
961 # Find slowest and fastest responses
962 slowest = max(conversations, key=lambda x: x.get("response_time", 0)) if conversations else None
963 fastest = min(conversations, key=lambda x: x.get("response_time", 0)) if conversations else None
964
965 return {
966 "total_conversations": total_conversations,
967 "total_response_time": total_response_time,
968 "avg_response_time": avg_response_time,
969 "total_tokens_estimated": total_tokens,
970 "avg_tokens_per_conversation": avg_tokens_per_conversation,
971 "slowest_response": {
972 "time": slowest.get("response_time", 0) if slowest else 0,
973 "message": slowest.get("user_message", "")[:50] + "..." if slowest and len(slowest.get("user_message", "")) > 50 else slowest.get("user_message", "") if slowest else ""
974 },
975 "fastest_response": {
976 "time": fastest.get("response_time", 0) if fastest else 0,
977 "message": fastest.get("user_message", "")[:50] + "..." if fastest and len(fastest.get("user_message", "")) > 50 else fastest.get("user_message", "") if fastest else ""
978 }
979 }
980
981 def _record_conversation(
982 self,
983 user_message: str,
984 assistant_response: str,
985 sender: str,
986 room_id: str,
987 response_time: float
988 ) -> None:
989 """
990 Record a conversation in the history.
991
992 Args:
993 user_message: The user's message
994 assistant_response: The assistant's response
995 sender: The Matrix user ID of the sender
996 room_id: The ID of the room where the conversation took place
997 response_time: The time taken to generate the response
998 """
999 if not self.enable_conversation_tracking:
1000 return
1001
1002 import time
1003
1004 # Create a conversation record
1005 conversation = {
1006 "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
1007 "user_message": user_message,
1008 "assistant_response": assistant_response,
1009 "sender": sender,
1010 "room_id": room_id,
1011 "response_time": response_time,
1012 "tokens_estimated": len(user_message.split()) + len(assistant_response.split())
1013 }
1014
1015 # Add to conversation history
1016 self.conversation_history.append(conversation)
1017
1018 # Trim conversation history if it gets too long
1019 if len(self.conversation_history) > self.max_conversation_history:
1020 self.conversation_history = self.conversation_history[-self.max_conversation_history:]
1021
1022 # Save conversation history
1023 self._save_conversation_history()
1024
1025
1026class MatrixConfig:
1027 """
1028 Configuration handler for Matrix integration.
1029 """
1030
1031 @staticmethod
1032 def load_config(config_path: str) -> Dict:
1033 """
1034 Load configuration from a YAML file.
1035
1036 Args:
1037 config_path: Path to the configuration file
1038
1039 Returns:
1040 Dict: The configuration as a dictionary
1041 """
1042 try:
1043 with open(config_path, 'r') as f:
1044 config = yaml.safe_load(f)
1045
1046 logger.info(f"Loaded configuration from {config_path}")
1047 return config
1048
1049 except Exception as e:
1050 logger.error(f"Error loading configuration from {config_path}: {e}")
1051 raise
1052
1053 @staticmethod
1054 def save_config(config: Dict, config_path: str) -> None:
1055 """
1056 Save configuration to a YAML file.
1057
1058 Args:
1059 config: The configuration as a dictionary
1060 config_path: Path to save the configuration to
1061 """
1062 try:
1063 # Create directory if it doesn't exist
1064 os.makedirs(os.path.dirname(config_path), exist_ok=True)
1065
1066 with open(config_path, 'w') as f:
1067 yaml.dump(config, f, default_flow_style=False)
1068
1069 logger.info(f"Saved configuration to {config_path}")
1070
1071 except Exception as e:
1072 logger.error(f"Error saving configuration to {config_path}: {e}")
1073 raise
1074
1075 @staticmethod
1076 def create_default_config() -> Dict:
1077 """
1078 Create a default configuration.
1079
1080 Returns:
1081 Dict: The default configuration
1082 """
1083 return {
1084 "MATRIX": {
1085 "HOMESERVER": "https://matrix.example.org",
1086 "USER": "@user:example.org",
1087 "PASSWORD": "your-password",
1088 "DEVICE_ID": "matrix-cmd-agent",
1089 "ROOMS": [
1090 "#room:example.org"
1091 ],
1092 "ADMIN_USERS": [
1093 "@admin:example.org"
1094 ]
1095 },
1096 "OLLAMA": {
1097 "HOST": "localhost:11434",
1098 "MODEL": "llama3:latest",
1099 "PROMPT": "You are a helpful assistant.",
1100 "AVAILABLE_MODELS": [
1101 "llama3:latest",
1102 "mistral:latest",
1103 "gemma:latest"
1104 ]
1105 },
1106 "ENHANCED": {
1107 "ENABLE_SEARCH": False,
1108 "ENABLE_CONVERSATION_TRACKING": False,
1109 "HISTORY_FILE": "~/.config/matrix-cmd/history.json",
1110 "MAX_CONVERSATION_HISTORY": 100,
1111 "RESPOND_TO_ALL_MESSAGES": False,
1112 "OPENSEARCH": {
1113 "BASE_URL": "http://localhost:8081",
1114 "ENGINES": [
1115 "google",
1116 "bing",
1117 "duckduckgo"
1118 ],
1119 "NUM_RESULTS": 5
1120 }
1121 }
1122 }
805 await self.send_message(room_id,