OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / c978028

Implement enhanced features from ollamatools Matrix integration
c9780285154ba7b0369980fbc49b7ed831e0de22
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-19T23:25:59.000Z

Changed files (3) +546 -7

ollamatools-new/matrix_cmd/README.md +51 -0
……@@ -17,6 +17,11 @@ A command-line agent for interacting with Matrix chat, with integrated Ollama AI
1717 - Automatic retry with exponential backoff
1818 - Clear feedback on rate limiting status
1919 - Graceful handling of connection interruptions
20- **Enhanced Features**:
21 - **Search Integration**: Search the web directly from Matrix
22 - **Conversation History**: Track and analyze conversation history
23 - **Topic Analysis**: Analyze conversation topics and trends
24 - **Performance Metrics**: Track response times and token usage
2025
2126## Installation
2227
……@@ -67,6 +72,19 @@ OLLAMA:
6772 - llama3:latest
6873 - mistral:latest
6974 - gemma:latest
75ENHANCED:
76 ENABLE_SEARCH: false
77 ENABLE_CONVERSATION_TRACKING: false
78 HISTORY_FILE: ~/.config/matrix-cmd/history.json
79 MAX_CONVERSATION_HISTORY: 100
80 RESPOND_TO_ALL_MESSAGES: false
81 OPENSEARCH:
82 BASE_URL: http://localhost:8081
83 ENGINES:
84 - google
85 - bing
86 - duckduckgo
87 NUM_RESULTS: 5
7088```
7189
7290### Connect to Matrix
……@@ -83,6 +101,29 @@ You can override configuration settings with command-line arguments:
83101matrix-cmd connect --homeserver https://matrix.example.org --user "@user:example.org" --room "#room:example.org"
84102```
85103
104### Enabling Enhanced Features
105
106You can enable enhanced features via command-line arguments:
107
108```bash
109# Enable search functionality
110matrix-cmd connect --enable-search
111
112# Enable conversation tracking
113matrix-cmd connect --enable-conversation-tracking
114
115# Enable both features
116matrix-cmd connect --enable-search --enable-conversation-tracking
117
118# Specify a custom history file
119matrix-cmd connect --enable-conversation-tracking --history-file ~/matrix_history.json
120
121# Respond to all messages, not just mentions
122matrix-cmd connect --respond-to-all-messages
123```
124
125Alternatively, you can enable these features in the configuration file by setting the appropriate values in the `ENHANCED` section.
126
86127### Send a Message
87128
88129Send a message to a Matrix room:
……@@ -120,6 +161,16 @@ When connected to Matrix, you can use the following commands in chat:
120161- `/help` - Show help information
121162- `/models` - List available Ollama models
122163- `/model <model_name>` - Change to a different Ollama model
164- `/ping` - Test if the bot is responsive
165
166### Enhanced Commands
167
168When enhanced features are enabled, additional commands become available:
169
170- `/search <query>` - Search the web for information
171- `/history [count]` - Show recent conversation history (default: 10)
172- `/topics` - Show conversation topic analysis
173- `/metrics` - Show performance metrics
123174
124175## Handling Rate Limiting
125176
ollamatools-new/matrix_cmd/main.py +49 -1
……@@ -81,6 +81,27 @@ def parse_args() -> argparse.Namespace:
8181 help="Room ID to join (can be specified multiple times, overrides config)"
8282 )
8383
84 # Enhanced features
85 connect_parser.add_argument(
86 "--enable-search",
87 action="store_true",
88 help="Enable search functionality"
89 )
90 connect_parser.add_argument(
91 "--enable-conversation-tracking",
92 action="store_true",
93 help="Enable conversation history tracking"
94 )
95 connect_parser.add_argument(
96 "--history-file",
97 help="Path to the conversation history file"
98 )
99 connect_parser.add_argument(
100 "--respond-to-all-messages",
101 action="store_true",
102 help="Respond to all messages, not just mentions"
103 )
104
84105 # Send command
85106 send_parser = subparsers.add_parser("send", help="Send a message to a Matrix room")
86107 send_parser.add_argument(
……@@ -224,6 +245,28 @@ async def run_connect_command(args: argparse.Namespace) -> None:
224245 # Get rooms to join
225246 rooms = args.room or config["MATRIX"]["ROOMS"]
226247
248 # Get enhanced features from config or command-line arguments
249 enable_search = args.enable_search
250 enable_conversation_tracking = args.enable_conversation_tracking
251 history_file = args.history_file
252 respond_to_all_messages = args.respond_to_all_messages
253
254 # If config has ENHANCED section, use those values as defaults
255 if "ENHANCED" in config:
256 if not args.enable_search and "ENABLE_SEARCH" in config["ENHANCED"]:
257 enable_search = config["ENHANCED"]["ENABLE_SEARCH"]
258 if not args.enable_conversation_tracking and "ENABLE_CONVERSATION_TRACKING" in config["ENHANCED"]:
259 enable_conversation_tracking = config["ENHANCED"]["ENABLE_CONVERSATION_TRACKING"]
260 if not args.history_file and "HISTORY_FILE" in config["ENHANCED"]:
261 history_file = config["ENHANCED"]["HISTORY_FILE"]
262 if not args.respond_to_all_messages and "RESPOND_TO_ALL_MESSAGES" in config["ENHANCED"]:
263 respond_to_all_messages = config["ENHANCED"]["RESPOND_TO_ALL_MESSAGES"]
264
265 # Get OpenSearch config if available
266 opensearch_config = None
267 if "ENHANCED" in config and "OPENSEARCH" in config["ENHANCED"]:
268 opensearch_config = config["ENHANCED"]["OPENSEARCH"]
269
227270 # Create Matrix client
228271 client = MatrixClient(
229272 homeserver=homeserver,
……@@ -234,7 +277,12 @@ async def run_connect_command(args: argparse.Namespace) -> None:
234277 ollama_model=config["OLLAMA"]["MODEL"],
235278 ollama_prompt=config["OLLAMA"]["PROMPT"],
236279 available_models=config["OLLAMA"]["AVAILABLE_MODELS"],
237 admin_users=config["MATRIX"]["ADMIN_USERS"]
280 admin_users=config["MATRIX"]["ADMIN_USERS"],
281 enable_search=enable_search,
282 enable_conversation_tracking=enable_conversation_tracking,
283 opensearch_config=opensearch_config,
284 history_file=history_file,
285 respond_to_all_messages=respond_to_all_messages
238286 )
239287
240288 # Connect to the server with retry capabilities
ollamatools-new/matrix_cmd/matrix_client.py +446 -6
……@@ -64,6 +64,10 @@ class MatrixClient:
6464 HELP_COMMAND_PATTERN = re.compile(r'^/help$')
6565 LIST_MODELS_COMMAND_PATTERN = re.compile(r'^/models$')
6666 PING_COMMAND_PATTERN = re.compile(r'^/ping$')
67 SEARCH_COMMAND_PATTERN = re.compile(r'^/search\s+(.+)$')
68 HISTORY_COMMAND_PATTERN = re.compile(r'^/history(?:\s+(\d+))?$')
69 TOPICS_COMMAND_PATTERN = re.compile(r'^/topics$')
70 METRICS_COMMAND_PATTERN = re.compile(r'^/metrics$')
6771
6872 def __init__(
6973 self,
……@@ -76,7 +80,13 @@ class MatrixClient:
7680 ollama_prompt: str = None,
7781 available_models: List[str] = None,
7882 admin_users: List[str] = None,
79 ssl: bool = True
83 ssl: bool = True,
84 enable_search: bool = False,
85 enable_conversation_tracking: bool = False,
86 opensearch_config: Dict[str, Any] = None,
87 history_file: str = None,
88 max_conversation_history: int = 100,
89 respond_to_all_messages: bool = False
8090 ):
8191 """
8292 Initialize a new Matrix client.
……@@ -102,6 +112,19 @@ class MatrixClient:
102112 self.ollama_prompt = ollama_prompt
103113 self.ssl = ssl
104114
115 # Enhanced features
116 self.enable_search = enable_search
117 self.enable_conversation_tracking = enable_conversation_tracking
118 self.opensearch_config = opensearch_config or self._get_default_opensearch_config()
119 self.history_file = history_file or os.path.expanduser("~/.config/matrix-cmd/history.json")
120 self.max_conversation_history = max_conversation_history
121 self.respond_to_all_messages = respond_to_all_messages
122
123 # Conversation history
124 self.conversation_history = []
125 if self.enable_conversation_tracking:
126 self.conversation_history = self._load_conversation_history()
127
105128 # Initialize the Matrix client
106129 self.client = AsyncClient(
107130 homeserver=self.homeserver,
……@@ -456,6 +479,42 @@ class MatrixClient:
456479 await self.send_message(room_id, "Pong!")
457480 return
458481
482 # Check if this is a search command
483 search_match = self.SEARCH_COMMAND_PATTERN.match(message)
484 if search_match:
485 if not self.enable_search:
486 await self.send_message(room_id, "Search functionality is not enabled.")
487 return
488 query = search_match.group(1)
489 await self._handle_search_command(query, room_id)
490 return
491
492 # Check if this is a history command
493 history_match = self.HISTORY_COMMAND_PATTERN.match(message)
494 if history_match:
495 if not self.enable_conversation_tracking:
496 await self.send_message(room_id, "Conversation tracking is not enabled.")
497 return
498 count = int(history_match.group(1)) if history_match.group(1) else 10
499 await self._handle_history_command(count, room_id)
500 return
501
502 # Check if this is a topics command
503 if self.TOPICS_COMMAND_PATTERN.match(message):
504 if not self.enable_conversation_tracking:
505 await self.send_message(room_id, "Conversation tracking is not enabled.")
506 return
507 await self._handle_topics_command(room_id)
508 return
509
510 # Check if this is a metrics command
511 if self.METRICS_COMMAND_PATTERN.match(message):
512 if not self.enable_conversation_tracking:
513 await self.send_message(room_id, "Conversation tracking is not enabled.")
514 return
515 await self._handle_metrics_command(room_id)
516 return
517
459518 # Unknown command
460519 await self.send_message(room_id, f"Unknown command: {message}\nType /help for available commands.")
461520
……@@ -494,16 +553,34 @@ class MatrixClient:
494553 Args:
495554 room_id: The ID of the room to send the help to
496555 """
556 # Build help text based on enabled features
557 commands = [
558 "- /help - Show this help message",
559 "- /models - List available models",
560 "- /model <model_name> - Change to a different model",
561 "- /ping - Test if the bot is responsive"
562 ]
563
564 # Add search command if enabled
565 if self.enable_search:
566 commands.append("- /search <query> - Search the web for information")
567
568 # Add conversation tracking commands if enabled
569 if self.enable_conversation_tracking:
570 commands.append("- /history [count] - Show recent conversation history")
571 commands.append("- /topics - Show conversation topic analysis")
572 commands.append("- /metrics - Show performance metrics")
573
497574 help_text = """
498575Available commands:
499- /help - Show this help message
500- /models - List available models
501- /model <model_name> - Change to a different model
502- /ping - Test if the bot is responsive
576{commands}
503577
504578You can also mention me by my username in your message and I'll respond.
505579For example: "Hey {username}, what's the weather like today?"
506 """.format(username=self.user_id.split(':')[0].lstrip('@'))
580 """.format(
581 commands="\n".join(commands),
582 username=self.user_id.split(':')[0].lstrip('@')
583 )
507584 await self.send_message(room_id, help_text.strip())
508585
509586 async def _list_models(self, room_id: str) -> None:
……@@ -597,6 +674,353 @@ For example: "Hey {username}, what's the weather like today?"
597674 await self.client.logout()
598675 await self.client.close()
599676 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()
6001024
6011025
6021026class MatrixConfig:
……@@ -678,5 +1102,21 @@ class MatrixConfig:
6781102 "mistral:latest",
6791103 "gemma:latest"
6801104 ]
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 }
6811121 }
6821122 }