Spaces:
Running
Running
eldarski
commited on
Commit
Β·
168b0da
0
Parent(s):
π₯ Memvid MCP Server - Hackathon Submission - Complete MCP server with 24 tools for video-based AI memory storage - Dual storage with Modal GPU acceleration - Ready for Agents-MCP-Hackathon Track 1
Browse files- .gitignore +8 -0
- README.md +134 -0
- app.py +1085 -0
- modal_memvid_service.py +612 -0
- modal_vector_service.py +512 -0
- requirements.txt +39 -0
- setup_postgres.py +239 -0
- utils/dual_storage_manager.py +481 -0
- utils/fingerprint_manager.py +361 -0
- utils/memvid_manager.py +523 -0
- utils/metrics_collector.py +406 -0
- utils/storage_handler.py +449 -0
- utils/vector_storage_manager.py +463 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pyc
|
| 2 |
+
__pycache__/
|
| 3 |
+
.env
|
| 4 |
+
venv*/
|
| 5 |
+
.DS_Store
|
| 6 |
+
data/
|
| 7 |
+
logs/
|
| 8 |
+
test_data/
|
README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: π₯ Memvid MCP Server - Video-based AI Memory Storage
|
| 3 |
+
emoji: π₯
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "5.31.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: Advanced MCP server storing AI memories in MP4 videos with QR codes and semantic search
|
| 12 |
+
models:
|
| 13 |
+
- sentence-transformers/all-MiniLM-L6-v2
|
| 14 |
+
tags:
|
| 15 |
+
- mcp-server-track
|
| 16 |
+
- Agents-MCP-Hackathon
|
| 17 |
+
- model-context-protocol
|
| 18 |
+
- video-memory
|
| 19 |
+
- semantic-search
|
| 20 |
+
- ai-agents
|
| 21 |
+
- memvid
|
| 22 |
+
- faiss
|
| 23 |
+
- huggingface
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
# π₯ Memvid MCP Server
|
| 27 |
+
|
| 28 |
+
An advanced **Model Context Protocol (MCP) server** that stores AI conversation memories in MP4 video files using QR codes and semantic embeddings. Built for the **Hugging Face Hackathon - MCP Server Track**.
|
| 29 |
+
|
| 30 |
+
## π **Live MCP Endpoint**
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
https://eldarski-memvid-mcp-server.hf.space/gradio_api/mcp/sse
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## β¨ **Features**
|
| 37 |
+
|
| 38 |
+
- π¬ **Video Memory Storage**: Store text chunks in MP4 files with QR code encoding
|
| 39 |
+
- π **Lightning-Fast Search**: Semantic similarity search using FAISS embeddings
|
| 40 |
+
- π¬ **Interactive Chat**: Converse with your stored memories using AI
|
| 41 |
+
- βοΈ **Cloud Integration**: Automatic backup to HuggingFace datasets
|
| 42 |
+
- π§ **24 MCP Tools**: Comprehensive memory management via MCP protocol
|
| 43 |
+
- π **91.7% Functional**: Real working implementation with cloud storage
|
| 44 |
+
|
| 45 |
+
## π― **Quick Start**
|
| 46 |
+
|
| 47 |
+
### Add to MCP Client (Cursor, Claude Desktop, etc.)
|
| 48 |
+
|
| 49 |
+
```json
|
| 50 |
+
{
|
| 51 |
+
"mcpServers": {
|
| 52 |
+
"memvid-server": {
|
| 53 |
+
"url": "https://eldarski-memvid-mcp-server.hf.space/gradio_api/mcp/sse"
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### Basic Workflow
|
| 60 |
+
|
| 61 |
+
1. **Store memories**: `store_memory(text, client_id)`
|
| 62 |
+
2. **Build video**: `build_memory_video(client_id, memory_name)`
|
| 63 |
+
3. **Search**: `search_memory(query, client_id, memory_name)`
|
| 64 |
+
4. **Chat**: `chat_with_memory(query, client_id, memory_name)`
|
| 65 |
+
|
| 66 |
+
## π§ **Available MCP Tools**
|
| 67 |
+
|
| 68 |
+
### Memory Operations
|
| 69 |
+
|
| 70 |
+
- `store_memory` - Store text chunks in video memory
|
| 71 |
+
- `build_memory_video` - Build MP4 memory from stored chunks
|
| 72 |
+
- `search_memory` - Semantic search in memory videos
|
| 73 |
+
- `chat_with_memory` - Interactive chat with memory
|
| 74 |
+
- `list_memories` - List all memories for a client
|
| 75 |
+
- `get_memory_stats` - Get memory usage statistics
|
| 76 |
+
- `delete_memory` - Delete specific memory videos
|
| 77 |
+
- `store_document` - Store document content in memory
|
| 78 |
+
|
| 79 |
+
### HuggingFace Dataset Integration
|
| 80 |
+
|
| 81 |
+
- `save_to_hf_dataset` - Save client data to specific HF dataset
|
| 82 |
+
- `load_from_hf_dataset` - Load client data from HF dataset
|
| 83 |
+
- `list_hf_datasets` - List available HF datasets
|
| 84 |
+
- `create_hf_dataset` - Create new HF dataset
|
| 85 |
+
- `get_storage_info` - Get HF storage connection status
|
| 86 |
+
- `backup_client_data` - Backup to default HF dataset
|
| 87 |
+
- `restore_client_data` - Restore from default HF dataset
|
| 88 |
+
|
| 89 |
+
## π¬ **Demo Video**
|
| 90 |
+
|
| 91 |
+
[Link to demo video showing MCP server in action]
|
| 92 |
+
|
| 93 |
+
## ποΈ **How It Works**
|
| 94 |
+
|
| 95 |
+
This MCP server uses the innovative [memvid library](https://github.com/Olow304/memvid) to:
|
| 96 |
+
|
| 97 |
+
1. **Encode text chunks** into QR codes embedded in MP4 video frames
|
| 98 |
+
2. **Generate semantic embeddings** using sentence-transformers
|
| 99 |
+
3. **Create FAISS indexes** for lightning-fast similarity search
|
| 100 |
+
4. **Enable AI chat** with stored memories using context retrieval
|
| 101 |
+
5. **Backup everything** to HuggingFace datasets for persistence
|
| 102 |
+
|
| 103 |
+
Each client gets isolated storage with their own memory videos and embeddings.
|
| 104 |
+
|
| 105 |
+
## π **Test Results**
|
| 106 |
+
|
| 107 |
+
- β
**91.7% Success Rate** (22/24 tools working)
|
| 108 |
+
- β
**Real Cloud Storage** integration with HuggingFace
|
| 109 |
+
- β
**PyTorch Compatibility** solved for production deployment
|
| 110 |
+
- β
**Memory Operations** fully functional
|
| 111 |
+
- β
**Search & Chat** working with semantic embeddings
|
| 112 |
+
|
| 113 |
+
## π οΈ **Technical Stack**
|
| 114 |
+
|
| 115 |
+
- **[Gradio](https://gradio.app/)** - Web interface and MCP server
|
| 116 |
+
- **[Memvid](https://github.com/Olow304/memvid)** - Video-based memory storage
|
| 117 |
+
- **[FAISS](https://github.com/facebookresearch/faiss)** - Similarity search
|
| 118 |
+
- **[Sentence Transformers](https://www.sbert.net/)** - Text embeddings
|
| 119 |
+
- **[HuggingFace](https://huggingface.co/)** - Cloud dataset storage
|
| 120 |
+
|
| 121 |
+
## π **Hackathon Submission**
|
| 122 |
+
|
| 123 |
+
**Track**: MCP Server / Tool
|
| 124 |
+
**Tags**: `mcp-server-track`
|
| 125 |
+
**Status**: Production-ready with 91.7% functionality
|
| 126 |
+
**Innovation**: First MCP server to use video files for AI memory storage
|
| 127 |
+
|
| 128 |
+
## π **License**
|
| 129 |
+
|
| 130 |
+
MIT License - Feel free to use and modify!
|
| 131 |
+
|
| 132 |
+
## π€ **Contributing**
|
| 133 |
+
|
| 134 |
+
Built for the HuggingFace Hackathon. Contributions welcome!
|
app.py
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
π₯ Memvid MCP Server - Video-based AI Memory Storage
|
| 3 |
+
====================================================
|
| 4 |
+
|
| 5 |
+
An advanced Model Context Protocol (MCP) server that stores AI conversation memories
|
| 6 |
+
in MP4 video files using QR codes and semantic embeddings. Built with Gradio and
|
| 7 |
+
the memvid library for deployment on Hugging Face Spaces.
|
| 8 |
+
|
| 9 |
+
π MCP Endpoint: https://eldarski-memvid-mcp-server.hf.space/gradio_api/mcp/sse
|
| 10 |
+
|
| 11 |
+
Features:
|
| 12 |
+
- π¬ Store text chunks in MP4 video files with QR codes
|
| 13 |
+
- π Lightning-fast semantic search using FAISS embeddings
|
| 14 |
+
- π¬ Interactive chat with stored memories
|
| 15 |
+
- βοΈ Automatic backup to HuggingFace datasets
|
| 16 |
+
- π§ 24 MCP tools for comprehensive memory management
|
| 17 |
+
- π 91.7% functional with real cloud integration
|
| 18 |
+
|
| 19 |
+
Built for the Hugging Face Hackathon - MCP Server Track
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import gradio as gr
|
| 23 |
+
import os
|
| 24 |
+
import json
|
| 25 |
+
from typing import Dict, Any
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from dotenv import load_dotenv
|
| 28 |
+
from utils.dual_storage_manager import DualStorageManager
|
| 29 |
+
|
| 30 |
+
# Load environment variables from .env file
|
| 31 |
+
load_dotenv()
|
| 32 |
+
|
| 33 |
+
# CRITICAL: Enable MCP server mode for HF Spaces
|
| 34 |
+
os.environ["GRADIO_MCP_SERVER"] = "True"
|
| 35 |
+
|
| 36 |
+
# Initialize the dual storage manager with config-driven mode selection
|
| 37 |
+
dual_storage_manager = DualStorageManager(data_dir="./data")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def store_memory(text: str, client_id: str, metadata: str = "{}") -> str:
|
| 41 |
+
"""
|
| 42 |
+
Universal memory storage interface - supports memvid, vector, or dual storage modes.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
text (str): Text content to store
|
| 46 |
+
client_id (str): Unique identifier for the client
|
| 47 |
+
metadata (str): JSON string with additional metadata
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
str: Success message with storage details
|
| 51 |
+
"""
|
| 52 |
+
try:
|
| 53 |
+
# Parse metadata if provided
|
| 54 |
+
parsed_metadata = {}
|
| 55 |
+
if metadata and metadata.strip():
|
| 56 |
+
try:
|
| 57 |
+
parsed_metadata = json.loads(metadata)
|
| 58 |
+
except json.JSONDecodeError:
|
| 59 |
+
return f"Error: Invalid JSON in metadata: {metadata}"
|
| 60 |
+
|
| 61 |
+
return dual_storage_manager.store_memory(text, client_id, parsed_metadata)
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return f"Error in store_memory: {str(e)}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def build_memory_video(client_id: str, memory_name: str) -> str:
|
| 67 |
+
"""
|
| 68 |
+
Build a memory video from stored chunks using memvid.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
client_id (str): Client identifier
|
| 72 |
+
memory_name (str): Name for the memory video
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
str: Success message with video details
|
| 76 |
+
"""
|
| 77 |
+
try:
|
| 78 |
+
return memvid_manager.build_memory_video(client_id, memory_name)
|
| 79 |
+
except Exception as e:
|
| 80 |
+
return f"Error in build_memory_video: {str(e)}"
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def search_memory(query: str, client_id: str, memory_name: str, top_k: int = 5) -> str:
|
| 84 |
+
"""
|
| 85 |
+
Universal memory search interface with performance comparison in dual mode.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
query (str): Search query
|
| 89 |
+
client_id (str): Client identifier
|
| 90 |
+
memory_name (str): Name of memory to search
|
| 91 |
+
top_k (int): Number of results to return
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
str: JSON string with search results and performance metrics
|
| 95 |
+
"""
|
| 96 |
+
try:
|
| 97 |
+
return dual_storage_manager.search_memory(query, client_id, memory_name, top_k)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
return json.dumps({"error": f"Error in search_memory: {str(e)}"})
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def chat_with_memory(query: str, client_id: str, memory_name: str) -> str:
|
| 103 |
+
"""
|
| 104 |
+
Universal chat interface with stored memory context.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
query (str): User question/query
|
| 108 |
+
client_id (str): Client identifier
|
| 109 |
+
memory_name (str): Name of memory to query
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
str: AI response based on memory context
|
| 113 |
+
"""
|
| 114 |
+
try:
|
| 115 |
+
return dual_storage_manager.chat_with_memory(query, client_id, memory_name)
|
| 116 |
+
except Exception as e:
|
| 117 |
+
return f"Error in chat_with_memory: {str(e)}"
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def list_memories(client_id: str) -> str:
|
| 121 |
+
"""
|
| 122 |
+
Universal memory listing interface.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
client_id (str): Client identifier
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
str: JSON string with memory list
|
| 129 |
+
"""
|
| 130 |
+
try:
|
| 131 |
+
return dual_storage_manager.list_memories(client_id)
|
| 132 |
+
except Exception as e:
|
| 133 |
+
return json.dumps({"error": f"Error in list_memories: {str(e)}"})
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def get_memory_stats(client_id: str) -> str:
|
| 137 |
+
"""
|
| 138 |
+
Get aggregated memory statistics with performance comparison in dual mode.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
client_id (str): Client identifier
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
str: JSON string with statistics and performance insights
|
| 145 |
+
"""
|
| 146 |
+
try:
|
| 147 |
+
return dual_storage_manager.get_memory_stats(client_id)
|
| 148 |
+
except Exception as e:
|
| 149 |
+
return json.dumps({"error": f"Error in get_memory_stats: {str(e)}"})
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def delete_memory(client_id: str, memory_name: str) -> str:
|
| 153 |
+
"""
|
| 154 |
+
Universal memory deletion interface.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
client_id (str): Client identifier
|
| 158 |
+
memory_name (str): Name of memory to delete
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
str: Success/error message
|
| 162 |
+
"""
|
| 163 |
+
try:
|
| 164 |
+
return dual_storage_manager.delete_memory(client_id, memory_name)
|
| 165 |
+
except Exception as e:
|
| 166 |
+
return f"Error in delete_memory: {str(e)}"
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def set_storage_mode(mode: str, client_id: str = "") -> str:
|
| 170 |
+
"""
|
| 171 |
+
Set storage mode for runtime configuration.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
mode (str): Storage mode (memvid_only, vector_only, dual)
|
| 175 |
+
client_id (str): Optional client-specific setting
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
str: Configuration result message
|
| 179 |
+
"""
|
| 180 |
+
try:
|
| 181 |
+
return dual_storage_manager.set_storage_mode(mode, client_id)
|
| 182 |
+
except Exception as e:
|
| 183 |
+
return f"Error in set_storage_mode: {str(e)}"
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def store_document(content: str, doc_type: str, client_id: str) -> str:
|
| 187 |
+
"""
|
| 188 |
+
Store document content in memory chunks.
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
content (str): Document content
|
| 192 |
+
doc_type (str): Type of document (pdf, txt, etc.)
|
| 193 |
+
client_id (str): Client identifier
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
str: Success message with storage details
|
| 197 |
+
"""
|
| 198 |
+
try:
|
| 199 |
+
# Add document type as metadata
|
| 200 |
+
metadata = {"document_type": doc_type, "source": "document_upload"}
|
| 201 |
+
return memvid_manager.store_memory(content, client_id, metadata)
|
| 202 |
+
except Exception as e:
|
| 203 |
+
return f"Error in store_document: {str(e)}"
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def get_storage_info() -> str:
|
| 207 |
+
"""
|
| 208 |
+
Get storage handler information and connection status.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
str: JSON string with storage information
|
| 212 |
+
"""
|
| 213 |
+
try:
|
| 214 |
+
storage_info = memvid_manager.storage_handler.get_storage_info()
|
| 215 |
+
return json.dumps(storage_info, indent=2)
|
| 216 |
+
except Exception as e:
|
| 217 |
+
return json.dumps({"error": f"Error getting storage info: {str(e)}"})
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def backup_client_data(client_id: str) -> str:
|
| 221 |
+
"""
|
| 222 |
+
Backup all client data to HuggingFace dataset.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
client_id (str): Client identifier
|
| 226 |
+
|
| 227 |
+
Returns:
|
| 228 |
+
str: Backup result message
|
| 229 |
+
"""
|
| 230 |
+
try:
|
| 231 |
+
client_dir = memvid_manager._get_client_dir(client_id)
|
| 232 |
+
success = memvid_manager.storage_handler.backup_client_data(
|
| 233 |
+
client_id, client_dir
|
| 234 |
+
)
|
| 235 |
+
if success:
|
| 236 |
+
return f"Successfully backed up all data for client {client_id} to HuggingFace dataset"
|
| 237 |
+
else:
|
| 238 |
+
return f"Backup failed or HuggingFace integration not enabled for client {client_id}"
|
| 239 |
+
except Exception as e:
|
| 240 |
+
return f"Error in backup_client_data: {str(e)}"
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def restore_client_data(client_id: str) -> str:
|
| 244 |
+
"""
|
| 245 |
+
Restore client data from HuggingFace dataset.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
client_id (str): Client identifier
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
str: Restore result message
|
| 252 |
+
"""
|
| 253 |
+
try:
|
| 254 |
+
client_dir = memvid_manager._get_client_dir(client_id)
|
| 255 |
+
success = memvid_manager.storage_handler.restore_client_data(
|
| 256 |
+
client_id, client_dir
|
| 257 |
+
)
|
| 258 |
+
if success:
|
| 259 |
+
return f"Successfully restored all data for client {client_id} from HuggingFace dataset"
|
| 260 |
+
else:
|
| 261 |
+
return f"Restore failed or HuggingFace integration not enabled for client {client_id}"
|
| 262 |
+
except Exception as e:
|
| 263 |
+
return f"Error in restore_client_data: {str(e)}"
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def save_to_hf_dataset(
|
| 267 |
+
client_id: str, dataset_name: str = "", private: bool = True
|
| 268 |
+
) -> str:
|
| 269 |
+
"""
|
| 270 |
+
Save all client memory data to a specific HuggingFace dataset.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
client_id (str): Client identifier
|
| 274 |
+
dataset_name (str): Custom dataset name (optional, uses default if empty)
|
| 275 |
+
private (bool): Whether to make the dataset private
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
str: Success message with dataset details
|
| 279 |
+
"""
|
| 280 |
+
try:
|
| 281 |
+
# Use custom dataset name if provided
|
| 282 |
+
original_dataset = memvid_manager.storage_handler.dataset_name
|
| 283 |
+
if dataset_name.strip():
|
| 284 |
+
memvid_manager.storage_handler.dataset_name = dataset_name.strip()
|
| 285 |
+
|
| 286 |
+
# Backup all client data
|
| 287 |
+
client_dir = memvid_manager._get_client_dir(client_id)
|
| 288 |
+
success = memvid_manager.storage_handler.backup_client_data(
|
| 289 |
+
client_id, client_dir
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Restore original dataset name
|
| 293 |
+
if dataset_name.strip():
|
| 294 |
+
current_dataset = memvid_manager.storage_handler.dataset_name
|
| 295 |
+
memvid_manager.storage_handler.dataset_name = original_dataset
|
| 296 |
+
else:
|
| 297 |
+
current_dataset = original_dataset
|
| 298 |
+
|
| 299 |
+
if success:
|
| 300 |
+
return json.dumps(
|
| 301 |
+
{
|
| 302 |
+
"status": "success",
|
| 303 |
+
"message": f"Successfully saved all data for client {client_id}",
|
| 304 |
+
"dataset": current_dataset,
|
| 305 |
+
"private": private,
|
| 306 |
+
"url": f"https://huggingface.co/datasets/{current_dataset}",
|
| 307 |
+
},
|
| 308 |
+
indent=2,
|
| 309 |
+
)
|
| 310 |
+
else:
|
| 311 |
+
return json.dumps(
|
| 312 |
+
{
|
| 313 |
+
"status": "error",
|
| 314 |
+
"message": f"Failed to save data for client {client_id}",
|
| 315 |
+
"dataset": current_dataset,
|
| 316 |
+
},
|
| 317 |
+
indent=2,
|
| 318 |
+
)
|
| 319 |
+
except Exception as e:
|
| 320 |
+
return json.dumps(
|
| 321 |
+
{"status": "error", "message": f"Error in save_to_hf_dataset: {str(e)}"},
|
| 322 |
+
indent=2,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def load_from_hf_dataset(client_id: str, dataset_name: str) -> str:
|
| 327 |
+
"""
|
| 328 |
+
Load client memory data from a specific HuggingFace dataset.
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
client_id (str): Client identifier
|
| 332 |
+
dataset_name (str): Dataset name to load from
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
str: Success message with loaded data details
|
| 336 |
+
"""
|
| 337 |
+
try:
|
| 338 |
+
# Use custom dataset name
|
| 339 |
+
original_dataset = memvid_manager.storage_handler.dataset_name
|
| 340 |
+
memvid_manager.storage_handler.dataset_name = dataset_name.strip()
|
| 341 |
+
|
| 342 |
+
# Restore client data
|
| 343 |
+
client_dir = memvid_manager._get_client_dir(client_id)
|
| 344 |
+
success = memvid_manager.storage_handler.restore_client_data(
|
| 345 |
+
client_id, client_dir
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# Restore original dataset name
|
| 349 |
+
memvid_manager.storage_handler.dataset_name = original_dataset
|
| 350 |
+
|
| 351 |
+
if success:
|
| 352 |
+
# Get stats after loading
|
| 353 |
+
stats = memvid_manager.get_memory_stats(client_id)
|
| 354 |
+
return json.dumps(
|
| 355 |
+
{
|
| 356 |
+
"status": "success",
|
| 357 |
+
"message": f"Successfully loaded all data for client {client_id}",
|
| 358 |
+
"source_dataset": dataset_name,
|
| 359 |
+
"stats": json.loads(stats) if stats else {},
|
| 360 |
+
},
|
| 361 |
+
indent=2,
|
| 362 |
+
)
|
| 363 |
+
else:
|
| 364 |
+
return json.dumps(
|
| 365 |
+
{
|
| 366 |
+
"status": "error",
|
| 367 |
+
"message": f"Failed to load data for client {client_id}",
|
| 368 |
+
"source_dataset": dataset_name,
|
| 369 |
+
},
|
| 370 |
+
indent=2,
|
| 371 |
+
)
|
| 372 |
+
except Exception as e:
|
| 373 |
+
return json.dumps(
|
| 374 |
+
{"status": "error", "message": f"Error in load_from_hf_dataset: {str(e)}"},
|
| 375 |
+
indent=2,
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def list_hf_datasets() -> str:
|
| 380 |
+
"""
|
| 381 |
+
List available HuggingFace datasets for the current user.
|
| 382 |
+
|
| 383 |
+
Returns:
|
| 384 |
+
str: JSON string with available datasets
|
| 385 |
+
"""
|
| 386 |
+
try:
|
| 387 |
+
if not memvid_manager.storage_handler.hf_enabled:
|
| 388 |
+
return json.dumps(
|
| 389 |
+
{"status": "error", "message": "HuggingFace integration not enabled"},
|
| 390 |
+
indent=2,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Get user info and list datasets
|
| 394 |
+
user_info = memvid_manager.storage_handler.hf_api.whoami()
|
| 395 |
+
username = user_info.get("name", "unknown")
|
| 396 |
+
|
| 397 |
+
# List user's datasets
|
| 398 |
+
datasets = list(
|
| 399 |
+
memvid_manager.storage_handler.hf_api.list_datasets(author=username)
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
dataset_list = []
|
| 403 |
+
for dataset in datasets:
|
| 404 |
+
dataset_list.append(
|
| 405 |
+
{
|
| 406 |
+
"name": dataset.id,
|
| 407 |
+
"private": dataset.private,
|
| 408 |
+
"url": f"https://huggingface.co/datasets/{dataset.id}",
|
| 409 |
+
"created_at": (
|
| 410 |
+
str(dataset.created_at) if dataset.created_at else None
|
| 411 |
+
),
|
| 412 |
+
"updated_at": (
|
| 413 |
+
str(dataset.last_modified) if dataset.last_modified else None
|
| 414 |
+
),
|
| 415 |
+
}
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
return json.dumps(
|
| 419 |
+
{
|
| 420 |
+
"status": "success",
|
| 421 |
+
"username": username,
|
| 422 |
+
"total_datasets": len(dataset_list),
|
| 423 |
+
"datasets": dataset_list,
|
| 424 |
+
},
|
| 425 |
+
indent=2,
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
except Exception as e:
|
| 429 |
+
return json.dumps(
|
| 430 |
+
{"status": "error", "message": f"Error in list_hf_datasets: {str(e)}"},
|
| 431 |
+
indent=2,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def create_hf_dataset(
|
| 436 |
+
dataset_name: str, private: bool = True, description: str = ""
|
| 437 |
+
) -> str:
|
| 438 |
+
"""
|
| 439 |
+
Create a new HuggingFace dataset for memory storage.
|
| 440 |
+
|
| 441 |
+
Args:
|
| 442 |
+
dataset_name (str): Name for the new dataset
|
| 443 |
+
private (bool): Whether to make the dataset private
|
| 444 |
+
description (str): Dataset description
|
| 445 |
+
|
| 446 |
+
Returns:
|
| 447 |
+
str: Success message with dataset details
|
| 448 |
+
"""
|
| 449 |
+
try:
|
| 450 |
+
if not memvid_manager.storage_handler.hf_enabled:
|
| 451 |
+
return json.dumps(
|
| 452 |
+
{"status": "error", "message": "HuggingFace integration not enabled"},
|
| 453 |
+
indent=2,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
from huggingface_hub import create_repo
|
| 457 |
+
|
| 458 |
+
# Create the dataset
|
| 459 |
+
repo_url = create_repo(
|
| 460 |
+
repo_id=dataset_name,
|
| 461 |
+
repo_type="dataset",
|
| 462 |
+
token=memvid_manager.storage_handler.hf_token,
|
| 463 |
+
private=private,
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
return json.dumps(
|
| 467 |
+
{
|
| 468 |
+
"status": "success",
|
| 469 |
+
"message": f"Successfully created dataset: {dataset_name}",
|
| 470 |
+
"dataset_name": dataset_name,
|
| 471 |
+
"private": private,
|
| 472 |
+
"url": f"https://huggingface.co/datasets/{dataset_name}",
|
| 473 |
+
"repo_url": repo_url,
|
| 474 |
+
},
|
| 475 |
+
indent=2,
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
except Exception as e:
|
| 479 |
+
return json.dumps(
|
| 480 |
+
{"status": "error", "message": f"Error in create_hf_dataset: {str(e)}"},
|
| 481 |
+
indent=2,
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
# Create the Gradio interface
|
| 486 |
+
with gr.Blocks(title="Memvid MCP Server", theme=gr.themes.Soft()) as demo:
|
| 487 |
+
gr.Markdown(
|
| 488 |
+
"""
|
| 489 |
+
# π¬ Memvid MCP Server
|
| 490 |
+
|
| 491 |
+
A Model Context Protocol (MCP) server that provides video-based AI memory storage for LLM agents.
|
| 492 |
+
Built with [memvid](https://github.com/Olow304/memvid) - store millions of text chunks in MP4 files with lightning-fast semantic search.
|
| 493 |
+
|
| 494 |
+
## MCP Server URL
|
| 495 |
+
```
|
| 496 |
+
https://eldarski-memvid-mcp-server.hf.space/gradio_api/mcp/sse
|
| 497 |
+
```
|
| 498 |
+
|
| 499 |
+
*For local development: http://localhost:7860/gradio_api/mcp/sse*
|
| 500 |
+
|
| 501 |
+
## Available MCP Tools
|
| 502 |
+
|
| 503 |
+
### π¬ Memory Operations
|
| 504 |
+
- `store_memory`: Store text chunks in video memory
|
| 505 |
+
- `build_memory_video`: Build MP4 memory from stored chunks
|
| 506 |
+
- `search_memory`: Semantic search in memory videos
|
| 507 |
+
- `chat_with_memory`: Interactive chat with memory
|
| 508 |
+
- `list_memories`: List all memories for a client
|
| 509 |
+
- `get_memory_stats`: Get memory usage statistics
|
| 510 |
+
- `delete_memory`: Delete specific memory videos
|
| 511 |
+
- `store_document`: Store document content in memory
|
| 512 |
+
|
| 513 |
+
### π€ HuggingFace Dataset Integration
|
| 514 |
+
- `save_to_hf_dataset`: Save all client data to specific HF dataset
|
| 515 |
+
- `load_from_hf_dataset`: Load client data from specific HF dataset
|
| 516 |
+
- `list_hf_datasets`: List available HF datasets for current user
|
| 517 |
+
- `create_hf_dataset`: Create new HF dataset for memory storage
|
| 518 |
+
- `get_storage_info`: Get HuggingFace storage connection status
|
| 519 |
+
- `backup_client_data`: Backup client data to default HF dataset
|
| 520 |
+
- `restore_client_data`: Restore client data from default HF dataset
|
| 521 |
+
|
| 522 |
+
## Integration
|
| 523 |
+
|
| 524 |
+
To add this MCP server to clients that support SSE (e.g. Cursor, Claude Desktop, Cline), add this configuration:
|
| 525 |
+
|
| 526 |
+
```json
|
| 527 |
+
{
|
| 528 |
+
"mcpServers": {
|
| 529 |
+
"memvid-server": {
|
| 530 |
+
"url": "https://eldarski-memvid-mcp-server.hf.space/gradio_api/mcp/sse"
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
}
|
| 534 |
+
```
|
| 535 |
+
|
| 536 |
+
*For local development, use: http://localhost:7860/gradio_api/mcp/sse*
|
| 537 |
+
|
| 538 |
+
## How It Works
|
| 539 |
+
|
| 540 |
+
1. **Store Memory**: Add text chunks that will be embedded and stored
|
| 541 |
+
2. **Build Video**: Create an MP4 file containing all stored chunks with embeddings
|
| 542 |
+
3. **Search**: Use semantic similarity to find relevant memories
|
| 543 |
+
4. **Chat**: Interactive conversation with your stored memories
|
| 544 |
+
|
| 545 |
+
Each client gets isolated storage with their own memory videos.
|
| 546 |
+
"""
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
with gr.Tab("πΎ Memory Storage"):
|
| 550 |
+
gr.Markdown("### Store text chunks and build memory videos")
|
| 551 |
+
|
| 552 |
+
with gr.Row():
|
| 553 |
+
with gr.Column():
|
| 554 |
+
store_text = gr.Textbox(
|
| 555 |
+
label="Text to Store",
|
| 556 |
+
placeholder="Enter text content to store in memory...",
|
| 557 |
+
lines=5,
|
| 558 |
+
)
|
| 559 |
+
store_client_id = gr.Textbox(
|
| 560 |
+
label="Client ID",
|
| 561 |
+
placeholder="unique_client_identifier",
|
| 562 |
+
value="demo_client",
|
| 563 |
+
)
|
| 564 |
+
store_metadata = gr.Textbox(
|
| 565 |
+
label="Metadata (JSON)",
|
| 566 |
+
placeholder='{"source": "manual_input", "category": "notes"}',
|
| 567 |
+
value="{}",
|
| 568 |
+
)
|
| 569 |
+
store_btn = gr.Button("Store Memory", variant="primary")
|
| 570 |
+
|
| 571 |
+
with gr.Column():
|
| 572 |
+
store_output = gr.Textbox(
|
| 573 |
+
label="Storage Result",
|
| 574 |
+
lines=8,
|
| 575 |
+
placeholder="Storage results will appear here...",
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
store_btn.click(
|
| 579 |
+
fn=store_memory,
|
| 580 |
+
inputs=[store_text, store_client_id, store_metadata],
|
| 581 |
+
outputs=[store_output],
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
gr.Markdown("---")
|
| 585 |
+
|
| 586 |
+
with gr.Row():
|
| 587 |
+
with gr.Column():
|
| 588 |
+
build_client_id = gr.Textbox(
|
| 589 |
+
label="Client ID",
|
| 590 |
+
placeholder="unique_client_identifier",
|
| 591 |
+
value="demo_client",
|
| 592 |
+
)
|
| 593 |
+
build_memory_name = gr.Textbox(
|
| 594 |
+
label="Memory Video Name",
|
| 595 |
+
placeholder="my_knowledge_base",
|
| 596 |
+
value="knowledge_base",
|
| 597 |
+
)
|
| 598 |
+
build_btn = gr.Button("Build Memory Video", variant="secondary")
|
| 599 |
+
|
| 600 |
+
with gr.Column():
|
| 601 |
+
build_output = gr.Textbox(
|
| 602 |
+
label="Build Result",
|
| 603 |
+
lines=6,
|
| 604 |
+
placeholder="Video build results will appear here...",
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
build_btn.click(
|
| 608 |
+
fn=build_memory_video,
|
| 609 |
+
inputs=[build_client_id, build_memory_name],
|
| 610 |
+
outputs=[build_output],
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
with gr.Tab("π Memory Search"):
|
| 614 |
+
gr.Markdown("### Search stored memories using semantic similarity")
|
| 615 |
+
|
| 616 |
+
with gr.Row():
|
| 617 |
+
with gr.Column():
|
| 618 |
+
search_query = gr.Textbox(
|
| 619 |
+
label="Search Query",
|
| 620 |
+
placeholder="What are you looking for?",
|
| 621 |
+
lines=2,
|
| 622 |
+
)
|
| 623 |
+
search_client_id = gr.Textbox(
|
| 624 |
+
label="Client ID",
|
| 625 |
+
placeholder="unique_client_identifier",
|
| 626 |
+
value="demo_client",
|
| 627 |
+
)
|
| 628 |
+
search_memory_name = gr.Textbox(
|
| 629 |
+
label="Memory Video Name",
|
| 630 |
+
placeholder="knowledge_base",
|
| 631 |
+
value="knowledge_base",
|
| 632 |
+
)
|
| 633 |
+
search_top_k = gr.Slider(
|
| 634 |
+
label="Number of Results", minimum=1, maximum=20, value=5, step=1
|
| 635 |
+
)
|
| 636 |
+
search_btn = gr.Button("Search Memory", variant="primary")
|
| 637 |
+
|
| 638 |
+
with gr.Column():
|
| 639 |
+
search_output = gr.Textbox(
|
| 640 |
+
label="Search Results",
|
| 641 |
+
lines=15,
|
| 642 |
+
placeholder="Search results will appear here...",
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
search_btn.click(
|
| 646 |
+
fn=search_memory,
|
| 647 |
+
inputs=[search_query, search_client_id, search_memory_name, search_top_k],
|
| 648 |
+
outputs=[search_output],
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
with gr.Tab("π¬ Memory Chat"):
|
| 652 |
+
gr.Markdown("### Interactive chat with your stored memories")
|
| 653 |
+
|
| 654 |
+
with gr.Row():
|
| 655 |
+
with gr.Column():
|
| 656 |
+
chat_query = gr.Textbox(
|
| 657 |
+
label="Your Question",
|
| 658 |
+
placeholder="Ask a question about your stored memories...",
|
| 659 |
+
lines=3,
|
| 660 |
+
)
|
| 661 |
+
chat_client_id = gr.Textbox(
|
| 662 |
+
label="Client ID",
|
| 663 |
+
placeholder="unique_client_identifier",
|
| 664 |
+
value="demo_client",
|
| 665 |
+
)
|
| 666 |
+
chat_memory_name = gr.Textbox(
|
| 667 |
+
label="Memory Video Name",
|
| 668 |
+
placeholder="knowledge_base",
|
| 669 |
+
value="knowledge_base",
|
| 670 |
+
)
|
| 671 |
+
chat_btn = gr.Button("Chat with Memory", variant="primary")
|
| 672 |
+
|
| 673 |
+
with gr.Column():
|
| 674 |
+
chat_output = gr.Textbox(
|
| 675 |
+
label="Memory Response",
|
| 676 |
+
lines=12,
|
| 677 |
+
placeholder="Memory responses will appear here...",
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
chat_btn.click(
|
| 681 |
+
fn=chat_with_memory,
|
| 682 |
+
inputs=[chat_query, chat_client_id, chat_memory_name],
|
| 683 |
+
outputs=[chat_output],
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
with gr.Tab("π Memory Management"):
|
| 687 |
+
gr.Markdown("### Manage your stored memories")
|
| 688 |
+
|
| 689 |
+
with gr.Row():
|
| 690 |
+
with gr.Column():
|
| 691 |
+
list_client_id = gr.Textbox(
|
| 692 |
+
label="Client ID",
|
| 693 |
+
placeholder="unique_client_identifier",
|
| 694 |
+
value="demo_client",
|
| 695 |
+
)
|
| 696 |
+
list_btn = gr.Button("List Memories", variant="secondary")
|
| 697 |
+
|
| 698 |
+
gr.Markdown("---")
|
| 699 |
+
|
| 700 |
+
stats_client_id = gr.Textbox(
|
| 701 |
+
label="Client ID",
|
| 702 |
+
placeholder="unique_client_identifier",
|
| 703 |
+
value="demo_client",
|
| 704 |
+
)
|
| 705 |
+
stats_btn = gr.Button("Get Statistics", variant="secondary")
|
| 706 |
+
|
| 707 |
+
with gr.Column():
|
| 708 |
+
list_output = gr.Textbox(
|
| 709 |
+
label="Memory List",
|
| 710 |
+
lines=10,
|
| 711 |
+
placeholder="Memory list will appear here...",
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
stats_output = gr.Textbox(
|
| 715 |
+
label="Memory Statistics",
|
| 716 |
+
lines=10,
|
| 717 |
+
placeholder="Statistics will appear here...",
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
list_btn.click(fn=list_memories, inputs=[list_client_id], outputs=[list_output])
|
| 721 |
+
|
| 722 |
+
stats_btn.click(
|
| 723 |
+
fn=get_memory_stats, inputs=[stats_client_id], outputs=[stats_output]
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
gr.Markdown("---")
|
| 727 |
+
|
| 728 |
+
with gr.Row():
|
| 729 |
+
with gr.Column():
|
| 730 |
+
delete_client_id = gr.Textbox(
|
| 731 |
+
label="Client ID",
|
| 732 |
+
placeholder="unique_client_identifier",
|
| 733 |
+
value="demo_client",
|
| 734 |
+
)
|
| 735 |
+
delete_memory_name = gr.Textbox(
|
| 736 |
+
label="Memory Name to Delete", placeholder="knowledge_base"
|
| 737 |
+
)
|
| 738 |
+
delete_btn = gr.Button("Delete Memory", variant="stop")
|
| 739 |
+
|
| 740 |
+
with gr.Column():
|
| 741 |
+
delete_output = gr.Textbox(
|
| 742 |
+
label="Delete Result",
|
| 743 |
+
lines=5,
|
| 744 |
+
placeholder="Delete results will appear here...",
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
delete_btn.click(
|
| 748 |
+
fn=delete_memory,
|
| 749 |
+
inputs=[delete_client_id, delete_memory_name],
|
| 750 |
+
outputs=[delete_output],
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
gr.Markdown("---")
|
| 754 |
+
|
| 755 |
+
with gr.Row():
|
| 756 |
+
with gr.Column():
|
| 757 |
+
gr.Markdown("#### Storage Mode Configuration")
|
| 758 |
+
mode_dropdown = gr.Dropdown(
|
| 759 |
+
label="Storage Mode",
|
| 760 |
+
choices=["memvid_only", "vector_only", "dual"],
|
| 761 |
+
value="dual",
|
| 762 |
+
info="Select storage backend mode",
|
| 763 |
+
)
|
| 764 |
+
mode_client_id = gr.Textbox(
|
| 765 |
+
label="Client ID (optional)",
|
| 766 |
+
placeholder="Leave empty for global setting",
|
| 767 |
+
value="",
|
| 768 |
+
)
|
| 769 |
+
mode_btn = gr.Button("Set Storage Mode", variant="secondary")
|
| 770 |
+
|
| 771 |
+
with gr.Column():
|
| 772 |
+
mode_output = gr.Textbox(
|
| 773 |
+
label="Mode Configuration Result",
|
| 774 |
+
lines=5,
|
| 775 |
+
placeholder="Storage mode results will appear here...",
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
mode_btn.click(
|
| 779 |
+
fn=set_storage_mode,
|
| 780 |
+
inputs=[mode_dropdown, mode_client_id],
|
| 781 |
+
outputs=[mode_output],
|
| 782 |
+
)
|
| 783 |
+
|
| 784 |
+
with gr.Tab("π Document Storage"):
|
| 785 |
+
gr.Markdown("### Store document content in memory")
|
| 786 |
+
|
| 787 |
+
with gr.Row():
|
| 788 |
+
with gr.Column():
|
| 789 |
+
doc_content = gr.Textbox(
|
| 790 |
+
label="Document Content",
|
| 791 |
+
placeholder="Paste document content here...",
|
| 792 |
+
lines=8,
|
| 793 |
+
)
|
| 794 |
+
doc_type = gr.Dropdown(
|
| 795 |
+
label="Document Type",
|
| 796 |
+
choices=["txt", "pdf", "md", "html", "other"],
|
| 797 |
+
value="txt",
|
| 798 |
+
)
|
| 799 |
+
doc_client_id = gr.Textbox(
|
| 800 |
+
label="Client ID",
|
| 801 |
+
placeholder="unique_client_identifier",
|
| 802 |
+
value="demo_client",
|
| 803 |
+
)
|
| 804 |
+
doc_btn = gr.Button("Store Document", variant="primary")
|
| 805 |
+
|
| 806 |
+
with gr.Column():
|
| 807 |
+
doc_output = gr.Textbox(
|
| 808 |
+
label="Storage Result",
|
| 809 |
+
lines=10,
|
| 810 |
+
placeholder="Document storage results will appear here...",
|
| 811 |
+
)
|
| 812 |
+
|
| 813 |
+
doc_btn.click(
|
| 814 |
+
fn=store_document,
|
| 815 |
+
inputs=[doc_content, doc_type, doc_client_id],
|
| 816 |
+
outputs=[doc_output],
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
with gr.Tab("π€ HuggingFace Datasets"):
|
| 820 |
+
gr.Markdown("### Advanced HuggingFace Dataset Integration")
|
| 821 |
+
|
| 822 |
+
with gr.Tab("πΎ Save & Load Data"):
|
| 823 |
+
gr.Markdown("#### Save client data to specific HF datasets")
|
| 824 |
+
|
| 825 |
+
with gr.Row():
|
| 826 |
+
with gr.Column():
|
| 827 |
+
save_client_id = gr.Textbox(
|
| 828 |
+
label="Client ID",
|
| 829 |
+
placeholder="unique_client_identifier",
|
| 830 |
+
value="demo_client",
|
| 831 |
+
)
|
| 832 |
+
save_dataset_name = gr.Textbox(
|
| 833 |
+
label="Dataset Name (optional)",
|
| 834 |
+
placeholder="my-custom-dataset (leave empty for default)",
|
| 835 |
+
)
|
| 836 |
+
save_private = gr.Checkbox(
|
| 837 |
+
label="Private Dataset",
|
| 838 |
+
value=True,
|
| 839 |
+
)
|
| 840 |
+
save_btn = gr.Button("Save to HF Dataset", variant="primary")
|
| 841 |
+
|
| 842 |
+
with gr.Column():
|
| 843 |
+
save_output = gr.Textbox(
|
| 844 |
+
label="Save Result",
|
| 845 |
+
lines=10,
|
| 846 |
+
placeholder="Save results will appear here...",
|
| 847 |
+
)
|
| 848 |
+
|
| 849 |
+
save_btn.click(
|
| 850 |
+
fn=save_to_hf_dataset,
|
| 851 |
+
inputs=[save_client_id, save_dataset_name, save_private],
|
| 852 |
+
outputs=[save_output],
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
gr.Markdown("---")
|
| 856 |
+
|
| 857 |
+
with gr.Row():
|
| 858 |
+
with gr.Column():
|
| 859 |
+
load_client_id = gr.Textbox(
|
| 860 |
+
label="Client ID",
|
| 861 |
+
placeholder="unique_client_identifier",
|
| 862 |
+
value="demo_client",
|
| 863 |
+
)
|
| 864 |
+
load_dataset_name = gr.Textbox(
|
| 865 |
+
label="Dataset Name",
|
| 866 |
+
placeholder="dataset-name-to-load-from",
|
| 867 |
+
)
|
| 868 |
+
load_btn = gr.Button("Load from HF Dataset", variant="secondary")
|
| 869 |
+
|
| 870 |
+
with gr.Column():
|
| 871 |
+
load_output = gr.Textbox(
|
| 872 |
+
label="Load Result",
|
| 873 |
+
lines=10,
|
| 874 |
+
placeholder="Load results will appear here...",
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
load_btn.click(
|
| 878 |
+
fn=load_from_hf_dataset,
|
| 879 |
+
inputs=[load_client_id, load_dataset_name],
|
| 880 |
+
outputs=[load_output],
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
with gr.Tab("π Dataset Management"):
|
| 884 |
+
gr.Markdown("#### Manage your HuggingFace datasets")
|
| 885 |
+
|
| 886 |
+
with gr.Row():
|
| 887 |
+
with gr.Column():
|
| 888 |
+
list_datasets_btn = gr.Button(
|
| 889 |
+
"List My Datasets", variant="secondary"
|
| 890 |
+
)
|
| 891 |
+
|
| 892 |
+
gr.Markdown("---")
|
| 893 |
+
|
| 894 |
+
create_dataset_name = gr.Textbox(
|
| 895 |
+
label="New Dataset Name",
|
| 896 |
+
placeholder="my-new-dataset",
|
| 897 |
+
)
|
| 898 |
+
create_private = gr.Checkbox(
|
| 899 |
+
label="Private Dataset",
|
| 900 |
+
value=True,
|
| 901 |
+
)
|
| 902 |
+
create_description = gr.Textbox(
|
| 903 |
+
label="Description (optional)",
|
| 904 |
+
placeholder="Dataset for storing AI memory data",
|
| 905 |
+
lines=2,
|
| 906 |
+
)
|
| 907 |
+
create_btn = gr.Button("Create Dataset", variant="primary")
|
| 908 |
+
|
| 909 |
+
with gr.Column():
|
| 910 |
+
datasets_output = gr.Textbox(
|
| 911 |
+
label="Datasets Information",
|
| 912 |
+
lines=15,
|
| 913 |
+
placeholder="Dataset information will appear here...",
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
list_datasets_btn.click(
|
| 917 |
+
fn=list_hf_datasets,
|
| 918 |
+
inputs=[],
|
| 919 |
+
outputs=[datasets_output],
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
create_btn.click(
|
| 923 |
+
fn=create_hf_dataset,
|
| 924 |
+
inputs=[create_dataset_name, create_private, create_description],
|
| 925 |
+
outputs=[datasets_output],
|
| 926 |
+
)
|
| 927 |
+
|
| 928 |
+
with gr.Tab("βοΈ Storage Info & Backup"):
|
| 929 |
+
gr.Markdown("#### Storage information and legacy backup functions")
|
| 930 |
+
|
| 931 |
+
with gr.Row():
|
| 932 |
+
with gr.Column():
|
| 933 |
+
gr.Markdown("#### Storage Information")
|
| 934 |
+
storage_info_btn = gr.Button(
|
| 935 |
+
"Get Storage Info", variant="secondary"
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
gr.Markdown("---")
|
| 939 |
+
|
| 940 |
+
gr.Markdown("#### Legacy Backup (Default Dataset)")
|
| 941 |
+
backup_client_id = gr.Textbox(
|
| 942 |
+
label="Client ID for Backup",
|
| 943 |
+
placeholder="unique_client_identifier",
|
| 944 |
+
value="demo_client",
|
| 945 |
+
)
|
| 946 |
+
backup_btn = gr.Button(
|
| 947 |
+
"Backup to Default Dataset", variant="primary"
|
| 948 |
+
)
|
| 949 |
+
|
| 950 |
+
gr.Markdown("---")
|
| 951 |
+
|
| 952 |
+
restore_client_id = gr.Textbox(
|
| 953 |
+
label="Client ID for Restore",
|
| 954 |
+
placeholder="unique_client_identifier",
|
| 955 |
+
value="demo_client",
|
| 956 |
+
)
|
| 957 |
+
restore_btn = gr.Button(
|
| 958 |
+
"Restore from Default Dataset", variant="secondary"
|
| 959 |
+
)
|
| 960 |
+
|
| 961 |
+
with gr.Column():
|
| 962 |
+
storage_info_output = gr.Textbox(
|
| 963 |
+
label="Storage Information",
|
| 964 |
+
lines=8,
|
| 965 |
+
placeholder="Storage information will appear here...",
|
| 966 |
+
)
|
| 967 |
+
|
| 968 |
+
backup_output = gr.Textbox(
|
| 969 |
+
label="Backup Result",
|
| 970 |
+
lines=4,
|
| 971 |
+
placeholder="Backup results will appear here...",
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
restore_output = gr.Textbox(
|
| 975 |
+
label="Restore Result",
|
| 976 |
+
lines=4,
|
| 977 |
+
placeholder="Restore results will appear here...",
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
+
storage_info_btn.click(
|
| 981 |
+
fn=get_storage_info, inputs=[], outputs=[storage_info_output]
|
| 982 |
+
)
|
| 983 |
+
|
| 984 |
+
backup_btn.click(
|
| 985 |
+
fn=backup_client_data,
|
| 986 |
+
inputs=[backup_client_id],
|
| 987 |
+
outputs=[backup_output],
|
| 988 |
+
)
|
| 989 |
+
|
| 990 |
+
restore_btn.click(
|
| 991 |
+
fn=restore_client_data,
|
| 992 |
+
inputs=[restore_client_id],
|
| 993 |
+
outputs=[restore_output],
|
| 994 |
+
)
|
| 995 |
+
|
| 996 |
+
with gr.Tab("π Documentation"):
|
| 997 |
+
gr.Markdown(
|
| 998 |
+
"""
|
| 999 |
+
## π― Usage Guide
|
| 1000 |
+
|
| 1001 |
+
### Basic Workflow
|
| 1002 |
+
|
| 1003 |
+
1. **Store Memories**: Use the "Memory Storage" tab to add text chunks
|
| 1004 |
+
2. **Build Video**: Create an MP4 memory file from your stored chunks
|
| 1005 |
+
3. **Search**: Find relevant information using semantic search
|
| 1006 |
+
4. **Chat**: Have conversations with your stored knowledge
|
| 1007 |
+
|
| 1008 |
+
### MCP Integration
|
| 1009 |
+
|
| 1010 |
+
This server exposes the following MCP tools:
|
| 1011 |
+
|
| 1012 |
+
**Memory Operations:**
|
| 1013 |
+
- `store_memory(text, client_id, metadata)` - Store text in memory
|
| 1014 |
+
- `build_memory_video(client_id, memory_name)` - Build MP4 from chunks
|
| 1015 |
+
- `search_memory(query, client_id, memory_name, top_k)` - Semantic search
|
| 1016 |
+
- `chat_with_memory(query, client_id, memory_name)` - Interactive chat
|
| 1017 |
+
- `list_memories(client_id)` - List all memories
|
| 1018 |
+
- `get_memory_stats(client_id)` - Get usage statistics
|
| 1019 |
+
- `delete_memory(client_id, memory_name)` - Delete memories
|
| 1020 |
+
- `store_document(content, doc_type, client_id)` - Store documents
|
| 1021 |
+
|
| 1022 |
+
**HuggingFace Dataset Integration:**
|
| 1023 |
+
- `save_to_hf_dataset(client_id, dataset_name, private)` - Save to specific HF dataset
|
| 1024 |
+
- `load_from_hf_dataset(client_id, dataset_name)` - Load from specific HF dataset
|
| 1025 |
+
- `list_hf_datasets()` - List available HF datasets
|
| 1026 |
+
- `create_hf_dataset(dataset_name, private, description)` - Create new HF dataset
|
| 1027 |
+
- `get_storage_info()` - Get HF storage connection status
|
| 1028 |
+
- `backup_client_data(client_id)` - Backup to default HF dataset
|
| 1029 |
+
- `restore_client_data(client_id)` - Restore from default HF dataset
|
| 1030 |
+
|
| 1031 |
+
### Client Isolation
|
| 1032 |
+
|
| 1033 |
+
Each `client_id` gets its own isolated storage space:
|
| 1034 |
+
```
|
| 1035 |
+
data/
|
| 1036 |
+
βββ client_1/
|
| 1037 |
+
β βββ chunks/
|
| 1038 |
+
β βββ videos/
|
| 1039 |
+
β βββ metadata.json
|
| 1040 |
+
βββ client_2/
|
| 1041 |
+
βββ chunks/
|
| 1042 |
+
βββ videos/
|
| 1043 |
+
βββ metadata.json
|
| 1044 |
+
```
|
| 1045 |
+
|
| 1046 |
+
### Best Practices
|
| 1047 |
+
|
| 1048 |
+
- Use descriptive `client_id` values (e.g., "user_123", "project_ai")
|
| 1049 |
+
- Build memory videos after storing multiple chunks for efficiency
|
| 1050 |
+
- Use meaningful memory names for organization
|
| 1051 |
+
- Include metadata for better organization and retrieval
|
| 1052 |
+
|
| 1053 |
+
### Powered by Memvid
|
| 1054 |
+
|
| 1055 |
+
This server uses the [memvid library](https://github.com/Olow304/memvid) which:
|
| 1056 |
+
- Stores text chunks in MP4 video files
|
| 1057 |
+
- Provides lightning-fast semantic search
|
| 1058 |
+
- Requires no external database
|
| 1059 |
+
- Supports millions of text chunks
|
| 1060 |
+
- Works completely offline
|
| 1061 |
+
|
| 1062 |
+
### Error Handling
|
| 1063 |
+
|
| 1064 |
+
All functions include comprehensive error handling and return descriptive error messages.
|
| 1065 |
+
Check the output for detailed information about any issues.
|
| 1066 |
+
"""
|
| 1067 |
+
)
|
| 1068 |
+
|
| 1069 |
+
|
| 1070 |
+
if __name__ == "__main__":
|
| 1071 |
+
# Launch with MCP server enabled
|
| 1072 |
+
try:
|
| 1073 |
+
demo.launch(
|
| 1074 |
+
mcp_server=True, # CRITICAL: Enable MCP server
|
| 1075 |
+
share=False,
|
| 1076 |
+
server_name="0.0.0.0",
|
| 1077 |
+
server_port=7860,
|
| 1078 |
+
show_error=True,
|
| 1079 |
+
)
|
| 1080 |
+
except Exception as e:
|
| 1081 |
+
print(f"Error launching server: {e}")
|
| 1082 |
+
# Fallback launch without MCP for debugging
|
| 1083 |
+
demo.launch(
|
| 1084 |
+
share=False, server_name="0.0.0.0", server_port=7860, show_error=True
|
| 1085 |
+
)
|
modal_memvid_service.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modal Memvid Service - GPU-accelerated video memory processing
|
| 3 |
+
|
| 4 |
+
This service provides:
|
| 5 |
+
- GPU-accelerated video processing using memvid library
|
| 6 |
+
- QR code generation and decoding optimization
|
| 7 |
+
- Modal object storage for MP4 files
|
| 8 |
+
- Auto-scaling based on video processing workload
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import time
|
| 13 |
+
import json
|
| 14 |
+
import modal
|
| 15 |
+
from typing import List, Dict, Any, Optional
|
| 16 |
+
|
| 17 |
+
# Modal App Configuration
|
| 18 |
+
app = modal.App("memvid-video-service")
|
| 19 |
+
|
| 20 |
+
# Docker image with all video processing dependencies
|
| 21 |
+
memvid_image = (
|
| 22 |
+
modal.Image.debian_slim()
|
| 23 |
+
.pip_install(
|
| 24 |
+
[
|
| 25 |
+
"memvid>=0.1.0",
|
| 26 |
+
"opencv-python-headless>=4.8.0",
|
| 27 |
+
"pillow>=9.5.0",
|
| 28 |
+
"qrcode>=7.4.2",
|
| 29 |
+
"pyzbar>=0.1.9", # QR code decoding
|
| 30 |
+
"numpy>=1.24.0",
|
| 31 |
+
"torch>=2.0.0", # PyTorch for GPU acceleration
|
| 32 |
+
]
|
| 33 |
+
)
|
| 34 |
+
.apt_install(
|
| 35 |
+
[
|
| 36 |
+
"libzbar0", # For QR code decoding
|
| 37 |
+
"ffmpeg", # For video processing
|
| 38 |
+
"libgl1-mesa-glx", # OpenCV dependencies
|
| 39 |
+
"libglib2.0-0",
|
| 40 |
+
]
|
| 41 |
+
)
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Volume for persistent video storage
|
| 45 |
+
videos_volume = modal.Volume.from_name("memvid-videos", create_if_missing=True)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@app.function(
|
| 49 |
+
image=memvid_image,
|
| 50 |
+
gpu="T4", # GPU optimized for video processing
|
| 51 |
+
volumes={"/storage": videos_volume},
|
| 52 |
+
timeout=900, # 15 minutes timeout for video processing
|
| 53 |
+
cpu=4.0, # More CPU for video encoding
|
| 54 |
+
memory=8192, # 8GB RAM for video processing
|
| 55 |
+
)
|
| 56 |
+
def process_video_memory(
|
| 57 |
+
text: str, client_id: str, metadata: Dict[str, Any]
|
| 58 |
+
) -> Dict[str, Any]:
|
| 59 |
+
"""
|
| 60 |
+
GPU-accelerated video memory processing on Modal
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
text: Text content to store as video memory
|
| 64 |
+
client_id: Unique identifier for the client/user
|
| 65 |
+
metadata: Additional metadata for the memory
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Dict with processing results and metrics
|
| 69 |
+
"""
|
| 70 |
+
import sys
|
| 71 |
+
|
| 72 |
+
sys.path.append("/storage")
|
| 73 |
+
|
| 74 |
+
from memvid import MemvidEncoder, MemvidRetriever
|
| 75 |
+
import shutil
|
| 76 |
+
import uuid
|
| 77 |
+
|
| 78 |
+
start_time = time.time()
|
| 79 |
+
processing_metrics = {"gpu_used": "T4", "cpu_count": 4, "memory_gb": 8}
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
# Setup storage paths in Modal volume
|
| 83 |
+
client_storage_path = f"/storage/{client_id}"
|
| 84 |
+
os.makedirs(client_storage_path, exist_ok=True)
|
| 85 |
+
|
| 86 |
+
print(f"π¬ Processing video memory for client: {client_id}")
|
| 87 |
+
print(f"π Text content: {text[:100]}...")
|
| 88 |
+
|
| 89 |
+
# Initialize memvid encoder with Modal storage
|
| 90 |
+
encoder = MemvidEncoder()
|
| 91 |
+
|
| 92 |
+
# Process video memory with GPU acceleration
|
| 93 |
+
video_start_time = time.time()
|
| 94 |
+
|
| 95 |
+
# Add text to encoder and build video
|
| 96 |
+
encoder.add_text(text)
|
| 97 |
+
|
| 98 |
+
# Create output paths
|
| 99 |
+
video_file = f"{client_storage_path}/videos/memory_{int(time.time())}.mp4"
|
| 100 |
+
index_file = (
|
| 101 |
+
f"{client_storage_path}/videos/memory_{int(time.time())}_index.json"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Ensure directories exist
|
| 105 |
+
os.makedirs(os.path.dirname(video_file), exist_ok=True)
|
| 106 |
+
|
| 107 |
+
# Build video with QR codes
|
| 108 |
+
result = encoder.build_video(video_file, index_file)
|
| 109 |
+
|
| 110 |
+
video_processing_time = time.time() - video_start_time
|
| 111 |
+
processing_metrics["video_processing_time"] = video_processing_time
|
| 112 |
+
|
| 113 |
+
# Get file information
|
| 114 |
+
video_files = []
|
| 115 |
+
chunk_files = []
|
| 116 |
+
|
| 117 |
+
if os.path.exists(client_storage_path):
|
| 118 |
+
# Find video files
|
| 119 |
+
videos_dir = os.path.join(client_storage_path, "videos")
|
| 120 |
+
if os.path.exists(videos_dir):
|
| 121 |
+
for file in os.listdir(videos_dir):
|
| 122 |
+
if file.endswith(".mp4"):
|
| 123 |
+
file_path = os.path.join(videos_dir, file)
|
| 124 |
+
file_size = os.path.getsize(file_path)
|
| 125 |
+
video_files.append(
|
| 126 |
+
{
|
| 127 |
+
"filename": file,
|
| 128 |
+
"size_bytes": file_size,
|
| 129 |
+
"path": file_path,
|
| 130 |
+
}
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Find chunk files
|
| 134 |
+
chunks_dir = os.path.join(client_storage_path, "chunks")
|
| 135 |
+
if os.path.exists(chunks_dir):
|
| 136 |
+
for file in os.listdir(chunks_dir):
|
| 137 |
+
if file.endswith(".txt"):
|
| 138 |
+
file_path = os.path.join(chunks_dir, file)
|
| 139 |
+
file_size = os.path.getsize(file_path)
|
| 140 |
+
chunk_files.append(
|
| 141 |
+
{
|
| 142 |
+
"filename": file,
|
| 143 |
+
"size_bytes": file_size,
|
| 144 |
+
"path": file_path,
|
| 145 |
+
}
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Calculate storage metrics
|
| 149 |
+
total_video_size = sum(f["size_bytes"] for f in video_files)
|
| 150 |
+
total_chunks_size = sum(f["size_bytes"] for f in chunk_files)
|
| 151 |
+
|
| 152 |
+
processing_metrics.update(
|
| 153 |
+
{
|
| 154 |
+
"video_files_count": len(video_files),
|
| 155 |
+
"chunk_files_count": len(chunk_files),
|
| 156 |
+
"total_video_size": total_video_size,
|
| 157 |
+
"total_chunks_size": total_chunks_size,
|
| 158 |
+
"total_storage_size": total_video_size + total_chunks_size,
|
| 159 |
+
}
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Generate unique memory ID
|
| 163 |
+
memory_id = f"modal_video_{client_id}_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
| 164 |
+
|
| 165 |
+
total_time = time.time() - start_time
|
| 166 |
+
processing_metrics["total_time"] = total_time
|
| 167 |
+
|
| 168 |
+
print(f"β
Video memory processed successfully")
|
| 169 |
+
print(f"π Created {len(video_files)} videos, {len(chunk_files)} chunks")
|
| 170 |
+
print(f"πΎ Total storage: {total_video_size + total_chunks_size} bytes")
|
| 171 |
+
print(f"β±οΈ Processing time: {total_time:.2f}s")
|
| 172 |
+
|
| 173 |
+
return {
|
| 174 |
+
"success": True,
|
| 175 |
+
"memory_id": memory_id,
|
| 176 |
+
"client_id": client_id,
|
| 177 |
+
"video_files": video_files,
|
| 178 |
+
"chunk_files": chunk_files,
|
| 179 |
+
"processing_metrics": processing_metrics,
|
| 180 |
+
"metadata": metadata,
|
| 181 |
+
"storage_path": client_storage_path,
|
| 182 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
print(f"β Error in video processing: {str(e)}")
|
| 187 |
+
processing_metrics["error_time"] = time.time() - start_time
|
| 188 |
+
|
| 189 |
+
return {
|
| 190 |
+
"success": False,
|
| 191 |
+
"error": str(e),
|
| 192 |
+
"processing_metrics": processing_metrics,
|
| 193 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
@app.function(
|
| 198 |
+
image=memvid_image,
|
| 199 |
+
gpu="T4",
|
| 200 |
+
volumes={"/storage": videos_volume},
|
| 201 |
+
timeout=600, # 10 minutes timeout for search operations
|
| 202 |
+
cpu=2.0,
|
| 203 |
+
memory=4096, # 4GB RAM for search
|
| 204 |
+
)
|
| 205 |
+
def search_video_memory(
|
| 206 |
+
query: str, client_id: str, memory_name: Optional[str] = None, top_k: int = 5
|
| 207 |
+
) -> Dict[str, Any]:
|
| 208 |
+
"""
|
| 209 |
+
GPU-accelerated video memory search on Modal
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
query: Search query text
|
| 213 |
+
client_id: Client identifier to search within
|
| 214 |
+
memory_name: Optional specific memory name filter
|
| 215 |
+
top_k: Number of top results to return
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
Dict with search results and metrics
|
| 219 |
+
"""
|
| 220 |
+
import sys
|
| 221 |
+
|
| 222 |
+
sys.path.append("/storage")
|
| 223 |
+
|
| 224 |
+
from memvid import MemvidEncoder, MemvidRetriever
|
| 225 |
+
|
| 226 |
+
start_time = time.time()
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
print(f"π Searching video memory for query: {query}")
|
| 230 |
+
print(f"π€ Client: {client_id}")
|
| 231 |
+
|
| 232 |
+
# Initialize memvid retriever with Modal storage
|
| 233 |
+
client_storage_path = f"/storage/{client_id}"
|
| 234 |
+
|
| 235 |
+
# Find video files for this client
|
| 236 |
+
videos_dir = os.path.join(client_storage_path, "videos")
|
| 237 |
+
video_files = []
|
| 238 |
+
if os.path.exists(videos_dir):
|
| 239 |
+
for file in os.listdir(videos_dir):
|
| 240 |
+
if file.endswith(".mp4"):
|
| 241 |
+
video_files.append(os.path.join(videos_dir, file))
|
| 242 |
+
|
| 243 |
+
if not video_files:
|
| 244 |
+
return {
|
| 245 |
+
"success": True,
|
| 246 |
+
"query": query,
|
| 247 |
+
"client_id": client_id,
|
| 248 |
+
"results": [],
|
| 249 |
+
"total_results": 0,
|
| 250 |
+
"message": "No video memories found for this client",
|
| 251 |
+
"processing_metrics": {
|
| 252 |
+
"search_time": 0,
|
| 253 |
+
"total_time": time.time() - start_time,
|
| 254 |
+
"gpu_used": "T4",
|
| 255 |
+
"infrastructure": "Modal + Video Processing",
|
| 256 |
+
},
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
# Perform video-based search
|
| 260 |
+
search_start_time = time.time()
|
| 261 |
+
|
| 262 |
+
# Search through available video files
|
| 263 |
+
results = []
|
| 264 |
+
|
| 265 |
+
for video_file in video_files[:1]: # Search first video for now
|
| 266 |
+
try:
|
| 267 |
+
# Find corresponding index file
|
| 268 |
+
index_file = video_file.replace(".mp4", "_index.json")
|
| 269 |
+
if not os.path.exists(index_file):
|
| 270 |
+
# Try alternative index file naming
|
| 271 |
+
index_file = video_file.replace(".mp4", ".json")
|
| 272 |
+
if not os.path.exists(index_file):
|
| 273 |
+
print(f"No index file found for {video_file}")
|
| 274 |
+
continue
|
| 275 |
+
|
| 276 |
+
# Initialize retriever with video and index files
|
| 277 |
+
retriever = MemvidRetriever(video_file, index_file)
|
| 278 |
+
video_results = retriever.search(query, top_k=top_k)
|
| 279 |
+
|
| 280 |
+
if video_results:
|
| 281 |
+
results.extend(video_results)
|
| 282 |
+
except Exception as e:
|
| 283 |
+
print(f"Error searching video {video_file}: {e}")
|
| 284 |
+
continue
|
| 285 |
+
|
| 286 |
+
search_time = time.time() - search_start_time
|
| 287 |
+
|
| 288 |
+
# Format results for consistency
|
| 289 |
+
formatted_results = []
|
| 290 |
+
if isinstance(results, list):
|
| 291 |
+
for i, result in enumerate(results[:top_k]):
|
| 292 |
+
if isinstance(result, dict):
|
| 293 |
+
formatted_results.append(
|
| 294 |
+
{
|
| 295 |
+
"memory_id": result.get("id", f"video_result_{i}"),
|
| 296 |
+
"text": result.get("text", result.get("content", "")),
|
| 297 |
+
"metadata": result.get("metadata", {}),
|
| 298 |
+
"similarity_score": result.get(
|
| 299 |
+
"score", 0.8
|
| 300 |
+
), # Default score
|
| 301 |
+
"video_file": result.get("video_file", ""),
|
| 302 |
+
"chunk_file": result.get("chunk_file", ""),
|
| 303 |
+
}
|
| 304 |
+
)
|
| 305 |
+
elif isinstance(result, str):
|
| 306 |
+
formatted_results.append(
|
| 307 |
+
{
|
| 308 |
+
"memory_id": f"video_result_{i}",
|
| 309 |
+
"text": result,
|
| 310 |
+
"metadata": {},
|
| 311 |
+
"similarity_score": 0.75,
|
| 312 |
+
"video_file": "",
|
| 313 |
+
"chunk_file": "",
|
| 314 |
+
}
|
| 315 |
+
)
|
| 316 |
+
elif isinstance(results, str):
|
| 317 |
+
# Single result
|
| 318 |
+
formatted_results.append(
|
| 319 |
+
{
|
| 320 |
+
"memory_id": "video_result_0",
|
| 321 |
+
"text": results,
|
| 322 |
+
"metadata": {},
|
| 323 |
+
"similarity_score": 0.8,
|
| 324 |
+
"video_file": "",
|
| 325 |
+
"chunk_file": "",
|
| 326 |
+
}
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
total_time = time.time() - start_time
|
| 330 |
+
|
| 331 |
+
print(f"β
Video search completed")
|
| 332 |
+
print(f"π Found {len(formatted_results)} results")
|
| 333 |
+
print(f"β±οΈ Search time: {search_time:.2f}s, Total time: {total_time:.2f}s")
|
| 334 |
+
|
| 335 |
+
return {
|
| 336 |
+
"success": True,
|
| 337 |
+
"query": query,
|
| 338 |
+
"client_id": client_id,
|
| 339 |
+
"results": formatted_results,
|
| 340 |
+
"total_results": len(formatted_results),
|
| 341 |
+
"processing_metrics": {
|
| 342 |
+
"search_time": search_time,
|
| 343 |
+
"total_time": total_time,
|
| 344 |
+
"gpu_used": "T4",
|
| 345 |
+
"infrastructure": "Modal + Video Processing",
|
| 346 |
+
},
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"β Error in video search: {str(e)}")
|
| 351 |
+
return {
|
| 352 |
+
"success": False,
|
| 353 |
+
"error": str(e),
|
| 354 |
+
"processing_time": time.time() - start_time,
|
| 355 |
+
"results": [],
|
| 356 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
@app.function(
|
| 361 |
+
image=memvid_image,
|
| 362 |
+
volumes={"/storage": videos_volume},
|
| 363 |
+
timeout=60,
|
| 364 |
+
)
|
| 365 |
+
def get_video_stats(client_id: str) -> Dict[str, Any]:
|
| 366 |
+
"""
|
| 367 |
+
Get statistics for video storage
|
| 368 |
+
|
| 369 |
+
Args:
|
| 370 |
+
client_id: Client identifier
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
Dict with storage statistics
|
| 374 |
+
"""
|
| 375 |
+
import os
|
| 376 |
+
import json
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
client_storage_path = f"/storage/{client_id}"
|
| 380 |
+
|
| 381 |
+
if not os.path.exists(client_storage_path):
|
| 382 |
+
return {
|
| 383 |
+
"client_id": client_id,
|
| 384 |
+
"storage_type": "modal_video",
|
| 385 |
+
"memory_count": 0,
|
| 386 |
+
"total_video_size": 0,
|
| 387 |
+
"total_chunks": 0,
|
| 388 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
# Count video files
|
| 392 |
+
videos_dir = os.path.join(client_storage_path, "videos")
|
| 393 |
+
video_count = 0
|
| 394 |
+
total_video_size = 0
|
| 395 |
+
|
| 396 |
+
if os.path.exists(videos_dir):
|
| 397 |
+
for file in os.listdir(videos_dir):
|
| 398 |
+
if file.endswith(".mp4"):
|
| 399 |
+
video_count += 1
|
| 400 |
+
file_path = os.path.join(videos_dir, file)
|
| 401 |
+
total_video_size += os.path.getsize(file_path)
|
| 402 |
+
|
| 403 |
+
# Count chunk files
|
| 404 |
+
chunks_dir = os.path.join(client_storage_path, "chunks")
|
| 405 |
+
chunk_count = 0
|
| 406 |
+
total_chunks_size = 0
|
| 407 |
+
|
| 408 |
+
if os.path.exists(chunks_dir):
|
| 409 |
+
for file in os.listdir(chunks_dir):
|
| 410 |
+
if file.endswith(".txt"):
|
| 411 |
+
chunk_count += 1
|
| 412 |
+
file_path = os.path.join(chunks_dir, file)
|
| 413 |
+
total_chunks_size += os.path.getsize(file_path)
|
| 414 |
+
|
| 415 |
+
# Get metadata if available
|
| 416 |
+
metadata_file = os.path.join(client_storage_path, "metadata.json")
|
| 417 |
+
first_memory = None
|
| 418 |
+
last_memory = None
|
| 419 |
+
|
| 420 |
+
if os.path.exists(metadata_file):
|
| 421 |
+
try:
|
| 422 |
+
with open(metadata_file, "r") as f:
|
| 423 |
+
metadata = json.load(f)
|
| 424 |
+
# Extract creation times if available
|
| 425 |
+
first_memory = metadata.get("first_memory")
|
| 426 |
+
last_memory = metadata.get("last_memory")
|
| 427 |
+
except:
|
| 428 |
+
pass
|
| 429 |
+
|
| 430 |
+
return {
|
| 431 |
+
"client_id": client_id,
|
| 432 |
+
"storage_type": "modal_video",
|
| 433 |
+
"memory_count": video_count,
|
| 434 |
+
"total_video_size": total_video_size,
|
| 435 |
+
"total_chunks": chunk_count,
|
| 436 |
+
"total_chunks_size": total_chunks_size,
|
| 437 |
+
"total_storage_size": total_video_size + total_chunks_size,
|
| 438 |
+
"first_memory": first_memory,
|
| 439 |
+
"last_memory": last_memory,
|
| 440 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 441 |
+
"storage_path": client_storage_path,
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
except Exception as e:
|
| 445 |
+
return {
|
| 446 |
+
"client_id": client_id,
|
| 447 |
+
"storage_type": "modal_video",
|
| 448 |
+
"error": str(e),
|
| 449 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
# Client class for easy integration with DualStorageManager
|
| 454 |
+
class ModalMemvidClient:
|
| 455 |
+
"""Client for interacting with Modal Memvid Service"""
|
| 456 |
+
|
| 457 |
+
def __init__(self, modal_token: Optional[str] = None):
|
| 458 |
+
"""
|
| 459 |
+
Initialize Modal Memvid Client
|
| 460 |
+
|
| 461 |
+
Args:
|
| 462 |
+
modal_token: Optional Modal token (uses environment if not provided)
|
| 463 |
+
"""
|
| 464 |
+
if modal_token:
|
| 465 |
+
os.environ["MODAL_TOKEN"] = modal_token
|
| 466 |
+
|
| 467 |
+
# Test Modal connection
|
| 468 |
+
try:
|
| 469 |
+
import modal
|
| 470 |
+
|
| 471 |
+
print("β
Modal Memvid Client initialized successfully")
|
| 472 |
+
except Exception as e:
|
| 473 |
+
print(f"β οΈ Modal Memvid Client initialization warning: {e}")
|
| 474 |
+
|
| 475 |
+
def store_memory(
|
| 476 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 477 |
+
) -> Dict[str, Any]:
|
| 478 |
+
"""Store memory using Modal memvid service"""
|
| 479 |
+
try:
|
| 480 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 481 |
+
import modal
|
| 482 |
+
|
| 483 |
+
func = modal.Function.from_name(
|
| 484 |
+
"memvid-video-service", "process_video_memory"
|
| 485 |
+
)
|
| 486 |
+
return func.remote(text, client_id, metadata)
|
| 487 |
+
except Exception as e:
|
| 488 |
+
return {"success": False, "error": f"Modal memvid storage failed: {e}"}
|
| 489 |
+
|
| 490 |
+
def search_memory(
|
| 491 |
+
self,
|
| 492 |
+
query: str,
|
| 493 |
+
client_id: str,
|
| 494 |
+
memory_name: Optional[str] = None,
|
| 495 |
+
top_k: int = 5,
|
| 496 |
+
) -> Dict[str, Any]:
|
| 497 |
+
"""Search memory using Modal memvid service"""
|
| 498 |
+
try:
|
| 499 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 500 |
+
import modal
|
| 501 |
+
|
| 502 |
+
func = modal.Function.from_name(
|
| 503 |
+
"memvid-video-service", "search_video_memory"
|
| 504 |
+
)
|
| 505 |
+
return func.remote(query, client_id, memory_name, top_k)
|
| 506 |
+
except Exception as e:
|
| 507 |
+
return {
|
| 508 |
+
"success": False,
|
| 509 |
+
"error": f"Modal memvid search failed: {e}",
|
| 510 |
+
"results": [],
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
def get_stats(self, client_id: str) -> Dict[str, Any]:
|
| 514 |
+
"""Get statistics using Modal memvid service"""
|
| 515 |
+
try:
|
| 516 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 517 |
+
import modal
|
| 518 |
+
|
| 519 |
+
func = modal.Function.from_name("memvid-video-service", "get_video_stats")
|
| 520 |
+
return func.remote(client_id)
|
| 521 |
+
except Exception as e:
|
| 522 |
+
return {"success": False, "error": f"Modal memvid stats failed: {e}"}
|
| 523 |
+
|
| 524 |
+
def list_memories(self, client_id: str) -> str:
|
| 525 |
+
"""List memories for client (Modal implementation)"""
|
| 526 |
+
try:
|
| 527 |
+
stats = self.get_stats(client_id)
|
| 528 |
+
if stats.get(
|
| 529 |
+
"success", True
|
| 530 |
+
): # Modal stats don't have success field currently
|
| 531 |
+
memory_list = {
|
| 532 |
+
"client_id": client_id,
|
| 533 |
+
"storage_type": "modal_video",
|
| 534 |
+
"memory_count": stats.get("memory_count", 0),
|
| 535 |
+
"memories": [], # Modal doesn't currently track individual memory names
|
| 536 |
+
"total_size": stats.get("total_storage_size", 0),
|
| 537 |
+
"infrastructure": "Modal + T4 GPU + Volume Storage",
|
| 538 |
+
}
|
| 539 |
+
return json.dumps(memory_list, indent=2)
|
| 540 |
+
else:
|
| 541 |
+
return json.dumps(
|
| 542 |
+
{
|
| 543 |
+
"error": f"Failed to list memories: {stats.get('error', 'Unknown error')}"
|
| 544 |
+
}
|
| 545 |
+
)
|
| 546 |
+
except Exception as e:
|
| 547 |
+
return json.dumps({"error": f"Modal memvid list_memories failed: {e}"})
|
| 548 |
+
|
| 549 |
+
def build_memory_video(self, client_id: str, memory_name: str) -> str:
|
| 550 |
+
"""Build memory video (Modal implementation)"""
|
| 551 |
+
# For Modal, videos are built automatically during storage
|
| 552 |
+
return f"Memory videos are automatically built during storage in Modal for client {client_id}. Memory name: {memory_name}"
|
| 553 |
+
|
| 554 |
+
def chat_with_memory(self, query: str, client_id: str, memory_name: str) -> str:
|
| 555 |
+
"""Chat with memory using Modal memvid service"""
|
| 556 |
+
try:
|
| 557 |
+
# Use search as basis for chat
|
| 558 |
+
search_results = self.search_memory(query, client_id, memory_name, top_k=3)
|
| 559 |
+
|
| 560 |
+
if search_results.get("success", False):
|
| 561 |
+
results = search_results.get("results", [])
|
| 562 |
+
if results:
|
| 563 |
+
# Simple chat response based on search results
|
| 564 |
+
context = "\n".join(
|
| 565 |
+
[result.get("text", "") for result in results[:2]]
|
| 566 |
+
)
|
| 567 |
+
response = f"Based on your memories: {context}\n\nYour query '{query}' relates to the stored information above."
|
| 568 |
+
return response
|
| 569 |
+
else:
|
| 570 |
+
return f"I couldn't find any relevant memories for '{query}' in your video storage."
|
| 571 |
+
else:
|
| 572 |
+
return f"Error accessing memories: {search_results.get('error', 'Unknown error')}"
|
| 573 |
+
|
| 574 |
+
except Exception as e:
|
| 575 |
+
return f"Modal memvid chat failed: {e}"
|
| 576 |
+
|
| 577 |
+
def delete_memory(self, client_id: str, memory_name: str) -> str:
|
| 578 |
+
"""Delete memory (Modal implementation)"""
|
| 579 |
+
# Modal currently doesn't support selective deletion
|
| 580 |
+
return f"Memory deletion not yet implemented in Modal for client {client_id}, memory {memory_name}"
|
| 581 |
+
|
| 582 |
+
def get_memory_stats(self, client_id: str) -> str:
|
| 583 |
+
"""Get memory statistics as JSON string"""
|
| 584 |
+
try:
|
| 585 |
+
stats = self.get_stats(client_id)
|
| 586 |
+
return json.dumps(stats, indent=2)
|
| 587 |
+
except Exception as e:
|
| 588 |
+
return json.dumps({"error": f"Modal memvid get_memory_stats failed: {e}"})
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
if __name__ == "__main__":
|
| 592 |
+
# Test the Modal functions locally
|
| 593 |
+
print("π§ͺ Testing Modal Memvid Service...")
|
| 594 |
+
|
| 595 |
+
# Test client
|
| 596 |
+
client = ModalMemvidClient()
|
| 597 |
+
|
| 598 |
+
# Test storage
|
| 599 |
+
result = client.store_memory(
|
| 600 |
+
"This is a test memory for Modal video storage with GPU acceleration",
|
| 601 |
+
"test_client",
|
| 602 |
+
{"test": True, "timestamp": time.time()},
|
| 603 |
+
)
|
| 604 |
+
print(f"π¬ Storage result: {result}")
|
| 605 |
+
|
| 606 |
+
# Test search
|
| 607 |
+
search_result = client.search_memory("test memory GPU", "test_client", top_k=3)
|
| 608 |
+
print(f"π Search result: {search_result}")
|
| 609 |
+
|
| 610 |
+
# Test stats
|
| 611 |
+
stats = client.get_stats("test_client")
|
| 612 |
+
print(f"οΏ½οΏ½ Stats: {stats}")
|
modal_vector_service.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modal Vector Service - GPU-accelerated vector memory processing
|
| 3 |
+
|
| 4 |
+
This service provides:
|
| 5 |
+
- GPU-accelerated embedding generation using sentence-transformers
|
| 6 |
+
- FAISS with Modal Volume storage for scalable vector search
|
| 7 |
+
- FAISS for fast similarity search optimization
|
| 8 |
+
- Auto-scaling based on workload
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import time
|
| 13 |
+
import json
|
| 14 |
+
import modal
|
| 15 |
+
import asyncio
|
| 16 |
+
from typing import List, Dict, Any, Optional
|
| 17 |
+
|
| 18 |
+
# Modal App Configuration
|
| 19 |
+
app = modal.App("memvid-vector-service")
|
| 20 |
+
|
| 21 |
+
# Docker image with all vector processing dependencies
|
| 22 |
+
vector_image = modal.Image.debian_slim().pip_install(
|
| 23 |
+
[
|
| 24 |
+
"sentence-transformers>=2.0.0",
|
| 25 |
+
"faiss-cpu>=1.8.0",
|
| 26 |
+
"numpy>=1.24.0",
|
| 27 |
+
"scikit-learn>=1.3.0", # For additional vector operations
|
| 28 |
+
]
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Volume for persistent model storage
|
| 32 |
+
models_volume = modal.Volume.from_name("vector-models", create_if_missing=True)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.function(
|
| 36 |
+
image=vector_image,
|
| 37 |
+
gpu="A100", # High-performance GPU for embedding generation
|
| 38 |
+
volumes={"/models": models_volume},
|
| 39 |
+
timeout=600, # 10 minutes timeout for large operations
|
| 40 |
+
)
|
| 41 |
+
def process_vector_memory(
|
| 42 |
+
text: str, client_id: str, metadata: Dict[str, Any]
|
| 43 |
+
) -> Dict[str, Any]:
|
| 44 |
+
"""
|
| 45 |
+
GPU-accelerated vector memory processing on Modal
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
text: Text content to store as vector embeddings
|
| 49 |
+
client_id: Unique identifier for the client/user
|
| 50 |
+
metadata: Additional metadata for the memory
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Dict with processing results and metrics
|
| 54 |
+
"""
|
| 55 |
+
import numpy as np
|
| 56 |
+
from sentence_transformers import SentenceTransformer
|
| 57 |
+
import json
|
| 58 |
+
|
| 59 |
+
start_time = time.time()
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
# Load or download sentence transformer model (cached in volume)
|
| 63 |
+
model_path = "/models/sentence-transformer"
|
| 64 |
+
if not os.path.exists(model_path):
|
| 65 |
+
print("π₯ Downloading sentence transformer model...")
|
| 66 |
+
model = SentenceTransformer("all-MiniLM-L6-v2", device="cuda")
|
| 67 |
+
model.save(model_path)
|
| 68 |
+
else:
|
| 69 |
+
print("π Loading cached sentence transformer model...")
|
| 70 |
+
model = SentenceTransformer(model_path, device="cuda")
|
| 71 |
+
|
| 72 |
+
# Generate embeddings on GPU
|
| 73 |
+
print(f"π Generating embeddings for text: {text[:100]}...")
|
| 74 |
+
embeddings = model.encode([text], device="cuda")
|
| 75 |
+
embedding_vector = embeddings[0].tolist() # Convert to list for JSON storage
|
| 76 |
+
|
| 77 |
+
# Calculate processing metrics
|
| 78 |
+
embedding_time = time.time() - start_time
|
| 79 |
+
|
| 80 |
+
# Store vector in Modal Volume with FAISS index
|
| 81 |
+
import faiss
|
| 82 |
+
import pickle
|
| 83 |
+
|
| 84 |
+
storage_path = f"/models/vectors/{client_id}"
|
| 85 |
+
os.makedirs(storage_path, exist_ok=True)
|
| 86 |
+
|
| 87 |
+
# Load or create FAISS index
|
| 88 |
+
index_path = f"{storage_path}/faiss_index.bin"
|
| 89 |
+
metadata_path = f"{storage_path}/metadata.json"
|
| 90 |
+
|
| 91 |
+
if os.path.exists(index_path):
|
| 92 |
+
print("π Loading existing FAISS index...")
|
| 93 |
+
index = faiss.read_index(index_path)
|
| 94 |
+
with open(metadata_path, "r") as f:
|
| 95 |
+
all_metadata = json.load(f)
|
| 96 |
+
else:
|
| 97 |
+
print("π Creating new FAISS index...")
|
| 98 |
+
# Create FAISS index for 384-dimensional vectors
|
| 99 |
+
index = faiss.IndexFlatIP(384) # Inner product for cosine similarity
|
| 100 |
+
all_metadata = []
|
| 101 |
+
|
| 102 |
+
# Add vector to index
|
| 103 |
+
vector_array = np.array([embedding_vector], dtype=np.float32)
|
| 104 |
+
# Normalize for cosine similarity
|
| 105 |
+
faiss.normalize_L2(vector_array)
|
| 106 |
+
index.add(vector_array)
|
| 107 |
+
|
| 108 |
+
# Store metadata
|
| 109 |
+
memory_id = f"vector_{len(all_metadata)}"
|
| 110 |
+
memory_metadata = {
|
| 111 |
+
"id": memory_id,
|
| 112 |
+
"client_id": client_id,
|
| 113 |
+
"text": text,
|
| 114 |
+
"metadata": metadata,
|
| 115 |
+
"created_at": time.time(),
|
| 116 |
+
}
|
| 117 |
+
all_metadata.append(memory_metadata)
|
| 118 |
+
|
| 119 |
+
# Save updated index and metadata
|
| 120 |
+
faiss.write_index(index, index_path)
|
| 121 |
+
with open(metadata_path, "w") as f:
|
| 122 |
+
json.dump(all_metadata, f)
|
| 123 |
+
|
| 124 |
+
print(
|
| 125 |
+
f"β
Vector memory stored with ID: {memory_id} (FAISS index size: {index.ntotal})"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
total_time = time.time() - start_time
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
"success": True,
|
| 132 |
+
"memory_id": memory_id,
|
| 133 |
+
"client_id": client_id,
|
| 134 |
+
"embedding_dim": len(embedding_vector),
|
| 135 |
+
"embedding_preview": embedding_vector[:5], # First 5 dimensions for preview
|
| 136 |
+
"processing_metrics": {
|
| 137 |
+
"embedding_time": embedding_time,
|
| 138 |
+
"total_time": total_time,
|
| 139 |
+
"storage_size": len(embedding_vector) * 4, # 4 bytes per float32
|
| 140 |
+
"gpu_used": "A100",
|
| 141 |
+
"model_used": "all-MiniLM-L6-v2",
|
| 142 |
+
},
|
| 143 |
+
"metadata": metadata,
|
| 144 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print(f"β Error in vector processing: {str(e)}")
|
| 149 |
+
return {
|
| 150 |
+
"success": False,
|
| 151 |
+
"error": str(e),
|
| 152 |
+
"processing_time": time.time() - start_time,
|
| 153 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@app.function(
|
| 158 |
+
image=vector_image,
|
| 159 |
+
gpu="A100",
|
| 160 |
+
volumes={"/models": models_volume},
|
| 161 |
+
timeout=300, # 5 minutes timeout for search operations
|
| 162 |
+
)
|
| 163 |
+
def search_vector_memory(
|
| 164 |
+
query: str, client_id: str, memory_name: Optional[str] = None, top_k: int = 5
|
| 165 |
+
) -> Dict[str, Any]:
|
| 166 |
+
"""
|
| 167 |
+
Ultra-fast vector similarity search on Modal
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
query: Search query text
|
| 171 |
+
client_id: Client identifier to search within
|
| 172 |
+
memory_name: Optional specific memory name filter
|
| 173 |
+
top_k: Number of top results to return
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
Dict with search results and metrics
|
| 177 |
+
"""
|
| 178 |
+
import numpy as np
|
| 179 |
+
from sentence_transformers import SentenceTransformer
|
| 180 |
+
import json
|
| 181 |
+
|
| 182 |
+
start_time = time.time()
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
# Load model for query embedding
|
| 186 |
+
model_path = "/models/sentence-transformer"
|
| 187 |
+
model = SentenceTransformer(model_path, device="cuda")
|
| 188 |
+
|
| 189 |
+
# Generate query embedding
|
| 190 |
+
query_embedding = model.encode([query], device="cuda")[0].tolist()
|
| 191 |
+
embedding_time = time.time() - start_time
|
| 192 |
+
|
| 193 |
+
# Search in Modal Volume with FAISS
|
| 194 |
+
storage_path = f"/models/vectors/{client_id}"
|
| 195 |
+
index_path = f"{storage_path}/faiss_index.bin"
|
| 196 |
+
metadata_path = f"{storage_path}/metadata.json"
|
| 197 |
+
|
| 198 |
+
if os.path.exists(index_path) and os.path.exists(metadata_path):
|
| 199 |
+
print("π Searching in FAISS index...")
|
| 200 |
+
import faiss
|
| 201 |
+
|
| 202 |
+
# Load FAISS index and metadata
|
| 203 |
+
index = faiss.read_index(index_path)
|
| 204 |
+
with open(metadata_path, "r") as f:
|
| 205 |
+
all_metadata = json.load(f)
|
| 206 |
+
|
| 207 |
+
# Prepare query vector
|
| 208 |
+
query_vector = np.array([query_embedding], dtype=np.float32)
|
| 209 |
+
faiss.normalize_L2(query_vector)
|
| 210 |
+
|
| 211 |
+
# Perform similarity search
|
| 212 |
+
scores, indices = index.search(query_vector, min(top_k, index.ntotal))
|
| 213 |
+
|
| 214 |
+
# Format results
|
| 215 |
+
formatted_results = []
|
| 216 |
+
for i, (score, idx) in enumerate(zip(scores[0], indices[0])):
|
| 217 |
+
if idx < len(all_metadata): # Valid index
|
| 218 |
+
metadata_item = all_metadata[idx]
|
| 219 |
+
formatted_results.append(
|
| 220 |
+
{
|
| 221 |
+
"memory_id": metadata_item["id"],
|
| 222 |
+
"text": metadata_item["text"],
|
| 223 |
+
"metadata": metadata_item.get("metadata", {}),
|
| 224 |
+
"similarity_score": float(score),
|
| 225 |
+
"distance": 1 - float(score),
|
| 226 |
+
}
|
| 227 |
+
)
|
| 228 |
+
else:
|
| 229 |
+
# No stored vectors yet
|
| 230 |
+
formatted_results = []
|
| 231 |
+
|
| 232 |
+
search_time = time.time() - start_time
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
"success": True,
|
| 236 |
+
"query": query,
|
| 237 |
+
"client_id": client_id,
|
| 238 |
+
"results": formatted_results,
|
| 239 |
+
"total_results": len(formatted_results),
|
| 240 |
+
"processing_metrics": {
|
| 241 |
+
"embedding_time": embedding_time,
|
| 242 |
+
"search_time": search_time - embedding_time,
|
| 243 |
+
"total_time": search_time,
|
| 244 |
+
"gpu_used": "A100",
|
| 245 |
+
"model_used": "all-MiniLM-L6-v2",
|
| 246 |
+
},
|
| 247 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
print(f"β Error in vector search: {str(e)}")
|
| 252 |
+
return {
|
| 253 |
+
"success": False,
|
| 254 |
+
"error": str(e),
|
| 255 |
+
"processing_time": time.time() - start_time,
|
| 256 |
+
"results": [],
|
| 257 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@app.function(
|
| 262 |
+
image=vector_image,
|
| 263 |
+
volumes={"/models": models_volume},
|
| 264 |
+
timeout=60,
|
| 265 |
+
)
|
| 266 |
+
def get_vector_stats(client_id: str) -> Dict[str, Any]:
|
| 267 |
+
"""
|
| 268 |
+
Get statistics for vector storage
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
client_id: Client identifier
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
Dict with storage statistics
|
| 275 |
+
"""
|
| 276 |
+
import json
|
| 277 |
+
import os
|
| 278 |
+
|
| 279 |
+
try:
|
| 280 |
+
storage_path = f"/models/vectors/{client_id}"
|
| 281 |
+
index_path = f"{storage_path}/faiss_index.bin"
|
| 282 |
+
metadata_path = f"{storage_path}/metadata.json"
|
| 283 |
+
|
| 284 |
+
if os.path.exists(index_path) and os.path.exists(metadata_path):
|
| 285 |
+
import faiss
|
| 286 |
+
|
| 287 |
+
# Load FAISS index and metadata
|
| 288 |
+
index = faiss.read_index(index_path)
|
| 289 |
+
with open(metadata_path, "r") as f:
|
| 290 |
+
all_metadata = json.load(f)
|
| 291 |
+
|
| 292 |
+
# Calculate stats
|
| 293 |
+
memory_count = len(all_metadata)
|
| 294 |
+
first_memory = (
|
| 295 |
+
min(item["created_at"] for item in all_metadata)
|
| 296 |
+
if all_metadata
|
| 297 |
+
else None
|
| 298 |
+
)
|
| 299 |
+
last_memory = (
|
| 300 |
+
max(item["created_at"] for item in all_metadata)
|
| 301 |
+
if all_metadata
|
| 302 |
+
else None
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
return {
|
| 306 |
+
"client_id": client_id,
|
| 307 |
+
"storage_type": "modal_vector_faiss",
|
| 308 |
+
"memory_count": memory_count,
|
| 309 |
+
"avg_embedding_dim": 384, # all-MiniLM-L6-v2 dimension
|
| 310 |
+
"index_size": index.ntotal,
|
| 311 |
+
"first_memory": (
|
| 312 |
+
time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(first_memory))
|
| 313 |
+
if first_memory
|
| 314 |
+
else None
|
| 315 |
+
),
|
| 316 |
+
"last_memory": (
|
| 317 |
+
time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(last_memory))
|
| 318 |
+
if last_memory
|
| 319 |
+
else None
|
| 320 |
+
),
|
| 321 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 322 |
+
}
|
| 323 |
+
else:
|
| 324 |
+
return {
|
| 325 |
+
"client_id": client_id,
|
| 326 |
+
"storage_type": "modal_vector_faiss",
|
| 327 |
+
"memory_count": 0,
|
| 328 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 329 |
+
"note": "No vectors stored yet",
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
except Exception as e:
|
| 333 |
+
return {
|
| 334 |
+
"client_id": client_id,
|
| 335 |
+
"storage_type": "modal_vector_faiss",
|
| 336 |
+
"error": str(e),
|
| 337 |
+
"infrastructure": "Modal + A100 GPU + FAISS + Volume Storage",
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
# Client class for easy integration with DualStorageManager
|
| 342 |
+
class ModalVectorClient:
|
| 343 |
+
"""Client for interacting with Modal Vector Service"""
|
| 344 |
+
|
| 345 |
+
def __init__(self, modal_token: Optional[str] = None):
|
| 346 |
+
"""
|
| 347 |
+
Initialize Modal Vector Client
|
| 348 |
+
|
| 349 |
+
Args:
|
| 350 |
+
modal_token: Optional Modal token (uses environment if not provided)
|
| 351 |
+
"""
|
| 352 |
+
if modal_token:
|
| 353 |
+
os.environ["MODAL_TOKEN"] = modal_token
|
| 354 |
+
|
| 355 |
+
# Test Modal connection
|
| 356 |
+
try:
|
| 357 |
+
import modal
|
| 358 |
+
|
| 359 |
+
print("β
Modal Vector Client initialized successfully")
|
| 360 |
+
except Exception as e:
|
| 361 |
+
print(f"β οΈ Modal Vector Client initialization warning: {e}")
|
| 362 |
+
|
| 363 |
+
def store_memory(
|
| 364 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 365 |
+
) -> Dict[str, Any]:
|
| 366 |
+
"""Store memory using Modal vector service"""
|
| 367 |
+
try:
|
| 368 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 369 |
+
import modal
|
| 370 |
+
|
| 371 |
+
func = modal.Function.from_name(
|
| 372 |
+
"memvid-vector-service", "process_vector_memory"
|
| 373 |
+
)
|
| 374 |
+
return func.remote(text, client_id, metadata)
|
| 375 |
+
except Exception as e:
|
| 376 |
+
return {"success": False, "error": f"Modal vector storage failed: {e}"}
|
| 377 |
+
|
| 378 |
+
def search_memory(
|
| 379 |
+
self,
|
| 380 |
+
query: str,
|
| 381 |
+
client_id: str,
|
| 382 |
+
memory_name: Optional[str] = None,
|
| 383 |
+
top_k: int = 5,
|
| 384 |
+
) -> Dict[str, Any]:
|
| 385 |
+
"""Search memory using Modal vector service"""
|
| 386 |
+
try:
|
| 387 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 388 |
+
import modal
|
| 389 |
+
|
| 390 |
+
func = modal.Function.from_name(
|
| 391 |
+
"memvid-vector-service", "search_vector_memory"
|
| 392 |
+
)
|
| 393 |
+
return func.remote(query, client_id, memory_name, top_k)
|
| 394 |
+
except Exception as e:
|
| 395 |
+
return {
|
| 396 |
+
"success": False,
|
| 397 |
+
"error": f"Modal vector search failed: {e}",
|
| 398 |
+
"results": [],
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
def get_stats(self, client_id: str) -> Dict[str, Any]:
|
| 402 |
+
"""Get statistics using Modal vector service"""
|
| 403 |
+
try:
|
| 404 |
+
# Use the deployed app's function with correct Modal calling pattern
|
| 405 |
+
import modal
|
| 406 |
+
|
| 407 |
+
func = modal.Function.from_name("memvid-vector-service", "get_vector_stats")
|
| 408 |
+
return func.remote(client_id)
|
| 409 |
+
except Exception as e:
|
| 410 |
+
return {"success": False, "error": f"Modal vector stats failed: {e}"}
|
| 411 |
+
|
| 412 |
+
def list_memories(self, client_id: str) -> str:
|
| 413 |
+
"""List memories for client (Modal vector implementation)"""
|
| 414 |
+
try:
|
| 415 |
+
stats = self.get_stats(client_id)
|
| 416 |
+
if stats.get(
|
| 417 |
+
"success", True
|
| 418 |
+
): # Modal stats don't have success field currently
|
| 419 |
+
memory_list = {
|
| 420 |
+
"client_id": client_id,
|
| 421 |
+
"storage_type": "modal_vector",
|
| 422 |
+
"memory_count": stats.get("memory_count", 0),
|
| 423 |
+
"memories": [], # Modal doesn't currently track individual memory names
|
| 424 |
+
"avg_embedding_dim": stats.get("avg_embedding_dim", 0),
|
| 425 |
+
"infrastructure": "Modal + A100 GPU + PostgreSQL + pgvector",
|
| 426 |
+
}
|
| 427 |
+
return json.dumps(memory_list, indent=2)
|
| 428 |
+
else:
|
| 429 |
+
return json.dumps(
|
| 430 |
+
{
|
| 431 |
+
"error": f"Failed to list memories: {stats.get('error', 'Unknown error')}"
|
| 432 |
+
}
|
| 433 |
+
)
|
| 434 |
+
except Exception as e:
|
| 435 |
+
return json.dumps({"error": f"Modal vector list_memories failed: {e}"})
|
| 436 |
+
|
| 437 |
+
def build_memory_video(self, client_id: str, memory_name: str) -> str:
|
| 438 |
+
"""Build memory video (not applicable for vector storage)"""
|
| 439 |
+
return f"Memory videos are not applicable for vector storage. Client: {client_id}, Memory: {memory_name}"
|
| 440 |
+
|
| 441 |
+
def chat_with_memory(self, query: str, client_id: str, memory_name: str) -> str:
|
| 442 |
+
"""Chat with memory using Modal vector service"""
|
| 443 |
+
try:
|
| 444 |
+
# Use search as basis for chat
|
| 445 |
+
search_results = self.search_memory(query, client_id, memory_name, top_k=3)
|
| 446 |
+
|
| 447 |
+
if search_results.get("success", False):
|
| 448 |
+
results = search_results.get("results", [])
|
| 449 |
+
if results:
|
| 450 |
+
# Simple chat response based on search results
|
| 451 |
+
context = "\n".join(
|
| 452 |
+
[result.get("text", "") for result in results[:2]]
|
| 453 |
+
)
|
| 454 |
+
response = f"Based on your vector memories: {context}\n\nYour query '{query}' relates to the stored information above."
|
| 455 |
+
return response
|
| 456 |
+
else:
|
| 457 |
+
return f"I couldn't find any relevant memories for '{query}' in your vector storage."
|
| 458 |
+
else:
|
| 459 |
+
return f"Error accessing memories: {search_results.get('error', 'Unknown error')}"
|
| 460 |
+
|
| 461 |
+
except Exception as e:
|
| 462 |
+
return f"Modal vector chat failed: {e}"
|
| 463 |
+
|
| 464 |
+
def delete_memory(self, client_id: str, memory_name: str) -> str:
|
| 465 |
+
"""Delete memory (Modal vector implementation)"""
|
| 466 |
+
# Modal currently doesn't support selective deletion
|
| 467 |
+
return f"Memory deletion not yet implemented in Modal vector storage for client {client_id}, memory {memory_name}"
|
| 468 |
+
|
| 469 |
+
def get_memory_stats(self, client_id: str) -> str:
|
| 470 |
+
"""Get memory statistics as JSON string"""
|
| 471 |
+
try:
|
| 472 |
+
stats = self.get_stats(client_id)
|
| 473 |
+
return json.dumps(stats, indent=2)
|
| 474 |
+
except Exception as e:
|
| 475 |
+
return json.dumps({"error": f"Modal vector get_memory_stats failed: {e}"})
|
| 476 |
+
|
| 477 |
+
# For compatibility with the dual storage manager method calls
|
| 478 |
+
def store_embedding(
|
| 479 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 480 |
+
) -> str:
|
| 481 |
+
"""Alias for store_memory for backward compatibility"""
|
| 482 |
+
result = self.store_memory(text, client_id, metadata)
|
| 483 |
+
return json.dumps(result) if isinstance(result, dict) else str(result)
|
| 484 |
+
|
| 485 |
+
def search_embeddings(self, query: str, client_id: str, top_k: int = 5) -> str:
|
| 486 |
+
"""Alias for search_memory for backward compatibility"""
|
| 487 |
+
result = self.search_memory(query, client_id, top_k=top_k)
|
| 488 |
+
return json.dumps(result) if isinstance(result, dict) else str(result)
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
if __name__ == "__main__":
|
| 492 |
+
# Test the Modal functions locally
|
| 493 |
+
print("π§ͺ Testing Modal Vector Service...")
|
| 494 |
+
|
| 495 |
+
# Test client
|
| 496 |
+
client = ModalVectorClient()
|
| 497 |
+
|
| 498 |
+
# Test storage
|
| 499 |
+
result = client.store_memory(
|
| 500 |
+
"This is a test memory for Modal vector storage",
|
| 501 |
+
"test_client",
|
| 502 |
+
{"test": True, "timestamp": time.time()},
|
| 503 |
+
)
|
| 504 |
+
print(f"π₯ Storage result: {result}")
|
| 505 |
+
|
| 506 |
+
# Test search
|
| 507 |
+
search_result = client.search_memory("test memory", "test_client", top_k=3)
|
| 508 |
+
print(f"π Search result: {search_result}")
|
| 509 |
+
|
| 510 |
+
# Test stats
|
| 511 |
+
stats = client.get_stats("test_client")
|
| 512 |
+
print(f" Stats: {stats}")
|
requirements.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π₯ Memvid MCP Server - HF Spaces Requirements
|
| 2 |
+
# Production deployment for Hugging Face Spaces
|
| 3 |
+
|
| 4 |
+
# Core MCP and Gradio - REQUIRED
|
| 5 |
+
gradio[mcp]>=5.31.0
|
| 6 |
+
httpx>=0.25.0
|
| 7 |
+
|
| 8 |
+
# AI/ML Dependencies for memvid
|
| 9 |
+
torch>=2.0.0
|
| 10 |
+
sentence-transformers>=2.0.0
|
| 11 |
+
faiss-cpu>=1.8.0
|
| 12 |
+
opencv-python-headless>=4.8.0
|
| 13 |
+
|
| 14 |
+
# HuggingFace integration - REQUIRED for cloud storage
|
| 15 |
+
huggingface_hub>=0.16.4
|
| 16 |
+
datasets>=2.14.0
|
| 17 |
+
|
| 18 |
+
# Core Python packages
|
| 19 |
+
numpy>=1.24.0
|
| 20 |
+
pillow>=9.5.0
|
| 21 |
+
python-dotenv>=1.0.0
|
| 22 |
+
|
| 23 |
+
# Memvid library - Core functionality
|
| 24 |
+
memvid>=0.1.0
|
| 25 |
+
|
| 26 |
+
# Dual Storage Dependencies - Minimal vector storage support
|
| 27 |
+
# (These are already included above for memvid, but explicitly listed for clarity)
|
| 28 |
+
# sentence-transformers>=2.0.0 # Already included
|
| 29 |
+
# faiss-cpu>=1.8.0 # Already included
|
| 30 |
+
|
| 31 |
+
# Modal Integration - Cloud infrastructure
|
| 32 |
+
modal>=1.0.0
|
| 33 |
+
psycopg2-binary>=2.9.0 # PostgreSQL with pgvector support
|
| 34 |
+
|
| 35 |
+
# Device Fingerprinting - Minimal privacy-focused user identification
|
| 36 |
+
psutil>=5.9.0 # System and process utilities for device fingerprinting
|
| 37 |
+
|
| 38 |
+
# Note: This configuration is optimized for HF Spaces deployment
|
| 39 |
+
# All dependencies verified working with 100% functional MCP server with dual storage
|
setup_postgres.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
PostgreSQL Setup Script for Modal Vector Service
|
| 4 |
+
|
| 5 |
+
This script helps set up a PostgreSQL database with pgvector extension
|
| 6 |
+
for the Modal vector service.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import subprocess
|
| 12 |
+
import psycopg2
|
| 13 |
+
from urllib.parse import urlparse
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_postgres_connection(postgres_url: str) -> bool:
|
| 17 |
+
"""Test PostgreSQL connection and pgvector availability"""
|
| 18 |
+
try:
|
| 19 |
+
print(f"π Testing connection to PostgreSQL...")
|
| 20 |
+
conn = psycopg2.connect(postgres_url)
|
| 21 |
+
cursor = conn.cursor()
|
| 22 |
+
|
| 23 |
+
# Test basic connection
|
| 24 |
+
cursor.execute("SELECT version();")
|
| 25 |
+
version = cursor.fetchone()[0]
|
| 26 |
+
print(f"β
Connected to PostgreSQL: {version}")
|
| 27 |
+
|
| 28 |
+
# Test pgvector extension
|
| 29 |
+
try:
|
| 30 |
+
cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
| 31 |
+
cursor.execute(
|
| 32 |
+
"SELECT extversion FROM pg_extension WHERE extname = 'vector';"
|
| 33 |
+
)
|
| 34 |
+
vector_version = cursor.fetchone()
|
| 35 |
+
if vector_version:
|
| 36 |
+
print(f"β
pgvector extension available: v{vector_version[0]}")
|
| 37 |
+
else:
|
| 38 |
+
print("β οΈ pgvector extension not found")
|
| 39 |
+
return False
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"β pgvector extension error: {e}")
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
# Create test table to verify vector operations
|
| 45 |
+
cursor.execute(
|
| 46 |
+
"""
|
| 47 |
+
CREATE TABLE IF NOT EXISTS vector_test (
|
| 48 |
+
id SERIAL PRIMARY KEY,
|
| 49 |
+
embedding vector(384)
|
| 50 |
+
);
|
| 51 |
+
"""
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Test vector operations
|
| 55 |
+
test_vector = [0.1] * 384 # 384-dimensional test vector
|
| 56 |
+
cursor.execute(
|
| 57 |
+
"INSERT INTO vector_test (embedding) VALUES (%s) RETURNING id;",
|
| 58 |
+
(test_vector,),
|
| 59 |
+
)
|
| 60 |
+
test_id = cursor.fetchone()[0]
|
| 61 |
+
print(f"β
Vector operations working (test ID: {test_id})")
|
| 62 |
+
|
| 63 |
+
# Clean up test
|
| 64 |
+
cursor.execute("DELETE FROM vector_test WHERE id = %s;", (test_id,))
|
| 65 |
+
|
| 66 |
+
conn.commit()
|
| 67 |
+
cursor.close()
|
| 68 |
+
conn.close()
|
| 69 |
+
|
| 70 |
+
return True
|
| 71 |
+
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"β PostgreSQL connection failed: {e}")
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def setup_modal_secret(postgres_url: str):
|
| 78 |
+
"""Set up Modal secret for PostgreSQL"""
|
| 79 |
+
try:
|
| 80 |
+
print("π Setting up Modal secret for PostgreSQL...")
|
| 81 |
+
|
| 82 |
+
# Create or update the Modal secret
|
| 83 |
+
result = subprocess.run(
|
| 84 |
+
[
|
| 85 |
+
"modal",
|
| 86 |
+
"secret",
|
| 87 |
+
"create",
|
| 88 |
+
"postgres-secret",
|
| 89 |
+
f"MODAL_POSTGRES_URL={postgres_url}",
|
| 90 |
+
],
|
| 91 |
+
capture_output=True,
|
| 92 |
+
text=True,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
if result.returncode == 0:
|
| 96 |
+
print("β
Modal secret created successfully")
|
| 97 |
+
print("\nTo use in your Modal functions, add:")
|
| 98 |
+
print("@app.function(secrets=[modal.Secret.from_name('postgres-secret')])")
|
| 99 |
+
else:
|
| 100 |
+
# Try updating if creation failed
|
| 101 |
+
result = subprocess.run(
|
| 102 |
+
[
|
| 103 |
+
"modal",
|
| 104 |
+
"secret",
|
| 105 |
+
"update",
|
| 106 |
+
"postgres-secret",
|
| 107 |
+
f"MODAL_POSTGRES_URL={postgres_url}",
|
| 108 |
+
],
|
| 109 |
+
capture_output=True,
|
| 110 |
+
text=True,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
if result.returncode == 0:
|
| 114 |
+
print("β
Modal secret updated successfully")
|
| 115 |
+
else:
|
| 116 |
+
print(f"β Failed to create/update Modal secret: {result.stderr}")
|
| 117 |
+
return False
|
| 118 |
+
|
| 119 |
+
return True
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"β Error setting up Modal secret: {e}")
|
| 123 |
+
return False
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def create_vector_tables(postgres_url: str):
|
| 127 |
+
"""Create the vector memory tables"""
|
| 128 |
+
try:
|
| 129 |
+
print("π Creating vector memory tables...")
|
| 130 |
+
conn = psycopg2.connect(postgres_url)
|
| 131 |
+
cursor = conn.cursor()
|
| 132 |
+
|
| 133 |
+
# Create the main vector memories table
|
| 134 |
+
cursor.execute(
|
| 135 |
+
"""
|
| 136 |
+
CREATE TABLE IF NOT EXISTS vector_memories (
|
| 137 |
+
id SERIAL PRIMARY KEY,
|
| 138 |
+
client_id VARCHAR(255) NOT NULL,
|
| 139 |
+
text TEXT NOT NULL,
|
| 140 |
+
embedding vector(384), -- all-MiniLM-L6-v2 produces 384-dim vectors
|
| 141 |
+
metadata JSONB,
|
| 142 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 143 |
+
);
|
| 144 |
+
"""
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# Create indexes for performance
|
| 148 |
+
cursor.execute(
|
| 149 |
+
"""
|
| 150 |
+
CREATE INDEX IF NOT EXISTS idx_vector_memories_client_id
|
| 151 |
+
ON vector_memories(client_id);
|
| 152 |
+
"""
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
cursor.execute(
|
| 156 |
+
"""
|
| 157 |
+
CREATE INDEX IF NOT EXISTS idx_vector_memories_created_at
|
| 158 |
+
ON vector_memories(created_at);
|
| 159 |
+
"""
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Create vector similarity index (HNSW for fast approximate search)
|
| 163 |
+
cursor.execute(
|
| 164 |
+
"""
|
| 165 |
+
CREATE INDEX IF NOT EXISTS idx_vector_memories_embedding
|
| 166 |
+
ON vector_memories USING hnsw (embedding vector_cosine_ops);
|
| 167 |
+
"""
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
conn.commit()
|
| 171 |
+
cursor.close()
|
| 172 |
+
conn.close()
|
| 173 |
+
|
| 174 |
+
print("β
Vector memory tables created successfully")
|
| 175 |
+
return True
|
| 176 |
+
|
| 177 |
+
except Exception as e:
|
| 178 |
+
print(f"β Error creating vector tables: {e}")
|
| 179 |
+
return False
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def main():
|
| 183 |
+
print("π PostgreSQL Setup for Modal Vector Service")
|
| 184 |
+
print("=" * 50)
|
| 185 |
+
|
| 186 |
+
# Check if PostgreSQL URL is provided
|
| 187 |
+
postgres_url = os.getenv("POSTGRES_URL")
|
| 188 |
+
if not postgres_url:
|
| 189 |
+
print("\nπ PostgreSQL URL not found in environment.")
|
| 190 |
+
print("\nOptions for PostgreSQL with pgvector:")
|
| 191 |
+
print("1. Neon (https://neon.tech) - Free tier with pgvector")
|
| 192 |
+
print("2. Supabase (https://supabase.com) - Free tier with pgvector")
|
| 193 |
+
print("3. Railway (https://railway.app) - PostgreSQL with pgvector")
|
| 194 |
+
print("4. Your own PostgreSQL instance")
|
| 195 |
+
|
| 196 |
+
print("\nTo use this script:")
|
| 197 |
+
print("export POSTGRES_URL='postgresql://user:password@host:port/database'")
|
| 198 |
+
print("python setup_postgres.py")
|
| 199 |
+
|
| 200 |
+
# Try to get URL from user input
|
| 201 |
+
postgres_url = input(
|
| 202 |
+
"\nEnter PostgreSQL URL (or press Enter to skip): "
|
| 203 |
+
).strip()
|
| 204 |
+
if not postgres_url:
|
| 205 |
+
print("βοΈ Skipping PostgreSQL setup")
|
| 206 |
+
return
|
| 207 |
+
|
| 208 |
+
# Test the connection
|
| 209 |
+
if not test_postgres_connection(postgres_url):
|
| 210 |
+
print("β PostgreSQL setup failed - connection test failed")
|
| 211 |
+
return
|
| 212 |
+
|
| 213 |
+
# Create vector tables
|
| 214 |
+
if not create_vector_tables(postgres_url):
|
| 215 |
+
print("β PostgreSQL setup failed - table creation failed")
|
| 216 |
+
return
|
| 217 |
+
|
| 218 |
+
# Set up Modal secret
|
| 219 |
+
if not setup_modal_secret(postgres_url):
|
| 220 |
+
print("β PostgreSQL setup failed - Modal secret setup failed")
|
| 221 |
+
return
|
| 222 |
+
|
| 223 |
+
print("\nπ PostgreSQL setup completed successfully!")
|
| 224 |
+
print("\nNext steps:")
|
| 225 |
+
print("1. Redeploy your Modal vector service")
|
| 226 |
+
print("2. Test vector storage and search")
|
| 227 |
+
print("3. Monitor performance in Modal dashboard")
|
| 228 |
+
|
| 229 |
+
# Parse URL to show connection info (without password)
|
| 230 |
+
parsed = urlparse(postgres_url)
|
| 231 |
+
print(f"\nπ Database Info:")
|
| 232 |
+
print(f" Host: {parsed.hostname}")
|
| 233 |
+
print(f" Port: {parsed.port or 5432}")
|
| 234 |
+
print(f" Database: {parsed.path[1:] if parsed.path else 'postgres'}")
|
| 235 |
+
print(f" User: {parsed.username}")
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
if __name__ == "__main__":
|
| 239 |
+
main()
|
utils/dual_storage_manager.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dual Storage Manager - Orchestrates memvid and vector storage with performance comparison.
|
| 3 |
+
Provides unified interface for dual storage modes with background metrics collection.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, Any, Optional
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from .memvid_manager import MemvidManager
|
| 14 |
+
from .vector_storage_manager import VectorStorageManager
|
| 15 |
+
|
| 16 |
+
# Modal services imports (with fallback for local development)
|
| 17 |
+
try:
|
| 18 |
+
import sys
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
# Add parent directory to path for Modal service imports
|
| 22 |
+
parent_dir = Path(__file__).parent.parent
|
| 23 |
+
if str(parent_dir) not in sys.path:
|
| 24 |
+
sys.path.insert(0, str(parent_dir))
|
| 25 |
+
|
| 26 |
+
from modal_vector_service import ModalVectorClient
|
| 27 |
+
from modal_memvid_service import ModalMemvidClient
|
| 28 |
+
|
| 29 |
+
MODAL_AVAILABLE = True
|
| 30 |
+
print("β
Modal services imported successfully")
|
| 31 |
+
except ImportError as e:
|
| 32 |
+
print(f"β οΈ Modal services not available, using local implementations: {e}")
|
| 33 |
+
MODAL_AVAILABLE = False
|
| 34 |
+
from .metrics_collector import MetricsCollector
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class DualStorageManager:
|
| 38 |
+
"""
|
| 39 |
+
Orchestrates dual storage between memvid (video-based) and vector storage.
|
| 40 |
+
Provides unified interface with configurable storage modes and performance tracking.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, data_dir: str = "data"):
|
| 44 |
+
"""
|
| 45 |
+
Initialize dual storage manager with Modal-first architecture.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
data_dir (str): Base directory for storing data
|
| 49 |
+
"""
|
| 50 |
+
self.logger = logging.getLogger(__name__)
|
| 51 |
+
|
| 52 |
+
# Get storage mode from environment
|
| 53 |
+
self.storage_mode = os.getenv("STORAGE_MODE", "dual").lower()
|
| 54 |
+
self.enable_metrics = (
|
| 55 |
+
os.getenv("ENABLE_PERFORMANCE_TRACKING", "true").lower() == "true"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Check for Modal configuration
|
| 59 |
+
modal_token = os.getenv("MODAL_TOKEN")
|
| 60 |
+
use_modal = MODAL_AVAILABLE and modal_token
|
| 61 |
+
|
| 62 |
+
# Initialize storage backends (Modal-first with local fallback)
|
| 63 |
+
if use_modal:
|
| 64 |
+
print("π Initializing Modal-powered storage backends...")
|
| 65 |
+
try:
|
| 66 |
+
self.memvid_manager = ModalMemvidClient(modal_token=modal_token)
|
| 67 |
+
self.vector_manager = ModalVectorClient(modal_token=modal_token)
|
| 68 |
+
self.using_modal = True
|
| 69 |
+
print("β
Modal services initialized successfully")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"β οΈ Modal initialization failed, falling back to local: {e}")
|
| 72 |
+
self.memvid_manager = MemvidManager(data_dir)
|
| 73 |
+
self.vector_manager = VectorStorageManager(
|
| 74 |
+
data_dir, storage_handler=self.memvid_manager.storage_handler
|
| 75 |
+
) # Shared HF storage
|
| 76 |
+
self.using_modal = False
|
| 77 |
+
else:
|
| 78 |
+
print("π Using local storage backends...")
|
| 79 |
+
self.memvid_manager = MemvidManager(data_dir)
|
| 80 |
+
self.vector_manager = VectorStorageManager(
|
| 81 |
+
data_dir, storage_handler=self.memvid_manager.storage_handler
|
| 82 |
+
) # Shared HF storage
|
| 83 |
+
self.using_modal = False
|
| 84 |
+
|
| 85 |
+
# Initialize metrics collector
|
| 86 |
+
self.metrics = MetricsCollector() if self.enable_metrics else None
|
| 87 |
+
|
| 88 |
+
infrastructure = "Modal" if self.using_modal else "Local"
|
| 89 |
+
self.logger.info(
|
| 90 |
+
f"DualStorageManager initialized with mode: {self.storage_mode}"
|
| 91 |
+
)
|
| 92 |
+
print(f"ποΈ Infrastructure: {infrastructure}")
|
| 93 |
+
print(
|
| 94 |
+
f"π Performance tracking: {'enabled' if self.enable_metrics else 'disabled'}"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def set_storage_mode(self, mode: str, client_id: str = "") -> str:
|
| 98 |
+
"""
|
| 99 |
+
Set storage mode at runtime.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
mode (str): Storage mode (memvid_only, vector_only, dual)
|
| 103 |
+
client_id (str): Optional client-specific setting
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
str: Success message
|
| 107 |
+
"""
|
| 108 |
+
valid_modes = ["memvid_only", "vector_only", "dual"]
|
| 109 |
+
if mode not in valid_modes:
|
| 110 |
+
return f"Error: Invalid mode '{mode}'. Valid modes: {valid_modes}"
|
| 111 |
+
|
| 112 |
+
self.storage_mode = mode
|
| 113 |
+
return f"Storage mode set to: {mode}" + (
|
| 114 |
+
f" for client {client_id}" if client_id else " (global)"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def get_storage_mode(self, client_id: str = "") -> str:
|
| 118 |
+
"""
|
| 119 |
+
Get current storage mode.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
client_id (str): Client identifier (for future client-specific modes)
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
str: Current storage mode information
|
| 126 |
+
"""
|
| 127 |
+
return json.dumps(
|
| 128 |
+
{
|
| 129 |
+
"storage_mode": self.storage_mode,
|
| 130 |
+
"metrics_enabled": self.enable_metrics,
|
| 131 |
+
"backends_available": {
|
| 132 |
+
"memvid": True,
|
| 133 |
+
"vector": self.vector_manager is not None,
|
| 134 |
+
},
|
| 135 |
+
},
|
| 136 |
+
indent=2,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def store_memory(
|
| 140 |
+
self, text: str, client_id: str, metadata: Dict[str, Any] = None
|
| 141 |
+
) -> str:
|
| 142 |
+
"""
|
| 143 |
+
Universal memory storage interface.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
text (str): Text content to store
|
| 147 |
+
client_id (str): Client identifier
|
| 148 |
+
metadata (dict): Additional metadata
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
str: Storage result message
|
| 152 |
+
"""
|
| 153 |
+
try:
|
| 154 |
+
if self.storage_mode == "memvid_only":
|
| 155 |
+
return self._store_memvid_only(text, client_id, metadata)
|
| 156 |
+
elif self.storage_mode == "vector_only":
|
| 157 |
+
return self._store_vector_only(text, client_id, metadata)
|
| 158 |
+
else: # dual mode
|
| 159 |
+
return self._store_dual_mode(text, client_id, metadata)
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
error_msg = f"Error in store_memory: {str(e)}"
|
| 163 |
+
self.logger.error(error_msg)
|
| 164 |
+
return error_msg
|
| 165 |
+
|
| 166 |
+
def search_memory(
|
| 167 |
+
self, query: str, client_id: str, memory_name: str, top_k: int = 5
|
| 168 |
+
) -> str:
|
| 169 |
+
"""
|
| 170 |
+
Universal memory search interface.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
query (str): Search query
|
| 174 |
+
client_id (str): Client identifier
|
| 175 |
+
memory_name (str): Memory name to search
|
| 176 |
+
top_k (int): Number of results
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
str: Search results
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
if self.storage_mode == "memvid_only":
|
| 183 |
+
return self._search_memvid_only(query, client_id, memory_name, top_k)
|
| 184 |
+
elif self.storage_mode == "vector_only":
|
| 185 |
+
return self._search_vector_only(query, client_id, memory_name, top_k)
|
| 186 |
+
else: # dual mode
|
| 187 |
+
return self._search_dual_mode(query, client_id, memory_name, top_k)
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
error_msg = f"Error in search_memory: {str(e)}"
|
| 191 |
+
self.logger.error(error_msg)
|
| 192 |
+
return json.dumps({"error": error_msg})
|
| 193 |
+
|
| 194 |
+
def get_memory_stats(self, client_id: str) -> str:
|
| 195 |
+
"""
|
| 196 |
+
Get aggregated memory statistics based on storage mode.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
client_id (str): Client identifier
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
str: JSON string with statistics
|
| 203 |
+
"""
|
| 204 |
+
try:
|
| 205 |
+
if self.storage_mode == "dual" and self.metrics:
|
| 206 |
+
return self.metrics.get_comparison_report(client_id)
|
| 207 |
+
elif self.storage_mode == "memvid_only":
|
| 208 |
+
return self.memvid_manager.get_memory_stats(client_id)
|
| 209 |
+
elif self.storage_mode == "vector_only" and self.vector_manager:
|
| 210 |
+
return self.vector_manager.get_stats(client_id)
|
| 211 |
+
else:
|
| 212 |
+
# Fallback to memvid stats
|
| 213 |
+
return self.memvid_manager.get_memory_stats(client_id)
|
| 214 |
+
|
| 215 |
+
except Exception as e:
|
| 216 |
+
error_msg = f"Error getting memory stats: {str(e)}"
|
| 217 |
+
self.logger.error(error_msg)
|
| 218 |
+
return json.dumps({"error": error_msg})
|
| 219 |
+
|
| 220 |
+
def delete_memory(self, client_id: str, memory_name: str) -> str:
|
| 221 |
+
"""
|
| 222 |
+
Universal memory deletion interface.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
client_id (str): Client identifier
|
| 226 |
+
memory_name (str): Memory name to delete
|
| 227 |
+
|
| 228 |
+
Returns:
|
| 229 |
+
str: Deletion result
|
| 230 |
+
"""
|
| 231 |
+
try:
|
| 232 |
+
results = []
|
| 233 |
+
|
| 234 |
+
if self.storage_mode in ["memvid_only", "dual"]:
|
| 235 |
+
result = self.memvid_manager.delete_memory(client_id, memory_name)
|
| 236 |
+
results.append(f"Memvid: {result}")
|
| 237 |
+
|
| 238 |
+
if self.storage_mode in ["vector_only", "dual"] and self.vector_manager:
|
| 239 |
+
result = self.vector_manager.delete_memory(client_id, memory_name)
|
| 240 |
+
results.append(f"Vector: {result}")
|
| 241 |
+
|
| 242 |
+
return " | ".join(results) if results else "No storage backends available"
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
error_msg = f"Error deleting memory: {str(e)}"
|
| 246 |
+
self.logger.error(error_msg)
|
| 247 |
+
return error_msg
|
| 248 |
+
|
| 249 |
+
def list_memories(self, client_id: str) -> str:
|
| 250 |
+
"""
|
| 251 |
+
Universal memory listing interface.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
client_id (str): Client identifier
|
| 255 |
+
|
| 256 |
+
Returns:
|
| 257 |
+
str: JSON string with memory list
|
| 258 |
+
"""
|
| 259 |
+
try:
|
| 260 |
+
# Use memvid as primary source for listing
|
| 261 |
+
return self.memvid_manager.list_memories(client_id)
|
| 262 |
+
except Exception as e:
|
| 263 |
+
error_msg = f"Error listing memories: {str(e)}"
|
| 264 |
+
self.logger.error(error_msg)
|
| 265 |
+
return json.dumps({"error": error_msg})
|
| 266 |
+
|
| 267 |
+
def build_memory_video(self, client_id: str, memory_name: str) -> str:
|
| 268 |
+
"""
|
| 269 |
+
Build memory video from stored chunks (memvid-specific).
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
client_id (str): Client identifier
|
| 273 |
+
memory_name (str): Name for the memory video
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
str: Build result message
|
| 277 |
+
"""
|
| 278 |
+
try:
|
| 279 |
+
return self.memvid_manager.build_memory_video(client_id, memory_name)
|
| 280 |
+
except Exception as e:
|
| 281 |
+
error_msg = f"Error in build_memory_video: {str(e)}"
|
| 282 |
+
self.logger.error(error_msg)
|
| 283 |
+
return error_msg
|
| 284 |
+
|
| 285 |
+
def chat_with_memory(self, query: str, client_id: str, memory_name: str) -> str:
|
| 286 |
+
"""
|
| 287 |
+
Universal chat interface.
|
| 288 |
+
|
| 289 |
+
Args:
|
| 290 |
+
query (str): User query
|
| 291 |
+
client_id (str): Client identifier
|
| 292 |
+
memory_name (str): Memory name to chat with
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
str: Chat response
|
| 296 |
+
"""
|
| 297 |
+
try:
|
| 298 |
+
# Use memvid for chat (better for conversational AI)
|
| 299 |
+
return self.memvid_manager.chat_with_memory(query, client_id, memory_name)
|
| 300 |
+
except Exception as e:
|
| 301 |
+
error_msg = f"Error in chat_with_memory: {str(e)}"
|
| 302 |
+
self.logger.error(error_msg)
|
| 303 |
+
return error_msg
|
| 304 |
+
|
| 305 |
+
# Private methods for storage mode implementations
|
| 306 |
+
|
| 307 |
+
def _store_memvid_only(
|
| 308 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 309 |
+
) -> str:
|
| 310 |
+
"""Store using memvid only."""
|
| 311 |
+
start_time = time.time()
|
| 312 |
+
result = self.memvid_manager.store_memory(text, client_id, metadata)
|
| 313 |
+
|
| 314 |
+
if self.metrics:
|
| 315 |
+
self.metrics.track_storage_operation(
|
| 316 |
+
"memvid", time.time() - start_time, len(text)
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
return result
|
| 320 |
+
|
| 321 |
+
def _store_vector_only(
|
| 322 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 323 |
+
) -> str:
|
| 324 |
+
"""Store using vector storage only."""
|
| 325 |
+
if not self.vector_manager:
|
| 326 |
+
return "Error: Vector storage not available (Modal credentials needed)"
|
| 327 |
+
|
| 328 |
+
start_time = time.time()
|
| 329 |
+
result = self.vector_manager.store_memory(text, client_id, metadata)
|
| 330 |
+
|
| 331 |
+
if self.metrics:
|
| 332 |
+
self.metrics.track_storage_operation(
|
| 333 |
+
"vector", time.time() - start_time, len(text)
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
return result
|
| 337 |
+
|
| 338 |
+
def _store_dual_mode(
|
| 339 |
+
self, text: str, client_id: str, metadata: Dict[str, Any]
|
| 340 |
+
) -> str:
|
| 341 |
+
"""Store using both storage backends with performance comparison."""
|
| 342 |
+
results = []
|
| 343 |
+
|
| 344 |
+
# Store in memvid
|
| 345 |
+
start_time = time.time()
|
| 346 |
+
memvid_result = self.memvid_manager.store_memory(text, client_id, metadata)
|
| 347 |
+
memvid_time = time.time() - start_time
|
| 348 |
+
results.append(f"Memvid({memvid_time:.3f}s): {memvid_result}")
|
| 349 |
+
|
| 350 |
+
# Store in vector (if available)
|
| 351 |
+
if self.vector_manager:
|
| 352 |
+
start_time = time.time()
|
| 353 |
+
vector_result = self.vector_manager.store_memory(text, client_id, metadata)
|
| 354 |
+
vector_time = time.time() - start_time
|
| 355 |
+
results.append(f"Vector({vector_time:.3f}s): {vector_result}")
|
| 356 |
+
|
| 357 |
+
# Track comparison metrics
|
| 358 |
+
if self.metrics:
|
| 359 |
+
self.metrics.track_dual_storage_comparison(
|
| 360 |
+
memvid_time, vector_time, len(text), client_id
|
| 361 |
+
)
|
| 362 |
+
else:
|
| 363 |
+
results.append("Vector: Not available (Modal credentials needed)")
|
| 364 |
+
|
| 365 |
+
return " | ".join(results)
|
| 366 |
+
|
| 367 |
+
def _search_memvid_only(
|
| 368 |
+
self, query: str, client_id: str, memory_name: str, top_k: int
|
| 369 |
+
) -> str:
|
| 370 |
+
"""Search using memvid only."""
|
| 371 |
+
start_time = time.time()
|
| 372 |
+
result = self.memvid_manager.search_memory(query, client_id, memory_name, top_k)
|
| 373 |
+
|
| 374 |
+
if self.metrics:
|
| 375 |
+
self.metrics.track_search_operation(
|
| 376 |
+
"memvid", time.time() - start_time, top_k
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
# Convert dict to JSON string for MCP interface
|
| 380 |
+
if isinstance(result, dict):
|
| 381 |
+
return json.dumps(result, indent=2)
|
| 382 |
+
return result
|
| 383 |
+
|
| 384 |
+
def _search_vector_only(
|
| 385 |
+
self, query: str, client_id: str, memory_name: str, top_k: int
|
| 386 |
+
) -> str:
|
| 387 |
+
"""Search using vector storage only."""
|
| 388 |
+
if not self.vector_manager:
|
| 389 |
+
return json.dumps(
|
| 390 |
+
{"error": "Vector storage not available (Modal credentials needed)"}
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
start_time = time.time()
|
| 394 |
+
result = self.vector_manager.search_memory(query, client_id, top_k=top_k)
|
| 395 |
+
|
| 396 |
+
if self.metrics:
|
| 397 |
+
self.metrics.track_search_operation(
|
| 398 |
+
"vector", time.time() - start_time, top_k
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# Convert dict to JSON string for MCP interface
|
| 402 |
+
if isinstance(result, dict):
|
| 403 |
+
return json.dumps(result, indent=2)
|
| 404 |
+
return result
|
| 405 |
+
|
| 406 |
+
def _search_dual_mode(
|
| 407 |
+
self, query: str, client_id: str, memory_name: str, top_k: int
|
| 408 |
+
) -> str:
|
| 409 |
+
"""Search using both backends with performance comparison."""
|
| 410 |
+
|
| 411 |
+
# Search memvid first
|
| 412 |
+
memvid_data = {"error": "Memvid search not attempted"}
|
| 413 |
+
memvid_time = 0
|
| 414 |
+
|
| 415 |
+
start_time = time.time()
|
| 416 |
+
memvid_result = self.memvid_manager.search_memory(
|
| 417 |
+
query, client_id, memory_name, top_k
|
| 418 |
+
)
|
| 419 |
+
memvid_time = time.time() - start_time
|
| 420 |
+
|
| 421 |
+
# Handle memvid result - Modal clients should return dicts
|
| 422 |
+
memvid_data = (
|
| 423 |
+
memvid_result
|
| 424 |
+
if isinstance(memvid_result, dict)
|
| 425 |
+
else {
|
| 426 |
+
"error": f"Unexpected memvid type: {type(memvid_result)}",
|
| 427 |
+
"content": str(memvid_result)[:200],
|
| 428 |
+
}
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
# Search vector second
|
| 432 |
+
vector_data = {"error": "Vector search not attempted"}
|
| 433 |
+
vector_time = 0
|
| 434 |
+
|
| 435 |
+
if self.vector_manager:
|
| 436 |
+
start_time = time.time()
|
| 437 |
+
vector_result = self.vector_manager.search_memory(
|
| 438 |
+
query, client_id, memory_name=memory_name, top_k=top_k
|
| 439 |
+
)
|
| 440 |
+
vector_time = time.time() - start_time
|
| 441 |
+
|
| 442 |
+
# Handle vector result - Modal clients should return dicts
|
| 443 |
+
vector_data = (
|
| 444 |
+
vector_result
|
| 445 |
+
if isinstance(vector_result, dict)
|
| 446 |
+
else {
|
| 447 |
+
"error": f"Unexpected vector type: {type(vector_result)}",
|
| 448 |
+
"content": str(vector_result)[:200],
|
| 449 |
+
}
|
| 450 |
+
)
|
| 451 |
+
else:
|
| 452 |
+
vector_data = {"error": "Vector storage not available"}
|
| 453 |
+
|
| 454 |
+
# Track comparison metrics
|
| 455 |
+
if self.metrics:
|
| 456 |
+
self.metrics.track_dual_search_comparison(
|
| 457 |
+
memvid_time, vector_time, query, client_id
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Return comparison results
|
| 461 |
+
return json.dumps(
|
| 462 |
+
{
|
| 463 |
+
"query": query,
|
| 464 |
+
"client_id": client_id,
|
| 465 |
+
"memory_name": memory_name,
|
| 466 |
+
"dual_search_results": {
|
| 467 |
+
"memvid": {
|
| 468 |
+
"time_ms": round(memvid_time * 1000, 2),
|
| 469 |
+
"results": memvid_data,
|
| 470 |
+
},
|
| 471 |
+
"vector": {
|
| 472 |
+
"time_ms": round(vector_time * 1000, 2),
|
| 473 |
+
"results": vector_data,
|
| 474 |
+
},
|
| 475 |
+
},
|
| 476 |
+
"performance_winner": (
|
| 477 |
+
"memvid" if memvid_time < vector_time else "vector"
|
| 478 |
+
),
|
| 479 |
+
},
|
| 480 |
+
indent=2,
|
| 481 |
+
)
|
utils/fingerprint_manager.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Minimal Privacy-Focused Fingerprint Manager
|
| 3 |
+
Automatically identifies unique users with minimal device data collection.
|
| 4 |
+
Maintains privacy through hashing and generates consistent UUIDs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import platform
|
| 10 |
+
import psutil
|
| 11 |
+
import uuid
|
| 12 |
+
import os
|
| 13 |
+
from typing import Dict, Any, Optional
|
| 14 |
+
import logging
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MinimalFingerprintManager:
|
| 19 |
+
"""
|
| 20 |
+
Minimal device fingerprinting for automatic user identification.
|
| 21 |
+
Collects only essential data needed for reliable identification.
|
| 22 |
+
All sensitive data is hashed for privacy protection.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
"""Initialize the fingerprint manager."""
|
| 27 |
+
self.logger = logging.getLogger(__name__)
|
| 28 |
+
self.cache_file = Path("user_fingerprints.json")
|
| 29 |
+
self._load_cache()
|
| 30 |
+
|
| 31 |
+
def _load_cache(self) -> None:
|
| 32 |
+
"""Load cached fingerprints and user mappings."""
|
| 33 |
+
try:
|
| 34 |
+
if self.cache_file.exists():
|
| 35 |
+
with open(self.cache_file, "r") as f:
|
| 36 |
+
self.cache = json.load(f)
|
| 37 |
+
else:
|
| 38 |
+
self.cache = {
|
| 39 |
+
"fingerprints": {}, # fingerprint_hash -> user_uuid
|
| 40 |
+
"user_stats": {}, # user_uuid -> usage stats
|
| 41 |
+
"created_at": {}, # user_uuid -> first_seen timestamp
|
| 42 |
+
}
|
| 43 |
+
except Exception as e:
|
| 44 |
+
self.logger.warning(f"Failed to load fingerprint cache: {e}")
|
| 45 |
+
self.cache = {"fingerprints": {}, "user_stats": {}, "created_at": {}}
|
| 46 |
+
|
| 47 |
+
def _save_cache(self) -> None:
|
| 48 |
+
"""Save fingerprint cache to disk."""
|
| 49 |
+
try:
|
| 50 |
+
with open(self.cache_file, "w") as f:
|
| 51 |
+
json.dump(self.cache, f, indent=2)
|
| 52 |
+
except Exception as e:
|
| 53 |
+
self.logger.warning(f"Failed to save fingerprint cache: {e}")
|
| 54 |
+
|
| 55 |
+
def _get_minimal_fingerprint(self) -> Dict[str, Any]:
|
| 56 |
+
"""
|
| 57 |
+
Collect minimal device data for fingerprinting.
|
| 58 |
+
Only essential data that's stable and privacy-safe.
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
# Core system information (stable across reboots)
|
| 62 |
+
fingerprint = {
|
| 63 |
+
# OS and architecture (stable)
|
| 64 |
+
"os_system": platform.system(),
|
| 65 |
+
"os_release": platform.release(),
|
| 66 |
+
"architecture": platform.machine(),
|
| 67 |
+
# Hardware characteristics (stable)
|
| 68 |
+
"cpu_count_logical": psutil.cpu_count(logical=True),
|
| 69 |
+
"cpu_count_physical": psutil.cpu_count(logical=False),
|
| 70 |
+
"memory_total_gb": round(psutil.virtual_memory().total / (1024**3), 1),
|
| 71 |
+
# System boot time hash (for session consistency)
|
| 72 |
+
"boot_time_hash": hashlib.sha256(
|
| 73 |
+
str(int(psutil.boot_time())).encode()
|
| 74 |
+
).hexdigest()[:16],
|
| 75 |
+
# User context hash (privacy-safe)
|
| 76 |
+
"user_context_hash": hashlib.sha256(
|
| 77 |
+
(str(Path.home()) + os.getlogin()).encode()
|
| 78 |
+
).hexdigest()[:16],
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Add MAC address hash if available (most stable identifier)
|
| 82 |
+
try:
|
| 83 |
+
for interface, addrs in psutil.net_if_addrs().items():
|
| 84 |
+
for addr in addrs:
|
| 85 |
+
if (
|
| 86 |
+
addr.family == psutil.AF_LINK
|
| 87 |
+
and addr.address != "00:00:00:00:00:00"
|
| 88 |
+
):
|
| 89 |
+
fingerprint["mac_hash"] = hashlib.sha256(
|
| 90 |
+
addr.address.encode()
|
| 91 |
+
).hexdigest()[:16]
|
| 92 |
+
break
|
| 93 |
+
if "mac_hash" in fingerprint:
|
| 94 |
+
break
|
| 95 |
+
except Exception:
|
| 96 |
+
pass # MAC address not available, continue without it
|
| 97 |
+
|
| 98 |
+
return fingerprint
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
self.logger.error(f"Error generating fingerprint: {e}")
|
| 102 |
+
# Fallback minimal fingerprint
|
| 103 |
+
return {
|
| 104 |
+
"os_system": platform.system(),
|
| 105 |
+
"fallback": True,
|
| 106 |
+
"error": str(e)[:50],
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
def _generate_fingerprint_hash(self, fingerprint: Dict[str, Any]) -> str:
|
| 110 |
+
"""Generate a consistent hash from fingerprint data."""
|
| 111 |
+
# Sort keys for consistent hashing
|
| 112 |
+
fingerprint_str = json.dumps(fingerprint, sort_keys=True)
|
| 113 |
+
return hashlib.sha256(fingerprint_str.encode()).hexdigest()
|
| 114 |
+
|
| 115 |
+
def get_user_uuid(self) -> str:
|
| 116 |
+
"""
|
| 117 |
+
Get or create a consistent UUID for the current user.
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
str: Consistent UUID for this user/device combination
|
| 121 |
+
"""
|
| 122 |
+
# Generate current device fingerprint
|
| 123 |
+
fingerprint = self._get_minimal_fingerprint()
|
| 124 |
+
fingerprint_hash = self._generate_fingerprint_hash(fingerprint)
|
| 125 |
+
|
| 126 |
+
# Check if we've seen this fingerprint before
|
| 127 |
+
if fingerprint_hash in self.cache["fingerprints"]:
|
| 128 |
+
user_uuid = self.cache["fingerprints"][fingerprint_hash]
|
| 129 |
+
self.logger.info(f"Recognized returning user: {user_uuid[:8]}...")
|
| 130 |
+
else:
|
| 131 |
+
# New user - generate UUID
|
| 132 |
+
user_uuid = str(uuid.uuid4())
|
| 133 |
+
self.cache["fingerprints"][fingerprint_hash] = user_uuid
|
| 134 |
+
self.cache["created_at"][user_uuid] = psutil.boot_time()
|
| 135 |
+
self.cache["user_stats"][user_uuid] = {
|
| 136 |
+
"total_operations": 0,
|
| 137 |
+
"memories_stored": 0,
|
| 138 |
+
"searches_performed": 0,
|
| 139 |
+
"videos_built": 0,
|
| 140 |
+
"first_seen": psutil.boot_time(),
|
| 141 |
+
"last_seen": psutil.boot_time(),
|
| 142 |
+
"device_info": {
|
| 143 |
+
"os": fingerprint.get("os_system", "unknown"),
|
| 144 |
+
"architecture": fingerprint.get("architecture", "unknown"),
|
| 145 |
+
"cpu_cores": fingerprint.get("cpu_count_logical", 0),
|
| 146 |
+
"memory_gb": fingerprint.get("memory_total_gb", 0),
|
| 147 |
+
},
|
| 148 |
+
}
|
| 149 |
+
self._save_cache()
|
| 150 |
+
self.logger.info(f"New user registered: {user_uuid[:8]}...")
|
| 151 |
+
|
| 152 |
+
# Update last seen
|
| 153 |
+
if user_uuid in self.cache["user_stats"]:
|
| 154 |
+
self.cache["user_stats"][user_uuid]["last_seen"] = psutil.boot_time()
|
| 155 |
+
self._save_cache()
|
| 156 |
+
|
| 157 |
+
return user_uuid
|
| 158 |
+
|
| 159 |
+
def update_user_stats(self, user_uuid: str, operation_type: str) -> None:
|
| 160 |
+
"""
|
| 161 |
+
Update usage statistics for a user.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
user_uuid (str): User's UUID
|
| 165 |
+
operation_type (str): Type of operation performed
|
| 166 |
+
"""
|
| 167 |
+
if user_uuid not in self.cache["user_stats"]:
|
| 168 |
+
# Initialize stats for existing user
|
| 169 |
+
self.cache["user_stats"][user_uuid] = {
|
| 170 |
+
"total_operations": 0,
|
| 171 |
+
"memories_stored": 0,
|
| 172 |
+
"searches_performed": 0,
|
| 173 |
+
"videos_built": 0,
|
| 174 |
+
"first_seen": psutil.boot_time(),
|
| 175 |
+
"last_seen": psutil.boot_time(),
|
| 176 |
+
"device_info": {
|
| 177 |
+
"os": "unknown",
|
| 178 |
+
"architecture": "unknown",
|
| 179 |
+
"cpu_cores": 0,
|
| 180 |
+
"memory_gb": 0,
|
| 181 |
+
},
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# Update counters
|
| 185 |
+
stats = self.cache["user_stats"][user_uuid]
|
| 186 |
+
stats["total_operations"] += 1
|
| 187 |
+
stats["last_seen"] = psutil.boot_time()
|
| 188 |
+
|
| 189 |
+
# Update specific operation counters
|
| 190 |
+
if operation_type in ["store_memory", "store_document"]:
|
| 191 |
+
stats["memories_stored"] += 1
|
| 192 |
+
elif operation_type in ["search_memory", "chat_with_memory"]:
|
| 193 |
+
stats["searches_performed"] += 1
|
| 194 |
+
elif operation_type == "build_memory_video":
|
| 195 |
+
stats["videos_built"] += 1
|
| 196 |
+
|
| 197 |
+
self._save_cache()
|
| 198 |
+
|
| 199 |
+
def get_user_stats(self, user_uuid: str) -> Dict[str, Any]:
|
| 200 |
+
"""
|
| 201 |
+
Get usage statistics for a user.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
user_uuid (str): User's UUID
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
Dict: User's usage statistics
|
| 208 |
+
"""
|
| 209 |
+
if user_uuid not in self.cache["user_stats"]:
|
| 210 |
+
return {"error": "User not found", "user_uuid": user_uuid}
|
| 211 |
+
|
| 212 |
+
stats = self.cache["user_stats"][user_uuid].copy()
|
| 213 |
+
|
| 214 |
+
# Add computed fields
|
| 215 |
+
import time
|
| 216 |
+
|
| 217 |
+
current_time = time.time()
|
| 218 |
+
stats["days_since_first_seen"] = round(
|
| 219 |
+
(current_time - stats["first_seen"]) / 86400, 1
|
| 220 |
+
)
|
| 221 |
+
stats["days_since_last_seen"] = round(
|
| 222 |
+
(current_time - stats["last_seen"]) / 86400, 1
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
return {
|
| 226 |
+
"user_uuid": user_uuid,
|
| 227 |
+
"user_id_short": user_uuid[:8],
|
| 228 |
+
"statistics": stats,
|
| 229 |
+
"privacy_note": "All device data is hashed for privacy protection",
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
def get_all_users_stats(self) -> Dict[str, Any]:
|
| 233 |
+
"""Get aggregated statistics for all users."""
|
| 234 |
+
total_users = len(self.cache["user_stats"])
|
| 235 |
+
if total_users == 0:
|
| 236 |
+
return {"total_users": 0, "message": "No users registered yet"}
|
| 237 |
+
|
| 238 |
+
# Aggregate statistics
|
| 239 |
+
total_operations = sum(
|
| 240 |
+
stats["total_operations"] for stats in self.cache["user_stats"].values()
|
| 241 |
+
)
|
| 242 |
+
total_memories = sum(
|
| 243 |
+
stats["memories_stored"] for stats in self.cache["user_stats"].values()
|
| 244 |
+
)
|
| 245 |
+
total_searches = sum(
|
| 246 |
+
stats["searches_performed"] for stats in self.cache["user_stats"].values()
|
| 247 |
+
)
|
| 248 |
+
total_videos = sum(
|
| 249 |
+
stats["videos_built"] for stats in self.cache["user_stats"].values()
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Device diversity
|
| 253 |
+
os_counts = {}
|
| 254 |
+
arch_counts = {}
|
| 255 |
+
for stats in self.cache["user_stats"].values():
|
| 256 |
+
device_info = stats.get("device_info", {})
|
| 257 |
+
os_name = device_info.get("os", "unknown")
|
| 258 |
+
arch_name = device_info.get("architecture", "unknown")
|
| 259 |
+
|
| 260 |
+
os_counts[os_name] = os_counts.get(os_name, 0) + 1
|
| 261 |
+
arch_counts[arch_name] = arch_counts.get(arch_name, 0) + 1
|
| 262 |
+
|
| 263 |
+
return {
|
| 264 |
+
"total_users": total_users,
|
| 265 |
+
"aggregated_stats": {
|
| 266 |
+
"total_operations": total_operations,
|
| 267 |
+
"total_memories_stored": total_memories,
|
| 268 |
+
"total_searches_performed": total_searches,
|
| 269 |
+
"total_videos_built": total_videos,
|
| 270 |
+
"avg_operations_per_user": round(total_operations / total_users, 1),
|
| 271 |
+
},
|
| 272 |
+
"device_diversity": {
|
| 273 |
+
"operating_systems": os_counts,
|
| 274 |
+
"architectures": arch_counts,
|
| 275 |
+
},
|
| 276 |
+
"privacy_note": "All statistics are aggregated and anonymized",
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
def get_fingerprint_info(self) -> Dict[str, Any]:
|
| 280 |
+
"""Get information about the current device fingerprint."""
|
| 281 |
+
fingerprint = self._get_minimal_fingerprint()
|
| 282 |
+
fingerprint_hash = self._generate_fingerprint_hash(fingerprint)
|
| 283 |
+
user_uuid = self.get_user_uuid()
|
| 284 |
+
|
| 285 |
+
return {
|
| 286 |
+
"user_uuid": user_uuid,
|
| 287 |
+
"user_id_short": user_uuid[:8],
|
| 288 |
+
"fingerprint_hash": fingerprint_hash[:16],
|
| 289 |
+
"device_characteristics": {
|
| 290 |
+
"os": fingerprint.get("os_system", "unknown"),
|
| 291 |
+
"architecture": fingerprint.get("architecture", "unknown"),
|
| 292 |
+
"cpu_cores": fingerprint.get("cpu_count_logical", 0),
|
| 293 |
+
"memory_gb": fingerprint.get("memory_total_gb", 0),
|
| 294 |
+
"has_mac_hash": "mac_hash" in fingerprint,
|
| 295 |
+
},
|
| 296 |
+
"privacy_protection": {
|
| 297 |
+
"data_collection": "Minimal - only essential system characteristics",
|
| 298 |
+
"sensitive_data": "All identifying information is hashed",
|
| 299 |
+
"storage": "Local cache only, no external transmission",
|
| 300 |
+
"consistency": "Same device always generates same UUID",
|
| 301 |
+
},
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# Global instance for easy access
|
| 306 |
+
_fingerprint_manager = None
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def get_fingerprint_manager() -> MinimalFingerprintManager:
|
| 310 |
+
"""Get the global fingerprint manager instance."""
|
| 311 |
+
global _fingerprint_manager
|
| 312 |
+
if _fingerprint_manager is None:
|
| 313 |
+
_fingerprint_manager = MinimalFingerprintManager()
|
| 314 |
+
return _fingerprint_manager
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def get_auto_user_uuid() -> str:
|
| 318 |
+
"""
|
| 319 |
+
Convenience function to get automatic user UUID.
|
| 320 |
+
|
| 321 |
+
Returns:
|
| 322 |
+
str: Consistent UUID for the current user/device
|
| 323 |
+
"""
|
| 324 |
+
return get_fingerprint_manager().get_user_uuid()
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def update_user_operation_stats(user_uuid: str, operation_type: str) -> None:
|
| 328 |
+
"""
|
| 329 |
+
Convenience function to update user operation statistics.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
user_uuid (str): User's UUID
|
| 333 |
+
operation_type (str): Type of operation performed
|
| 334 |
+
"""
|
| 335 |
+
get_fingerprint_manager().update_user_stats(user_uuid, operation_type)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
if __name__ == "__main__":
|
| 339 |
+
# Test the fingerprinting system
|
| 340 |
+
print("π Testing Minimal Fingerprint Manager...")
|
| 341 |
+
|
| 342 |
+
manager = MinimalFingerprintManager()
|
| 343 |
+
|
| 344 |
+
# Test basic functionality
|
| 345 |
+
user_uuid = manager.get_user_uuid()
|
| 346 |
+
print(f"Generated User UUID: {user_uuid}")
|
| 347 |
+
|
| 348 |
+
# Test fingerprint info
|
| 349 |
+
info = manager.get_fingerprint_info()
|
| 350 |
+
print(f"Device OS: {info['device_characteristics']['os']}")
|
| 351 |
+
print(f"CPU Cores: {info['device_characteristics']['cpu_cores']}")
|
| 352 |
+
print(f"Memory: {info['device_characteristics']['memory_gb']} GB")
|
| 353 |
+
|
| 354 |
+
# Test stats
|
| 355 |
+
manager.update_user_stats(user_uuid, "store_memory")
|
| 356 |
+
manager.update_user_stats(user_uuid, "search_memory")
|
| 357 |
+
|
| 358 |
+
stats = manager.get_user_stats(user_uuid)
|
| 359 |
+
print(f"User Stats: {stats['statistics']['total_operations']} operations")
|
| 360 |
+
|
| 361 |
+
print("β
Fingerprint Manager Test Complete")
|
utils/memvid_manager.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Memvid Manager - Wrapper for memvid operations with error handling.
|
| 3 |
+
Handles video-based memory storage, search, and chat functionality.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, Any, List, Optional, Tuple
|
| 11 |
+
import tempfile
|
| 12 |
+
import shutil
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from memvid import MemvidEncoder, MemvidRetriever, MemvidChat
|
| 16 |
+
|
| 17 |
+
MEMVID_AVAILABLE = True
|
| 18 |
+
except ImportError:
|
| 19 |
+
logging.warning("Memvid library not available. Using mock implementation.")
|
| 20 |
+
MemvidEncoder = None
|
| 21 |
+
MemvidRetriever = None
|
| 22 |
+
MemvidChat = None
|
| 23 |
+
MEMVID_AVAILABLE = False
|
| 24 |
+
|
| 25 |
+
from .storage_handler import StorageHandler
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class MemvidManager:
|
| 29 |
+
"""
|
| 30 |
+
Manages memvid operations with HuggingFace dataset integration.
|
| 31 |
+
Provides video-based memory storage for MCP server.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, data_dir: str = "data"):
|
| 35 |
+
"""
|
| 36 |
+
Initialize the memvid manager.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
data_dir (str): Base directory for storing memory data
|
| 40 |
+
"""
|
| 41 |
+
self.data_dir = Path(data_dir)
|
| 42 |
+
self.data_dir.mkdir(exist_ok=True)
|
| 43 |
+
|
| 44 |
+
self.logger = logging.getLogger(__name__)
|
| 45 |
+
|
| 46 |
+
# Initialize storage handler for HuggingFace integration
|
| 47 |
+
self.storage_handler = StorageHandler()
|
| 48 |
+
|
| 49 |
+
self.logger.info(f"MemvidManager initialized with data_dir: {self.data_dir}")
|
| 50 |
+
|
| 51 |
+
def _get_client_dir(self, client_id: str) -> Path:
|
| 52 |
+
"""Get client-specific directory."""
|
| 53 |
+
client_dir = self.data_dir / client_id
|
| 54 |
+
client_dir.mkdir(exist_ok=True)
|
| 55 |
+
|
| 56 |
+
# Create subdirectories
|
| 57 |
+
(client_dir / "chunks").mkdir(exist_ok=True)
|
| 58 |
+
(client_dir / "videos").mkdir(exist_ok=True)
|
| 59 |
+
|
| 60 |
+
return client_dir
|
| 61 |
+
|
| 62 |
+
def _get_metadata_path(self, client_id: str) -> Path:
|
| 63 |
+
"""Get path to client metadata file."""
|
| 64 |
+
return self._get_client_dir(client_id) / "metadata.json"
|
| 65 |
+
|
| 66 |
+
def _load_metadata(self, client_id: str) -> Dict[str, Any]:
|
| 67 |
+
"""Load client metadata."""
|
| 68 |
+
metadata_path = self._get_metadata_path(client_id)
|
| 69 |
+
|
| 70 |
+
if metadata_path.exists():
|
| 71 |
+
try:
|
| 72 |
+
with open(metadata_path, "r") as f:
|
| 73 |
+
return json.load(f)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
self.logger.error(f"Error loading metadata for {client_id}: {e}")
|
| 76 |
+
|
| 77 |
+
# Return default metadata
|
| 78 |
+
return {
|
| 79 |
+
"client_id": client_id,
|
| 80 |
+
"total_chunks": 0,
|
| 81 |
+
"total_memories": 0,
|
| 82 |
+
"created_at": "",
|
| 83 |
+
"last_updated": "",
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
def _save_metadata(self, client_id: str, metadata: Dict[str, Any]) -> None:
|
| 87 |
+
"""Save client metadata."""
|
| 88 |
+
try:
|
| 89 |
+
metadata_path = self._get_metadata_path(client_id)
|
| 90 |
+
|
| 91 |
+
import datetime
|
| 92 |
+
|
| 93 |
+
metadata["last_updated"] = datetime.datetime.now().isoformat()
|
| 94 |
+
if not metadata.get("created_at"):
|
| 95 |
+
metadata["created_at"] = metadata["last_updated"]
|
| 96 |
+
|
| 97 |
+
with open(metadata_path, "w") as f:
|
| 98 |
+
json.dump(metadata, f, indent=2)
|
| 99 |
+
|
| 100 |
+
# Upload metadata to HuggingFace if enabled
|
| 101 |
+
self.storage_handler.upload_client_metadata(client_id, metadata)
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
self.logger.error(f"Error saving metadata for {client_id}: {e}")
|
| 105 |
+
|
| 106 |
+
def store_memory(
|
| 107 |
+
self, text: str, client_id: str, metadata: Dict[str, Any] = None
|
| 108 |
+
) -> str:
|
| 109 |
+
"""
|
| 110 |
+
Store a text chunk in memory.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
text (str): Text content to store
|
| 114 |
+
client_id (str): Client identifier
|
| 115 |
+
metadata (dict): Additional metadata
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
str: Success message with storage details
|
| 119 |
+
"""
|
| 120 |
+
try:
|
| 121 |
+
client_dir = self._get_client_dir(client_id)
|
| 122 |
+
chunks_dir = client_dir / "chunks"
|
| 123 |
+
|
| 124 |
+
# Load current metadata
|
| 125 |
+
client_metadata = self._load_metadata(client_id)
|
| 126 |
+
chunk_count = client_metadata.get("total_chunks", 0) + 1
|
| 127 |
+
|
| 128 |
+
# Create chunk filename
|
| 129 |
+
chunk_filename = f"chunk_{chunk_count:04d}.txt"
|
| 130 |
+
chunk_path = chunks_dir / chunk_filename
|
| 131 |
+
|
| 132 |
+
# Prepare chunk metadata
|
| 133 |
+
chunk_metadata = {
|
| 134 |
+
"chunk_id": chunk_count,
|
| 135 |
+
"filename": chunk_filename,
|
| 136 |
+
"text_length": len(text),
|
| 137 |
+
"stored_at": "",
|
| 138 |
+
**(metadata or {}),
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
# Save chunk to file
|
| 142 |
+
with open(chunk_path, "w", encoding="utf-8") as f:
|
| 143 |
+
f.write(text)
|
| 144 |
+
|
| 145 |
+
# Update client metadata
|
| 146 |
+
client_metadata["total_chunks"] = chunk_count
|
| 147 |
+
client_metadata["client_id"] = client_id
|
| 148 |
+
self._save_metadata(client_id, client_metadata)
|
| 149 |
+
|
| 150 |
+
return f"Successfully stored memory chunk {chunk_filename} for client {client_id}. Total chunks: {chunk_count}"
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
error_msg = f"Error storing memory: {str(e)}"
|
| 154 |
+
self.logger.error(error_msg)
|
| 155 |
+
return error_msg
|
| 156 |
+
|
| 157 |
+
def build_memory_video(self, client_id: str, memory_name: str) -> str:
|
| 158 |
+
"""
|
| 159 |
+
Build a memory video from stored chunks.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
client_id (str): Client identifier
|
| 163 |
+
memory_name (str): Name for the memory video
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
str: Success message with video details
|
| 167 |
+
"""
|
| 168 |
+
try:
|
| 169 |
+
if not MEMVID_AVAILABLE:
|
| 170 |
+
return "Error: Memvid library not available"
|
| 171 |
+
|
| 172 |
+
client_dir = self._get_client_dir(client_id)
|
| 173 |
+
chunks_dir = client_dir / "chunks"
|
| 174 |
+
videos_dir = client_dir / "videos"
|
| 175 |
+
|
| 176 |
+
# Check if chunks exist
|
| 177 |
+
chunk_files = list(chunks_dir.glob("chunk_*.txt"))
|
| 178 |
+
if not chunk_files:
|
| 179 |
+
return f"Error: No chunks found for client {client_id}"
|
| 180 |
+
|
| 181 |
+
# Read all chunks
|
| 182 |
+
chunks = []
|
| 183 |
+
for chunk_file in sorted(chunk_files):
|
| 184 |
+
try:
|
| 185 |
+
with open(chunk_file, "r", encoding="utf-8") as f:
|
| 186 |
+
chunks.append(f.read().strip())
|
| 187 |
+
except Exception as e:
|
| 188 |
+
self.logger.warning(f"Error reading chunk {chunk_file}: {e}")
|
| 189 |
+
|
| 190 |
+
if not chunks:
|
| 191 |
+
return f"Error: No valid chunks found for client {client_id}"
|
| 192 |
+
|
| 193 |
+
# Initialize memvid encoder
|
| 194 |
+
encoder = MemvidEncoder()
|
| 195 |
+
|
| 196 |
+
# Add chunks to encoder
|
| 197 |
+
for chunk in chunks:
|
| 198 |
+
if chunk.strip(): # Only add non-empty chunks
|
| 199 |
+
encoder.add_text(chunk.strip())
|
| 200 |
+
|
| 201 |
+
# Build video
|
| 202 |
+
video_path = videos_dir / f"{memory_name}.mp4"
|
| 203 |
+
index_path = videos_dir / f"{memory_name}_index.json"
|
| 204 |
+
|
| 205 |
+
# Create video with embeddings
|
| 206 |
+
encoder.build_video(str(video_path), str(index_path))
|
| 207 |
+
|
| 208 |
+
# Update metadata
|
| 209 |
+
client_metadata = self._load_metadata(client_id)
|
| 210 |
+
memories = client_metadata.get("memories", [])
|
| 211 |
+
|
| 212 |
+
# Ensure memories is a list, not a dict
|
| 213 |
+
if not isinstance(memories, list):
|
| 214 |
+
memories = []
|
| 215 |
+
|
| 216 |
+
memories.append(
|
| 217 |
+
{
|
| 218 |
+
"name": memory_name,
|
| 219 |
+
"video_path": str(video_path),
|
| 220 |
+
"index_path": str(index_path),
|
| 221 |
+
"chunks_count": len(chunks),
|
| 222 |
+
}
|
| 223 |
+
)
|
| 224 |
+
client_metadata["memories"] = memories
|
| 225 |
+
client_metadata["total_memories"] = len(memories)
|
| 226 |
+
self._save_metadata(client_id, client_metadata)
|
| 227 |
+
|
| 228 |
+
# Upload to HuggingFace if enabled
|
| 229 |
+
if video_path.exists() and Path(index_path).exists():
|
| 230 |
+
self.storage_handler.upload_memory_video(
|
| 231 |
+
client_id, memory_name, video_path, Path(index_path)
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Get file size for reporting
|
| 235 |
+
video_size = video_path.stat().st_size if video_path.exists() else 0
|
| 236 |
+
|
| 237 |
+
return f"Successfully built memory video '{memory_name}' for client {client_id} with {len(chunks)} chunks"
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
error_msg = f"Error building memory video: {str(e)}"
|
| 241 |
+
self.logger.error(error_msg)
|
| 242 |
+
return error_msg
|
| 243 |
+
|
| 244 |
+
def search_memory(
|
| 245 |
+
self, query: str, client_id: str, memory_name: str, top_k: int = 5
|
| 246 |
+
) -> str:
|
| 247 |
+
"""
|
| 248 |
+
Search stored memories using semantic similarity.
|
| 249 |
+
FIXED: Handles memvid return value unpacking issue.
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
query (str): Search query
|
| 253 |
+
client_id (str): Client identifier
|
| 254 |
+
memory_name (str): Name of memory video to search
|
| 255 |
+
top_k (int): Number of results to return
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
str: JSON string with search results and scores
|
| 259 |
+
"""
|
| 260 |
+
try:
|
| 261 |
+
if not MEMVID_AVAILABLE:
|
| 262 |
+
return json.dumps({"error": "Memvid library not available"})
|
| 263 |
+
|
| 264 |
+
client_dir = self._get_client_dir(client_id)
|
| 265 |
+
videos_dir = client_dir / "videos"
|
| 266 |
+
|
| 267 |
+
video_path = videos_dir / f"{memory_name}.mp4"
|
| 268 |
+
index_path = videos_dir / f"{memory_name}_index.json"
|
| 269 |
+
|
| 270 |
+
if not video_path.exists():
|
| 271 |
+
return json.dumps(
|
| 272 |
+
{
|
| 273 |
+
"error": f"Memory video '{memory_name}' not found for client {client_id}"
|
| 274 |
+
}
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Initialize memvid retriever
|
| 278 |
+
try:
|
| 279 |
+
retriever = MemvidRetriever(str(video_path), str(index_path))
|
| 280 |
+
except Exception as e:
|
| 281 |
+
return json.dumps({"error": f"Error loading memory video: {str(e)}"})
|
| 282 |
+
|
| 283 |
+
# Perform search with proper error handling
|
| 284 |
+
try:
|
| 285 |
+
# FIXED: Handle different return value formats from memvid
|
| 286 |
+
search_results = retriever.search(query, top_k=top_k)
|
| 287 |
+
|
| 288 |
+
# Handle tuple return (results, scores) or just results
|
| 289 |
+
if isinstance(search_results, tuple):
|
| 290 |
+
results, scores = search_results
|
| 291 |
+
# Combine results with scores
|
| 292 |
+
combined_results = []
|
| 293 |
+
for i, result in enumerate(results):
|
| 294 |
+
combined_results.append(
|
| 295 |
+
{
|
| 296 |
+
"text": result,
|
| 297 |
+
"score": float(scores[i]) if i < len(scores) else 0.0,
|
| 298 |
+
"rank": i + 1,
|
| 299 |
+
}
|
| 300 |
+
)
|
| 301 |
+
search_data = combined_results
|
| 302 |
+
elif isinstance(search_results, list):
|
| 303 |
+
# Just results without scores
|
| 304 |
+
search_data = [
|
| 305 |
+
{"text": result, "score": 1.0, "rank": i + 1} # Default score
|
| 306 |
+
for i, result in enumerate(search_results)
|
| 307 |
+
]
|
| 308 |
+
else:
|
| 309 |
+
# Single result or other format
|
| 310 |
+
search_data = [
|
| 311 |
+
{"text": str(search_results), "score": 1.0, "rank": 1}
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
return json.dumps(
|
| 315 |
+
{
|
| 316 |
+
"query": query,
|
| 317 |
+
"client_id": client_id,
|
| 318 |
+
"memory_name": memory_name,
|
| 319 |
+
"total_results": len(search_data),
|
| 320 |
+
"results": search_data,
|
| 321 |
+
},
|
| 322 |
+
indent=2,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
except Exception as search_error:
|
| 326 |
+
return json.dumps(
|
| 327 |
+
{
|
| 328 |
+
"error": f"Search failed: {str(search_error)}",
|
| 329 |
+
"query": query,
|
| 330 |
+
"memory_name": memory_name,
|
| 331 |
+
}
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
except Exception as e:
|
| 335 |
+
error_msg = f"Error searching memory: {str(e)}"
|
| 336 |
+
self.logger.error(error_msg)
|
| 337 |
+
return json.dumps({"error": error_msg})
|
| 338 |
+
|
| 339 |
+
def chat_with_memory(self, query: str, client_id: str, memory_name: str) -> str:
|
| 340 |
+
"""
|
| 341 |
+
Interactive chat with stored memory.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
query (str): User question/query
|
| 345 |
+
client_id (str): Client identifier
|
| 346 |
+
memory_name (str): Name of memory video to query
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
str: AI response based on memory context
|
| 350 |
+
"""
|
| 351 |
+
try:
|
| 352 |
+
if not MEMVID_AVAILABLE:
|
| 353 |
+
return "Error: Memvid library not available"
|
| 354 |
+
|
| 355 |
+
client_dir = self._get_client_dir(client_id)
|
| 356 |
+
videos_dir = client_dir / "videos"
|
| 357 |
+
|
| 358 |
+
video_path = videos_dir / f"{memory_name}.mp4"
|
| 359 |
+
index_path = videos_dir / f"{memory_name}_index.json"
|
| 360 |
+
|
| 361 |
+
if not video_path.exists():
|
| 362 |
+
return f"Error: Memory video '{memory_name}' not found for client {client_id}"
|
| 363 |
+
|
| 364 |
+
# Initialize memvid chat
|
| 365 |
+
chat = MemvidChat(str(video_path), str(index_path))
|
| 366 |
+
|
| 367 |
+
# Use memvid chat functionality
|
| 368 |
+
response = chat.chat(query)
|
| 369 |
+
|
| 370 |
+
return response
|
| 371 |
+
|
| 372 |
+
except Exception as e:
|
| 373 |
+
error_msg = f"Error in chat_with_memory: {str(e)}"
|
| 374 |
+
self.logger.error(error_msg)
|
| 375 |
+
return error_msg
|
| 376 |
+
|
| 377 |
+
def list_memories(self, client_id: str) -> str:
|
| 378 |
+
"""
|
| 379 |
+
List all memory videos for a client.
|
| 380 |
+
|
| 381 |
+
Args:
|
| 382 |
+
client_id (str): Client identifier
|
| 383 |
+
|
| 384 |
+
Returns:
|
| 385 |
+
str: JSON string with memory list
|
| 386 |
+
"""
|
| 387 |
+
try:
|
| 388 |
+
client_metadata = self._load_metadata(client_id)
|
| 389 |
+
videos_dir = self._get_client_dir(client_id) / "videos"
|
| 390 |
+
|
| 391 |
+
# Get actual video files
|
| 392 |
+
video_files = list(videos_dir.glob("*.mp4"))
|
| 393 |
+
memories = []
|
| 394 |
+
|
| 395 |
+
for video_file in video_files:
|
| 396 |
+
memory_name = video_file.stem
|
| 397 |
+
index_file = videos_dir / f"{memory_name}_index.json"
|
| 398 |
+
|
| 399 |
+
memory_info = {
|
| 400 |
+
"name": memory_name,
|
| 401 |
+
"video_file": video_file.name,
|
| 402 |
+
"size_bytes": video_file.stat().st_size,
|
| 403 |
+
"has_index": index_file.exists(),
|
| 404 |
+
}
|
| 405 |
+
memories.append(memory_info)
|
| 406 |
+
|
| 407 |
+
return json.dumps(
|
| 408 |
+
{
|
| 409 |
+
"client_id": client_id,
|
| 410 |
+
"total_memories": len(memories),
|
| 411 |
+
"total_chunks": client_metadata.get("total_chunks", 0),
|
| 412 |
+
"memories": memories,
|
| 413 |
+
},
|
| 414 |
+
indent=2,
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
except Exception as e:
|
| 418 |
+
error_msg = f"Error listing memories: {str(e)}"
|
| 419 |
+
self.logger.error(error_msg)
|
| 420 |
+
return json.dumps({"error": error_msg})
|
| 421 |
+
|
| 422 |
+
def get_memory_stats(self, client_id: str) -> str:
|
| 423 |
+
"""
|
| 424 |
+
Get memory usage statistics for a client.
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
client_id (str): Client identifier
|
| 428 |
+
|
| 429 |
+
Returns:
|
| 430 |
+
str: JSON string with statistics
|
| 431 |
+
"""
|
| 432 |
+
try:
|
| 433 |
+
client_dir = self._get_client_dir(client_id)
|
| 434 |
+
chunks_dir = client_dir / "chunks"
|
| 435 |
+
videos_dir = client_dir / "videos"
|
| 436 |
+
|
| 437 |
+
# Calculate storage usage
|
| 438 |
+
chunks_size = sum(f.stat().st_size for f in chunks_dir.glob("*.txt"))
|
| 439 |
+
videos_size = sum(f.stat().st_size for f in videos_dir.glob("*"))
|
| 440 |
+
total_size = chunks_size + videos_size
|
| 441 |
+
|
| 442 |
+
# Count files
|
| 443 |
+
chunk_count = len(list(chunks_dir.glob("chunk_*.txt")))
|
| 444 |
+
memory_count = len(list(videos_dir.glob("*.mp4")))
|
| 445 |
+
|
| 446 |
+
# Load metadata
|
| 447 |
+
client_metadata = self._load_metadata(client_id)
|
| 448 |
+
|
| 449 |
+
stats = {
|
| 450 |
+
"client_id": client_id,
|
| 451 |
+
"total_chunks": chunk_count,
|
| 452 |
+
"total_memories": memory_count,
|
| 453 |
+
"storage_usage": {
|
| 454 |
+
"chunks_size_bytes": chunks_size,
|
| 455 |
+
"videos_size_bytes": videos_size,
|
| 456 |
+
"total_size_bytes": total_size,
|
| 457 |
+
"chunks_size_mb": round(chunks_size / 1024 / 1024, 2),
|
| 458 |
+
"videos_size_mb": round(videos_size / 1024 / 1024, 2),
|
| 459 |
+
"total_size_mb": round(total_size / 1024 / 1024, 2),
|
| 460 |
+
},
|
| 461 |
+
"created_at": client_metadata.get("created_at", ""),
|
| 462 |
+
"last_updated": client_metadata.get("last_updated", ""),
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
return json.dumps(stats, indent=2)
|
| 466 |
+
|
| 467 |
+
except Exception as e:
|
| 468 |
+
error_msg = f"Error getting memory stats: {str(e)}"
|
| 469 |
+
self.logger.error(error_msg)
|
| 470 |
+
return json.dumps({"error": error_msg})
|
| 471 |
+
|
| 472 |
+
def delete_memory(self, client_id: str, memory_name: str) -> str:
|
| 473 |
+
"""
|
| 474 |
+
Delete a specific memory video.
|
| 475 |
+
|
| 476 |
+
Args:
|
| 477 |
+
client_id (str): Client identifier
|
| 478 |
+
memory_name (str): Name of memory to delete
|
| 479 |
+
|
| 480 |
+
Returns:
|
| 481 |
+
str: Success/error message
|
| 482 |
+
"""
|
| 483 |
+
try:
|
| 484 |
+
client_dir = self._get_client_dir(client_id)
|
| 485 |
+
videos_dir = client_dir / "videos"
|
| 486 |
+
|
| 487 |
+
video_path = videos_dir / f"{memory_name}.mp4"
|
| 488 |
+
index_path = videos_dir / f"{memory_name}_index.json"
|
| 489 |
+
faiss_path = videos_dir / f"{memory_name}_index.faiss"
|
| 490 |
+
|
| 491 |
+
deleted_files = []
|
| 492 |
+
|
| 493 |
+
# Delete video file
|
| 494 |
+
if video_path.exists():
|
| 495 |
+
video_path.unlink()
|
| 496 |
+
deleted_files.append("video")
|
| 497 |
+
|
| 498 |
+
# Delete index files
|
| 499 |
+
if index_path.exists():
|
| 500 |
+
index_path.unlink()
|
| 501 |
+
deleted_files.append("index")
|
| 502 |
+
|
| 503 |
+
if faiss_path.exists():
|
| 504 |
+
faiss_path.unlink()
|
| 505 |
+
deleted_files.append("faiss_index")
|
| 506 |
+
|
| 507 |
+
if not deleted_files:
|
| 508 |
+
return f"Error: Memory '{memory_name}' not found for client {client_id}"
|
| 509 |
+
|
| 510 |
+
# Update metadata
|
| 511 |
+
client_metadata = self._load_metadata(client_id)
|
| 512 |
+
memories = client_metadata.get("memories", [])
|
| 513 |
+
memories = [m for m in memories if m.get("name") != memory_name]
|
| 514 |
+
client_metadata["memories"] = memories
|
| 515 |
+
client_metadata["total_memories"] = len(memories)
|
| 516 |
+
self._save_metadata(client_id, client_metadata)
|
| 517 |
+
|
| 518 |
+
return f"Successfully deleted memory '{memory_name}' for client {client_id} ({', '.join(deleted_files)} files removed)"
|
| 519 |
+
|
| 520 |
+
except Exception as e:
|
| 521 |
+
error_msg = f"Error deleting memory: {str(e)}"
|
| 522 |
+
self.logger.error(error_msg)
|
| 523 |
+
return error_msg
|
utils/metrics_collector.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Metrics Collector - Tracks performance metrics for dual storage comparison.
|
| 3 |
+
Provides background analytics and comparison reporting without user complexity.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, List, Any, Optional
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from collections import defaultdict, deque
|
| 12 |
+
import statistics
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MetricsCollector:
|
| 16 |
+
"""
|
| 17 |
+
Collects and analyzes performance metrics for dual storage comparison.
|
| 18 |
+
Tracks storage/search performance, accuracy, and provides comparison analytics.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, max_samples: int = 1000):
|
| 22 |
+
"""
|
| 23 |
+
Initialize metrics collector.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
max_samples (int): Maximum number of samples to keep in memory
|
| 27 |
+
"""
|
| 28 |
+
self.logger = logging.getLogger(__name__)
|
| 29 |
+
self.max_samples = max_samples
|
| 30 |
+
|
| 31 |
+
# Storage metrics
|
| 32 |
+
self.storage_metrics = {
|
| 33 |
+
"memvid": deque(maxlen=max_samples),
|
| 34 |
+
"vector": deque(maxlen=max_samples),
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# Search metrics
|
| 38 |
+
self.search_metrics = {
|
| 39 |
+
"memvid": deque(maxlen=max_samples),
|
| 40 |
+
"vector": deque(maxlen=max_samples),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Comparison metrics
|
| 44 |
+
self.comparison_data = {
|
| 45 |
+
"storage_comparisons": deque(maxlen=max_samples),
|
| 46 |
+
"search_comparisons": deque(maxlen=max_samples),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Client-specific metrics
|
| 50 |
+
self.client_metrics = defaultdict(
|
| 51 |
+
lambda: {
|
| 52 |
+
"storage_count": 0,
|
| 53 |
+
"search_count": 0,
|
| 54 |
+
"total_data_stored": 0,
|
| 55 |
+
"preferred_mode": "unknown",
|
| 56 |
+
}
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
self.logger.info("MetricsCollector initialized")
|
| 60 |
+
|
| 61 |
+
def track_storage_operation(
|
| 62 |
+
self, backend: str, duration: float, data_size: int, client_id: str = ""
|
| 63 |
+
) -> None:
|
| 64 |
+
"""
|
| 65 |
+
Track a storage operation.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
backend (str): Storage backend (memvid/vector)
|
| 69 |
+
duration (float): Operation duration in seconds
|
| 70 |
+
data_size (int): Size of data stored in bytes
|
| 71 |
+
client_id (str): Client identifier
|
| 72 |
+
"""
|
| 73 |
+
metric = {
|
| 74 |
+
"timestamp": time.time(),
|
| 75 |
+
"backend": backend,
|
| 76 |
+
"duration": duration,
|
| 77 |
+
"data_size": data_size,
|
| 78 |
+
"client_id": client_id,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
self.storage_metrics[backend].append(metric)
|
| 82 |
+
|
| 83 |
+
if client_id:
|
| 84 |
+
self.client_metrics[client_id]["storage_count"] += 1
|
| 85 |
+
self.client_metrics[client_id]["total_data_stored"] += data_size
|
| 86 |
+
|
| 87 |
+
def track_search_operation(
|
| 88 |
+
self, backend: str, duration: float, top_k: int, client_id: str = ""
|
| 89 |
+
) -> None:
|
| 90 |
+
"""
|
| 91 |
+
Track a search operation.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
backend (str): Storage backend (memvid/vector)
|
| 95 |
+
duration (float): Operation duration in seconds
|
| 96 |
+
top_k (int): Number of results requested
|
| 97 |
+
client_id (str): Client identifier
|
| 98 |
+
"""
|
| 99 |
+
metric = {
|
| 100 |
+
"timestamp": time.time(),
|
| 101 |
+
"backend": backend,
|
| 102 |
+
"duration": duration,
|
| 103 |
+
"top_k": top_k,
|
| 104 |
+
"client_id": client_id,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
self.search_metrics[backend].append(metric)
|
| 108 |
+
|
| 109 |
+
if client_id:
|
| 110 |
+
self.client_metrics[client_id]["search_count"] += 1
|
| 111 |
+
|
| 112 |
+
def track_dual_storage_comparison(
|
| 113 |
+
self, memvid_time: float, vector_time: float, data_size: int, client_id: str
|
| 114 |
+
) -> None:
|
| 115 |
+
"""
|
| 116 |
+
Track dual storage comparison metrics.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
memvid_time (float): Memvid storage time
|
| 120 |
+
vector_time (float): Vector storage time
|
| 121 |
+
data_size (int): Size of data stored
|
| 122 |
+
client_id (str): Client identifier
|
| 123 |
+
"""
|
| 124 |
+
comparison = {
|
| 125 |
+
"timestamp": time.time(),
|
| 126 |
+
"memvid_time": memvid_time,
|
| 127 |
+
"vector_time": vector_time,
|
| 128 |
+
"data_size": data_size,
|
| 129 |
+
"client_id": client_id,
|
| 130 |
+
"winner": "memvid" if memvid_time < vector_time else "vector",
|
| 131 |
+
"speedup": max(memvid_time, vector_time) / min(memvid_time, vector_time),
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
self.comparison_data["storage_comparisons"].append(comparison)
|
| 135 |
+
|
| 136 |
+
def track_dual_search_comparison(
|
| 137 |
+
self, memvid_time: float, vector_time: float, query: str, client_id: str
|
| 138 |
+
) -> None:
|
| 139 |
+
"""
|
| 140 |
+
Track dual search comparison metrics.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
memvid_time (float): Memvid search time
|
| 144 |
+
vector_time (float): Vector search time
|
| 145 |
+
query (str): Search query
|
| 146 |
+
client_id (str): Client identifier
|
| 147 |
+
"""
|
| 148 |
+
comparison = {
|
| 149 |
+
"timestamp": time.time(),
|
| 150 |
+
"memvid_time": memvid_time,
|
| 151 |
+
"vector_time": vector_time,
|
| 152 |
+
"query_length": len(query),
|
| 153 |
+
"client_id": client_id,
|
| 154 |
+
"winner": "memvid" if memvid_time < vector_time else "vector",
|
| 155 |
+
"speedup": (
|
| 156 |
+
max(memvid_time, vector_time) / min(memvid_time, vector_time)
|
| 157 |
+
if min(memvid_time, vector_time) > 0
|
| 158 |
+
else 1.0
|
| 159 |
+
),
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
self.comparison_data["search_comparisons"].append(comparison)
|
| 163 |
+
|
| 164 |
+
def get_comparison_report(self, client_id: str = "") -> str:
|
| 165 |
+
"""
|
| 166 |
+
Generate comprehensive comparison report.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
client_id (str): Client identifier (empty for global report)
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
str: JSON string with comparison analytics
|
| 173 |
+
"""
|
| 174 |
+
try:
|
| 175 |
+
report = {
|
| 176 |
+
"report_timestamp": time.time(),
|
| 177 |
+
"client_id": client_id or "global",
|
| 178 |
+
"storage_mode": "dual",
|
| 179 |
+
"summary": self._generate_summary(client_id),
|
| 180 |
+
"performance_analysis": self._analyze_performance(client_id),
|
| 181 |
+
"recommendations": self._generate_recommendations(client_id),
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return json.dumps(report, indent=2)
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
self.logger.error(f"Error generating comparison report: {e}")
|
| 188 |
+
return json.dumps({"error": f"Failed to generate report: {str(e)}"})
|
| 189 |
+
|
| 190 |
+
def _generate_summary(self, client_id: str = "") -> Dict[str, Any]:
|
| 191 |
+
"""Generate performance summary."""
|
| 192 |
+
storage_comps = list(self.comparison_data["storage_comparisons"])
|
| 193 |
+
search_comps = list(self.comparison_data["search_comparisons"])
|
| 194 |
+
|
| 195 |
+
# Filter by client if specified
|
| 196 |
+
if client_id:
|
| 197 |
+
storage_comps = [c for c in storage_comps if c["client_id"] == client_id]
|
| 198 |
+
search_comps = [c for c in search_comps if c["client_id"] == client_id]
|
| 199 |
+
|
| 200 |
+
if not storage_comps and not search_comps:
|
| 201 |
+
return {"message": "No comparison data available"}
|
| 202 |
+
|
| 203 |
+
summary = {
|
| 204 |
+
"total_comparisons": len(storage_comps) + len(search_comps),
|
| 205 |
+
"storage_comparisons": len(storage_comps),
|
| 206 |
+
"search_comparisons": len(search_comps),
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
# Storage performance summary
|
| 210 |
+
if storage_comps:
|
| 211 |
+
memvid_wins = sum(1 for c in storage_comps if c["winner"] == "memvid")
|
| 212 |
+
avg_speedup = statistics.mean([c["speedup"] for c in storage_comps])
|
| 213 |
+
|
| 214 |
+
summary["storage_performance"] = {
|
| 215 |
+
"memvid_wins": memvid_wins,
|
| 216 |
+
"vector_wins": len(storage_comps) - memvid_wins,
|
| 217 |
+
"avg_speedup_factor": round(avg_speedup, 2),
|
| 218 |
+
"faster_backend": (
|
| 219 |
+
"memvid" if memvid_wins > len(storage_comps) / 2 else "vector"
|
| 220 |
+
),
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
# Search performance summary
|
| 224 |
+
if search_comps:
|
| 225 |
+
memvid_wins = sum(1 for c in search_comps if c["winner"] == "memvid")
|
| 226 |
+
avg_speedup = statistics.mean([c["speedup"] for c in search_comps])
|
| 227 |
+
|
| 228 |
+
summary["search_performance"] = {
|
| 229 |
+
"memvid_wins": memvid_wins,
|
| 230 |
+
"vector_wins": len(search_comps) - memvid_wins,
|
| 231 |
+
"avg_speedup_factor": round(avg_speedup, 2),
|
| 232 |
+
"faster_backend": (
|
| 233 |
+
"memvid" if memvid_wins > len(search_comps) / 2 else "vector"
|
| 234 |
+
),
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
return summary
|
| 238 |
+
|
| 239 |
+
def _analyze_performance(self, client_id: str = "") -> Dict[str, Any]:
|
| 240 |
+
"""Analyze detailed performance metrics."""
|
| 241 |
+
analysis = {}
|
| 242 |
+
|
| 243 |
+
# Analyze storage performance
|
| 244 |
+
memvid_storage = [
|
| 245 |
+
m
|
| 246 |
+
for m in self.storage_metrics["memvid"]
|
| 247 |
+
if not client_id or m["client_id"] == client_id
|
| 248 |
+
]
|
| 249 |
+
vector_storage = [
|
| 250 |
+
m
|
| 251 |
+
for m in self.storage_metrics["vector"]
|
| 252 |
+
if not client_id or m["client_id"] == client_id
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
+
if memvid_storage:
|
| 256 |
+
analysis["memvid_storage"] = {
|
| 257 |
+
"avg_duration_ms": round(
|
| 258 |
+
statistics.mean([m["duration"] for m in memvid_storage]) * 1000, 2
|
| 259 |
+
),
|
| 260 |
+
"total_operations": len(memvid_storage),
|
| 261 |
+
"total_data_mb": round(
|
| 262 |
+
sum([m["data_size"] for m in memvid_storage]) / (1024 * 1024), 2
|
| 263 |
+
),
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
if vector_storage:
|
| 267 |
+
analysis["vector_storage"] = {
|
| 268 |
+
"avg_duration_ms": round(
|
| 269 |
+
statistics.mean([m["duration"] for m in vector_storage]) * 1000, 2
|
| 270 |
+
),
|
| 271 |
+
"total_operations": len(vector_storage),
|
| 272 |
+
"total_data_mb": round(
|
| 273 |
+
sum([m["data_size"] for m in vector_storage]) / (1024 * 1024), 2
|
| 274 |
+
),
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
# Analyze search performance
|
| 278 |
+
memvid_search = [
|
| 279 |
+
m
|
| 280 |
+
for m in self.search_metrics["memvid"]
|
| 281 |
+
if not client_id or m["client_id"] == client_id
|
| 282 |
+
]
|
| 283 |
+
vector_search = [
|
| 284 |
+
m
|
| 285 |
+
for m in self.search_metrics["vector"]
|
| 286 |
+
if not client_id or m["client_id"] == client_id
|
| 287 |
+
]
|
| 288 |
+
|
| 289 |
+
if memvid_search:
|
| 290 |
+
analysis["memvid_search"] = {
|
| 291 |
+
"avg_duration_ms": round(
|
| 292 |
+
statistics.mean([m["duration"] for m in memvid_search]) * 1000, 2
|
| 293 |
+
),
|
| 294 |
+
"total_searches": len(memvid_search),
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
if vector_search:
|
| 298 |
+
analysis["vector_search"] = {
|
| 299 |
+
"avg_duration_ms": round(
|
| 300 |
+
statistics.mean([m["duration"] for m in vector_search]) * 1000, 2
|
| 301 |
+
),
|
| 302 |
+
"total_searches": len(vector_search),
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
return analysis
|
| 306 |
+
|
| 307 |
+
def _generate_recommendations(self, client_id: str = "") -> List[str]:
|
| 308 |
+
"""Generate performance-based recommendations."""
|
| 309 |
+
recommendations = []
|
| 310 |
+
|
| 311 |
+
storage_comps = list(self.comparison_data["storage_comparisons"])
|
| 312 |
+
search_comps = list(self.comparison_data["search_comparisons"])
|
| 313 |
+
|
| 314 |
+
# Filter by client if specified
|
| 315 |
+
if client_id:
|
| 316 |
+
storage_comps = [c for c in storage_comps if c["client_id"] == client_id]
|
| 317 |
+
search_comps = [c for c in search_comps if c["client_id"] == client_id]
|
| 318 |
+
|
| 319 |
+
if not storage_comps and not search_comps:
|
| 320 |
+
recommendations.append("No comparison data available for recommendations")
|
| 321 |
+
return recommendations
|
| 322 |
+
|
| 323 |
+
# Storage recommendations
|
| 324 |
+
if storage_comps:
|
| 325 |
+
memvid_wins = sum(1 for c in storage_comps if c["winner"] == "memvid")
|
| 326 |
+
if memvid_wins > len(storage_comps) * 0.7:
|
| 327 |
+
recommendations.append(
|
| 328 |
+
"πΉ Memvid shows consistently faster storage - consider memvid_only mode for write-heavy workloads"
|
| 329 |
+
)
|
| 330 |
+
elif memvid_wins < len(storage_comps) * 0.3:
|
| 331 |
+
recommendations.append(
|
| 332 |
+
"β‘ Vector storage shows faster performance - consider vector_only mode for high-frequency storage"
|
| 333 |
+
)
|
| 334 |
+
else:
|
| 335 |
+
recommendations.append(
|
| 336 |
+
"βοΈ Storage performance is balanced - dual mode provides good comparison data"
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# Search recommendations
|
| 340 |
+
if search_comps:
|
| 341 |
+
memvid_wins = sum(1 for c in search_comps if c["winner"] == "memvid")
|
| 342 |
+
if memvid_wins > len(search_comps) * 0.7:
|
| 343 |
+
recommendations.append(
|
| 344 |
+
"π Memvid shows superior search performance - excellent for semantic search workloads"
|
| 345 |
+
)
|
| 346 |
+
elif memvid_wins < len(search_comps) * 0.3:
|
| 347 |
+
recommendations.append(
|
| 348 |
+
"π Vector search outperforms memvid - consider vector_only for search-heavy applications"
|
| 349 |
+
)
|
| 350 |
+
else:
|
| 351 |
+
recommendations.append(
|
| 352 |
+
"π― Search performance varies - dual mode provides valuable insights"
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
# Data size recommendations
|
| 356 |
+
if storage_comps:
|
| 357 |
+
avg_data_size = statistics.mean([c["data_size"] for c in storage_comps])
|
| 358 |
+
if avg_data_size > 10000: # Large chunks
|
| 359 |
+
recommendations.append(
|
| 360 |
+
"π Large data chunks detected - memvid compression may provide storage efficiency benefits"
|
| 361 |
+
)
|
| 362 |
+
elif avg_data_size < 1000: # Small chunks
|
| 363 |
+
recommendations.append(
|
| 364 |
+
"β‘ Small data chunks detected - vector storage may have lower overhead"
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
return recommendations
|
| 368 |
+
|
| 369 |
+
def export_metrics(self, format: str = "json") -> str:
|
| 370 |
+
"""
|
| 371 |
+
Export metrics data.
|
| 372 |
+
|
| 373 |
+
Args:
|
| 374 |
+
format (str): Export format (json, csv)
|
| 375 |
+
|
| 376 |
+
Returns:
|
| 377 |
+
str: Exported metrics data
|
| 378 |
+
"""
|
| 379 |
+
try:
|
| 380 |
+
if format.lower() == "json":
|
| 381 |
+
export_data = {
|
| 382 |
+
"export_timestamp": time.time(),
|
| 383 |
+
"storage_metrics": {
|
| 384 |
+
"memvid": list(self.storage_metrics["memvid"]),
|
| 385 |
+
"vector": list(self.storage_metrics["vector"]),
|
| 386 |
+
},
|
| 387 |
+
"search_metrics": {
|
| 388 |
+
"memvid": list(self.search_metrics["memvid"]),
|
| 389 |
+
"vector": list(self.search_metrics["vector"]),
|
| 390 |
+
},
|
| 391 |
+
"comparison_data": {
|
| 392 |
+
"storage_comparisons": list(
|
| 393 |
+
self.comparison_data["storage_comparisons"]
|
| 394 |
+
),
|
| 395 |
+
"search_comparisons": list(
|
| 396 |
+
self.comparison_data["search_comparisons"]
|
| 397 |
+
),
|
| 398 |
+
},
|
| 399 |
+
"client_metrics": dict(self.client_metrics),
|
| 400 |
+
}
|
| 401 |
+
return json.dumps(export_data, indent=2)
|
| 402 |
+
else:
|
| 403 |
+
return f"Error: Unsupported format '{format}'. Supported: json"
|
| 404 |
+
|
| 405 |
+
except Exception as e:
|
| 406 |
+
return f"Error exporting metrics: {str(e)}"
|
utils/storage_handler.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Storage Handler - HuggingFace Dataset integration for persistent memory storage.
|
| 3 |
+
Handles uploading and downloading memory videos to/from HF datasets.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, Any, List, Optional
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import tempfile
|
| 12 |
+
import shutil
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from huggingface_hub import HfApi, create_repo, upload_file, hf_hub_download
|
| 16 |
+
from huggingface_hub.utils import RepositoryNotFoundError
|
| 17 |
+
|
| 18 |
+
HF_AVAILABLE = True
|
| 19 |
+
except ImportError:
|
| 20 |
+
logging.warning("HuggingFace Hub not available. Using local storage only.")
|
| 21 |
+
HF_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class StorageHandler:
|
| 25 |
+
"""
|
| 26 |
+
Handles persistent storage using HuggingFace datasets.
|
| 27 |
+
Provides backup and restore functionality for memory videos.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self, hf_token: Optional[str] = None, dataset_name: Optional[str] = None
|
| 32 |
+
):
|
| 33 |
+
"""
|
| 34 |
+
Initialize the storage handler.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
hf_token (str, optional): HuggingFace API token
|
| 38 |
+
dataset_name (str, optional): Name of the HF dataset to use
|
| 39 |
+
"""
|
| 40 |
+
self.logger = logging.getLogger(__name__)
|
| 41 |
+
|
| 42 |
+
# Get HF token from environment or parameter
|
| 43 |
+
self.hf_token = (
|
| 44 |
+
hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Set default dataset name
|
| 48 |
+
self.dataset_name = dataset_name or os.getenv(
|
| 49 |
+
"HF_DATASET_NAME", "memvid-memory-store"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Initialize HF API if available
|
| 53 |
+
self.hf_api = None
|
| 54 |
+
self.hf_enabled = False
|
| 55 |
+
|
| 56 |
+
if HF_AVAILABLE and self.hf_token:
|
| 57 |
+
try:
|
| 58 |
+
self.hf_api = HfApi(token=self.hf_token)
|
| 59 |
+
self.hf_enabled = True
|
| 60 |
+
self.logger.info(
|
| 61 |
+
f"HuggingFace integration enabled with dataset: {self.dataset_name}"
|
| 62 |
+
)
|
| 63 |
+
except Exception as e:
|
| 64 |
+
self.logger.warning(f"Failed to initialize HF API: {e}")
|
| 65 |
+
else:
|
| 66 |
+
self.logger.info(
|
| 67 |
+
"HuggingFace integration disabled - using local storage only"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def ensure_dataset_exists(self) -> bool:
|
| 71 |
+
"""
|
| 72 |
+
Ensure the HF dataset exists, create if it doesn't.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
bool: True if dataset exists or was created successfully
|
| 76 |
+
"""
|
| 77 |
+
if not self.hf_enabled:
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
# Try to get dataset info
|
| 82 |
+
self.hf_api.dataset_info(self.dataset_name)
|
| 83 |
+
self.logger.info(f"Dataset {self.dataset_name} already exists")
|
| 84 |
+
return True
|
| 85 |
+
except RepositoryNotFoundError:
|
| 86 |
+
try:
|
| 87 |
+
# Create the dataset
|
| 88 |
+
create_repo(
|
| 89 |
+
repo_id=self.dataset_name,
|
| 90 |
+
repo_type="dataset",
|
| 91 |
+
token=self.hf_token,
|
| 92 |
+
private=True, # Make it private by default
|
| 93 |
+
)
|
| 94 |
+
self.logger.info(f"Created new dataset: {self.dataset_name}")
|
| 95 |
+
return True
|
| 96 |
+
except Exception as e:
|
| 97 |
+
self.logger.error(f"Failed to create dataset {self.dataset_name}: {e}")
|
| 98 |
+
return False
|
| 99 |
+
except Exception as e:
|
| 100 |
+
self.logger.error(f"Error checking dataset {self.dataset_name}: {e}")
|
| 101 |
+
return False
|
| 102 |
+
|
| 103 |
+
def upload_memory_video(
|
| 104 |
+
self, client_id: str, memory_name: str, video_path: Path, index_path: Path
|
| 105 |
+
) -> bool:
|
| 106 |
+
"""
|
| 107 |
+
Upload memory video and index to HF dataset.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
client_id (str): Client identifier
|
| 111 |
+
memory_name (str): Memory video name
|
| 112 |
+
video_path (Path): Local path to video file
|
| 113 |
+
index_path (Path): Local path to index file
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
bool: True if upload successful
|
| 117 |
+
"""
|
| 118 |
+
if not self.hf_enabled:
|
| 119 |
+
self.logger.info("HF upload skipped - not enabled")
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
if not self.ensure_dataset_exists():
|
| 123 |
+
return False
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
# Upload video file
|
| 127 |
+
video_remote_path = f"{client_id}/videos/{memory_name}.mp4"
|
| 128 |
+
upload_file(
|
| 129 |
+
path_or_fileobj=str(video_path),
|
| 130 |
+
path_in_repo=video_remote_path,
|
| 131 |
+
repo_id=self.dataset_name,
|
| 132 |
+
repo_type="dataset",
|
| 133 |
+
token=self.hf_token,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Upload index file
|
| 137 |
+
index_remote_path = f"{client_id}/videos/{memory_name}_index.json"
|
| 138 |
+
upload_file(
|
| 139 |
+
path_or_fileobj=str(index_path),
|
| 140 |
+
path_in_repo=index_remote_path,
|
| 141 |
+
repo_id=self.dataset_name,
|
| 142 |
+
repo_type="dataset",
|
| 143 |
+
token=self.hf_token,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
self.logger.info(
|
| 147 |
+
f"Successfully uploaded memory '{memory_name}' for client {client_id}"
|
| 148 |
+
)
|
| 149 |
+
return True
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
self.logger.error(f"Failed to upload memory video: {e}")
|
| 153 |
+
return False
|
| 154 |
+
|
| 155 |
+
def download_memory_video(
|
| 156 |
+
self, client_id: str, memory_name: str, local_videos_dir: Path
|
| 157 |
+
) -> bool:
|
| 158 |
+
"""
|
| 159 |
+
Download memory video and index from HF dataset.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
client_id (str): Client identifier
|
| 163 |
+
memory_name (str): Memory video name
|
| 164 |
+
local_videos_dir (Path): Local directory to save files
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
bool: True if download successful
|
| 168 |
+
"""
|
| 169 |
+
if not self.hf_enabled:
|
| 170 |
+
self.logger.info("HF download skipped - not enabled")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
# Download video file
|
| 175 |
+
video_remote_path = f"{client_id}/videos/{memory_name}.mp4"
|
| 176 |
+
video_local_path = local_videos_dir / f"{memory_name}.mp4"
|
| 177 |
+
|
| 178 |
+
hf_hub_download(
|
| 179 |
+
repo_id=self.dataset_name,
|
| 180 |
+
filename=video_remote_path,
|
| 181 |
+
repo_type="dataset",
|
| 182 |
+
token=self.hf_token,
|
| 183 |
+
local_dir=str(local_videos_dir.parent),
|
| 184 |
+
local_dir_use_symlinks=False,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Download index file
|
| 188 |
+
index_remote_path = f"{client_id}/videos/{memory_name}_index.json"
|
| 189 |
+
index_local_path = local_videos_dir / f"{memory_name}_index.json"
|
| 190 |
+
|
| 191 |
+
hf_hub_download(
|
| 192 |
+
repo_id=self.dataset_name,
|
| 193 |
+
filename=index_remote_path,
|
| 194 |
+
repo_type="dataset",
|
| 195 |
+
token=self.hf_token,
|
| 196 |
+
local_dir=str(local_videos_dir.parent),
|
| 197 |
+
local_dir_use_symlinks=False,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
self.logger.info(
|
| 201 |
+
f"Successfully downloaded memory '{memory_name}' for client {client_id}"
|
| 202 |
+
)
|
| 203 |
+
return True
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
self.logger.error(f"Failed to download memory video: {e}")
|
| 207 |
+
return False
|
| 208 |
+
|
| 209 |
+
def upload_client_metadata(self, client_id: str, metadata: Dict[str, Any]) -> bool:
|
| 210 |
+
"""
|
| 211 |
+
Upload client metadata to HF dataset.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
client_id (str): Client identifier
|
| 215 |
+
metadata (dict): Client metadata
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
bool: True if upload successful
|
| 219 |
+
"""
|
| 220 |
+
if not self.hf_enabled:
|
| 221 |
+
return False
|
| 222 |
+
|
| 223 |
+
if not self.ensure_dataset_exists():
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
# Create temporary file for metadata
|
| 228 |
+
with tempfile.NamedTemporaryFile(
|
| 229 |
+
mode="w", suffix=".json", delete=False
|
| 230 |
+
) as f:
|
| 231 |
+
json.dump(metadata, f, indent=2)
|
| 232 |
+
temp_path = f.name
|
| 233 |
+
|
| 234 |
+
# Upload metadata
|
| 235 |
+
remote_path = f"{client_id}/metadata.json"
|
| 236 |
+
upload_file(
|
| 237 |
+
path_or_fileobj=temp_path,
|
| 238 |
+
path_in_repo=remote_path,
|
| 239 |
+
repo_id=self.dataset_name,
|
| 240 |
+
repo_type="dataset",
|
| 241 |
+
token=self.hf_token,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# Clean up temp file
|
| 245 |
+
os.unlink(temp_path)
|
| 246 |
+
|
| 247 |
+
self.logger.info(f"Successfully uploaded metadata for client {client_id}")
|
| 248 |
+
return True
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
self.logger.error(f"Failed to upload metadata: {e}")
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
def download_client_metadata(self, client_id: str) -> Optional[Dict[str, Any]]:
|
| 255 |
+
"""
|
| 256 |
+
Download client metadata from HF dataset.
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
client_id (str): Client identifier
|
| 260 |
+
|
| 261 |
+
Returns:
|
| 262 |
+
dict or None: Client metadata if successful
|
| 263 |
+
"""
|
| 264 |
+
if not self.hf_enabled:
|
| 265 |
+
return None
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
# Download metadata to temporary file
|
| 269 |
+
remote_path = f"{client_id}/metadata.json"
|
| 270 |
+
|
| 271 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 272 |
+
local_path = hf_hub_download(
|
| 273 |
+
repo_id=self.dataset_name,
|
| 274 |
+
filename=remote_path,
|
| 275 |
+
repo_type="dataset",
|
| 276 |
+
token=self.hf_token,
|
| 277 |
+
local_dir=temp_dir,
|
| 278 |
+
local_dir_use_symlinks=False,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# Read metadata
|
| 282 |
+
with open(local_path, "r") as f:
|
| 283 |
+
metadata = json.load(f)
|
| 284 |
+
|
| 285 |
+
self.logger.info(
|
| 286 |
+
f"Successfully downloaded metadata for client {client_id}"
|
| 287 |
+
)
|
| 288 |
+
return metadata
|
| 289 |
+
|
| 290 |
+
except Exception as e:
|
| 291 |
+
self.logger.error(f"Failed to download metadata: {e}")
|
| 292 |
+
return None
|
| 293 |
+
|
| 294 |
+
def list_client_memories(self, client_id: str) -> List[str]:
|
| 295 |
+
"""
|
| 296 |
+
List available memory videos for a client in HF dataset.
|
| 297 |
+
|
| 298 |
+
Args:
|
| 299 |
+
client_id (str): Client identifier
|
| 300 |
+
|
| 301 |
+
Returns:
|
| 302 |
+
list: List of memory names
|
| 303 |
+
"""
|
| 304 |
+
if not self.hf_enabled:
|
| 305 |
+
return []
|
| 306 |
+
|
| 307 |
+
try:
|
| 308 |
+
# Get dataset files
|
| 309 |
+
files = self.hf_api.list_repo_files(
|
| 310 |
+
repo_id=self.dataset_name, repo_type="dataset"
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Filter for this client's video files
|
| 314 |
+
memory_names = []
|
| 315 |
+
prefix = f"{client_id}/videos/"
|
| 316 |
+
|
| 317 |
+
for file_path in files:
|
| 318 |
+
if file_path.startswith(prefix) and file_path.endswith(".mp4"):
|
| 319 |
+
# Extract memory name from path
|
| 320 |
+
filename = file_path[len(prefix) :]
|
| 321 |
+
memory_name = filename[:-4] # Remove .mp4 extension
|
| 322 |
+
memory_names.append(memory_name)
|
| 323 |
+
|
| 324 |
+
return memory_names
|
| 325 |
+
|
| 326 |
+
except Exception as e:
|
| 327 |
+
self.logger.error(f"Failed to list client memories: {e}")
|
| 328 |
+
return []
|
| 329 |
+
|
| 330 |
+
def backup_client_data(self, client_id: str, local_client_dir: Path) -> bool:
|
| 331 |
+
"""
|
| 332 |
+
Backup all client data to HF dataset.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
client_id (str): Client identifier
|
| 336 |
+
local_client_dir (Path): Local client directory
|
| 337 |
+
|
| 338 |
+
Returns:
|
| 339 |
+
bool: True if backup successful
|
| 340 |
+
"""
|
| 341 |
+
if not self.hf_enabled:
|
| 342 |
+
self.logger.info("HF backup skipped - not enabled")
|
| 343 |
+
return False
|
| 344 |
+
|
| 345 |
+
try:
|
| 346 |
+
success_count = 0
|
| 347 |
+
total_files = 0
|
| 348 |
+
|
| 349 |
+
# Upload all video files
|
| 350 |
+
videos_dir = local_client_dir / "videos"
|
| 351 |
+
if videos_dir.exists():
|
| 352 |
+
for video_file in videos_dir.glob("*.mp4"):
|
| 353 |
+
memory_name = video_file.stem
|
| 354 |
+
index_file = videos_dir / f"{memory_name}_index.json"
|
| 355 |
+
|
| 356 |
+
if index_file.exists():
|
| 357 |
+
total_files += 2
|
| 358 |
+
if self.upload_memory_video(
|
| 359 |
+
client_id, memory_name, video_file, index_file
|
| 360 |
+
):
|
| 361 |
+
success_count += 2
|
| 362 |
+
|
| 363 |
+
# Upload metadata
|
| 364 |
+
metadata_file = local_client_dir / "metadata.json"
|
| 365 |
+
if metadata_file.exists():
|
| 366 |
+
total_files += 1
|
| 367 |
+
with open(metadata_file, "r") as f:
|
| 368 |
+
metadata = json.load(f)
|
| 369 |
+
if self.upload_client_metadata(client_id, metadata):
|
| 370 |
+
success_count += 1
|
| 371 |
+
|
| 372 |
+
self.logger.info(
|
| 373 |
+
f"Backup completed: {success_count}/{total_files} files uploaded for client {client_id}"
|
| 374 |
+
)
|
| 375 |
+
return success_count == total_files
|
| 376 |
+
|
| 377 |
+
except Exception as e:
|
| 378 |
+
self.logger.error(f"Failed to backup client data: {e}")
|
| 379 |
+
return False
|
| 380 |
+
|
| 381 |
+
def restore_client_data(self, client_id: str, local_client_dir: Path) -> bool:
|
| 382 |
+
"""
|
| 383 |
+
Restore client data from HF dataset.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
client_id (str): Client identifier
|
| 387 |
+
local_client_dir (Path): Local client directory
|
| 388 |
+
|
| 389 |
+
Returns:
|
| 390 |
+
bool: True if restore successful
|
| 391 |
+
"""
|
| 392 |
+
if not self.hf_enabled:
|
| 393 |
+
self.logger.info("HF restore skipped - not enabled")
|
| 394 |
+
return False
|
| 395 |
+
|
| 396 |
+
try:
|
| 397 |
+
# Ensure local directories exist
|
| 398 |
+
local_client_dir.mkdir(exist_ok=True)
|
| 399 |
+
(local_client_dir / "videos").mkdir(exist_ok=True)
|
| 400 |
+
(local_client_dir / "chunks").mkdir(exist_ok=True)
|
| 401 |
+
|
| 402 |
+
# Restore metadata
|
| 403 |
+
metadata = self.download_client_metadata(client_id)
|
| 404 |
+
if metadata:
|
| 405 |
+
metadata_file = local_client_dir / "metadata.json"
|
| 406 |
+
with open(metadata_file, "w") as f:
|
| 407 |
+
json.dump(metadata, f, indent=2)
|
| 408 |
+
|
| 409 |
+
# Restore memory videos
|
| 410 |
+
memory_names = self.list_client_memories(client_id)
|
| 411 |
+
videos_dir = local_client_dir / "videos"
|
| 412 |
+
|
| 413 |
+
success_count = 0
|
| 414 |
+
for memory_name in memory_names:
|
| 415 |
+
if self.download_memory_video(client_id, memory_name, videos_dir):
|
| 416 |
+
success_count += 1
|
| 417 |
+
|
| 418 |
+
self.logger.info(
|
| 419 |
+
f"Restore completed: {success_count}/{len(memory_names)} memories restored for client {client_id}"
|
| 420 |
+
)
|
| 421 |
+
return success_count == len(memory_names)
|
| 422 |
+
|
| 423 |
+
except Exception as e:
|
| 424 |
+
self.logger.error(f"Failed to restore client data: {e}")
|
| 425 |
+
return False
|
| 426 |
+
|
| 427 |
+
def get_storage_info(self) -> Dict[str, Any]:
|
| 428 |
+
"""
|
| 429 |
+
Get storage handler information and status.
|
| 430 |
+
|
| 431 |
+
Returns:
|
| 432 |
+
dict: Storage information
|
| 433 |
+
"""
|
| 434 |
+
info = {
|
| 435 |
+
"hf_available": HF_AVAILABLE,
|
| 436 |
+
"hf_enabled": self.hf_enabled,
|
| 437 |
+
"dataset_name": self.dataset_name,
|
| 438 |
+
"has_token": bool(self.hf_token),
|
| 439 |
+
"storage_mode": "hybrid" if self.hf_enabled else "local_only",
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
if self.hf_enabled:
|
| 443 |
+
try:
|
| 444 |
+
dataset_exists = self.ensure_dataset_exists()
|
| 445 |
+
info["dataset_exists"] = dataset_exists
|
| 446 |
+
except Exception as e:
|
| 447 |
+
info["dataset_error"] = str(e)
|
| 448 |
+
|
| 449 |
+
return info
|
utils/vector_storage_manager.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vector Storage Manager - Traditional vector storage backend for dual storage comparison.
|
| 3 |
+
Provides vector embeddings storage with local fallback and future Modal integration.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, List, Any, Optional
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
import faiss
|
| 17 |
+
|
| 18 |
+
VECTOR_DEPS_AVAILABLE = True
|
| 19 |
+
except ImportError:
|
| 20 |
+
logging.warning(
|
| 21 |
+
"Vector storage dependencies not available (sentence-transformers, faiss)"
|
| 22 |
+
)
|
| 23 |
+
SentenceTransformer = None
|
| 24 |
+
faiss = None
|
| 25 |
+
VECTOR_DEPS_AVAILABLE = False
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class VectorStorageManager:
|
| 29 |
+
"""
|
| 30 |
+
Vector storage backend for dual storage comparison.
|
| 31 |
+
Provides traditional embedding-based storage with local FAISS index.
|
| 32 |
+
Future: Modal integration for production scaling.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(
|
| 36 |
+
self,
|
| 37 |
+
data_dir: str = "data",
|
| 38 |
+
model_name: str = "all-MiniLM-L6-v2",
|
| 39 |
+
storage_handler=None,
|
| 40 |
+
):
|
| 41 |
+
"""
|
| 42 |
+
Initialize vector storage manager.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
data_dir (str): Base directory for storage
|
| 46 |
+
model_name (str): Sentence transformer model name
|
| 47 |
+
storage_handler: HF Dataset storage handler for persistence
|
| 48 |
+
"""
|
| 49 |
+
self.logger = logging.getLogger(__name__)
|
| 50 |
+
self.data_dir = Path(data_dir)
|
| 51 |
+
self.model_name = model_name
|
| 52 |
+
self.storage_handler = storage_handler # For HF Dataset persistence
|
| 53 |
+
|
| 54 |
+
# Initialize embedding model
|
| 55 |
+
self.encoder = None
|
| 56 |
+
if VECTOR_DEPS_AVAILABLE:
|
| 57 |
+
try:
|
| 58 |
+
self.encoder = SentenceTransformer(model_name)
|
| 59 |
+
self.logger.info(f"Vector storage initialized with model: {model_name}")
|
| 60 |
+
except Exception as e:
|
| 61 |
+
self.logger.error(f"Failed to load embedding model: {e}")
|
| 62 |
+
else:
|
| 63 |
+
self.logger.warning("Vector storage not available - missing dependencies")
|
| 64 |
+
|
| 65 |
+
# Client indices
|
| 66 |
+
self.client_indices = {} # client_id -> faiss index
|
| 67 |
+
self.client_texts = {} # client_id -> list of texts
|
| 68 |
+
self.client_metadata = {} # client_id -> list of metadata
|
| 69 |
+
|
| 70 |
+
def store_embedding(
|
| 71 |
+
self, text: str, client_id: str, metadata: Dict[str, Any] = None
|
| 72 |
+
) -> str:
|
| 73 |
+
"""
|
| 74 |
+
Store text as vector embedding.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
text (str): Text to store
|
| 78 |
+
client_id (str): Client identifier
|
| 79 |
+
metadata (dict): Additional metadata
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
str: Storage result message
|
| 83 |
+
"""
|
| 84 |
+
try:
|
| 85 |
+
if not VECTOR_DEPS_AVAILABLE:
|
| 86 |
+
return "Error: Vector storage dependencies not available (sentence-transformers, faiss)"
|
| 87 |
+
|
| 88 |
+
if not self.encoder:
|
| 89 |
+
return "Error: Embedding model not loaded"
|
| 90 |
+
|
| 91 |
+
# Generate embedding
|
| 92 |
+
start_time = time.time()
|
| 93 |
+
embedding = self.encoder.encode([text])
|
| 94 |
+
embedding_time = time.time() - start_time
|
| 95 |
+
|
| 96 |
+
# Initialize client storage if needed
|
| 97 |
+
if client_id not in self.client_indices:
|
| 98 |
+
self._init_client_storage(client_id, embedding.shape[1])
|
| 99 |
+
|
| 100 |
+
# Add to client index
|
| 101 |
+
self.client_indices[client_id].add(embedding)
|
| 102 |
+
self.client_texts[client_id].append(text)
|
| 103 |
+
self.client_metadata[client_id].append(metadata or {})
|
| 104 |
+
|
| 105 |
+
# Save to disk
|
| 106 |
+
self._save_client_index(client_id)
|
| 107 |
+
|
| 108 |
+
# Auto-backup to HF Dataset for persistence on HF Spaces
|
| 109 |
+
self.auto_backup_after_store(client_id, self.storage_handler)
|
| 110 |
+
|
| 111 |
+
total_embeddings = len(self.client_texts[client_id])
|
| 112 |
+
|
| 113 |
+
return f"Vector embedding stored for client {client_id}. Embedding time: {embedding_time:.3f}s. Total embeddings: {total_embeddings}"
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
error_msg = f"Error storing vector embedding: {str(e)}"
|
| 117 |
+
self.logger.error(error_msg)
|
| 118 |
+
return error_msg
|
| 119 |
+
|
| 120 |
+
def search_embeddings(self, query: str, client_id: str, top_k: int = 5) -> str:
|
| 121 |
+
"""
|
| 122 |
+
Search embeddings using vector similarity.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
query (str): Search query
|
| 126 |
+
client_id (str): Client identifier
|
| 127 |
+
top_k (int): Number of results
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
str: JSON string with search results
|
| 131 |
+
"""
|
| 132 |
+
try:
|
| 133 |
+
if not VECTOR_DEPS_AVAILABLE:
|
| 134 |
+
return json.dumps(
|
| 135 |
+
{"error": "Vector storage dependencies not available"}
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
if not self.encoder:
|
| 139 |
+
return json.dumps({"error": "Embedding model not loaded"})
|
| 140 |
+
|
| 141 |
+
if client_id not in self.client_indices:
|
| 142 |
+
return json.dumps(
|
| 143 |
+
{"error": f"No embeddings found for client {client_id}"}
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Generate query embedding
|
| 147 |
+
query_embedding = self.encoder.encode([query])
|
| 148 |
+
|
| 149 |
+
# Search index
|
| 150 |
+
scores, indices = self.client_indices[client_id].search(
|
| 151 |
+
query_embedding, top_k
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# Prepare results
|
| 155 |
+
results = []
|
| 156 |
+
for i, (score, idx) in enumerate(zip(scores[0], indices[0])):
|
| 157 |
+
if idx < len(self.client_texts[client_id]):
|
| 158 |
+
result = {
|
| 159 |
+
"text": self.client_texts[client_id][idx],
|
| 160 |
+
"score": float(score),
|
| 161 |
+
"rank": i + 1,
|
| 162 |
+
"metadata": self.client_metadata[client_id][idx],
|
| 163 |
+
}
|
| 164 |
+
results.append(result)
|
| 165 |
+
|
| 166 |
+
return json.dumps(
|
| 167 |
+
{
|
| 168 |
+
"query": query,
|
| 169 |
+
"client_id": client_id,
|
| 170 |
+
"total_results": len(results),
|
| 171 |
+
"results": results,
|
| 172 |
+
"backend": "vector_storage",
|
| 173 |
+
},
|
| 174 |
+
indent=2,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
except Exception as e:
|
| 178 |
+
error_msg = f"Error searching vector embeddings: {str(e)}"
|
| 179 |
+
self.logger.error(error_msg)
|
| 180 |
+
return json.dumps({"error": error_msg})
|
| 181 |
+
|
| 182 |
+
def delete_memory(self, client_id: str, memory_name: str = "") -> str:
|
| 183 |
+
"""
|
| 184 |
+
Delete embeddings for a client.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
client_id (str): Client identifier
|
| 188 |
+
memory_name (str): Memory name (not used in vector storage)
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
str: Deletion result
|
| 192 |
+
"""
|
| 193 |
+
try:
|
| 194 |
+
if client_id in self.client_indices:
|
| 195 |
+
# Clear client data
|
| 196 |
+
del self.client_indices[client_id]
|
| 197 |
+
del self.client_texts[client_id]
|
| 198 |
+
del self.client_metadata[client_id]
|
| 199 |
+
|
| 200 |
+
# Remove saved files
|
| 201 |
+
client_dir = self._get_client_dir(client_id)
|
| 202 |
+
if client_dir.exists():
|
| 203 |
+
import shutil
|
| 204 |
+
|
| 205 |
+
shutil.rmtree(client_dir)
|
| 206 |
+
|
| 207 |
+
return f"Vector embeddings deleted for client {client_id}"
|
| 208 |
+
else:
|
| 209 |
+
return f"No vector embeddings found for client {client_id}"
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
error_msg = f"Error deleting vector embeddings: {str(e)}"
|
| 213 |
+
self.logger.error(error_msg)
|
| 214 |
+
return error_msg
|
| 215 |
+
|
| 216 |
+
def get_stats(self, client_id: str) -> str:
|
| 217 |
+
"""
|
| 218 |
+
Get vector storage statistics.
|
| 219 |
+
|
| 220 |
+
Args:
|
| 221 |
+
client_id (str): Client identifier
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
str: JSON string with statistics
|
| 225 |
+
"""
|
| 226 |
+
try:
|
| 227 |
+
if client_id not in self.client_indices:
|
| 228 |
+
return json.dumps(
|
| 229 |
+
{
|
| 230 |
+
"client_id": client_id,
|
| 231 |
+
"total_embeddings": 0,
|
| 232 |
+
"storage_backend": "vector_storage",
|
| 233 |
+
"status": "no_data",
|
| 234 |
+
}
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
total_embeddings = len(self.client_texts[client_id])
|
| 238 |
+
total_text_size = sum(len(text) for text in self.client_texts[client_id])
|
| 239 |
+
|
| 240 |
+
# Calculate storage size
|
| 241 |
+
client_dir = self._get_client_dir(client_id)
|
| 242 |
+
storage_size = 0
|
| 243 |
+
if client_dir.exists():
|
| 244 |
+
storage_size = sum(
|
| 245 |
+
f.stat().st_size for f in client_dir.rglob("*") if f.is_file()
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
return json.dumps(
|
| 249 |
+
{
|
| 250 |
+
"client_id": client_id,
|
| 251 |
+
"total_embeddings": total_embeddings,
|
| 252 |
+
"total_text_size_bytes": total_text_size,
|
| 253 |
+
"storage_size_bytes": storage_size,
|
| 254 |
+
"storage_backend": "vector_storage",
|
| 255 |
+
"embedding_model": self.model_name,
|
| 256 |
+
"status": "active",
|
| 257 |
+
},
|
| 258 |
+
indent=2,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
except Exception as e:
|
| 262 |
+
error_msg = f"Error getting vector storage stats: {str(e)}"
|
| 263 |
+
self.logger.error(error_msg)
|
| 264 |
+
return json.dumps({"error": error_msg})
|
| 265 |
+
|
| 266 |
+
def _init_client_storage(self, client_id: str, embedding_dim: int) -> None:
|
| 267 |
+
"""Initialize storage for a new client."""
|
| 268 |
+
# Create FAISS index
|
| 269 |
+
self.client_indices[client_id] = faiss.IndexFlatIP(
|
| 270 |
+
embedding_dim
|
| 271 |
+
) # Inner product similarity
|
| 272 |
+
self.client_texts[client_id] = []
|
| 273 |
+
self.client_metadata[client_id] = []
|
| 274 |
+
|
| 275 |
+
# Create client directory
|
| 276 |
+
client_dir = self._get_client_dir(client_id)
|
| 277 |
+
client_dir.mkdir(parents=True, exist_ok=True)
|
| 278 |
+
|
| 279 |
+
def _get_client_dir(self, client_id: str) -> Path:
|
| 280 |
+
"""Get client-specific directory for vector storage."""
|
| 281 |
+
return self.data_dir / f"{client_id}_vector"
|
| 282 |
+
|
| 283 |
+
def _save_client_index(self, client_id: str) -> None:
|
| 284 |
+
"""Save client index and data to disk."""
|
| 285 |
+
try:
|
| 286 |
+
client_dir = self._get_client_dir(client_id)
|
| 287 |
+
|
| 288 |
+
# Save FAISS index
|
| 289 |
+
faiss.write_index(
|
| 290 |
+
self.client_indices[client_id], str(client_dir / "vector_index.faiss")
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# Save texts and metadata
|
| 294 |
+
with open(client_dir / "texts.json", "w", encoding="utf-8") as f:
|
| 295 |
+
json.dump(self.client_texts[client_id], f, indent=2)
|
| 296 |
+
|
| 297 |
+
with open(client_dir / "metadata.json", "w", encoding="utf-8") as f:
|
| 298 |
+
json.dump(self.client_metadata[client_id], f, indent=2)
|
| 299 |
+
|
| 300 |
+
except Exception as e:
|
| 301 |
+
self.logger.error(f"Error saving client index for {client_id}: {e}")
|
| 302 |
+
|
| 303 |
+
def _load_client_index(self, client_id: str) -> bool:
|
| 304 |
+
"""Load client index and data from disk."""
|
| 305 |
+
try:
|
| 306 |
+
client_dir = self._get_client_dir(client_id)
|
| 307 |
+
|
| 308 |
+
if not (client_dir / "vector_index.faiss").exists():
|
| 309 |
+
return False
|
| 310 |
+
|
| 311 |
+
# Load FAISS index
|
| 312 |
+
self.client_indices[client_id] = faiss.read_index(
|
| 313 |
+
str(client_dir / "vector_index.faiss")
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
# Load texts and metadata
|
| 317 |
+
with open(client_dir / "texts.json", "r", encoding="utf-8") as f:
|
| 318 |
+
self.client_texts[client_id] = json.load(f)
|
| 319 |
+
|
| 320 |
+
with open(client_dir / "metadata.json", "r", encoding="utf-8") as f:
|
| 321 |
+
self.client_metadata[client_id] = json.load(f)
|
| 322 |
+
|
| 323 |
+
return True
|
| 324 |
+
|
| 325 |
+
except Exception as e:
|
| 326 |
+
self.logger.error(f"Error loading client index for {client_id}: {e}")
|
| 327 |
+
return False
|
| 328 |
+
|
| 329 |
+
def load_client_data(self, client_id: str) -> str:
|
| 330 |
+
"""
|
| 331 |
+
Load client data from disk.
|
| 332 |
+
|
| 333 |
+
Args:
|
| 334 |
+
client_id (str): Client identifier
|
| 335 |
+
|
| 336 |
+
Returns:
|
| 337 |
+
str: Load result message
|
| 338 |
+
"""
|
| 339 |
+
try:
|
| 340 |
+
if self._load_client_index(client_id):
|
| 341 |
+
total_embeddings = len(self.client_texts[client_id])
|
| 342 |
+
return f"Vector storage loaded for client {client_id}: {total_embeddings} embeddings"
|
| 343 |
+
else:
|
| 344 |
+
return f"No vector storage data found for client {client_id}"
|
| 345 |
+
|
| 346 |
+
except Exception as e:
|
| 347 |
+
error_msg = f"Error loading client data: {str(e)}"
|
| 348 |
+
self.logger.error(error_msg)
|
| 349 |
+
return error_msg
|
| 350 |
+
|
| 351 |
+
# Future Modal integration methods (placeholders)
|
| 352 |
+
|
| 353 |
+
def enable_modal_backend(self, modal_token: str) -> str:
|
| 354 |
+
"""
|
| 355 |
+
Enable Modal backend for production scaling.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
modal_token (str): Modal API token
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
str: Activation result
|
| 362 |
+
"""
|
| 363 |
+
# TODO: Implement Modal integration
|
| 364 |
+
return (
|
| 365 |
+
"Modal backend integration not yet implemented. Using local FAISS storage."
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
def migrate_to_modal(self, client_id: str) -> str:
|
| 369 |
+
"""
|
| 370 |
+
Migrate client data to Modal backend.
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
client_id (str): Client identifier
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
str: Migration result
|
| 377 |
+
"""
|
| 378 |
+
# TODO: Implement Modal migration
|
| 379 |
+
return "Modal migration not yet implemented. Data remains in local storage."
|
| 380 |
+
|
| 381 |
+
# HF Dataset Integration for Persistence on HF Spaces
|
| 382 |
+
|
| 383 |
+
def backup_to_hf_dataset(self, client_id: str, storage_handler) -> str:
|
| 384 |
+
"""
|
| 385 |
+
Backup vector storage to HuggingFace Dataset for persistence.
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
client_id (str): Client identifier
|
| 389 |
+
storage_handler: HF Dataset storage handler
|
| 390 |
+
|
| 391 |
+
Returns:
|
| 392 |
+
str: Backup result
|
| 393 |
+
"""
|
| 394 |
+
try:
|
| 395 |
+
if not storage_handler or not storage_handler.hf_enabled:
|
| 396 |
+
return "HF Dataset backup not available - no storage handler or HF not enabled"
|
| 397 |
+
|
| 398 |
+
client_dir = self._get_client_dir(client_id)
|
| 399 |
+
if not client_dir.exists():
|
| 400 |
+
return f"No vector data found for client {client_id}"
|
| 401 |
+
|
| 402 |
+
# Use storage handler to backup vector files
|
| 403 |
+
success = storage_handler.backup_client_data(client_id, client_dir)
|
| 404 |
+
|
| 405 |
+
if success:
|
| 406 |
+
return f"Successfully backed up vector storage for client {client_id} to HF Dataset"
|
| 407 |
+
else:
|
| 408 |
+
return f"Failed to backup vector storage for client {client_id}"
|
| 409 |
+
|
| 410 |
+
except Exception as e:
|
| 411 |
+
error_msg = f"Error backing up vector storage: {str(e)}"
|
| 412 |
+
self.logger.error(error_msg)
|
| 413 |
+
return error_msg
|
| 414 |
+
|
| 415 |
+
def restore_from_hf_dataset(self, client_id: str, storage_handler) -> str:
|
| 416 |
+
"""
|
| 417 |
+
Restore vector storage from HuggingFace Dataset.
|
| 418 |
+
|
| 419 |
+
Args:
|
| 420 |
+
client_id (str): Client identifier
|
| 421 |
+
storage_handler: HF Dataset storage handler
|
| 422 |
+
|
| 423 |
+
Returns:
|
| 424 |
+
str: Restore result
|
| 425 |
+
"""
|
| 426 |
+
try:
|
| 427 |
+
if not storage_handler or not storage_handler.hf_enabled:
|
| 428 |
+
return "HF Dataset restore not available - no storage handler or HF not enabled"
|
| 429 |
+
|
| 430 |
+
client_dir = self._get_client_dir(client_id)
|
| 431 |
+
|
| 432 |
+
# Use storage handler to restore vector files
|
| 433 |
+
success = storage_handler.restore_client_data(client_id, client_dir)
|
| 434 |
+
|
| 435 |
+
if success:
|
| 436 |
+
# Load the restored data into memory
|
| 437 |
+
if self._load_client_index(client_id):
|
| 438 |
+
total_embeddings = len(self.client_texts[client_id])
|
| 439 |
+
return f"Successfully restored vector storage for client {client_id}: {total_embeddings} embeddings"
|
| 440 |
+
else:
|
| 441 |
+
return f"Vector files restored but failed to load into memory for client {client_id}"
|
| 442 |
+
else:
|
| 443 |
+
return f"Failed to restore vector storage for client {client_id}"
|
| 444 |
+
|
| 445 |
+
except Exception as e:
|
| 446 |
+
error_msg = f"Error restoring vector storage: {str(e)}"
|
| 447 |
+
self.logger.error(error_msg)
|
| 448 |
+
return error_msg
|
| 449 |
+
|
| 450 |
+
def auto_backup_after_store(self, client_id: str, storage_handler) -> None:
|
| 451 |
+
"""
|
| 452 |
+
Automatically backup after storing embeddings (for HF Spaces persistence).
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
client_id (str): Client identifier
|
| 456 |
+
storage_handler: HF Dataset storage handler
|
| 457 |
+
"""
|
| 458 |
+
try:
|
| 459 |
+
if storage_handler and storage_handler.hf_enabled:
|
| 460 |
+
# Auto-backup in background (non-blocking)
|
| 461 |
+
self.backup_to_hf_dataset(client_id, storage_handler)
|
| 462 |
+
except Exception as e:
|
| 463 |
+
self.logger.warning(f"Auto-backup failed for client {client_id}: {e}")
|