Long Term Memory MCP Server
Browse files- README.md +147 -6
- app.py +58 -0
- demo_script.md +121 -0
- dockerfile +30 -0
- gradio_demo.py +359 -0
- langchain_memory_tools.py +176 -0
- local_run.py +61 -0
- ltm_mcp_server.py +353 -0
- requirements.txt +18 -0
- run_mcp_server.py +18 -0
README.md
CHANGED
|
@@ -1,14 +1,155 @@
|
|
| 1 |
---
|
| 2 |
title: Long Term Memory MCP Server
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Long Term Memory MCP Server
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.0.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
tags:
|
| 12 |
+
- mcp-server-track
|
| 13 |
+
- mcp
|
| 14 |
+
- memory
|
| 15 |
+
- rag
|
| 16 |
+
- llm
|
| 17 |
+
- conversation
|
| 18 |
+
short_description: MCP Server providing long-term memory for LLM conversations
|
| 19 |
---
|
| 20 |
|
| 21 |
+
# 🧠 Long Term Memory MCP Server
|
| 22 |
+
|
| 23 |
+
**Tags**: mcp-server-track
|
| 24 |
+
|
| 25 |
+
A Model Context Protocol (MCP) server that provides long-term memory capabilities for LLM conversations. This allows users to save important insights, conclusions, and context from conversations and retrieve them in future interactions.
|
| 26 |
+
|
| 27 |
+
## 🎯 Problem Solved
|
| 28 |
+
|
| 29 |
+
Current LLM interactions are stateless - they don't remember previous conversations or insights you've shared. This MCP server solves that by providing:
|
| 30 |
+
|
| 31 |
+
- **Persistent Memory**: Save important insights and context from conversations
|
| 32 |
+
- **Semantic Search**: Find relevant memories using natural language queries
|
| 33 |
+
- **Context Continuity**: Build upon previous conversations and learnings
|
| 34 |
+
- **Knowledge Accumulation**: Build a personal knowledge base over time
|
| 35 |
+
|
| 36 |
+
## 🚀 Features
|
| 37 |
+
|
| 38 |
+
### MCP Server Tools
|
| 39 |
+
- `save_memory` - Save insights, conclusions, or context to long-term memory
|
| 40 |
+
- `search_memory` - Search through memories using semantic similarity
|
| 41 |
+
- `list_memories` - Browse all stored memories
|
| 42 |
+
- `delete_memory` - Remove specific memories
|
| 43 |
+
|
| 44 |
+
### Gradio Demo Interface
|
| 45 |
+
- Interactive web interface for testing all MCP tools
|
| 46 |
+
- Real-time memory statistics
|
| 47 |
+
- Semantic search with adjustable similarity thresholds
|
| 48 |
+
- Memory browsing and management
|
| 49 |
+
|
| 50 |
+
## 🛠️ Technical Architecture
|
| 51 |
+
|
| 52 |
+
- **MCP Protocol**: Standards-compliant MCP server
|
| 53 |
+
- **Vector Storage**: ChromaDB for efficient semantic search
|
| 54 |
+
- **Embeddings**: SentenceTransformers (all-MiniLM-L6-v2) for semantic understanding
|
| 55 |
+
- **Interface**: Gradio web app for demonstration and testing
|
| 56 |
+
- **Storage**: Persistent local database
|
| 57 |
+
|
| 58 |
+
## 📦 Installation & Usage
|
| 59 |
+
|
| 60 |
+
### Local Development
|
| 61 |
+
```bash
|
| 62 |
+
# Clone and install dependencies
|
| 63 |
+
pip install -r requirements.txt
|
| 64 |
+
|
| 65 |
+
# Run the application
|
| 66 |
+
python app.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### Hugging Face Spaces
|
| 70 |
+
This Space runs both the MCP server and Gradio demo simultaneously.
|
| 71 |
+
|
| 72 |
+
## 🎮 Demo Video
|
| 73 |
+
|
| 74 |
+
[Demo Video Link](https://your-demo-video-link.com) - *Recording of the MCP server working with Claude Desktop*
|
| 75 |
+
|
| 76 |
+
## 💡 Use Cases
|
| 77 |
+
|
| 78 |
+
### Example Scenario
|
| 79 |
+
1. **Initial Conversation**: You discuss quantum consciousness theories with an LLM
|
| 80 |
+
2. **Save Insight**: Use `save_memory` to store key conclusions
|
| 81 |
+
3. **Future Conversation**: LLM can `search_memory` to find relevant context
|
| 82 |
+
4. **Continuity**: Build upon previous insights in new discussions
|
| 83 |
+
|
| 84 |
+
### Sample Usage with Claude Desktop
|
| 85 |
+
|
| 86 |
+
**Saving a memory:**
|
| 87 |
+
```
|
| 88 |
+
User: "Save this insight to memory: 'Consciousness might emerge from quantum processes in microtubules, as proposed by Penrose-Hameroff theory. This could explain the hard problem of consciousness.' Title: 'Quantum Consciousness Theory', Tags: 'consciousness, quantum, penrose, microtubules'"
|
| 89 |
+
|
| 90 |
+
LLM: *Uses save_memory tool*
|
| 91 |
+
✅ Memory saved successfully! ID: abc123...
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
**Searching memories:**
|
| 95 |
+
```
|
| 96 |
+
User: "What did we previously discuss about consciousness and quantum physics?"
|
| 97 |
+
|
| 98 |
+
LLM: *Uses search_memory tool*
|
| 99 |
+
🔍 Found relevant memory: "Quantum Consciousness Theory" - discusses how consciousness might emerge from quantum processes in microtubules...
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## 🔧 MCP Client Configuration
|
| 103 |
+
|
| 104 |
+
### Claude Desktop
|
| 105 |
+
Add to your `claude_desktop_config.json`:
|
| 106 |
+
```json
|
| 107 |
+
{
|
| 108 |
+
"mcpServers": {
|
| 109 |
+
"long-term-memory": {
|
| 110 |
+
"command": "python",
|
| 111 |
+
"args": ["path/to/mcp_server.py"],
|
| 112 |
+
"env": {}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Cursor IDE
|
| 119 |
+
Configure in your MCP settings to connect to the server.
|
| 120 |
+
|
| 121 |
+
## 📊 Memory Statistics
|
| 122 |
+
|
| 123 |
+
The system tracks:
|
| 124 |
+
- Total memories stored
|
| 125 |
+
- Content length statistics
|
| 126 |
+
- Tag usage patterns
|
| 127 |
+
- Timestamp-based organization
|
| 128 |
+
|
| 129 |
+
## 🔐 Privacy & Data
|
| 130 |
+
|
| 131 |
+
- All data stored locally in ChromaDB
|
| 132 |
+
- No external API calls for embeddings (uses local SentenceTransformers)
|
| 133 |
+
- Full control over your memory data
|
| 134 |
+
- Easy export/import capabilities
|
| 135 |
+
|
| 136 |
+
## 🚧 Future Enhancements
|
| 137 |
+
|
| 138 |
+
- [ ] Memory categorization and hierarchical organization
|
| 139 |
+
- [ ] Conversation threading and context linking
|
| 140 |
+
- [ ] Export/import functionality
|
| 141 |
+
- [ ] Advanced search filters (date, tags, content type)
|
| 142 |
+
- [ ] Memory summarization and consolidation
|
| 143 |
+
- [ ] Integration with external knowledge bases
|
| 144 |
+
|
| 145 |
+
## 🤝 Contributing
|
| 146 |
+
|
| 147 |
+
This project was created for the [Hugging Face MCP Hackathon](https://huggingface.co/Agents-MCP-Hackathon). Contributions welcome!
|
| 148 |
+
|
| 149 |
+
## 📝 License
|
| 150 |
+
|
| 151 |
+
MIT License - Feel free to use and modify!
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
*Built with ❤️ for the Hugging Face MCP Hackathon - Track 1: MCP Server/Tool*
|
app.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Long Term Memory MCP Server & Gradio Demo
|
| 4 |
+
A Model Context Protocol server that provides long-term memory capabilities for LLM conversations.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import threading
|
| 10 |
+
import time
|
| 11 |
+
import subprocess
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
# Add the current directory to the Python path
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 16 |
+
|
| 17 |
+
from gradio_demo import demo
|
| 18 |
+
|
| 19 |
+
def run_mcp_server():
|
| 20 |
+
"""Run the MCP server in a separate thread."""
|
| 21 |
+
try:
|
| 22 |
+
# Import and run the MCP server
|
| 23 |
+
from mcp_server import main
|
| 24 |
+
import asyncio
|
| 25 |
+
|
| 26 |
+
# Create new event loop for this thread
|
| 27 |
+
loop = asyncio.new_event_loop()
|
| 28 |
+
asyncio.set_event_loop(loop)
|
| 29 |
+
|
| 30 |
+
# Run the server
|
| 31 |
+
loop.run_until_complete(main())
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"MCP Server error: {e}")
|
| 34 |
+
|
| 35 |
+
def main():
|
| 36 |
+
"""Main entry point."""
|
| 37 |
+
print("🧠 Starting Long Term Memory MCP Server & Demo...")
|
| 38 |
+
|
| 39 |
+
# Start MCP server in background thread
|
| 40 |
+
mcp_thread = threading.Thread(target=run_mcp_server, daemon=True)
|
| 41 |
+
mcp_thread.start()
|
| 42 |
+
|
| 43 |
+
# Give the MCP server a moment to start
|
| 44 |
+
time.sleep(2)
|
| 45 |
+
|
| 46 |
+
print("✅ MCP Server started!")
|
| 47 |
+
print("🚀 Launching Gradio demo...")
|
| 48 |
+
|
| 49 |
+
# Launch Gradio demo
|
| 50 |
+
demo.launch(
|
| 51 |
+
server_name="0.0.0.0",
|
| 52 |
+
server_port=7860,
|
| 53 |
+
share=True,
|
| 54 |
+
show_error=True
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
demo_script.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎬 Demo Script & Testing Guide
|
| 2 |
+
|
| 3 |
+
## Quick Start Testing
|
| 4 |
+
|
| 5 |
+
### 1. Test Gradio Interface
|
| 6 |
+
1. Visit the Hugging Face Space
|
| 7 |
+
2. Try saving a memory in the "Save Memory" tab
|
| 8 |
+
3. Search for it in the "Search Memory" tab
|
| 9 |
+
4. Browse all memories in the "Browse Memories" tab
|
| 10 |
+
|
| 11 |
+
### 2. Test MCP Server with Claude Desktop
|
| 12 |
+
|
| 13 |
+
#### Setup:
|
| 14 |
+
1. Add to your `claude_desktop_config.json`:
|
| 15 |
+
```json
|
| 16 |
+
{
|
| 17 |
+
"mcpServers": {
|
| 18 |
+
"long-term-memory": {
|
| 19 |
+
"command": "python",
|
| 20 |
+
"args": ["run_mcp_server.py"],
|
| 21 |
+
"env": {},
|
| 22 |
+
"cwd": "/path/to/your/project"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
2. Restart Claude Desktop
|
| 29 |
+
|
| 30 |
+
#### Demo Script for Video:
|
| 31 |
+
|
| 32 |
+
**Scene 1: Introduction**
|
| 33 |
+
"Hi! I'm demonstrating a Long Term Memory MCP Server that solves a key problem with LLM conversations - they don't remember previous discussions. Let me show you how this works."
|
| 34 |
+
|
| 35 |
+
**Scene 2: Save a Memory**
|
| 36 |
+
```
|
| 37 |
+
User: "I want to save an important insight to my long-term memory. Use the save_memory tool with this content: 'Consciousness might emerge from quantum processes in microtubules according to the Penrose-Hameroff theory. This could explain the binding problem and why we have unified conscious experience.' Use the title 'Quantum Consciousness Theory' and tags 'consciousness, quantum, penrose, microtubules, binding-problem'"
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**Scene 3: Save Another Memory**
|
| 41 |
+
```
|
| 42 |
+
User: "Save another insight: 'Free will might be an illusion created by our brain's narrative construction. The feeling of choice comes after neural commitment to action.' Title: 'Free Will Illusion Theory', tags: 'free-will, neuroscience, consciousness, illusion'"
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Scene 4: Search Memories**
|
| 46 |
+
```
|
| 47 |
+
User: "Search my memories for information about consciousness and how it relates to quantum physics"
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
**Scene 5: Demonstrate Context Continuity**
|
| 51 |
+
```
|
| 52 |
+
User: "Based on what we previously discussed about consciousness, what are the implications for artificial intelligence? Use my memories to inform your response."
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
**Scene 6: List All Memories**
|
| 56 |
+
```
|
| 57 |
+
User: "Show me all the memories I have stored using the list_memories tool"
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## Demo Scenarios
|
| 61 |
+
|
| 62 |
+
### Scenario 1: Philosophy Student
|
| 63 |
+
1. Save insights from reading different philosophers
|
| 64 |
+
2. Search for connections between ideas
|
| 65 |
+
3. Build upon previous understanding in new discussions
|
| 66 |
+
|
| 67 |
+
### Scenario 2: Research Notes
|
| 68 |
+
1. Save key findings from papers
|
| 69 |
+
2. Find related research using semantic search
|
| 70 |
+
3. Synthesize knowledge across sessions
|
| 71 |
+
|
| 72 |
+
### Scenario 3: Personal Development
|
| 73 |
+
1. Save insights from conversations about goals
|
| 74 |
+
2. Track progress and learnings over time
|
| 75 |
+
3. Reference past conclusions in future planning
|
| 76 |
+
|
| 77 |
+
## Testing Checklist
|
| 78 |
+
|
| 79 |
+
### Basic Functionality
|
| 80 |
+
- [ ] Save memory with all fields
|
| 81 |
+
- [ ] Save memory with minimal fields
|
| 82 |
+
- [ ] Search with various queries
|
| 83 |
+
- [ ] Search with different thresholds
|
| 84 |
+
- [ ] List memories with different limits
|
| 85 |
+
- [ ] View memory statistics
|
| 86 |
+
|
| 87 |
+
### Edge Cases
|
| 88 |
+
- [ ] Empty search query
|
| 89 |
+
- [ ] Search with no results
|
| 90 |
+
- [ ] Search when no memories exist
|
| 91 |
+
- [ ] Very long content
|
| 92 |
+
- [ ] Special characters in content
|
| 93 |
+
- [ ] Multiple identical memories
|
| 94 |
+
|
| 95 |
+
### MCP Integration
|
| 96 |
+
- [ ] Server starts correctly
|
| 97 |
+
- [ ] Tools are discovered by client
|
| 98 |
+
- [ ] All tools execute successfully
|
| 99 |
+
- [ ] Error handling works
|
| 100 |
+
- [ ] Multiple concurrent requests
|
| 101 |
+
|
| 102 |
+
## Performance Benchmarks
|
| 103 |
+
|
| 104 |
+
### Expected Performance
|
| 105 |
+
- **Save Memory**: < 1 second
|
| 106 |
+
- **Search 100 memories**: < 2 seconds
|
| 107 |
+
- **List memories**: < 0.5 seconds
|
| 108 |
+
- **Memory footprint**: ~50MB for 1000 memories
|
| 109 |
+
|
| 110 |
+
### Scalability Limits
|
| 111 |
+
- **ChromaDB**: Handles 100K+ documents efficiently
|
| 112 |
+
- **Embeddings**: 384-dimensional vectors (all-MiniLM-L6-v2)
|
| 113 |
+
- **Storage**: ~1KB per memory average
|
| 114 |
+
|
| 115 |
+
## Troubleshooting
|
| 116 |
+
|
| 117 |
+
### Common Issues
|
| 118 |
+
1. **"MCP not available"**: Install missing dependencies
|
| 119 |
+
2. **Embedding model fails**: Check internet connection for initial download
|
| 120 |
+
3. **ChromaDB errors**: Check write permissions for memory_db directory
|
| 121 |
+
4. **Claude Desktop not connecting**: Verify config.json path an
|
dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements first for better caching
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
|
| 13 |
+
# Install Python dependencies
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copy application code
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Create directory for database
|
| 20 |
+
RUN mkdir -p /app/memory_db
|
| 21 |
+
|
| 22 |
+
# Expose port
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Set environment variables
|
| 26 |
+
ENV GRADIO_SERVER_NAME="0.0.0.0"
|
| 27 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 28 |
+
|
| 29 |
+
# Run the application
|
| 30 |
+
CMD ["python", "app.py"]
|
gradio_demo.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import asyncio
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
import uuid
|
| 7 |
+
import chromadb
|
| 8 |
+
from chromadb.config import Settings
|
| 9 |
+
from sentence_transformers import SentenceTransformer
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
class LongTermMemoryDemo:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.db_path = "./memory_db"
|
| 15 |
+
Path(self.db_path).mkdir(exist_ok=True)
|
| 16 |
+
|
| 17 |
+
# Initialize ChromaDB
|
| 18 |
+
self.chroma_client = chromadb.PersistentClient(
|
| 19 |
+
path=self.db_path,
|
| 20 |
+
settings=Settings(anonymized_telemetry=False)
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Get or create collection
|
| 24 |
+
try:
|
| 25 |
+
self.collection = self.chroma_client.get_collection("memories")
|
| 26 |
+
except:
|
| 27 |
+
self.collection = self.chroma_client.create_collection(
|
| 28 |
+
name="memories",
|
| 29 |
+
metadata={"description": "Long-term memory storage for conversations"}
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Initialize sentence transformer for embeddings
|
| 33 |
+
print("Loading SentenceTransformer model...")
|
| 34 |
+
self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
|
| 35 |
+
print("Model loaded successfully!")
|
| 36 |
+
|
| 37 |
+
def save_memory(self, content: str, title: str, tags: str = "", context: str = "") -> str:
|
| 38 |
+
"""Save content to long-term memory."""
|
| 39 |
+
if not content or not title:
|
| 40 |
+
return "❌ Error: Content and title are required!"
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()] if tags else []
|
| 44 |
+
|
| 45 |
+
memory_id = str(uuid.uuid4())
|
| 46 |
+
timestamp = datetime.now().isoformat()
|
| 47 |
+
|
| 48 |
+
# Create embedding
|
| 49 |
+
embedding = self.encoder.encode(f"{title} {content}").tolist()
|
| 50 |
+
|
| 51 |
+
# Prepare metadata
|
| 52 |
+
metadata = {
|
| 53 |
+
"title": title,
|
| 54 |
+
"timestamp": timestamp,
|
| 55 |
+
"tags": json.dumps(tags_list),
|
| 56 |
+
"context": context,
|
| 57 |
+
"content_length": len(content)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
# Save to ChromaDB
|
| 61 |
+
self.collection.add(
|
| 62 |
+
documents=[content],
|
| 63 |
+
embeddings=[embedding],
|
| 64 |
+
metadatas=[metadata],
|
| 65 |
+
ids=[memory_id]
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
result = f"✅ **Memory saved successfully!**\n\n"
|
| 69 |
+
result += f"**ID**: `{memory_id}`\n"
|
| 70 |
+
result += f"**Title**: {title}\n"
|
| 71 |
+
result += f"**Timestamp**: {timestamp}\n"
|
| 72 |
+
if tags_list:
|
| 73 |
+
result += f"**Tags**: {', '.join(tags_list)}\n"
|
| 74 |
+
if context:
|
| 75 |
+
result += f"**Context**: {context}\n"
|
| 76 |
+
result += f"**Content Preview**: {content[:200]}{'...' if len(content) > 200 else ''}"
|
| 77 |
+
|
| 78 |
+
return result
|
| 79 |
+
except Exception as e:
|
| 80 |
+
return f"❌ Error saving memory: {str(e)}"
|
| 81 |
+
|
| 82 |
+
def search_memory(self, query: str, limit: int = 5, threshold: float = 0.3) -> str:
|
| 83 |
+
"""Search through memories."""
|
| 84 |
+
if not query:
|
| 85 |
+
return "❌ Error: Search query is required!"
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
if self.collection.count() == 0:
|
| 89 |
+
return "📭 No memories stored yet. Save some memories first!"
|
| 90 |
+
|
| 91 |
+
# Create query embedding
|
| 92 |
+
query_embedding = self.encoder.encode(query).tolist()
|
| 93 |
+
|
| 94 |
+
# Search in ChromaDB
|
| 95 |
+
results = self.collection.query(
|
| 96 |
+
query_embeddings=[query_embedding],
|
| 97 |
+
n_results=min(limit, self.collection.count())
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
if not results['documents'][0]:
|
| 101 |
+
return "🔍 No relevant memories found."
|
| 102 |
+
|
| 103 |
+
# Filter by threshold and format results
|
| 104 |
+
response = f"🔍 **Search Results for**: \"{query}\"\n\n"
|
| 105 |
+
|
| 106 |
+
found_relevant = False
|
| 107 |
+
for i, (doc, metadata, distance) in enumerate(zip(
|
| 108 |
+
results['documents'][0],
|
| 109 |
+
results['metadatas'][0],
|
| 110 |
+
results['distances'][0]
|
| 111 |
+
)):
|
| 112 |
+
similarity = 1 - distance
|
| 113 |
+
if similarity >= threshold:
|
| 114 |
+
found_relevant = True
|
| 115 |
+
tags = json.loads(metadata.get('tags', '[]'))
|
| 116 |
+
|
| 117 |
+
response += f"### {i+1}. {metadata['title']} (Similarity: {similarity:.2f})\n"
|
| 118 |
+
response += f"**Saved**: {metadata['timestamp']}\n"
|
| 119 |
+
if tags:
|
| 120 |
+
response += f"**Tags**: {', '.join(tags)}\n"
|
| 121 |
+
if metadata.get('context'):
|
| 122 |
+
response += f"**Context**: {metadata['context']}\n"
|
| 123 |
+
response += f"**Content**: {doc}\n\n"
|
| 124 |
+
response += "---\n\n"
|
| 125 |
+
|
| 126 |
+
if not found_relevant:
|
| 127 |
+
response += f"No memories found above similarity threshold of {threshold:.2f}"
|
| 128 |
+
|
| 129 |
+
return response
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return f"❌ Error searching memories: {str(e)}"
|
| 132 |
+
|
| 133 |
+
def list_memories(self, limit: int = 10) -> str:
|
| 134 |
+
"""List all memories."""
|
| 135 |
+
try:
|
| 136 |
+
if self.collection.count() == 0:
|
| 137 |
+
return "📭 No memories stored yet."
|
| 138 |
+
|
| 139 |
+
# Get all memories
|
| 140 |
+
results = self.collection.get()
|
| 141 |
+
|
| 142 |
+
if not results['documents']:
|
| 143 |
+
return "📭 No memories found."
|
| 144 |
+
|
| 145 |
+
response = f"📚 **All Memories** (showing up to {limit})\n\n"
|
| 146 |
+
|
| 147 |
+
# Sort by timestamp (newest first)
|
| 148 |
+
memories = list(zip(results['ids'], results['documents'], results['metadatas']))
|
| 149 |
+
memories.sort(key=lambda x: x[2]['timestamp'], reverse=True)
|
| 150 |
+
|
| 151 |
+
for i, (memory_id, doc, metadata) in enumerate(memories[:limit]):
|
| 152 |
+
tags = json.loads(metadata.get('tags', '[]'))
|
| 153 |
+
|
| 154 |
+
response += f"### {i+1}. {metadata['title']}\n"
|
| 155 |
+
response += f"**ID**: `{memory_id}`\n"
|
| 156 |
+
response += f"**Saved**: {metadata['timestamp']}\n"
|
| 157 |
+
if tags:
|
| 158 |
+
response += f"**Tags**: {', '.join(tags)}\n"
|
| 159 |
+
response += f"**Preview**: {doc[:150]}{'...' if len(doc) > 150 else ''}\n\n"
|
| 160 |
+
response += "---\n\n"
|
| 161 |
+
|
| 162 |
+
if len(memories) > limit:
|
| 163 |
+
response += f"... and {len(memories) - limit} more memories"
|
| 164 |
+
|
| 165 |
+
return response
|
| 166 |
+
except Exception as e:
|
| 167 |
+
return f"❌ Error listing memories: {str(e)}"
|
| 168 |
+
|
| 169 |
+
def get_memory_stats(self) -> str:
|
| 170 |
+
"""Get statistics about stored memories."""
|
| 171 |
+
try:
|
| 172 |
+
count = self.collection.count()
|
| 173 |
+
if count == 0:
|
| 174 |
+
return "📊 **Memory Statistics**: No memories stored yet."
|
| 175 |
+
|
| 176 |
+
results = self.collection.get()
|
| 177 |
+
|
| 178 |
+
# Calculate stats
|
| 179 |
+
total_content_length = sum(metadata['content_length'] for metadata in results['metadatas'])
|
| 180 |
+
avg_content_length = total_content_length / count if count > 0 else 0
|
| 181 |
+
|
| 182 |
+
# Get all tags
|
| 183 |
+
all_tags = []
|
| 184 |
+
for metadata in results['metadatas']:
|
| 185 |
+
tags = json.loads(metadata.get('tags', '[]'))
|
| 186 |
+
all_tags.extend(tags)
|
| 187 |
+
|
| 188 |
+
unique_tags = list(set(all_tags))
|
| 189 |
+
|
| 190 |
+
stats = f"📊 **Memory Statistics**\n\n"
|
| 191 |
+
stats += f"**Total Memories**: {count}\n"
|
| 192 |
+
stats += f"**Total Content Length**: {total_content_length:,} characters\n"
|
| 193 |
+
stats += f"**Average Content Length**: {avg_content_length:.0f} characters\n"
|
| 194 |
+
stats += f"**Unique Tags**: {len(unique_tags)}\n"
|
| 195 |
+
if unique_tags:
|
| 196 |
+
stats += f"**Tags**: {', '.join(unique_tags[:10])}{'...' if len(unique_tags) > 10 else ''}\n"
|
| 197 |
+
|
| 198 |
+
return stats
|
| 199 |
+
except Exception as e:
|
| 200 |
+
return f"❌ Error getting statistics: {str(e)}"
|
| 201 |
+
|
| 202 |
+
# Initialize the demo
|
| 203 |
+
print("Initializing Long Term Memory Demo...")
|
| 204 |
+
ltm_demo = LongTermMemoryDemo()
|
| 205 |
+
|
| 206 |
+
# Create Gradio interface
|
| 207 |
+
with gr.Blocks(title="Long Term Memory MCP Server Demo", theme=gr.themes.Soft()) as demo:
|
| 208 |
+
gr.Markdown("""
|
| 209 |
+
# 🧠 Long Term Memory MCP Server Demo
|
| 210 |
+
|
| 211 |
+
This is a demonstration of an MCP (Model Context Protocol) Server that provides long-term memory capabilities for LLM conversations.
|
| 212 |
+
|
| 213 |
+
## Features:
|
| 214 |
+
- 💾 **Save Memory**: Store important insights, conclusions, or context
|
| 215 |
+
- 🔍 **Search Memory**: Find relevant information using semantic search
|
| 216 |
+
- 📚 **List Memories**: Browse all stored memories
|
| 217 |
+
- 📊 **Statistics**: View memory usage statistics
|
| 218 |
+
|
| 219 |
+
## How it works:
|
| 220 |
+
1. **Embeddings**: Uses SentenceTransformers to create semantic embeddings
|
| 221 |
+
2. **Vector Storage**: ChromaDB for efficient similarity search
|
| 222 |
+
3. **MCP Protocol**: Exposes tools that any MCP-compatible client can use
|
| 223 |
+
""")
|
| 224 |
+
|
| 225 |
+
with gr.Tabs():
|
| 226 |
+
# Save Memory Tab
|
| 227 |
+
with gr.Tab("💾 Save Memory"):
|
| 228 |
+
gr.Markdown("### Save important insights or context to long-term memory")
|
| 229 |
+
|
| 230 |
+
with gr.Row():
|
| 231 |
+
with gr.Column():
|
| 232 |
+
save_title = gr.Textbox(
|
| 233 |
+
label="Title",
|
| 234 |
+
placeholder="Brief title for this memory...",
|
| 235 |
+
lines=1
|
| 236 |
+
)
|
| 237 |
+
save_content = gr.Textbox(
|
| 238 |
+
label="Content",
|
| 239 |
+
placeholder="The insight, conclusion, or context you want to remember...",
|
| 240 |
+
lines=5
|
| 241 |
+
)
|
| 242 |
+
save_tags = gr.Textbox(
|
| 243 |
+
label="Tags (optional)",
|
| 244 |
+
placeholder="quantum physics, consciousness, philosophy",
|
| 245 |
+
lines=1
|
| 246 |
+
)
|
| 247 |
+
save_context = gr.Textbox(
|
| 248 |
+
label="Context (optional)",
|
| 249 |
+
placeholder="Why is this important? When was it discussed?",
|
| 250 |
+
lines=2
|
| 251 |
+
)
|
| 252 |
+
save_btn = gr.Button("💾 Save Memory", variant="primary")
|
| 253 |
+
|
| 254 |
+
with gr.Column():
|
| 255 |
+
save_output = gr.Markdown()
|
| 256 |
+
|
| 257 |
+
save_btn.click(
|
| 258 |
+
ltm_demo.save_memory,
|
| 259 |
+
inputs=[save_content, save_title, save_tags, save_context],
|
| 260 |
+
outputs=[save_output]
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Search Memory Tab
|
| 264 |
+
with gr.Tab("🔍 Search Memory"):
|
| 265 |
+
gr.Markdown("### Search through your memories using semantic similarity")
|
| 266 |
+
|
| 267 |
+
with gr.Row():
|
| 268 |
+
with gr.Column():
|
| 269 |
+
search_query = gr.Textbox(
|
| 270 |
+
label="Search Query",
|
| 271 |
+
placeholder="quantum consciousness, reality nature, philosophical insights...",
|
| 272 |
+
lines=2
|
| 273 |
+
)
|
| 274 |
+
with gr.Row():
|
| 275 |
+
search_limit = gr.Slider(
|
| 276 |
+
label="Max Results",
|
| 277 |
+
minimum=1,
|
| 278 |
+
maximum=20,
|
| 279 |
+
value=5,
|
| 280 |
+
step=1
|
| 281 |
+
)
|
| 282 |
+
search_threshold = gr.Slider(
|
| 283 |
+
label="Similarity Threshold",
|
| 284 |
+
minimum=0.0,
|
| 285 |
+
maximum=1.0,
|
| 286 |
+
value=0.3,
|
| 287 |
+
step=0.05
|
| 288 |
+
)
|
| 289 |
+
search_btn = gr.Button("🔍 Search Memories", variant="primary")
|
| 290 |
+
|
| 291 |
+
with gr.Column():
|
| 292 |
+
search_output = gr.Markdown()
|
| 293 |
+
|
| 294 |
+
search_btn.click(
|
| 295 |
+
ltm_demo.search_memory,
|
| 296 |
+
inputs=[search_query, search_limit, search_threshold],
|
| 297 |
+
outputs=[search_output]
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# List Memories Tab
|
| 301 |
+
with gr.Tab("📚 Browse Memories"):
|
| 302 |
+
gr.Markdown("### Browse all stored memories")
|
| 303 |
+
|
| 304 |
+
with gr.Row():
|
| 305 |
+
with gr.Column(scale=1):
|
| 306 |
+
list_limit = gr.Slider(
|
| 307 |
+
label="Number of memories to show",
|
| 308 |
+
minimum=5,
|
| 309 |
+
maximum=50,
|
| 310 |
+
value=10,
|
| 311 |
+
step=5
|
| 312 |
+
)
|
| 313 |
+
list_btn = gr.Button("📚 List Memories", variant="primary")
|
| 314 |
+
stats_btn = gr.Button("📊 Show Statistics", variant="secondary")
|
| 315 |
+
|
| 316 |
+
with gr.Column(scale=3):
|
| 317 |
+
list_output = gr.Markdown()
|
| 318 |
+
|
| 319 |
+
list_btn.click(
|
| 320 |
+
ltm_demo.list_memories,
|
| 321 |
+
inputs=[list_limit],
|
| 322 |
+
outputs=[list_output]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
stats_btn.click(
|
| 326 |
+
ltm_demo.get_memory_stats,
|
| 327 |
+
outputs=[list_output]
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
gr.Markdown("""
|
| 331 |
+
---
|
| 332 |
+
|
| 333 |
+
## 🔧 MCP Server Usage
|
| 334 |
+
|
| 335 |
+
This Gradio app is also an MCP Server! You can connect to it from MCP-compatible clients like:
|
| 336 |
+
- Claude Desktop
|
| 337 |
+
- Cursor IDE
|
| 338 |
+
- Other MCP clients
|
| 339 |
+
|
| 340 |
+
### Available MCP Tools:
|
| 341 |
+
- `save_memory` - Save content to long-term memory
|
| 342 |
+
- `search_memory` - Search through memories
|
| 343 |
+
- `list_memories` - List all memories
|
| 344 |
+
- `delete_memory` - Delete a specific memory
|
| 345 |
+
|
| 346 |
+
### Example Usage in Claude Desktop:
|
| 347 |
+
```
|
| 348 |
+
"Save this insight to memory: 'Consciousness might be a quantum phenomenon
|
| 349 |
+
that emerges from the collapse of wave functions in microtubules.'
|
| 350 |
+
Title: 'Quantum Consciousness Theory', Tags: 'quantum, consciousness, microtubules'"
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
Then later:
|
| 354 |
+
```
|
| 355 |
+
"Search my memories for information about consciousness and quantum physics"
|
| 356 |
+
```
|
| 357 |
+
""")
|
| 358 |
+
|
| 359 |
+
print("Gradio interface created successfully!")
|
langchain_memory_tools.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
LangChain tools for Long Term Memory integration with Ollama
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from langchain.tools import tool
|
| 7 |
+
from langchain_ollama import OllamaLLM
|
| 8 |
+
from langchain.agents import create_react_agent, AgentExecutor
|
| 9 |
+
from langchain import hub
|
| 10 |
+
from typing import Optional, Dict, Any, List
|
| 11 |
+
import requests
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
# Import your existing LTM demo class
|
| 15 |
+
from gradio_demo import LongTermMemoryDemo
|
| 16 |
+
|
| 17 |
+
# Initialize shared memory instance
|
| 18 |
+
ltm = LongTermMemoryDemo()
|
| 19 |
+
|
| 20 |
+
@tool
|
| 21 |
+
def save_memory(content: str, title: str, tags: str = "", context: str = "") -> str:
|
| 22 |
+
"""
|
| 23 |
+
Save important insights, conclusions, or context to long-term memory.
|
| 24 |
+
Use this to remember key information from conversations that might be useful later.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
content: The insight or information to save
|
| 28 |
+
title: A brief descriptive title
|
| 29 |
+
tags: Optional comma-separated tags
|
| 30 |
+
context: Optional additional context
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
return ltm.save_memory(content, title, tags, context)
|
| 34 |
+
except Exception as e:
|
| 35 |
+
return f"Error saving memory: {str(e)}"
|
| 36 |
+
|
| 37 |
+
@tool
|
| 38 |
+
def search_memory(query: str, limit: int = 5, threshold: float = 0.3) -> str:
|
| 39 |
+
"""
|
| 40 |
+
Search through long-term memory for relevant information.
|
| 41 |
+
Use this to find previously saved insights or context related to current discussion.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
query: What to search for
|
| 45 |
+
limit: Max number of results (default: 5)
|
| 46 |
+
threshold: Similarity threshold 0-1 (default: 0.3)
|
| 47 |
+
"""
|
| 48 |
+
try:
|
| 49 |
+
return ltm.search_memory(query, limit, threshold)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return f"Error searching memory: {str(e)}"
|
| 52 |
+
|
| 53 |
+
@tool
|
| 54 |
+
def list_memories(limit: int = 10) -> str:
|
| 55 |
+
"""
|
| 56 |
+
List all stored memories to see what information is available.
|
| 57 |
+
Useful for getting an overview of stored knowledge.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
limit: Maximum number of memories to show (default: 10)
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
return ltm.list_memories(limit)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
return f"Error listing memories: {str(e)}"
|
| 66 |
+
|
| 67 |
+
@tool
|
| 68 |
+
def memory_stats() -> str:
|
| 69 |
+
"""
|
| 70 |
+
Get statistics about stored memories.
|
| 71 |
+
Shows total count, tags, and other metadata.
|
| 72 |
+
"""
|
| 73 |
+
try:
|
| 74 |
+
return ltm.get_memory_stats()
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return f"Error getting stats: {str(e)}"
|
| 77 |
+
|
| 78 |
+
# Example usage with Ollama
|
| 79 |
+
def create_memory_enabled_agent(model_name: str = "llama3.2"):
|
| 80 |
+
"""Create a LangChain agent with memory capabilities"""
|
| 81 |
+
|
| 82 |
+
# Initialize Ollama LLM
|
| 83 |
+
llm = OllamaLLM(model=model_name)
|
| 84 |
+
|
| 85 |
+
# Create tools list
|
| 86 |
+
tools = [save_memory, search_memory, list_memories, memory_stats]
|
| 87 |
+
|
| 88 |
+
# Get the react prompt from hub
|
| 89 |
+
try:
|
| 90 |
+
prompt = hub.pull("hwchase17/react")
|
| 91 |
+
except:
|
| 92 |
+
# Fallback prompt if hub is not available
|
| 93 |
+
from langchain.prompts import PromptTemplate
|
| 94 |
+
|
| 95 |
+
template = """Answer the following questions as best you can. You have access to the following tools:
|
| 96 |
+
|
| 97 |
+
{tools}
|
| 98 |
+
|
| 99 |
+
Use the following format:
|
| 100 |
+
|
| 101 |
+
Question: the input question you must answer
|
| 102 |
+
Thought: you should always think about what to do
|
| 103 |
+
Action: the action to take, should be one of [{tool_names}]
|
| 104 |
+
Action Input: the input to the action
|
| 105 |
+
Observation: the result of the action
|
| 106 |
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
| 107 |
+
Thought: I now know the final answer
|
| 108 |
+
Final Answer: the final answer to the original input question
|
| 109 |
+
|
| 110 |
+
Begin!
|
| 111 |
+
|
| 112 |
+
Question: {input}
|
| 113 |
+
Thought:{agent_scratchpad}"""
|
| 114 |
+
|
| 115 |
+
prompt = PromptTemplate.from_template(template)
|
| 116 |
+
|
| 117 |
+
# Create agent
|
| 118 |
+
agent = create_react_agent(llm, tools, prompt)
|
| 119 |
+
|
| 120 |
+
# Create agent executor
|
| 121 |
+
agent_executor = AgentExecutor(
|
| 122 |
+
agent=agent,
|
| 123 |
+
tools=tools,
|
| 124 |
+
verbose=True,
|
| 125 |
+
handle_parsing_errors=True,
|
| 126 |
+
max_iterations=10
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
return agent_executor
|
| 130 |
+
|
| 131 |
+
# Example conversation loop
|
| 132 |
+
def main():
|
| 133 |
+
"""Example usage"""
|
| 134 |
+
print("🧠 Initializing Memory-Enabled Agent with Ollama...")
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
agent = create_memory_enabled_agent("llama3.2") # или любая другая модель в Ollama
|
| 138 |
+
|
| 139 |
+
print("✅ Agent ready! Type 'quit' to exit.")
|
| 140 |
+
print("💡 Try commands like:")
|
| 141 |
+
print(" - 'Save this insight: quantum computers might revolutionize AI with title Quantum AI and tags quantum,ai,future' ")
|
| 142 |
+
print(" - 'Search my memories for information about quantum computing'")
|
| 143 |
+
print(" - 'What memories do I have stored?'")
|
| 144 |
+
print(" - 'Show me memory statistics'")
|
| 145 |
+
print()
|
| 146 |
+
|
| 147 |
+
while True:
|
| 148 |
+
try:
|
| 149 |
+
user_input = input("You: ").strip()
|
| 150 |
+
|
| 151 |
+
if user_input.lower() in ['quit', 'exit', 'bye']:
|
| 152 |
+
print("Goodbye!")
|
| 153 |
+
break
|
| 154 |
+
|
| 155 |
+
if not user_input:
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
# Run the agent
|
| 159 |
+
response = agent.invoke({"input": user_input})
|
| 160 |
+
print(f"Agent: {response['output']}")
|
| 161 |
+
print()
|
| 162 |
+
|
| 163 |
+
except KeyboardInterrupt:
|
| 164 |
+
print("\nGoodbye!")
|
| 165 |
+
break
|
| 166 |
+
except Exception as e:
|
| 167 |
+
print(f"Error: {e}")
|
| 168 |
+
continue
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"Failed to initialize agent: {e}")
|
| 172 |
+
print("Make sure Ollama is running and the model is available.")
|
| 173 |
+
print("Try: ollama pull llama3.2")
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|
local_run.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Local runner for Memory-Enabled LangChain Agent
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from langchain_memory_tools import create_memory_enabled_agent
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
print("🧠 Starting Memory-Enabled Chat with Ollama...")
|
| 11 |
+
|
| 12 |
+
# Убедитесь, что Ollama запущен и модель доступна
|
| 13 |
+
model_name = "llama3.2:3b" # или другая модель в вашем Ollama
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
agent = create_memory_enabled_agent(model_name)
|
| 17 |
+
|
| 18 |
+
print(f"✅ Agent with model '{model_name}' ready!")
|
| 19 |
+
print("💡 Example commands:")
|
| 20 |
+
print(
|
| 21 |
+
" - Save this insight: 'Quantum computers use qubits' with title 'Quantum Computing Basics' and tags 'quantum,computing,physics'")
|
| 22 |
+
print(" - Search my memories for quantum")
|
| 23 |
+
print(" - List all my memories")
|
| 24 |
+
print(" - Show memory statistics")
|
| 25 |
+
print(" - Type 'quit' to exit")
|
| 26 |
+
print()
|
| 27 |
+
|
| 28 |
+
while True:
|
| 29 |
+
try:
|
| 30 |
+
user_input = input("You: ").strip()
|
| 31 |
+
|
| 32 |
+
if user_input.lower() in ['quit', 'exit', 'bye']:
|
| 33 |
+
print("👋 Goodbye!")
|
| 34 |
+
break
|
| 35 |
+
|
| 36 |
+
if not user_input:
|
| 37 |
+
continue
|
| 38 |
+
|
| 39 |
+
print("🤔 Thinking...")
|
| 40 |
+
response = agent.invoke({"input": user_input})
|
| 41 |
+
print(f"🤖 Agent: {response['output']}")
|
| 42 |
+
print("-" * 50)
|
| 43 |
+
|
| 44 |
+
except KeyboardInterrupt:
|
| 45 |
+
print("\n👋 Goodbye!")
|
| 46 |
+
break
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"❌ Error: {e}")
|
| 49 |
+
print("Continuing...")
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"❌ Failed to initialize agent: {e}")
|
| 54 |
+
print("\n🔧 Troubleshooting:")
|
| 55 |
+
print("1. Make sure Ollama is running: ollama serve")
|
| 56 |
+
print(f"2. Make sure model is available: ollama pull {model_name}")
|
| 57 |
+
print("3. Check if Ollama is accessible at http://localhost:11434")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
main()
|
ltm_mcp_server.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Any, Dict, List, Optional, Sequence
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
import chromadb
|
| 10 |
+
from chromadb.config import Settings
|
| 11 |
+
import numpy as np
|
| 12 |
+
from sentence_transformers import SentenceTransformer
|
| 13 |
+
|
| 14 |
+
# MCP imports
|
| 15 |
+
try:
|
| 16 |
+
from mcp.server import Server
|
| 17 |
+
from mcp.server.models import InitializationOptions
|
| 18 |
+
from mcp.server.stdio import stdio_server
|
| 19 |
+
from mcp.types import (
|
| 20 |
+
Resource,
|
| 21 |
+
Tool,
|
| 22 |
+
TextContent,
|
| 23 |
+
ImageContent,
|
| 24 |
+
EmbeddedResource,
|
| 25 |
+
LoggingLevel
|
| 26 |
+
)
|
| 27 |
+
MCP_AVAILABLE = True
|
| 28 |
+
except ImportError:
|
| 29 |
+
print("MCP library not available, running in demo mode only")
|
| 30 |
+
MCP_AVAILABLE = False
|
| 31 |
+
|
| 32 |
+
# Mock classes for demo mode
|
| 33 |
+
class Server:
|
| 34 |
+
def __init__(self, name):
|
| 35 |
+
self.name = name
|
| 36 |
+
def list_tools(self): return lambda: None
|
| 37 |
+
def call_tool(self): return lambda: None
|
| 38 |
+
|
| 39 |
+
class TextContent:
|
| 40 |
+
def __init__(self, type, text):
|
| 41 |
+
self.type = type
|
| 42 |
+
self.text = text
|
| 43 |
+
|
| 44 |
+
# Configure logging
|
| 45 |
+
logging.basicConfig(level=logging.INFO)
|
| 46 |
+
logger = logging.getLogger("ltm-mcp-server")
|
| 47 |
+
|
| 48 |
+
class LongTermMemoryServer:
|
| 49 |
+
def __init__(self):
|
| 50 |
+
self.server = Server("long-term-memory")
|
| 51 |
+
self.db_path = "./memory_db"
|
| 52 |
+
Path(self.db_path).mkdir(exist_ok=True)
|
| 53 |
+
|
| 54 |
+
# Initialize ChromaDB
|
| 55 |
+
self.chroma_client = chromadb.PersistentClient(
|
| 56 |
+
path=self.db_path,
|
| 57 |
+
settings=Settings(anonymized_telemetry=False)
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Get or create collection
|
| 61 |
+
try:
|
| 62 |
+
self.collection = self.chroma_client.get_collection("memories")
|
| 63 |
+
except:
|
| 64 |
+
self.collection = self.chroma_client.create_collection(
|
| 65 |
+
name="memories",
|
| 66 |
+
metadata={"description": "Long-term memory storage for conversations"}
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Initialize sentence transformer for embeddings
|
| 70 |
+
self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
|
| 71 |
+
|
| 72 |
+
self.setup_handlers()
|
| 73 |
+
|
| 74 |
+
def setup_handlers(self):
|
| 75 |
+
@self.server.list_tools()
|
| 76 |
+
async def handle_list_tools() -> List[Tool]:
|
| 77 |
+
"""List available tools."""
|
| 78 |
+
return [
|
| 79 |
+
Tool(
|
| 80 |
+
name="save_memory",
|
| 81 |
+
description="Save important insights, conclusions, or context from conversation to long-term memory",
|
| 82 |
+
inputSchema={
|
| 83 |
+
"type": "object",
|
| 84 |
+
"properties": {
|
| 85 |
+
"content": {
|
| 86 |
+
"type": "string",
|
| 87 |
+
"description": "The content/insight to save to memory"
|
| 88 |
+
},
|
| 89 |
+
"title": {
|
| 90 |
+
"type": "string",
|
| 91 |
+
"description": "A short title/summary for this memory"
|
| 92 |
+
},
|
| 93 |
+
"tags": {
|
| 94 |
+
"type": "array",
|
| 95 |
+
"items": {"type": "string"},
|
| 96 |
+
"description": "Optional tags to categorize this memory",
|
| 97 |
+
"default": []
|
| 98 |
+
},
|
| 99 |
+
"context": {
|
| 100 |
+
"type": "string",
|
| 101 |
+
"description": "Additional context about when/why this was saved",
|
| 102 |
+
"default": ""
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"required": ["content", "title"]
|
| 106 |
+
}
|
| 107 |
+
),
|
| 108 |
+
Tool(
|
| 109 |
+
name="search_memory",
|
| 110 |
+
description="Search through long-term memory for relevant information",
|
| 111 |
+
inputSchema={
|
| 112 |
+
"type": "object",
|
| 113 |
+
"properties": {
|
| 114 |
+
"query": {
|
| 115 |
+
"type": "string",
|
| 116 |
+
"description": "Search query to find relevant memories"
|
| 117 |
+
},
|
| 118 |
+
"limit": {
|
| 119 |
+
"type": "integer",
|
| 120 |
+
"description": "Maximum number of results to return",
|
| 121 |
+
"default": 5
|
| 122 |
+
},
|
| 123 |
+
"threshold": {
|
| 124 |
+
"type": "number",
|
| 125 |
+
"description": "Similarity threshold (0-1, higher = more similar)",
|
| 126 |
+
"default": 0.3
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"required": ["query"]
|
| 130 |
+
}
|
| 131 |
+
),
|
| 132 |
+
Tool(
|
| 133 |
+
name="list_memories",
|
| 134 |
+
description="List all stored memories with basic info",
|
| 135 |
+
inputSchema={
|
| 136 |
+
"type": "object",
|
| 137 |
+
"properties": {
|
| 138 |
+
"limit": {
|
| 139 |
+
"type": "integer",
|
| 140 |
+
"description": "Maximum number of memories to return",
|
| 141 |
+
"default": 10
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
),
|
| 146 |
+
Tool(
|
| 147 |
+
name="delete_memory",
|
| 148 |
+
description="Delete a specific memory by ID",
|
| 149 |
+
inputSchema={
|
| 150 |
+
"type": "object",
|
| 151 |
+
"properties": {
|
| 152 |
+
"memory_id": {
|
| 153 |
+
"type": "string",
|
| 154 |
+
"description": "The ID of the memory to delete"
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
"required": ["memory_id"]
|
| 158 |
+
}
|
| 159 |
+
)
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
@self.server.call_tool()
|
| 163 |
+
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> Sequence[TextContent]:
|
| 164 |
+
"""Handle tool calls."""
|
| 165 |
+
try:
|
| 166 |
+
if name == "save_memory":
|
| 167 |
+
return await self._save_memory(**arguments)
|
| 168 |
+
elif name == "search_memory":
|
| 169 |
+
return await self._search_memory(**arguments)
|
| 170 |
+
elif name == "list_memories":
|
| 171 |
+
return await self._list_memories(**arguments)
|
| 172 |
+
elif name == "delete_memory":
|
| 173 |
+
return await self._delete_memory(**arguments)
|
| 174 |
+
else:
|
| 175 |
+
raise ValueError(f"Unknown tool: {name}")
|
| 176 |
+
except Exception as e:
|
| 177 |
+
logger.error(f"Error in tool {name}: {e}")
|
| 178 |
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
| 179 |
+
|
| 180 |
+
async def _save_memory(self, content: str, title: str, tags: List[str] = None, context: str = "") -> Sequence[TextContent]:
|
| 181 |
+
"""Save content to long-term memory."""
|
| 182 |
+
if tags is None:
|
| 183 |
+
tags = []
|
| 184 |
+
|
| 185 |
+
memory_id = str(uuid.uuid4())
|
| 186 |
+
timestamp = datetime.now().isoformat()
|
| 187 |
+
|
| 188 |
+
# Create embedding
|
| 189 |
+
embedding = self.encoder.encode(f"{title} {content}").tolist()
|
| 190 |
+
|
| 191 |
+
# Prepare metadata
|
| 192 |
+
metadata = {
|
| 193 |
+
"title": title,
|
| 194 |
+
"timestamp": timestamp,
|
| 195 |
+
"tags": json.dumps(tags),
|
| 196 |
+
"context": context,
|
| 197 |
+
"content_length": len(content)
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# Save to ChromaDB
|
| 201 |
+
self.collection.add(
|
| 202 |
+
documents=[content],
|
| 203 |
+
embeddings=[embedding],
|
| 204 |
+
metadatas=[metadata],
|
| 205 |
+
ids=[memory_id]
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
logger.info(f"Saved memory: {title} (ID: {memory_id})")
|
| 209 |
+
|
| 210 |
+
result = f"✅ Memory saved successfully!\n\n"
|
| 211 |
+
result += f"**ID**: {memory_id}\n"
|
| 212 |
+
result += f"**Title**: {title}\n"
|
| 213 |
+
result += f"**Timestamp**: {timestamp}\n"
|
| 214 |
+
if tags:
|
| 215 |
+
result += f"**Tags**: {', '.join(tags)}\n"
|
| 216 |
+
if context:
|
| 217 |
+
result += f"**Context**: {context}\n"
|
| 218 |
+
result += f"**Content Preview**: {content[:200]}{'...' if len(content) > 200 else ''}"
|
| 219 |
+
|
| 220 |
+
return [TextContent(type="text", text=result)]
|
| 221 |
+
|
| 222 |
+
async def _search_memory(self, query: str, limit: int = 5, threshold: float = 0.3) -> Sequence[TextContent]:
|
| 223 |
+
"""Search through memories."""
|
| 224 |
+
if self.collection.count() == 0:
|
| 225 |
+
return [TextContent(type="text", text="No memories stored yet.")]
|
| 226 |
+
|
| 227 |
+
# Create query embedding
|
| 228 |
+
query_embedding = self.encoder.encode(query).tolist()
|
| 229 |
+
|
| 230 |
+
# Search in ChromaDB
|
| 231 |
+
results = self.collection.query(
|
| 232 |
+
query_embeddings=[query_embedding],
|
| 233 |
+
n_results=min(limit, self.collection.count())
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
if not results['documents'][0]:
|
| 237 |
+
return [TextContent(type="text", text="No relevant memories found.")]
|
| 238 |
+
|
| 239 |
+
# Filter by threshold and format results
|
| 240 |
+
response = f"🔍 **Search Results for**: \"{query}\"\n\n"
|
| 241 |
+
|
| 242 |
+
found_relevant = False
|
| 243 |
+
for i, (doc, metadata, distance) in enumerate(zip(
|
| 244 |
+
results['documents'][0],
|
| 245 |
+
results['metadatas'][0],
|
| 246 |
+
results['distances'][0]
|
| 247 |
+
)):
|
| 248 |
+
similarity = 1 - distance
|
| 249 |
+
if similarity >= threshold:
|
| 250 |
+
found_relevant = True
|
| 251 |
+
tags = json.loads(metadata.get('tags', '[]'))
|
| 252 |
+
|
| 253 |
+
response += f"**{i+1}. {metadata['title']}** (Similarity: {similarity:.2f})\n"
|
| 254 |
+
response += f"*Saved*: {metadata['timestamp']}\n"
|
| 255 |
+
if tags:
|
| 256 |
+
response += f"*Tags*: {', '.join(tags)}\n"
|
| 257 |
+
if metadata.get('context'):
|
| 258 |
+
response += f"*Context*: {metadata['context']}\n"
|
| 259 |
+
response += f"*Content*: {doc}\n\n"
|
| 260 |
+
response += "---\n\n"
|
| 261 |
+
|
| 262 |
+
if not found_relevant:
|
| 263 |
+
response += f"No memories found above similarity threshold of {threshold:.2f}"
|
| 264 |
+
|
| 265 |
+
return [TextContent(type="text", text=response)]
|
| 266 |
+
|
| 267 |
+
async def _list_memories(self, limit: int = 10) -> Sequence[TextContent]:
|
| 268 |
+
"""List all memories."""
|
| 269 |
+
if self.collection.count() == 0:
|
| 270 |
+
return [TextContent(type="text", text="No memories stored yet.")]
|
| 271 |
+
|
| 272 |
+
# Get all memories (ChromaDB doesn't have a direct "get all" with limit)
|
| 273 |
+
results = self.collection.get()
|
| 274 |
+
|
| 275 |
+
if not results['documents']:
|
| 276 |
+
return [TextContent(type="text", text="No memories found.")]
|
| 277 |
+
|
| 278 |
+
response = f"📚 **All Memories** (showing up to {limit})\n\n"
|
| 279 |
+
|
| 280 |
+
# Sort by timestamp (newest first)
|
| 281 |
+
memories = list(zip(results['ids'], results['documents'], results['metadatas']))
|
| 282 |
+
memories.sort(key=lambda x: x[2]['timestamp'], reverse=True)
|
| 283 |
+
|
| 284 |
+
for i, (memory_id, doc, metadata) in enumerate(memories[:limit]):
|
| 285 |
+
tags = json.loads(metadata.get('tags', '[]'))
|
| 286 |
+
|
| 287 |
+
response += f"**{i+1}. {metadata['title']}**\n"
|
| 288 |
+
response += f"*ID*: {memory_id}\n"
|
| 289 |
+
response += f"*Saved*: {metadata['timestamp']}\n"
|
| 290 |
+
if tags:
|
| 291 |
+
response += f"*Tags*: {', '.join(tags)}\n"
|
| 292 |
+
response += f"*Preview*: {doc[:150]}{'...' if len(doc) > 150 else ''}\n\n"
|
| 293 |
+
response += "---\n\n"
|
| 294 |
+
|
| 295 |
+
if len(memories) > limit:
|
| 296 |
+
response += f"... and {len(memories) - limit} more memories"
|
| 297 |
+
|
| 298 |
+
return [TextContent(type="text", text=response)]
|
| 299 |
+
|
| 300 |
+
async def _delete_memory(self, memory_id: str) -> Sequence[TextContent]:
|
| 301 |
+
"""Delete a memory by ID."""
|
| 302 |
+
try:
|
| 303 |
+
# Check if memory exists
|
| 304 |
+
result = self.collection.get(ids=[memory_id])
|
| 305 |
+
if not result['documents']:
|
| 306 |
+
return [TextContent(type="text", text=f"❌ Memory with ID {memory_id} not found.")]
|
| 307 |
+
|
| 308 |
+
# Get memory info before deletion
|
| 309 |
+
metadata = result['metadatas'][0]
|
| 310 |
+
title = metadata['title']
|
| 311 |
+
|
| 312 |
+
# Delete from ChromaDB
|
| 313 |
+
self.collection.delete(ids=[memory_id])
|
| 314 |
+
|
| 315 |
+
logger.info(f"Deleted memory: {title} (ID: {memory_id})")
|
| 316 |
+
|
| 317 |
+
return [TextContent(type="text", text=f"✅ Memory deleted successfully!\n\n**Title**: {title}\n**ID**: {memory_id}")]
|
| 318 |
+
|
| 319 |
+
except Exception as e:
|
| 320 |
+
logger.error(f"Error deleting memory {memory_id}: {e}")
|
| 321 |
+
return [TextContent(type="text", text=f"❌ Error deleting memory: {str(e)}")]
|
| 322 |
+
|
| 323 |
+
async def run(self):
|
| 324 |
+
"""Run the MCP server."""
|
| 325 |
+
if not MCP_AVAILABLE:
|
| 326 |
+
print("MCP not available, cannot run server")
|
| 327 |
+
return
|
| 328 |
+
|
| 329 |
+
async with stdio_server() as (read_stream, write_stream):
|
| 330 |
+
await self.server.run(
|
| 331 |
+
read_stream,
|
| 332 |
+
write_stream,
|
| 333 |
+
InitializationOptions(
|
| 334 |
+
server_name="long-term-memory",
|
| 335 |
+
server_version="1.0.0",
|
| 336 |
+
capabilities=self.server.get_capabilities(
|
| 337 |
+
notification_options=None,
|
| 338 |
+
experimental_capabilities=None,
|
| 339 |
+
),
|
| 340 |
+
),
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Main entry point
|
| 344 |
+
async def main():
|
| 345 |
+
if not MCP_AVAILABLE:
|
| 346 |
+
print("MCP server cannot run without MCP library")
|
| 347 |
+
return
|
| 348 |
+
|
| 349 |
+
server = LongTermMemoryServer()
|
| 350 |
+
await server.run()
|
| 351 |
+
|
| 352 |
+
if __name__ == "__main__":
|
| 353 |
+
asyncio.run(main())
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Existing dependencies
|
| 2 |
+
gradio>=4.0.0
|
| 3 |
+
mcp>=1.0.0
|
| 4 |
+
chromadb>=0.4.0
|
| 5 |
+
sentence-transformers>=2.2.0
|
| 6 |
+
numpy>=1.24.0
|
| 7 |
+
asyncio-mqtt>=0.11.0
|
| 8 |
+
pydantic>=2.0.0
|
| 9 |
+
typing-extensions>=4.5.0
|
| 10 |
+
|
| 11 |
+
# New dependencies for Ollama integration
|
| 12 |
+
fastapi>=0.104.0
|
| 13 |
+
uvicorn>=0.24.0
|
| 14 |
+
langchain>=0.3.0
|
| 15 |
+
langchain-ollama>=0.2.0
|
| 16 |
+
langchain-community>=0.3.0
|
| 17 |
+
langchainhub>=0.1.0
|
| 18 |
+
requests>=2.31.0
|
run_mcp_server.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Standalone MCP Server runner
|
| 4 |
+
Use this script to run only the MCP server without Gradio
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import asyncio
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Add current directory to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
from mcp_server import main
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
print("🧠 Starting Long Term Memory MCP Server...")
|
| 18 |
+
asyncio.run(main())
|