Spaces:
Runtime error
Runtime error
Julian Vanecek commited on
Commit ·
bb80caa
0
Parent(s):
Initial commit: AI Assistant Multi-Agent System for HuggingFace Spaces
Browse files- .gitignore +33 -0
- README.md +277 -0
- README_HF.md +59 -0
- app.py +20 -0
- py/agents/__init__.py +8 -0
- py/agents/base_agent.py +317 -0
- py/agents/document_reader.py +168 -0
- py/agents/network_config.py +5 -0
- py/agents/policy_dnn.py +5 -0
- py/agents/profile_settings.py +180 -0
- py/agents/subscriber_management.py +5 -0
- py/agents/system_query.py +5 -0
- py/app.py +33 -0
- py/backend/__init__.py +1 -0
- py/backend/api_gateway.py +322 -0
- py/backend/chat_history_manager.py +315 -0
- py/backend/chromadb_manager.py +118 -0
- py/backend/simple_vector_db.py +251 -0
- py/config.yaml +259 -0
- py/config_loader.py +217 -0
- py/data/embeddings/chorus_1_1.json +0 -0
- py/data/embeddings/harmony_1_2.json +0 -0
- py/data/embeddings/harmony_1_5.json +0 -0
- py/data/embeddings/harmony_1_6.json +0 -0
- py/data/embeddings/harmony_1_8.json +0 -0
- py/frontend/__init__.py +1 -0
- py/frontend/gradio_app.py +723 -0
- py/requirements.txt +23 -0
- py/scripts/migrate_to_chromadb.py +210 -0
- py/tools/README.md +21 -0
- py/tools/__init__.py +17 -0
- py/tools/agent_tools.py +343 -0
- py/tools/document_tools.py +151 -0
- py/tools/profile_read_tools.py +2 -0
- py/tools/profile_write_tools.py +2 -0
- requirements.txt +23 -0
.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# macOS
|
| 13 |
+
.DS_Store
|
| 14 |
+
|
| 15 |
+
# Data files
|
| 16 |
+
py/data/chat_history.db
|
| 17 |
+
*.db
|
| 18 |
+
|
| 19 |
+
# IDE
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
|
| 23 |
+
# Logs
|
| 24 |
+
*.log
|
| 25 |
+
|
| 26 |
+
# Temporary files
|
| 27 |
+
*.tmp
|
| 28 |
+
*.bak
|
| 29 |
+
*~
|
| 30 |
+
|
| 31 |
+
# Gradio
|
| 32 |
+
flagged/
|
| 33 |
+
gradio_cached_examples/
|
README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Assistant Multi-Agent System
|
| 2 |
+
|
| 3 |
+
A modern multi-agent conversational AI system built with LangChain, featuring specialized agents for documentation, settings, and system management. The system uses RAG (Retrieval-Augmented Generation) for intelligent documentation search and provides a clean Gradio web interface.
|
| 4 |
+
|
| 5 |
+
## 🚀 Quick Start
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# Clone the repository
|
| 9 |
+
git clone <repository-url>
|
| 10 |
+
cd ai_assistant_python/v3
|
| 11 |
+
|
| 12 |
+
# Create virtual environment
|
| 13 |
+
python -m venv venv
|
| 14 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 15 |
+
|
| 16 |
+
# Install dependencies
|
| 17 |
+
pip install -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Set OpenAI API key
|
| 20 |
+
export OPENAI_API_KEY="your-api-key"
|
| 21 |
+
|
| 22 |
+
# Run the application
|
| 23 |
+
python py/app.py
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
Visit http://localhost:7860 to start chatting!
|
| 27 |
+
|
| 28 |
+
## 🏗️ Architecture
|
| 29 |
+
|
| 30 |
+
### System Overview
|
| 31 |
+
|
| 32 |
+
```mermaid
|
| 33 |
+
graph TB
|
| 34 |
+
subgraph "Frontend"
|
| 35 |
+
UI[Gradio Web UI]
|
| 36 |
+
end
|
| 37 |
+
|
| 38 |
+
subgraph "Agent System"
|
| 39 |
+
AR[Agent Registry<br/>Singleton Manager]
|
| 40 |
+
DR[📚 Document Reader]
|
| 41 |
+
PS[⚙️ Profile Settings]
|
| 42 |
+
NC[🌐 Network Config]
|
| 43 |
+
SM[👥 Subscriber Mgmt]
|
| 44 |
+
SQ[📊 System Query]
|
| 45 |
+
PD[📋 Policy & DNN]
|
| 46 |
+
end
|
| 47 |
+
|
| 48 |
+
subgraph "Storage"
|
| 49 |
+
CDB[(ChromaDB<br/>Vector Store)]
|
| 50 |
+
CHM[(Chat History<br/>SQLite)]
|
| 51 |
+
end
|
| 52 |
+
|
| 53 |
+
UI <--> AR
|
| 54 |
+
AR --> DR & PS & NC & SM & SQ & PD
|
| 55 |
+
DR <--> CDB
|
| 56 |
+
DR & PS & NC & SM & SQ & PD <--> CHM
|
| 57 |
+
|
| 58 |
+
classDef active fill:#4CAF50,color:#fff
|
| 59 |
+
classDef mock fill:#FFC107,color:#000
|
| 60 |
+
classDef future fill:#9E9E9E,color:#fff
|
| 61 |
+
|
| 62 |
+
class DR,PS active
|
| 63 |
+
class NC,SM,SQ,PD future
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Agent Communication Flow
|
| 67 |
+
|
| 68 |
+
```mermaid
|
| 69 |
+
sequenceDiagram
|
| 70 |
+
participant User
|
| 71 |
+
participant Agent1 as Current Agent
|
| 72 |
+
participant Registry as Agent Registry
|
| 73 |
+
participant Agent2 as Target Agent
|
| 74 |
+
|
| 75 |
+
User->>Agent1: "Set my profile name"
|
| 76 |
+
Agent1->>Agent1: Detect need to switch
|
| 77 |
+
Agent1->>Registry: switch_to_profile_settings()
|
| 78 |
+
Registry->>Agent2: run_agent(message, context)
|
| 79 |
+
Agent2->>Agent2: Process request
|
| 80 |
+
Agent2-->>Registry: Response
|
| 81 |
+
Registry-->>Agent1: __SWITCH_AGENT__|response
|
| 82 |
+
Agent1-->>User: Profile updated!
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## 🤖 Available Agents
|
| 86 |
+
|
| 87 |
+
### Active Agents
|
| 88 |
+
|
| 89 |
+
#### 📚 Document Reader (Default)
|
| 90 |
+
- **Purpose**: Search and retrieve technical documentation
|
| 91 |
+
- **Features**:
|
| 92 |
+
- RAG-powered semantic search
|
| 93 |
+
- Version-specific documentation queries
|
| 94 |
+
- Multi-product support (Harmony/Chorus)
|
| 95 |
+
- Vector similarity awareness
|
| 96 |
+
- **Example**: "How do I install Chorus 1.1?"
|
| 97 |
+
|
| 98 |
+
#### ⚙️ Profile Settings
|
| 99 |
+
- **Purpose**: Manage user preferences and settings
|
| 100 |
+
- **Features**:
|
| 101 |
+
- View current profile
|
| 102 |
+
- Update preferences (mock implementation)
|
| 103 |
+
- Notification management
|
| 104 |
+
- **Example**: "Set my profile name to John"
|
| 105 |
+
|
| 106 |
+
### Future Agents (Stubs)
|
| 107 |
+
|
| 108 |
+
#### 🌐 Network Configuration
|
| 109 |
+
- Configure NAT, VLAN, IP addresses
|
| 110 |
+
- Bulk network operations
|
| 111 |
+
- Template application
|
| 112 |
+
|
| 113 |
+
#### 👥 Subscriber Management
|
| 114 |
+
- CRUD operations for subscribers
|
| 115 |
+
- CSV import/export
|
| 116 |
+
- IMSI range management
|
| 117 |
+
|
| 118 |
+
#### 📊 System Query
|
| 119 |
+
- Analytics and performance metrics
|
| 120 |
+
- System health monitoring
|
| 121 |
+
- Read-only insights
|
| 122 |
+
|
| 123 |
+
#### 📋 Policy & DNN
|
| 124 |
+
- DNN creation and management
|
| 125 |
+
- QoS profile configuration
|
| 126 |
+
- Policy rule management
|
| 127 |
+
|
| 128 |
+
## 🔧 Technical Details
|
| 129 |
+
|
| 130 |
+
### Core Components
|
| 131 |
+
|
| 132 |
+
```
|
| 133 |
+
v3/py/
|
| 134 |
+
├── agents/ # Agent implementations
|
| 135 |
+
│ ├── base_agent.py # Base class with common functionality
|
| 136 |
+
│ ├── document_reader.py # RAG-enabled documentation agent
|
| 137 |
+
│ └── profile_settings.py # User settings agent
|
| 138 |
+
├── tools/
|
| 139 |
+
│ ├── agent_tools.py # Agent registry & switching
|
| 140 |
+
│ └── document_tools.py # Documentation search tools
|
| 141 |
+
├── backend/
|
| 142 |
+
│ ├── chromadb_manager.py # Vector database management
|
| 143 |
+
│ └── chat_history_manager.py # Conversation persistence
|
| 144 |
+
├── frontend/
|
| 145 |
+
│ └── gradio_app.py # Web interface
|
| 146 |
+
└── config.yaml # Configuration
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Key Technologies
|
| 150 |
+
|
| 151 |
+
- **LangChain**: Agent orchestration and tool management
|
| 152 |
+
- **ChromaDB**: Vector database for semantic search
|
| 153 |
+
- **OpenAI**: LLM provider (GPT-4o-mini by default)
|
| 154 |
+
- **Gradio**: Modern web UI framework
|
| 155 |
+
- **SQLite**: Chat history persistence
|
| 156 |
+
|
| 157 |
+
### Agent Registry Pattern
|
| 158 |
+
|
| 159 |
+
The system uses a singleton registry pattern for agent management:
|
| 160 |
+
|
| 161 |
+
```python
|
| 162 |
+
# Initialize agents once
|
| 163 |
+
agent_tools.initialize_agents(llm, api_gateway)
|
| 164 |
+
|
| 165 |
+
# Run any agent by ID
|
| 166 |
+
result = agent_tools.run_agent('document_reader', message, context)
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
### Smart Agent Switching
|
| 170 |
+
|
| 171 |
+
Agents can seamlessly transfer conversations:
|
| 172 |
+
|
| 173 |
+
```python
|
| 174 |
+
# Using StructuredTool for proper parameter handling
|
| 175 |
+
@tool
|
| 176 |
+
def switch_to_profile_settings(reason: str = "") -> str:
|
| 177 |
+
"""Transfer to Profile Settings agent"""
|
| 178 |
+
return run_target_agent_with_context()
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
## 🎯 Usage Examples
|
| 182 |
+
|
| 183 |
+
### Basic Interaction
|
| 184 |
+
```
|
| 185 |
+
You: Hello
|
| 186 |
+
Assistant: Hello! How can I assist you today?
|
| 187 |
+
|
| 188 |
+
You: How do I install Harmony 1.8?
|
| 189 |
+
Assistant: [Searches documentation and provides installation steps...]
|
| 190 |
+
|
| 191 |
+
You: Set my profile name to Alice
|
| 192 |
+
Assistant: [Switches to Profile Settings] I've updated your profile name to 'Alice'!
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
### Version-Specific Queries
|
| 196 |
+
```
|
| 197 |
+
You: Show me Chorus 1.1 webhook documentation
|
| 198 |
+
Assistant: [Retrieves Chorus 1.1 specific webhook docs...]
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## 🔍 Advanced Features
|
| 202 |
+
|
| 203 |
+
### RAG-Powered Search
|
| 204 |
+
- Semantic similarity search using embeddings
|
| 205 |
+
- Metadata filtering by product and version
|
| 206 |
+
- Context-aware responses with source citations
|
| 207 |
+
|
| 208 |
+
### Graceful Error Handling
|
| 209 |
+
- Iteration limit handling with fallback responses
|
| 210 |
+
- User-friendly error messages
|
| 211 |
+
- Comprehensive logging for debugging
|
| 212 |
+
|
| 213 |
+
### Chat History Management
|
| 214 |
+
- Automatic history cleaning (removes transfer noise)
|
| 215 |
+
- Persistent conversation storage
|
| 216 |
+
- Context preservation across agent switches
|
| 217 |
+
|
| 218 |
+
## 🛠️ Configuration
|
| 219 |
+
|
| 220 |
+
### config.yaml
|
| 221 |
+
```yaml
|
| 222 |
+
# Model settings
|
| 223 |
+
llm:
|
| 224 |
+
model: "gpt-4o-mini"
|
| 225 |
+
temperature: 0.0
|
| 226 |
+
|
| 227 |
+
# RAG settings
|
| 228 |
+
rag:
|
| 229 |
+
k: 6 # Number of documents to retrieve
|
| 230 |
+
|
| 231 |
+
# Available products/versions
|
| 232 |
+
products:
|
| 233 |
+
harmony: ["1.8", "1.6", "1.5", "1.2"]
|
| 234 |
+
chorus: ["1.1"]
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Environment Variables
|
| 238 |
+
```bash
|
| 239 |
+
OPENAI_API_KEY="sk-..." # Required
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
## 🚧 Development
|
| 243 |
+
|
| 244 |
+
### Adding a New Agent
|
| 245 |
+
|
| 246 |
+
1. Create agent class inheriting from `BaseAgent`:
|
| 247 |
+
```python
|
| 248 |
+
class MyAgent(BaseAgent):
|
| 249 |
+
def _create_tools(self) -> List[Tool]:
|
| 250 |
+
# Define agent-specific tools
|
| 251 |
+
|
| 252 |
+
def _get_system_prompt(self) -> str:
|
| 253 |
+
# Define agent behavior
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
2. Register in `agent_tools.py`:
|
| 257 |
+
```python
|
| 258 |
+
AGENT_INFO["my_agent"] = {
|
| 259 |
+
"display_name": "My Agent",
|
| 260 |
+
"description": "What it does",
|
| 261 |
+
"tools": {"read": [...], "write": [...]}
|
| 262 |
+
}
|
| 263 |
+
```
|
| 264 |
+
|
| 265 |
+
3. Initialize in registry:
|
| 266 |
+
```python
|
| 267 |
+
_AGENT_INSTANCES["my_agent"] = MyAgent(llm)
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
### Testing
|
| 271 |
+
```bash
|
| 272 |
+
# Run tests (when implemented)
|
| 273 |
+
pytest tests/
|
| 274 |
+
|
| 275 |
+
# Run with debug logging
|
| 276 |
+
python py/app.py --debug
|
| 277 |
+
```
|
README_HF.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Assistant Multi Agent System
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# AI Assistant Multi-Agent System
|
| 14 |
+
|
| 15 |
+
A modern multi-agent conversational AI system built with LangChain, featuring specialized agents for documentation, settings, and system management.
|
| 16 |
+
|
| 17 |
+
## 🌟 Features
|
| 18 |
+
|
| 19 |
+
- **📚 Document Reader Agent**: RAG-powered semantic search across technical documentation
|
| 20 |
+
- **⚙️ Profile Settings Agent**: Manage user preferences and settings
|
| 21 |
+
- **🔄 Smart Agent Switching**: Seamless handoff between specialized agents
|
| 22 |
+
- **🚀 Fast In-Memory Search**: Optimized vector similarity search without external databases
|
| 23 |
+
|
| 24 |
+
## 🔧 Configuration
|
| 25 |
+
|
| 26 |
+
Set your OpenAI API key in the Space settings:
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
OPENAI_API_KEY=your-api-key-here
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## 💬 Usage Examples
|
| 33 |
+
|
| 34 |
+
Simply start chatting! The Document Reader agent handles documentation queries by default, and will automatically transfer you to the appropriate agent when needed.
|
| 35 |
+
|
| 36 |
+
### Example Queries:
|
| 37 |
+
- "How do I install Harmony 1.8?"
|
| 38 |
+
- "Show me webhook documentation for Chorus"
|
| 39 |
+
- "Set my profile name to Alice"
|
| 40 |
+
- "What are the system requirements for Harmony?"
|
| 41 |
+
|
| 42 |
+
## 🏗️ Architecture
|
| 43 |
+
|
| 44 |
+
This system uses:
|
| 45 |
+
- **LangChain** for agent orchestration
|
| 46 |
+
- **OpenAI** embeddings and LLMs
|
| 47 |
+
- **In-memory vector search** for fast document retrieval
|
| 48 |
+
- **Gradio** for the web interface
|
| 49 |
+
|
| 50 |
+
## 📊 Available Documentation
|
| 51 |
+
|
| 52 |
+
- **Harmony**: Versions 1.2, 1.5, 1.6, 1.8
|
| 53 |
+
- **Chorus**: Version 1.1
|
| 54 |
+
|
| 55 |
+
The system searches through pre-embedded technical documentation to provide accurate, version-specific answers.
|
| 56 |
+
|
| 57 |
+
## 🤝 Contributing
|
| 58 |
+
|
| 59 |
+
This is an open-source project. Feel free to contribute or report issues on our GitHub repository.
|
app.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Spaces entry point for the AI Assistant Multi-Agent System
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Add the py directory to the Python path
|
| 9 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'py'))
|
| 10 |
+
|
| 11 |
+
# Import and run the actual app
|
| 12 |
+
from frontend.gradio_app import demo
|
| 13 |
+
|
| 14 |
+
if __name__ == "__main__":
|
| 15 |
+
# Launch with HuggingFace Spaces settings
|
| 16 |
+
demo.launch(
|
| 17 |
+
server_name="0.0.0.0",
|
| 18 |
+
server_port=7860,
|
| 19 |
+
share=False
|
| 20 |
+
)
|
py/agents/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangChain-based agent implementations
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .document_reader import DocumentReaderAgent
|
| 6 |
+
from .profile_settings import ProfileSettingsAgent
|
| 7 |
+
|
| 8 |
+
__all__ = ['DocumentReaderAgent', 'ProfileSettingsAgent']
|
py/agents/base_agent.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Base Agent class with common functionality for all agents
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import traceback
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
from typing import Dict, List, Optional, Any
|
| 9 |
+
|
| 10 |
+
import tiktoken
|
| 11 |
+
from langchain.agents import Tool, AgentExecutor
|
| 12 |
+
from langchain.schema import OutputParserException
|
| 13 |
+
from langchain.agents.format_scratchpad import format_to_openai_function_messages
|
| 14 |
+
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
| 15 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 16 |
+
from langchain.schema import SystemMessage
|
| 17 |
+
from langchain_openai import ChatOpenAI
|
| 18 |
+
from langchain.memory import ConversationBufferMemory
|
| 19 |
+
|
| 20 |
+
from tools import agent_tools
|
| 21 |
+
from tools.agent_tools import convert_tool_to_openai_function
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class BaseAgent(ABC):
|
| 27 |
+
"""Base class for all agents with common functionality."""
|
| 28 |
+
|
| 29 |
+
def __init__(self, agent_id: str, llm: Optional[ChatOpenAI] = None, **kwargs):
|
| 30 |
+
"""
|
| 31 |
+
Initialize base agent.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
agent_id: Unique identifier for the agent
|
| 35 |
+
llm: Optional LangChain LLM instance
|
| 36 |
+
**kwargs: Additional arguments for specific agents
|
| 37 |
+
"""
|
| 38 |
+
self.agent_id = agent_id
|
| 39 |
+
|
| 40 |
+
# Get agent info from central registry
|
| 41 |
+
if agent_id not in agent_tools.AGENT_INFO:
|
| 42 |
+
raise ValueError(f"Unknown agent ID: {agent_id}")
|
| 43 |
+
|
| 44 |
+
agent_info = agent_tools.AGENT_INFO[agent_id]
|
| 45 |
+
self.name = agent_info["display_name"]
|
| 46 |
+
self.description = agent_info["description"]
|
| 47 |
+
|
| 48 |
+
# Initialize LLM with default model
|
| 49 |
+
self.model = kwargs.get("model", "gpt-4o-mini")
|
| 50 |
+
self.temperature = kwargs.get("temperature", 0)
|
| 51 |
+
self.llm = llm or ChatOpenAI(model=self.model, temperature=self.temperature)
|
| 52 |
+
|
| 53 |
+
# Initialize memory
|
| 54 |
+
self.memory = ConversationBufferMemory(
|
| 55 |
+
memory_key="chat_history",
|
| 56 |
+
return_messages=True
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Allow subclasses to do custom initialization
|
| 60 |
+
self._custom_init(**kwargs)
|
| 61 |
+
|
| 62 |
+
# Create tools (must be implemented by subclass)
|
| 63 |
+
self.tools = self._create_tools()
|
| 64 |
+
|
| 65 |
+
# Create agent
|
| 66 |
+
self.agent = self._create_agent()
|
| 67 |
+
|
| 68 |
+
def _custom_init(self, **kwargs):
|
| 69 |
+
"""Override this method for agent-specific initialization."""
|
| 70 |
+
pass
|
| 71 |
+
|
| 72 |
+
@abstractmethod
|
| 73 |
+
def _create_tools(self) -> List[Tool]:
|
| 74 |
+
"""Create tools for this agent. Must be implemented by subclass."""
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
@abstractmethod
|
| 78 |
+
def _get_system_prompt(self) -> str:
|
| 79 |
+
"""Get the system prompt for this agent. Must be implemented by subclass."""
|
| 80 |
+
pass
|
| 81 |
+
|
| 82 |
+
def _create_agent(self) -> AgentExecutor:
|
| 83 |
+
"""Create the LangChain agent with common setup."""
|
| 84 |
+
# Get system prompt from subclass
|
| 85 |
+
system_prompt = self._get_system_prompt()
|
| 86 |
+
system_message = SystemMessage(content=system_prompt)
|
| 87 |
+
|
| 88 |
+
# Create prompt template
|
| 89 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 90 |
+
system_message,
|
| 91 |
+
MessagesPlaceholder(variable_name="chat_history"),
|
| 92 |
+
("user", "{input}"),
|
| 93 |
+
MessagesPlaceholder(variable_name="agent_scratchpad"),
|
| 94 |
+
])
|
| 95 |
+
|
| 96 |
+
# Bind tools to LLM
|
| 97 |
+
llm_with_tools = self.llm.bind_functions(
|
| 98 |
+
functions=[convert_tool_to_openai_function(tool) for tool in self.tools]
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Create agent chain
|
| 102 |
+
agent = (
|
| 103 |
+
{
|
| 104 |
+
"input": lambda x: x["input"],
|
| 105 |
+
"agent_scratchpad": lambda x: format_to_openai_function_messages(x["intermediate_steps"]),
|
| 106 |
+
"chat_history": lambda x: x.get("chat_history", [])
|
| 107 |
+
}
|
| 108 |
+
| prompt
|
| 109 |
+
| llm_with_tools
|
| 110 |
+
| OpenAIFunctionsAgentOutputParser()
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Create agent executor
|
| 114 |
+
agent_executor = AgentExecutor(
|
| 115 |
+
agent=agent,
|
| 116 |
+
tools=self.tools,
|
| 117 |
+
verbose=True,
|
| 118 |
+
return_intermediate_steps=True,
|
| 119 |
+
max_iterations=5
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return agent_executor
|
| 123 |
+
|
| 124 |
+
def run(self, query: str, context: Optional[Dict] = None) -> Dict[str, Any]:
|
| 125 |
+
"""
|
| 126 |
+
Run the agent with a query.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
query: User query
|
| 130 |
+
context: Optional context dictionary
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
Dictionary with output, agent_id, and optional agent_switch
|
| 134 |
+
"""
|
| 135 |
+
# Allow subclasses to enhance the query
|
| 136 |
+
query = self._enhance_query(query, context)
|
| 137 |
+
|
| 138 |
+
# Get chat history from memory
|
| 139 |
+
chat_history = self.memory.chat_memory.messages
|
| 140 |
+
|
| 141 |
+
# Count tokens before sending
|
| 142 |
+
try:
|
| 143 |
+
# Get the encoding for the model
|
| 144 |
+
try:
|
| 145 |
+
encoding = tiktoken.encoding_for_model(self.model)
|
| 146 |
+
except KeyError:
|
| 147 |
+
# Fall back to cl100k_base encoding for newer models
|
| 148 |
+
encoding = tiktoken.get_encoding("cl100k_base")
|
| 149 |
+
|
| 150 |
+
# Estimate token count (this is approximate since we can't easily access the full prompt)
|
| 151 |
+
system_prompt = self._get_system_prompt()
|
| 152 |
+
chat_history_str = " ".join([msg.content for msg in chat_history])
|
| 153 |
+
total_input = f"{system_prompt}\n{chat_history_str}\n{query}"
|
| 154 |
+
token_count = len(encoding.encode(total_input))
|
| 155 |
+
|
| 156 |
+
logger.info(f"[{self.name}] Sending request to {self.model}")
|
| 157 |
+
logger.info(f"[{self.name}] Query length: {len(query)} chars")
|
| 158 |
+
logger.info(f"[{self.name}] Estimated tokens: ~{token_count}")
|
| 159 |
+
logger.info(f"[{self.name}] Chat history messages: {len(chat_history)}")
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.warning(f"[{self.name}] Could not count tokens: {e}")
|
| 163 |
+
|
| 164 |
+
# Run agent with graceful iteration limit handling
|
| 165 |
+
try:
|
| 166 |
+
result = self.agent.invoke({
|
| 167 |
+
"input": query,
|
| 168 |
+
"chat_history": chat_history
|
| 169 |
+
})
|
| 170 |
+
except Exception as agent_error:
|
| 171 |
+
# Check if this is an iteration limit error
|
| 172 |
+
error_msg = str(agent_error).lower()
|
| 173 |
+
if "iteration limit" in error_msg or "time limit" in error_msg:
|
| 174 |
+
logger.info(f"[{self.name}] Hit iteration limit, making final call without tools")
|
| 175 |
+
|
| 176 |
+
# Extract intermediate steps if available
|
| 177 |
+
intermediate_steps = []
|
| 178 |
+
if hasattr(agent_error, 'intermediate_steps'):
|
| 179 |
+
intermediate_steps = agent_error.intermediate_steps
|
| 180 |
+
|
| 181 |
+
# Make a final call with no tools to generate answer
|
| 182 |
+
final_prompt = f"""Based on the information gathered so far, provide a comprehensive answer to the user's question.
|
| 183 |
+
|
| 184 |
+
User's question: {query}
|
| 185 |
+
|
| 186 |
+
You must provide a complete answer using the search results you've already obtained."""
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# Use raw LLM without tools
|
| 190 |
+
final_response = self.llm.invoke(final_prompt)
|
| 191 |
+
result = {
|
| 192 |
+
"output": final_response.content if hasattr(final_response, 'content') else str(final_response),
|
| 193 |
+
"intermediate_steps": intermediate_steps
|
| 194 |
+
}
|
| 195 |
+
except Exception as final_error:
|
| 196 |
+
logger.error(f"[{self.name}] Failed to generate final answer: {final_error}")
|
| 197 |
+
raise agent_error # Re-raise original error
|
| 198 |
+
else:
|
| 199 |
+
# Not an iteration limit error, re-raise
|
| 200 |
+
raise
|
| 201 |
+
|
| 202 |
+
try:
|
| 203 |
+
# Check if any tool requested agent switching
|
| 204 |
+
target_agent_id = self.agent_id # Default to current agent
|
| 205 |
+
final_output = result["output"]
|
| 206 |
+
|
| 207 |
+
# Look through intermediate steps for switching signals
|
| 208 |
+
for action, observation in result.get("intermediate_steps", []):
|
| 209 |
+
if isinstance(observation, str) and observation.startswith("__SWITCH_AGENT__|"):
|
| 210 |
+
parts = observation.split("|", 2)
|
| 211 |
+
if len(parts) >= 3:
|
| 212 |
+
target_agent_id = parts[1]
|
| 213 |
+
final_output = parts[2]
|
| 214 |
+
logger.info(f"Agent switch detected: {self.agent_id} -> {target_agent_id}")
|
| 215 |
+
break
|
| 216 |
+
|
| 217 |
+
# Save to memory - clean the output first
|
| 218 |
+
self.memory.chat_memory.add_user_message(query)
|
| 219 |
+
cleaned_output = self._clean_output_for_history(final_output)
|
| 220 |
+
self.memory.chat_memory.add_ai_message(cleaned_output)
|
| 221 |
+
|
| 222 |
+
# Build response
|
| 223 |
+
response = {
|
| 224 |
+
"output": final_output,
|
| 225 |
+
"intermediate_steps": result.get("intermediate_steps", []),
|
| 226 |
+
"agent_id": target_agent_id # This will be the new agent if switching occurred
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
# Allow subclasses to add custom response data
|
| 230 |
+
self._enhance_response(response, result)
|
| 231 |
+
|
| 232 |
+
return response
|
| 233 |
+
|
| 234 |
+
except Exception as e:
|
| 235 |
+
error_type = type(e).__name__
|
| 236 |
+
error_msg = str(e)
|
| 237 |
+
|
| 238 |
+
logger.error(f"[{self.name}] Error type: {error_type}")
|
| 239 |
+
logger.error(f"[{self.name}] Error message: {error_msg}")
|
| 240 |
+
logger.error(f"[{self.name}] Query that caused error: {query[:500]}...")
|
| 241 |
+
logger.error(f"[{self.name}] Model: {self.model}")
|
| 242 |
+
logger.error(f"[{self.name}] Stack trace:\n{traceback.format_exc()}")
|
| 243 |
+
|
| 244 |
+
# Provide user-friendly error message
|
| 245 |
+
if "server had an error" in error_msg.lower():
|
| 246 |
+
user_message = "The AI service is temporarily unavailable. This might be due to high demand or a long request. Please try again with a shorter query."
|
| 247 |
+
else:
|
| 248 |
+
user_message = f"I encountered an error: {error_msg}"
|
| 249 |
+
|
| 250 |
+
return {
|
| 251 |
+
"output": f"I apologize, but {user_message}",
|
| 252 |
+
"error": error_msg,
|
| 253 |
+
"error_type": error_type,
|
| 254 |
+
"agent_id": self.agent_id
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
def _enhance_query(self, query: str, context: Optional[Dict] = None) -> str:
|
| 258 |
+
"""Override this method to enhance the query before processing."""
|
| 259 |
+
return query
|
| 260 |
+
|
| 261 |
+
def _clean_output_for_history(self, output: str) -> str:
|
| 262 |
+
"""
|
| 263 |
+
Clean output before saving to chat history.
|
| 264 |
+
Removes transfer messages, tool usage details, and other noise.
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
output: Raw output string
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
Cleaned output suitable for chat history
|
| 271 |
+
"""
|
| 272 |
+
import re
|
| 273 |
+
|
| 274 |
+
# Remove transfer messages
|
| 275 |
+
output = re.sub(r'Transferring to [^:]+:[^\n]+\n*', '', output)
|
| 276 |
+
|
| 277 |
+
# Remove tool usage details section
|
| 278 |
+
if '---\n**Tool Usage Details:**' in output:
|
| 279 |
+
output = output.split('---\n**Tool Usage Details:**')[0]
|
| 280 |
+
elif 'Tool Usage Details:' in output:
|
| 281 |
+
output = output.split('Tool Usage Details:')[0]
|
| 282 |
+
|
| 283 |
+
# Remove __SWITCH_AGENT__ markers
|
| 284 |
+
output = re.sub(r'__SWITCH_AGENT__\|[^|]+\|[^\n]+\n*', '', output)
|
| 285 |
+
|
| 286 |
+
# Remove any trailing whitespace
|
| 287 |
+
output = output.strip()
|
| 288 |
+
|
| 289 |
+
return output
|
| 290 |
+
|
| 291 |
+
def _enhance_response(self, response: Dict[str, Any], result: Dict[str, Any]):
|
| 292 |
+
"""Override this method to add custom data to the response."""
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
def clear_memory(self):
|
| 296 |
+
"""Clear conversation memory."""
|
| 297 |
+
self.memory.clear()
|
| 298 |
+
|
| 299 |
+
def _build_tool_descriptions_and_agents(self) -> tuple[List[str], List[str]]:
|
| 300 |
+
"""
|
| 301 |
+
Build tool descriptions and switching agent lists for system prompt.
|
| 302 |
+
|
| 303 |
+
Returns:
|
| 304 |
+
Tuple of (tool_descriptions, switching_agents)
|
| 305 |
+
"""
|
| 306 |
+
tool_descriptions = []
|
| 307 |
+
switching_agents = []
|
| 308 |
+
|
| 309 |
+
for tool in self.tools:
|
| 310 |
+
if tool.name.startswith("switch_to_"):
|
| 311 |
+
agent_id = tool.name.replace("switch_to_", "")
|
| 312 |
+
if agent_id in agent_tools.AGENT_INFO:
|
| 313 |
+
switching_agents.append(agent_tools.AGENT_INFO[agent_id]["display_name"])
|
| 314 |
+
else:
|
| 315 |
+
tool_descriptions.append(f"- {tool.name}: {tool.description}")
|
| 316 |
+
|
| 317 |
+
return tool_descriptions, switching_agents
|
py/agents/document_reader.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Document Reader Agent using LangChain with modular tools
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Dict, List, Optional, Any
|
| 7 |
+
|
| 8 |
+
from agents.base_agent import BaseAgent
|
| 9 |
+
from langchain.agents import Tool
|
| 10 |
+
|
| 11 |
+
# Import modular tools
|
| 12 |
+
from tools import document_tools, agent_tools
|
| 13 |
+
from config_loader import get_config
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DocumentReaderAgent(BaseAgent):
|
| 19 |
+
"""Document reader agent using LangChain with RAG capabilities."""
|
| 20 |
+
|
| 21 |
+
def __init__(self, llm: Optional[Any] = None):
|
| 22 |
+
# Initialize base agent
|
| 23 |
+
super().__init__(agent_id="document_reader", llm=llm)
|
| 24 |
+
|
| 25 |
+
def _custom_init(self, **kwargs):
|
| 26 |
+
"""Custom initialization for document reader."""
|
| 27 |
+
self.config = get_config()
|
| 28 |
+
# Initialize ChromaDB manager for RAG pre-query
|
| 29 |
+
from backend.chromadb_manager import ChromaDBManager
|
| 30 |
+
self.db_manager = ChromaDBManager()
|
| 31 |
+
|
| 32 |
+
def _create_tools(self) -> List[Tool]:
|
| 33 |
+
"""Create tools for the document reader agent."""
|
| 34 |
+
# Add document-specific tools
|
| 35 |
+
tools = [
|
| 36 |
+
document_tools.search_documentation_tool(),
|
| 37 |
+
document_tools.list_available_versions_tool()
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
# Add switching tools for all other agents
|
| 41 |
+
tools.extend(agent_tools.create_switching_tools_for_agent(self.agent_id))
|
| 42 |
+
|
| 43 |
+
return tools
|
| 44 |
+
|
| 45 |
+
def _get_system_prompt(self) -> str:
|
| 46 |
+
"""Get the system prompt for document reader agent."""
|
| 47 |
+
# Build tool descriptions and switching agents
|
| 48 |
+
tool_descriptions, switching_agents = self._build_tool_descriptions_and_agents()
|
| 49 |
+
|
| 50 |
+
return f"""You are a technical documentation assistant for Harmony and Chorus products.
|
| 51 |
+
|
| 52 |
+
Your approach:
|
| 53 |
+
1. ALWAYS respond to the CURRENT user message - ignore previous searches or queries
|
| 54 |
+
2. You receive initial documentation context - check if it answers the user's question
|
| 55 |
+
3. If not, search for the specific information they need
|
| 56 |
+
4. Provide COMPLETE, self-contained answers with all relevant details from the documentation
|
| 57 |
+
5. Quote extensively from the documents you find - users want the actual content
|
| 58 |
+
6. Transfer to other agents when users need help beyond documentation
|
| 59 |
+
|
| 60 |
+
CRITICAL Answer Requirements:
|
| 61 |
+
- Your answers must be comprehensive and self-sufficient
|
| 62 |
+
- Include ALL relevant information you find in the documentation
|
| 63 |
+
- NEVER tell users to "refer to the guide" or "see page X" - instead, include that information in your response
|
| 64 |
+
- If you mention something exists in the documentation, quote it fully
|
| 65 |
+
- Users come to you to avoid reading documents - give them complete answers
|
| 66 |
+
|
| 67 |
+
Search principles:
|
| 68 |
+
- The search tool uses vector RAG (semantic similarity), so similar terms return similar results
|
| 69 |
+
- When users mention a product and version, use them as separate parameters (e.g., "install harmony 1.5" → query="install", product="harmony", version="1.5")
|
| 70 |
+
- Products are lowercase: "harmony" or "chorus"
|
| 71 |
+
- After searching, provide ALL the relevant content you found, not just a summary
|
| 72 |
+
|
| 73 |
+
Available tools:
|
| 74 |
+
{chr(10).join(tool_descriptions)}
|
| 75 |
+
|
| 76 |
+
You can transfer the conversation to these agents:
|
| 77 |
+
{', '.join(switching_agents)}
|
| 78 |
+
|
| 79 |
+
When to transfer:
|
| 80 |
+
- Profile Settings: User wants to view/update their settings, preferences, or account
|
| 81 |
+
Examples: "set my profile name", "update my email", "change my settings", "view my profile"
|
| 82 |
+
- Network Configuration: User needs to configure NAT, VLANs, IP addresses, or network settings
|
| 83 |
+
- Subscriber Management: User needs to create/manage subscribers, import CSV data, or handle IMSI ranges
|
| 84 |
+
- System Query: User wants analytics, performance metrics, or system status reports
|
| 85 |
+
- Policy & DNN: User needs to manage DNNs, QoS profiles, or policy rules
|
| 86 |
+
|
| 87 |
+
CRITICAL: When switching agents, ONLY use the switching tool. Do NOT add any text, explanations, or messages - the tool handles everything."""
|
| 88 |
+
|
| 89 |
+
def _enhance_query(self, query: str, context: Optional[Dict] = None) -> str:
|
| 90 |
+
"""Enhance query with RAG context."""
|
| 91 |
+
if not context:
|
| 92 |
+
context = {}
|
| 93 |
+
|
| 94 |
+
product = context.get("product", "harmony")
|
| 95 |
+
version = context.get("version", "1.8")
|
| 96 |
+
|
| 97 |
+
# Query RAG with current product/version
|
| 98 |
+
try:
|
| 99 |
+
rag_results = self.db_manager.query_with_filter(
|
| 100 |
+
query,
|
| 101 |
+
product,
|
| 102 |
+
version,
|
| 103 |
+
k=self.config.get_rag_k()
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Format RAG results for context - TRUNCATE to prevent token overflow
|
| 107 |
+
rag_context_parts = []
|
| 108 |
+
total_chars = 0
|
| 109 |
+
max_total_chars = 3000 # Limit total context
|
| 110 |
+
|
| 111 |
+
for i, doc in enumerate(rag_results):
|
| 112 |
+
content = doc.page_content
|
| 113 |
+
|
| 114 |
+
# Check if adding this would exceed total limit
|
| 115 |
+
if total_chars + len(content) > max_total_chars:
|
| 116 |
+
logger.info(f"Truncating RAG context at document {i+1} to stay within limits")
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
rag_context_parts.append(f"[{i+1}] {content}")
|
| 120 |
+
total_chars += len(content)
|
| 121 |
+
|
| 122 |
+
rag_context = "\n\n".join(rag_context_parts)
|
| 123 |
+
|
| 124 |
+
logger.info(f"RAG context: {len(rag_results)} documents found, {len(rag_context_parts)} used")
|
| 125 |
+
logger.info(f"RAG context size: {len(rag_context)} chars")
|
| 126 |
+
|
| 127 |
+
# Create enhanced prompt with RAG context
|
| 128 |
+
enhanced_query = f"""User Query: {query}
|
| 129 |
+
|
| 130 |
+
Initial Documentation Context:
|
| 131 |
+
{rag_context}
|
| 132 |
+
|
| 133 |
+
Please answer the user's query. If the initial documentation above doesn't contain the answer, use your search tools to find the relevant information."""
|
| 134 |
+
|
| 135 |
+
logger.info(f"Enhanced query total size: {len(enhanced_query)} chars")
|
| 136 |
+
return enhanced_query
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.error(f"Error during RAG pre-query: {e}")
|
| 140 |
+
# Fall back to original query with context
|
| 141 |
+
return f"[Context: {product} {version}] {query}"
|
| 142 |
+
|
| 143 |
+
def _enhance_response(self, response: Dict[str, Any], result: Dict[str, Any]):
|
| 144 |
+
"""Add tool usage information to the response."""
|
| 145 |
+
# Extract tool calls from intermediate_steps
|
| 146 |
+
tool_calls = []
|
| 147 |
+
|
| 148 |
+
for action, observation in result.get("intermediate_steps", []):
|
| 149 |
+
if hasattr(action, 'tool') and hasattr(action, 'tool_input'):
|
| 150 |
+
tool_info = {
|
| 151 |
+
'tool': action.tool,
|
| 152 |
+
'inputs': action.tool_input
|
| 153 |
+
}
|
| 154 |
+
tool_calls.append(tool_info)
|
| 155 |
+
|
| 156 |
+
# Append tool usage summary to output
|
| 157 |
+
if tool_calls:
|
| 158 |
+
tool_summary = "\n\n---\n**Tool Usage Details:**\n"
|
| 159 |
+
for i, call in enumerate(tool_calls, 1):
|
| 160 |
+
tool_summary += f"{i}. `{call['tool']}`"
|
| 161 |
+
# Format inputs based on type
|
| 162 |
+
if isinstance(call['inputs'], dict):
|
| 163 |
+
params = ", ".join([f"{k}='{v}'" for k, v in call['inputs'].items() if v is not None])
|
| 164 |
+
tool_summary += f"({params})\n"
|
| 165 |
+
else:
|
| 166 |
+
tool_summary += f"({call['inputs']})\n"
|
| 167 |
+
|
| 168 |
+
response["output"] += tool_summary
|
py/agents/network_config.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TODO
|
| 2 |
+
|
| 3 |
+
class NetworkConfigurationAgent:
|
| 4 |
+
"""Placeholder for NetworkConfigurationAgent - not implemented yet"""
|
| 5 |
+
pass
|
py/agents/policy_dnn.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TODO
|
| 2 |
+
|
| 3 |
+
class PolicyDNNAgent:
|
| 4 |
+
"""Placeholder for PolicyDNNAgent - not implemented yet"""
|
| 5 |
+
pass
|
py/agents/profile_settings.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Profile Settings Agent using LangChain
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from agents.base_agent import BaseAgent
|
| 11 |
+
from langchain.tools import tool
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
from tools import agent_tools
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Pydantic models for structured tools
|
| 19 |
+
class PreferenceSetting(BaseModel):
|
| 20 |
+
"""Model for preference updates."""
|
| 21 |
+
setting_name: str = Field(description="Name of the setting to update")
|
| 22 |
+
new_value: str = Field(description="New value for the setting")
|
| 23 |
+
|
| 24 |
+
class NotificationSetting(BaseModel):
|
| 25 |
+
"""Model for notification settings."""
|
| 26 |
+
email_updates: Optional[bool] = Field(None, description="Enable email updates")
|
| 27 |
+
system_alerts: Optional[bool] = Field(None, description="Enable system alerts")
|
| 28 |
+
newsletter: Optional[bool] = Field(None, description="Subscribe to newsletter")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ProfileSettingsAgent(BaseAgent):
|
| 32 |
+
"""Profile settings agent using LangChain tools."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, llm: Optional[Any] = None):
|
| 35 |
+
# Initialize base agent with slightly higher temperature for more natural responses
|
| 36 |
+
super().__init__(agent_id="profile_settings", llm=llm, temperature=0.3)
|
| 37 |
+
|
| 38 |
+
def _custom_init(self, **kwargs):
|
| 39 |
+
"""Custom initialization for profile settings."""
|
| 40 |
+
# Initialize dummy profile database
|
| 41 |
+
self.profile_db = self._init_profile_db()
|
| 42 |
+
|
| 43 |
+
# Track pending changes
|
| 44 |
+
self.pending_changes = {}
|
| 45 |
+
self.awaiting_confirmation = False
|
| 46 |
+
|
| 47 |
+
def _init_profile_db(self) -> Dict:
|
| 48 |
+
"""Initialize dummy profile database."""
|
| 49 |
+
db_path = Path(__file__).parent.parent / "data" / "profiles.json"
|
| 50 |
+
|
| 51 |
+
if db_path.exists():
|
| 52 |
+
with open(db_path, 'r') as f:
|
| 53 |
+
return json.load(f)
|
| 54 |
+
|
| 55 |
+
# Default profile
|
| 56 |
+
return {
|
| 57 |
+
"users": {
|
| 58 |
+
"default_user": {
|
| 59 |
+
"name": "Default User",
|
| 60 |
+
"email": "user@example.com",
|
| 61 |
+
"preferences": {
|
| 62 |
+
"default_product": "harmony",
|
| 63 |
+
"default_version": "1.8",
|
| 64 |
+
"preferred_model": "gpt-4o",
|
| 65 |
+
"temperature": 0.0,
|
| 66 |
+
"max_tokens": 4000,
|
| 67 |
+
"theme": "light",
|
| 68 |
+
"language": "en"
|
| 69 |
+
},
|
| 70 |
+
"notifications": {
|
| 71 |
+
"email_updates": True,
|
| 72 |
+
"system_alerts": True,
|
| 73 |
+
"newsletter": False
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
def _create_tools(self) -> List:
|
| 80 |
+
"""Create tools for profile management."""
|
| 81 |
+
tools = []
|
| 82 |
+
|
| 83 |
+
# Tool to view profile
|
| 84 |
+
@tool
|
| 85 |
+
def view_profile() -> str:
|
| 86 |
+
"""View current user profile and settings."""
|
| 87 |
+
# Mock implementation - just return example data
|
| 88 |
+
return """**Your Current Profile:**
|
| 89 |
+
|
| 90 |
+
**Name:** John Doe
|
| 91 |
+
**Email:** john.doe@example.com
|
| 92 |
+
|
| 93 |
+
**Preferences:**
|
| 94 |
+
- Default Product: harmony
|
| 95 |
+
- Default Version: 1.8
|
| 96 |
+
- Preferred Model: gpt-4o
|
| 97 |
+
- Temperature: 0.0
|
| 98 |
+
- Max Tokens: 4000
|
| 99 |
+
- Theme: light
|
| 100 |
+
- Language: en
|
| 101 |
+
|
| 102 |
+
**Notification Settings:**
|
| 103 |
+
- Email Updates: Enabled
|
| 104 |
+
- System Alerts: Enabled
|
| 105 |
+
- Newsletter: Disabled"""
|
| 106 |
+
|
| 107 |
+
tools.append(view_profile)
|
| 108 |
+
|
| 109 |
+
# Tool to update preferences
|
| 110 |
+
@tool
|
| 111 |
+
def update_preference(setting_name: str, new_value: str) -> str:
|
| 112 |
+
"""Update a user preference setting. Requires confirmation."""
|
| 113 |
+
# Mock implementation - accept any setting
|
| 114 |
+
return f"I've updated your {setting_name} to '{new_value}'. Change applied successfully! (mock)"
|
| 115 |
+
|
| 116 |
+
tools.append(update_preference)
|
| 117 |
+
|
| 118 |
+
# Tool to update notifications
|
| 119 |
+
@tool
|
| 120 |
+
def update_notifications(email_updates: Optional[bool] = None,
|
| 121 |
+
system_alerts: Optional[bool] = None,
|
| 122 |
+
newsletter: Optional[bool] = None) -> str:
|
| 123 |
+
"""Update notification settings. Requires confirmation."""
|
| 124 |
+
changes = []
|
| 125 |
+
|
| 126 |
+
if email_updates is not None:
|
| 127 |
+
changes.append(f"Email updates: {'Enabled' if email_updates else 'Disabled'}")
|
| 128 |
+
|
| 129 |
+
if system_alerts is not None:
|
| 130 |
+
changes.append(f"System alerts: {'Enabled' if system_alerts else 'Disabled'}")
|
| 131 |
+
|
| 132 |
+
if newsletter is not None:
|
| 133 |
+
changes.append(f"Newsletter: {'Enabled' if newsletter else 'Disabled'}")
|
| 134 |
+
|
| 135 |
+
if not changes:
|
| 136 |
+
return "No changes specified."
|
| 137 |
+
|
| 138 |
+
# Mock response
|
| 139 |
+
return f"Updated notification settings:\n" + "\n".join(f" - {c}" for c in changes) + "\n\nChanges applied successfully! (mock)"
|
| 140 |
+
|
| 141 |
+
tools.append(update_notifications)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# Add switching tools for all other agents
|
| 145 |
+
tools.extend(agent_tools.create_switching_tools_for_agent(self.agent_id))
|
| 146 |
+
|
| 147 |
+
return tools
|
| 148 |
+
|
| 149 |
+
def _get_system_prompt(self) -> str:
|
| 150 |
+
"""Get the system prompt for profile settings agent."""
|
| 151 |
+
# Build tool descriptions and switching agents
|
| 152 |
+
tool_descriptions, switching_agents = self._build_tool_descriptions_and_agents()
|
| 153 |
+
|
| 154 |
+
return f"""You are a basic profile settings agent. You handle user profile and preference updates.
|
| 155 |
+
|
| 156 |
+
What you do:
|
| 157 |
+
- View profile settings
|
| 158 |
+
- Update any setting when asked (all updates are mock)
|
| 159 |
+
- Transfer to other agents when needed
|
| 160 |
+
|
| 161 |
+
Transfer immediately for:
|
| 162 |
+
- Documentation/how-to questions → switch_to_document_reader
|
| 163 |
+
- Technical questions → switch_to_document_reader
|
| 164 |
+
- Network configuration → switch_to_network_config
|
| 165 |
+
- Other non-settings questions → appropriate agent
|
| 166 |
+
|
| 167 |
+
CRITICAL Tool Usage:
|
| 168 |
+
- To use a tool, you must CALL it properly, not just write its name
|
| 169 |
+
- When switching agents, ONLY use the switching tool - no additional text
|
| 170 |
+
- The tool invocation format is handled by the system - just use the tool normally
|
| 171 |
+
|
| 172 |
+
Keep responses simple and always include "(mock)" for changes.
|
| 173 |
+
|
| 174 |
+
Available tools:
|
| 175 |
+
{chr(10).join(tool_descriptions)}"""
|
| 176 |
+
|
| 177 |
+
def _enhance_response(self, response: Dict[str, Any], result: Dict[str, Any]):
|
| 178 |
+
"""Add pending changes status to response."""
|
| 179 |
+
# Add pending changes status
|
| 180 |
+
response["has_pending_changes"] = len(self.pending_changes) > 0
|
py/agents/subscriber_management.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TODO
|
| 2 |
+
|
| 3 |
+
class SubscriberManagementAgent:
|
| 4 |
+
"""Placeholder for SubscriberManagementAgent - not implemented yet"""
|
| 5 |
+
pass
|
py/agents/system_query.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TODO
|
| 2 |
+
|
| 3 |
+
class SystemQueryAgent:
|
| 4 |
+
"""Placeholder for SystemQueryAgent - not implemented yet"""
|
| 5 |
+
pass
|
py/app.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main application entry point for LangChain-based Multi-Agent System
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Add current directory to path
|
| 10 |
+
sys.path.append(str(Path(__file__).parent))
|
| 11 |
+
|
| 12 |
+
# Import and create the Gradio interface directly
|
| 13 |
+
from frontend.gradio_app import PeerToPeerApp, create_gradio_interface
|
| 14 |
+
|
| 15 |
+
# Launch the app
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
# Check if we need to run migration first
|
| 18 |
+
chroma_db_path = Path(__file__).parent / "data" / "chroma_db"
|
| 19 |
+
|
| 20 |
+
if not chroma_db_path.exists():
|
| 21 |
+
print("ChromaDB not found. Running migration script...")
|
| 22 |
+
from scripts.migrate_to_chromadb import main as migrate
|
| 23 |
+
migrate()
|
| 24 |
+
print("\nMigration complete! Starting application...\n")
|
| 25 |
+
|
| 26 |
+
# Create and launch the interface
|
| 27 |
+
app = create_gradio_interface()
|
| 28 |
+
app.launch(
|
| 29 |
+
server_name="0.0.0.0",
|
| 30 |
+
server_port=7860,
|
| 31 |
+
share=False,
|
| 32 |
+
show_error=True
|
| 33 |
+
)
|
py/backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend package
|
py/backend/api_gateway.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Gateway for handling all backend requests with user confirmation
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Dict, List, Optional, Any
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ConfirmationRequest:
|
| 16 |
+
"""Represents a pending action awaiting user confirmation"""
|
| 17 |
+
confirmation_id: str
|
| 18 |
+
user_id: str
|
| 19 |
+
action_type: str
|
| 20 |
+
action_details: Dict[str, Any]
|
| 21 |
+
created_at: datetime
|
| 22 |
+
expires_at: datetime
|
| 23 |
+
status: str # pending, confirmed, denied, expired
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class APIGateway:
|
| 27 |
+
"""
|
| 28 |
+
Central gateway for all API requests from agents to backend services.
|
| 29 |
+
Handles authentication, authorization, and user confirmation flows.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
# TODO: Initialize connection to backend services
|
| 34 |
+
# TODO: Set up authentication mechanism
|
| 35 |
+
# TODO: Configure rate limiting
|
| 36 |
+
self.pending_confirmations: Dict[str, ConfirmationRequest] = {}
|
| 37 |
+
|
| 38 |
+
def authenticate(self, user_id: str, session_token: str) -> bool:
|
| 39 |
+
"""
|
| 40 |
+
Authenticate user request
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
user_id: User identifier
|
| 44 |
+
session_token: Session authentication token
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
bool: True if authenticated
|
| 48 |
+
|
| 49 |
+
TODO: Implement actual authentication logic
|
| 50 |
+
TODO: Validate session tokens
|
| 51 |
+
TODO: Check user permissions
|
| 52 |
+
"""
|
| 53 |
+
raise NotImplementedError("Authentication not implemented")
|
| 54 |
+
|
| 55 |
+
def authorize(self, user_id: str, action: str, resource: str) -> bool:
|
| 56 |
+
"""
|
| 57 |
+
Check if user is authorized to perform action on resource
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
user_id: User identifier
|
| 61 |
+
action: Action to perform (create, read, update, delete)
|
| 62 |
+
resource: Resource type (network, subscriber, policy, etc.)
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
bool: True if authorized
|
| 66 |
+
|
| 67 |
+
TODO: Implement RBAC (Role-Based Access Control)
|
| 68 |
+
TODO: Check tenant boundaries
|
| 69 |
+
TODO: Validate resource ownership
|
| 70 |
+
"""
|
| 71 |
+
raise NotImplementedError("Authorization not implemented")
|
| 72 |
+
|
| 73 |
+
def request_confirmation(self, user_id: str, action_type: str,
|
| 74 |
+
action_details: Dict[str, Any]) -> str:
|
| 75 |
+
"""
|
| 76 |
+
Create a confirmation request for user action
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
user_id: User requesting the action
|
| 80 |
+
action_type: Type of action (e.g., 'network_configure', 'subscriber_create')
|
| 81 |
+
action_details: Details of the action to be performed
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
str: Confirmation ID for tracking
|
| 85 |
+
|
| 86 |
+
TODO: Store confirmation request
|
| 87 |
+
TODO: Set appropriate expiration time
|
| 88 |
+
TODO: Send notification to user
|
| 89 |
+
"""
|
| 90 |
+
raise NotImplementedError("Confirmation request not implemented")
|
| 91 |
+
|
| 92 |
+
def get_confirmation_status(self, confirmation_id: str) -> Optional[ConfirmationRequest]:
|
| 93 |
+
"""
|
| 94 |
+
Get status of a confirmation request
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
confirmation_id: ID of the confirmation request
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
ConfirmationRequest or None if not found
|
| 101 |
+
|
| 102 |
+
TODO: Retrieve from storage
|
| 103 |
+
TODO: Check expiration
|
| 104 |
+
"""
|
| 105 |
+
raise NotImplementedError("Get confirmation status not implemented")
|
| 106 |
+
|
| 107 |
+
def confirm_action(self, confirmation_id: str, user_id: str, confirmed: bool) -> bool:
|
| 108 |
+
"""
|
| 109 |
+
Confirm or deny a pending action
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
confirmation_id: ID of the confirmation request
|
| 113 |
+
user_id: User confirming (must match requester)
|
| 114 |
+
confirmed: True to confirm, False to deny
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
bool: Success status
|
| 118 |
+
|
| 119 |
+
TODO: Validate confirmation exists
|
| 120 |
+
TODO: Verify user matches requester
|
| 121 |
+
TODO: Update confirmation status
|
| 122 |
+
"""
|
| 123 |
+
raise NotImplementedError("Confirm action not implemented")
|
| 124 |
+
|
| 125 |
+
def execute_action(self, confirmation_id: str) -> Dict[str, Any]:
|
| 126 |
+
"""
|
| 127 |
+
Execute a confirmed action
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
confirmation_id: ID of confirmed action
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
dict: Result of the action execution
|
| 134 |
+
|
| 135 |
+
TODO: Verify action is confirmed
|
| 136 |
+
TODO: Route to appropriate backend service
|
| 137 |
+
TODO: Handle errors and rollback if needed
|
| 138 |
+
"""
|
| 139 |
+
raise NotImplementedError("Execute action not implemented")
|
| 140 |
+
|
| 141 |
+
# Network Configuration APIs
|
| 142 |
+
|
| 143 |
+
def configure_network(self, config: Dict[str, Any], user_id: str) -> str:
|
| 144 |
+
"""
|
| 145 |
+
Configure network settings (NAT, VLAN, etc.)
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
config: Network configuration details
|
| 149 |
+
user_id: User making the request
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
str: Confirmation ID
|
| 153 |
+
|
| 154 |
+
TODO: Validate network configuration
|
| 155 |
+
TODO: Check for conflicts
|
| 156 |
+
TODO: Create confirmation request
|
| 157 |
+
"""
|
| 158 |
+
raise NotImplementedError("Network configuration not implemented")
|
| 159 |
+
|
| 160 |
+
def bulk_configure_networks(self, configs: List[Dict[str, Any]], user_id: str) -> str:
|
| 161 |
+
"""
|
| 162 |
+
Configure multiple network settings in bulk
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
configs: List of network configurations
|
| 166 |
+
user_id: User making the request
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
str: Confirmation ID for bulk operation
|
| 170 |
+
|
| 171 |
+
TODO: Validate all configurations
|
| 172 |
+
TODO: Check resource limits
|
| 173 |
+
TODO: Create bulk confirmation request
|
| 174 |
+
"""
|
| 175 |
+
raise NotImplementedError("Bulk network configuration not implemented")
|
| 176 |
+
|
| 177 |
+
# Subscriber Management APIs
|
| 178 |
+
|
| 179 |
+
def create_subscriber(self, imsi: str, config: Dict[str, Any], user_id: str) -> str:
|
| 180 |
+
"""
|
| 181 |
+
Create a single subscriber
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
imsi: IMSI of the subscriber
|
| 185 |
+
config: Subscriber configuration
|
| 186 |
+
user_id: User making the request
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
str: Confirmation ID
|
| 190 |
+
|
| 191 |
+
TODO: Validate IMSI format
|
| 192 |
+
TODO: Check if IMSI already exists
|
| 193 |
+
TODO: Validate configuration
|
| 194 |
+
"""
|
| 195 |
+
raise NotImplementedError("Create subscriber not implemented")
|
| 196 |
+
|
| 197 |
+
def bulk_create_subscribers(self, subscribers: List[Dict[str, Any]], user_id: str) -> str:
|
| 198 |
+
"""
|
| 199 |
+
Create multiple subscribers in bulk
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
subscribers: List of subscriber configurations
|
| 203 |
+
user_id: User making the request
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
str: Confirmation ID for bulk operation
|
| 207 |
+
|
| 208 |
+
TODO: Validate all IMSIs
|
| 209 |
+
TODO: Check for duplicates
|
| 210 |
+
TODO: Validate bulk limits
|
| 211 |
+
"""
|
| 212 |
+
raise NotImplementedError("Bulk create subscribers not implemented")
|
| 213 |
+
|
| 214 |
+
def import_subscribers_csv(self, csv_data: str, template_id: Optional[str], user_id: str) -> str:
|
| 215 |
+
"""
|
| 216 |
+
Import subscribers from CSV data
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
csv_data: CSV content with subscriber data
|
| 220 |
+
template_id: Optional template to apply
|
| 221 |
+
user_id: User making the request
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
str: Confirmation ID
|
| 225 |
+
|
| 226 |
+
TODO: Parse CSV data
|
| 227 |
+
TODO: Validate CSV format
|
| 228 |
+
TODO: Apply template if provided
|
| 229 |
+
"""
|
| 230 |
+
raise NotImplementedError("CSV import not implemented")
|
| 231 |
+
|
| 232 |
+
# System Query APIs (Read-only, no confirmation needed)
|
| 233 |
+
|
| 234 |
+
def query_system_status(self, query: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 235 |
+
"""
|
| 236 |
+
Query system status and metrics (read-only)
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
query: Query parameters
|
| 240 |
+
user_id: User making the request
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
dict: Query results
|
| 244 |
+
|
| 245 |
+
TODO: Route to appropriate backend
|
| 246 |
+
TODO: Apply user filters/permissions
|
| 247 |
+
TODO: Format response
|
| 248 |
+
"""
|
| 249 |
+
raise NotImplementedError("System query not implemented")
|
| 250 |
+
|
| 251 |
+
def get_analytics(self, metric_type: str, time_range: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 252 |
+
"""
|
| 253 |
+
Get analytics and insights (read-only)
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
metric_type: Type of metric to retrieve
|
| 257 |
+
time_range: Time range for analytics
|
| 258 |
+
user_id: User making the request
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
dict: Analytics data
|
| 262 |
+
|
| 263 |
+
TODO: Fetch metrics from backend
|
| 264 |
+
TODO: Apply aggregations
|
| 265 |
+
TODO: Generate insights
|
| 266 |
+
"""
|
| 267 |
+
raise NotImplementedError("Analytics not implemented")
|
| 268 |
+
|
| 269 |
+
# Policy & DNN APIs
|
| 270 |
+
|
| 271 |
+
def create_dnn(self, dnn_config: Dict[str, Any], user_id: str) -> str:
|
| 272 |
+
"""
|
| 273 |
+
Create a new DNN configuration
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
dnn_config: DNN configuration details
|
| 277 |
+
user_id: User making the request
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
str: Confirmation ID
|
| 281 |
+
|
| 282 |
+
TODO: Validate DNN parameters
|
| 283 |
+
TODO: Check for naming conflicts
|
| 284 |
+
TODO: Create confirmation request
|
| 285 |
+
"""
|
| 286 |
+
raise NotImplementedError("Create DNN not implemented")
|
| 287 |
+
|
| 288 |
+
def update_policy(self, policy_id: str, updates: Dict[str, Any], user_id: str) -> str:
|
| 289 |
+
"""
|
| 290 |
+
Update policy configuration
|
| 291 |
+
|
| 292 |
+
Args:
|
| 293 |
+
policy_id: ID of policy to update
|
| 294 |
+
updates: Policy updates
|
| 295 |
+
user_id: User making the request
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
str: Confirmation ID
|
| 299 |
+
|
| 300 |
+
TODO: Validate policy exists
|
| 301 |
+
TODO: Validate update parameters
|
| 302 |
+
TODO: Check impact analysis
|
| 303 |
+
"""
|
| 304 |
+
raise NotImplementedError("Update policy not implemented")
|
| 305 |
+
|
| 306 |
+
def apply_policy_template(self, template_id: str, target_subscribers: List[str], user_id: str) -> str:
|
| 307 |
+
"""
|
| 308 |
+
Apply policy template to multiple subscribers
|
| 309 |
+
|
| 310 |
+
Args:
|
| 311 |
+
template_id: ID of policy template
|
| 312 |
+
target_subscribers: List of subscriber IMSIs
|
| 313 |
+
user_id: User making the request
|
| 314 |
+
|
| 315 |
+
Returns:
|
| 316 |
+
str: Confirmation ID
|
| 317 |
+
|
| 318 |
+
TODO: Validate template exists
|
| 319 |
+
TODO: Validate all subscribers exist
|
| 320 |
+
TODO: Create bulk update request
|
| 321 |
+
"""
|
| 322 |
+
raise NotImplementedError("Apply policy template not implemented")
|
py/backend/chat_history_manager.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SQLite-based Chat History Manager for persistent conversation storage
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sqlite3
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Dict, Optional, Any
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import uuid
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ChatHistoryManager:
|
| 17 |
+
def __init__(self, db_path: Optional[str] = None):
|
| 18 |
+
"""Initialize the chat history manager with SQLite database."""
|
| 19 |
+
if db_path is None:
|
| 20 |
+
db_path = Path(__file__).parent.parent / "data" / "chat_history.db"
|
| 21 |
+
|
| 22 |
+
self.db_path = Path(db_path)
|
| 23 |
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# Initialize database
|
| 26 |
+
self._init_database()
|
| 27 |
+
|
| 28 |
+
def _init_database(self):
|
| 29 |
+
"""Initialize the SQLite database with required tables."""
|
| 30 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 31 |
+
cursor = conn.cursor()
|
| 32 |
+
|
| 33 |
+
# Create users table
|
| 34 |
+
cursor.execute("""
|
| 35 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 36 |
+
user_id TEXT PRIMARY KEY,
|
| 37 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 38 |
+
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 39 |
+
preferences TEXT
|
| 40 |
+
)
|
| 41 |
+
""")
|
| 42 |
+
|
| 43 |
+
# Create conversations table
|
| 44 |
+
cursor.execute("""
|
| 45 |
+
CREATE TABLE IF NOT EXISTS conversations (
|
| 46 |
+
conversation_id TEXT PRIMARY KEY,
|
| 47 |
+
user_id TEXT NOT NULL,
|
| 48 |
+
title TEXT,
|
| 49 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 50 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 51 |
+
active_agent TEXT,
|
| 52 |
+
metadata TEXT,
|
| 53 |
+
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
| 54 |
+
)
|
| 55 |
+
""")
|
| 56 |
+
|
| 57 |
+
# Create messages table
|
| 58 |
+
cursor.execute("""
|
| 59 |
+
CREATE TABLE IF NOT EXISTS messages (
|
| 60 |
+
message_id TEXT PRIMARY KEY,
|
| 61 |
+
conversation_id TEXT NOT NULL,
|
| 62 |
+
role TEXT NOT NULL,
|
| 63 |
+
content TEXT NOT NULL,
|
| 64 |
+
agent_id TEXT,
|
| 65 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 66 |
+
metadata TEXT,
|
| 67 |
+
FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id)
|
| 68 |
+
)
|
| 69 |
+
""")
|
| 70 |
+
|
| 71 |
+
# Create indexes for better performance
|
| 72 |
+
cursor.execute("""
|
| 73 |
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation
|
| 74 |
+
ON messages(conversation_id, timestamp)
|
| 75 |
+
""")
|
| 76 |
+
|
| 77 |
+
cursor.execute("""
|
| 78 |
+
CREATE INDEX IF NOT EXISTS idx_conversations_user
|
| 79 |
+
ON conversations(user_id, updated_at)
|
| 80 |
+
""")
|
| 81 |
+
|
| 82 |
+
conn.commit()
|
| 83 |
+
|
| 84 |
+
logger.info(f"Initialized chat history database at {self.db_path}")
|
| 85 |
+
|
| 86 |
+
def create_user(self, user_id: Optional[str] = None, preferences: Optional[Dict] = None) -> str:
|
| 87 |
+
"""Create a new user or return existing user_id."""
|
| 88 |
+
if user_id is None:
|
| 89 |
+
user_id = f"user_{uuid.uuid4().hex[:8]}"
|
| 90 |
+
|
| 91 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 92 |
+
cursor = conn.cursor()
|
| 93 |
+
|
| 94 |
+
# Check if user exists
|
| 95 |
+
cursor.execute("SELECT user_id FROM users WHERE user_id = ?", (user_id,))
|
| 96 |
+
if cursor.fetchone():
|
| 97 |
+
# Update last active
|
| 98 |
+
cursor.execute("""
|
| 99 |
+
UPDATE users SET last_active = CURRENT_TIMESTAMP
|
| 100 |
+
WHERE user_id = ?
|
| 101 |
+
""", (user_id,))
|
| 102 |
+
else:
|
| 103 |
+
# Create new user
|
| 104 |
+
cursor.execute("""
|
| 105 |
+
INSERT INTO users (user_id, preferences)
|
| 106 |
+
VALUES (?, ?)
|
| 107 |
+
""", (user_id, json.dumps(preferences or {})))
|
| 108 |
+
|
| 109 |
+
conn.commit()
|
| 110 |
+
|
| 111 |
+
return user_id
|
| 112 |
+
|
| 113 |
+
def create_conversation(self, user_id: str, title: Optional[str] = None,
|
| 114 |
+
metadata: Optional[Dict] = None) -> str:
|
| 115 |
+
"""Create a new conversation for a user."""
|
| 116 |
+
conversation_id = f"conv_{uuid.uuid4().hex[:12]}"
|
| 117 |
+
|
| 118 |
+
if title is None:
|
| 119 |
+
title = f"Conversation {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
| 120 |
+
|
| 121 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 122 |
+
cursor = conn.cursor()
|
| 123 |
+
cursor.execute("""
|
| 124 |
+
INSERT INTO conversations
|
| 125 |
+
(conversation_id, user_id, title, active_agent, metadata)
|
| 126 |
+
VALUES (?, ?, ?, ?, ?)
|
| 127 |
+
""", (
|
| 128 |
+
conversation_id,
|
| 129 |
+
user_id,
|
| 130 |
+
title,
|
| 131 |
+
"document_reader", # Default agent
|
| 132 |
+
json.dumps(metadata or {})
|
| 133 |
+
))
|
| 134 |
+
conn.commit()
|
| 135 |
+
|
| 136 |
+
logger.info(f"Created conversation {conversation_id} for user {user_id}")
|
| 137 |
+
return conversation_id
|
| 138 |
+
|
| 139 |
+
def add_message(self, conversation_id: str, role: str, content: str,
|
| 140 |
+
agent_id: Optional[str] = None, metadata: Optional[Dict] = None):
|
| 141 |
+
"""Add a message to a conversation."""
|
| 142 |
+
message_id = f"msg_{uuid.uuid4().hex[:12]}"
|
| 143 |
+
|
| 144 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 145 |
+
cursor = conn.cursor()
|
| 146 |
+
|
| 147 |
+
# Add message
|
| 148 |
+
cursor.execute("""
|
| 149 |
+
INSERT INTO messages
|
| 150 |
+
(message_id, conversation_id, role, content, agent_id, metadata)
|
| 151 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 152 |
+
""", (
|
| 153 |
+
message_id,
|
| 154 |
+
conversation_id,
|
| 155 |
+
role,
|
| 156 |
+
content,
|
| 157 |
+
agent_id,
|
| 158 |
+
json.dumps(metadata or {})
|
| 159 |
+
))
|
| 160 |
+
|
| 161 |
+
# Update conversation timestamp
|
| 162 |
+
cursor.execute("""
|
| 163 |
+
UPDATE conversations
|
| 164 |
+
SET updated_at = CURRENT_TIMESTAMP
|
| 165 |
+
WHERE conversation_id = ?
|
| 166 |
+
""", (conversation_id,))
|
| 167 |
+
|
| 168 |
+
conn.commit()
|
| 169 |
+
|
| 170 |
+
logger.debug(f"Added {role} message to conversation {conversation_id}")
|
| 171 |
+
|
| 172 |
+
def get_conversation_history(self, conversation_id: str,
|
| 173 |
+
limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 174 |
+
"""Get message history for a conversation."""
|
| 175 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 176 |
+
conn.row_factory = sqlite3.Row
|
| 177 |
+
cursor = conn.cursor()
|
| 178 |
+
|
| 179 |
+
query = """
|
| 180 |
+
SELECT message_id, role, content, agent_id, timestamp, metadata
|
| 181 |
+
FROM messages
|
| 182 |
+
WHERE conversation_id = ?
|
| 183 |
+
ORDER BY timestamp ASC
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
if limit:
|
| 187 |
+
query += f" LIMIT {limit}"
|
| 188 |
+
|
| 189 |
+
cursor.execute(query, (conversation_id,))
|
| 190 |
+
messages = []
|
| 191 |
+
|
| 192 |
+
for row in cursor.fetchall():
|
| 193 |
+
msg = dict(row)
|
| 194 |
+
# Parse metadata JSON
|
| 195 |
+
if msg['metadata']:
|
| 196 |
+
msg['metadata'] = json.loads(msg['metadata'])
|
| 197 |
+
messages.append(msg)
|
| 198 |
+
|
| 199 |
+
return messages
|
| 200 |
+
|
| 201 |
+
def get_user_conversations(self, user_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
| 202 |
+
"""Get recent conversations for a user."""
|
| 203 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 204 |
+
conn.row_factory = sqlite3.Row
|
| 205 |
+
cursor = conn.cursor()
|
| 206 |
+
|
| 207 |
+
cursor.execute("""
|
| 208 |
+
SELECT c.conversation_id, c.title, c.created_at, c.updated_at,
|
| 209 |
+
c.active_agent, c.metadata,
|
| 210 |
+
COUNT(m.message_id) as message_count
|
| 211 |
+
FROM conversations c
|
| 212 |
+
LEFT JOIN messages m ON c.conversation_id = m.conversation_id
|
| 213 |
+
WHERE c.user_id = ?
|
| 214 |
+
GROUP BY c.conversation_id
|
| 215 |
+
ORDER BY c.updated_at DESC
|
| 216 |
+
LIMIT ?
|
| 217 |
+
""", (user_id, limit))
|
| 218 |
+
|
| 219 |
+
conversations = []
|
| 220 |
+
for row in cursor.fetchall():
|
| 221 |
+
conv = dict(row)
|
| 222 |
+
if conv['metadata']:
|
| 223 |
+
conv['metadata'] = json.loads(conv['metadata'])
|
| 224 |
+
conversations.append(conv)
|
| 225 |
+
|
| 226 |
+
return conversations
|
| 227 |
+
|
| 228 |
+
def update_active_agent(self, conversation_id: str, agent_id: str):
|
| 229 |
+
"""Update the active agent for a conversation."""
|
| 230 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 231 |
+
cursor = conn.cursor()
|
| 232 |
+
cursor.execute("""
|
| 233 |
+
UPDATE conversations
|
| 234 |
+
SET active_agent = ?, updated_at = CURRENT_TIMESTAMP
|
| 235 |
+
WHERE conversation_id = ?
|
| 236 |
+
""", (agent_id, conversation_id))
|
| 237 |
+
conn.commit()
|
| 238 |
+
|
| 239 |
+
def get_formatted_history(self, conversation_id: str,
|
| 240 |
+
format_type: str = "langchain") -> Any:
|
| 241 |
+
"""Get conversation history in different formats."""
|
| 242 |
+
messages = self.get_conversation_history(conversation_id)
|
| 243 |
+
|
| 244 |
+
if format_type == "langchain":
|
| 245 |
+
# Format for LangChain memory
|
| 246 |
+
from langchain.schema import HumanMessage, AIMessage
|
| 247 |
+
|
| 248 |
+
formatted = []
|
| 249 |
+
for msg in messages:
|
| 250 |
+
if msg['role'] == 'user':
|
| 251 |
+
formatted.append(HumanMessage(content=msg['content']))
|
| 252 |
+
elif msg['role'] == 'assistant':
|
| 253 |
+
formatted.append(AIMessage(content=msg['content']))
|
| 254 |
+
return formatted
|
| 255 |
+
|
| 256 |
+
elif format_type == "openai":
|
| 257 |
+
# Format for OpenAI messages
|
| 258 |
+
return [
|
| 259 |
+
{"role": msg['role'], "content": msg['content']}
|
| 260 |
+
for msg in messages
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
elif format_type == "string":
|
| 264 |
+
# Format as string for context
|
| 265 |
+
lines = []
|
| 266 |
+
for msg in messages:
|
| 267 |
+
role = "User" if msg['role'] == 'user' else "Assistant"
|
| 268 |
+
if msg['agent_id']:
|
| 269 |
+
role += f" ({msg['agent_id']})"
|
| 270 |
+
lines.append(f"{role}: {msg['content']}")
|
| 271 |
+
return "\n\n".join(lines)
|
| 272 |
+
|
| 273 |
+
else:
|
| 274 |
+
return messages
|
| 275 |
+
|
| 276 |
+
def delete_conversation(self, conversation_id: str):
|
| 277 |
+
"""Delete a conversation and all its messages."""
|
| 278 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 279 |
+
cursor = conn.cursor()
|
| 280 |
+
|
| 281 |
+
# Delete messages first
|
| 282 |
+
cursor.execute("DELETE FROM messages WHERE conversation_id = ?",
|
| 283 |
+
(conversation_id,))
|
| 284 |
+
|
| 285 |
+
# Delete conversation
|
| 286 |
+
cursor.execute("DELETE FROM conversations WHERE conversation_id = ?",
|
| 287 |
+
(conversation_id,))
|
| 288 |
+
|
| 289 |
+
conn.commit()
|
| 290 |
+
|
| 291 |
+
logger.info(f"Deleted conversation {conversation_id}")
|
| 292 |
+
|
| 293 |
+
def export_conversation(self, conversation_id: str) -> Dict[str, Any]:
|
| 294 |
+
"""Export a conversation with all its data."""
|
| 295 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 296 |
+
conn.row_factory = sqlite3.Row
|
| 297 |
+
cursor = conn.cursor()
|
| 298 |
+
|
| 299 |
+
# Get conversation details
|
| 300 |
+
cursor.execute("""
|
| 301 |
+
SELECT * FROM conversations WHERE conversation_id = ?
|
| 302 |
+
""", (conversation_id,))
|
| 303 |
+
|
| 304 |
+
conv_row = cursor.fetchone()
|
| 305 |
+
if not conv_row:
|
| 306 |
+
raise ValueError(f"Conversation {conversation_id} not found")
|
| 307 |
+
|
| 308 |
+
conversation = dict(conv_row)
|
| 309 |
+
if conversation['metadata']:
|
| 310 |
+
conversation['metadata'] = json.loads(conversation['metadata'])
|
| 311 |
+
|
| 312 |
+
# Get messages
|
| 313 |
+
conversation['messages'] = self.get_conversation_history(conversation_id)
|
| 314 |
+
|
| 315 |
+
return conversation
|
py/backend/chromadb_manager.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ChromaDB Manager - HuggingFace version using SimpleVectorDB
|
| 3 |
+
This version uses in-memory vector search instead of ChromaDB for simplicity
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, Tuple
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from langchain.schema import Document
|
| 10 |
+
|
| 11 |
+
# Import config loader
|
| 12 |
+
import sys
|
| 13 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 14 |
+
from config_loader import get_config
|
| 15 |
+
|
| 16 |
+
# Import our simple vector DB instead of ChromaDB
|
| 17 |
+
from .simple_vector_db import get_simple_vector_db
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ChromaDBManager:
|
| 23 |
+
"""
|
| 24 |
+
ChromaDB Manager interface that uses SimpleVectorDB underneath.
|
| 25 |
+
Maintains the same interface for compatibility with existing code.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self, persist_directory: Optional[Path] = None):
|
| 29 |
+
"""Initialize the manager with SimpleVectorDB."""
|
| 30 |
+
# Load config
|
| 31 |
+
self.config = get_config()
|
| 32 |
+
|
| 33 |
+
# Use SimpleVectorDB instead of ChromaDB
|
| 34 |
+
self.db = get_simple_vector_db(self.config)
|
| 35 |
+
|
| 36 |
+
# For compatibility - these aren't used in simple version
|
| 37 |
+
self.persist_directory = str(persist_directory) if persist_directory else None
|
| 38 |
+
self.vectorstore = None
|
| 39 |
+
self.client = None
|
| 40 |
+
self.embeddings = self.db.embeddings_model
|
| 41 |
+
|
| 42 |
+
# Cache for available versions
|
| 43 |
+
self._available_versions = None
|
| 44 |
+
|
| 45 |
+
logger.info("Initialized ChromaDBManager with SimpleVectorDB backend")
|
| 46 |
+
|
| 47 |
+
def query_with_filter(self, query: str, product: str, version: str, k: int = 5) -> List[Document]:
|
| 48 |
+
"""Query with product and version filter."""
|
| 49 |
+
return self.db.query_with_filter(query, product, version, k)
|
| 50 |
+
|
| 51 |
+
def query_product_all_versions(self, query: str, product: str, k: int = 5) -> List[Document]:
|
| 52 |
+
"""Query across all versions of a product."""
|
| 53 |
+
return self.db.query_product_all_versions(query, product, k)
|
| 54 |
+
|
| 55 |
+
def query_version_and_general(self, product: str, version: str, query: str,
|
| 56 |
+
max_results: int = 5) -> Tuple[List[Dict], List[Dict]]:
|
| 57 |
+
"""Query both version-specific and general docs (compatible with old interface)."""
|
| 58 |
+
# Query version-specific
|
| 59 |
+
version_docs = self.query_with_filter(query, product, version, max_results)
|
| 60 |
+
version_results = self._docs_to_results(version_docs)
|
| 61 |
+
|
| 62 |
+
# Query general/FAQ
|
| 63 |
+
general_docs = self.query_with_filter(query, "general", "all", max_results)
|
| 64 |
+
general_results = self._docs_to_results(general_docs)
|
| 65 |
+
|
| 66 |
+
return version_results, general_results
|
| 67 |
+
|
| 68 |
+
def _docs_to_results(self, docs: List[Document]) -> List[Dict]:
|
| 69 |
+
"""Convert LangChain documents to old result format for compatibility."""
|
| 70 |
+
results = []
|
| 71 |
+
|
| 72 |
+
for doc in docs:
|
| 73 |
+
# Extract similarity score if available
|
| 74 |
+
similarity = 0.8 # Default if not available
|
| 75 |
+
if hasattr(doc, 'metadata') and '_score' in doc.metadata:
|
| 76 |
+
similarity = doc.metadata['_score']
|
| 77 |
+
|
| 78 |
+
result = {
|
| 79 |
+
'text': doc.page_content,
|
| 80 |
+
'quote': doc.page_content, # For compatibility
|
| 81 |
+
'chunk_id': doc.metadata.get('chunk_id', 'unknown'),
|
| 82 |
+
'file_id': doc.metadata.get('source', 'unknown'),
|
| 83 |
+
'similarity': similarity,
|
| 84 |
+
'metadata': doc.metadata
|
| 85 |
+
}
|
| 86 |
+
results.append(result)
|
| 87 |
+
|
| 88 |
+
return results
|
| 89 |
+
|
| 90 |
+
def search_across_stores(self, query: str, store_names: Optional[List[str]] = None,
|
| 91 |
+
max_results_per_store: int = 3) -> Dict[str, List[Dict]]:
|
| 92 |
+
"""Search across multiple stores (compatible with old interface)."""
|
| 93 |
+
results = {}
|
| 94 |
+
|
| 95 |
+
if store_names is None:
|
| 96 |
+
# Get all unique product-version combinations
|
| 97 |
+
store_names = []
|
| 98 |
+
for product, versions in self.list_available_versions().items():
|
| 99 |
+
for version in versions:
|
| 100 |
+
store_name = f"{product}_{version.replace('.', '_')}"
|
| 101 |
+
store_names.append(store_name)
|
| 102 |
+
|
| 103 |
+
for store_name in store_names:
|
| 104 |
+
# Parse product and version from store name
|
| 105 |
+
if "_" in store_name:
|
| 106 |
+
parts = store_name.split("_", 1)
|
| 107 |
+
product = parts[0]
|
| 108 |
+
version = parts[1].replace("_", ".")
|
| 109 |
+
|
| 110 |
+
docs = self.query_with_filter(query, product, version, max_results_per_store)
|
| 111 |
+
if docs:
|
| 112 |
+
results[store_name] = self._docs_to_results(docs)
|
| 113 |
+
|
| 114 |
+
return results
|
| 115 |
+
|
| 116 |
+
def list_available_versions(self) -> Dict[str, List[str]]:
|
| 117 |
+
"""List all available product versions."""
|
| 118 |
+
return self.db.list_available_versions()
|
py/backend/simple_vector_db.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple in-memory vector database for HuggingFace deployment
|
| 3 |
+
Replaces ChromaDB with O(N) similarity search
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import List, Dict, Optional, Tuple
|
| 10 |
+
import numpy as np
|
| 11 |
+
from langchain.schema import Document
|
| 12 |
+
from langchain_openai import OpenAIEmbeddings
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SimpleVectorDB:
|
| 18 |
+
"""Simple in-memory vector database using numpy for similarity search."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, config=None):
|
| 21 |
+
"""Initialize the vector database."""
|
| 22 |
+
self.config = config or {}
|
| 23 |
+
self.embeddings_model = OpenAIEmbeddings(
|
| 24 |
+
model=self.config.get("rag.embedding_model", "text-embedding-3-small")
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Storage for documents and vectors
|
| 28 |
+
self.documents: List[Dict] = []
|
| 29 |
+
self.vectors: Optional[np.ndarray] = None
|
| 30 |
+
self._available_versions = None
|
| 31 |
+
|
| 32 |
+
# Load embeddings on initialization
|
| 33 |
+
self._load_embeddings()
|
| 34 |
+
|
| 35 |
+
def _load_embeddings(self):
|
| 36 |
+
"""Load all embedding files into memory."""
|
| 37 |
+
embeddings_dir = Path(__file__).parent.parent / "data" / "embeddings"
|
| 38 |
+
|
| 39 |
+
if not embeddings_dir.exists():
|
| 40 |
+
logger.warning(f"Embeddings directory not found: {embeddings_dir}")
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
all_documents = []
|
| 44 |
+
all_vectors = []
|
| 45 |
+
|
| 46 |
+
# Load each JSON file
|
| 47 |
+
for json_file in sorted(embeddings_dir.glob("*.json")):
|
| 48 |
+
logger.info(f"Loading embeddings from {json_file.name}")
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
with open(json_file, 'r') as f:
|
| 52 |
+
data = json.load(f)
|
| 53 |
+
|
| 54 |
+
# Extract metadata from filename
|
| 55 |
+
store_name = json_file.stem
|
| 56 |
+
if store_name == "general_faq":
|
| 57 |
+
product = "general"
|
| 58 |
+
version = "all"
|
| 59 |
+
else:
|
| 60 |
+
parts = store_name.split("_", 1)
|
| 61 |
+
if len(parts) == 2:
|
| 62 |
+
product = parts[0]
|
| 63 |
+
version = parts[1].replace("_", ".")
|
| 64 |
+
else:
|
| 65 |
+
product = "unknown"
|
| 66 |
+
version = "unknown"
|
| 67 |
+
|
| 68 |
+
# Process chunks
|
| 69 |
+
for i, chunk in enumerate(data.get("chunks", [])):
|
| 70 |
+
doc = {
|
| 71 |
+
"content": chunk.get("text", ""),
|
| 72 |
+
"metadata": {
|
| 73 |
+
"product": product,
|
| 74 |
+
"version": version,
|
| 75 |
+
"store_name": store_name,
|
| 76 |
+
"chunk_index": i,
|
| 77 |
+
"chunk_id": f"{store_name}_chunk_{i}"
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Add optional metadata if available
|
| 82 |
+
if "metadata" in chunk:
|
| 83 |
+
chunk_meta = chunk["metadata"]
|
| 84 |
+
doc["metadata"].update({
|
| 85 |
+
"source": chunk_meta.get("source", ""),
|
| 86 |
+
"page": chunk_meta.get("page", -1),
|
| 87 |
+
"document": chunk_meta.get("document", ""),
|
| 88 |
+
"token_count": chunk_meta.get("token_count", 0)
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
all_documents.append(doc)
|
| 92 |
+
all_vectors.append(chunk.get("embedding", []))
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"Error loading {json_file.name}: {e}")
|
| 96 |
+
continue
|
| 97 |
+
|
| 98 |
+
# Convert to numpy array for efficient computation
|
| 99 |
+
if all_vectors:
|
| 100 |
+
self.documents = all_documents
|
| 101 |
+
self.vectors = np.array(all_vectors, dtype=np.float32)
|
| 102 |
+
logger.info(f"Loaded {len(self.documents)} documents with embeddings")
|
| 103 |
+
else:
|
| 104 |
+
logger.warning("No embeddings loaded")
|
| 105 |
+
|
| 106 |
+
def _cosine_similarity(self, query_vector: np.ndarray, vectors: np.ndarray) -> np.ndarray:
|
| 107 |
+
"""Compute cosine similarity between query vector and all vectors."""
|
| 108 |
+
# Normalize query vector
|
| 109 |
+
query_norm = query_vector / (np.linalg.norm(query_vector) + 1e-10)
|
| 110 |
+
|
| 111 |
+
# Normalize all vectors
|
| 112 |
+
norms = np.linalg.norm(vectors, axis=1, keepdims=True) + 1e-10
|
| 113 |
+
vectors_norm = vectors / norms
|
| 114 |
+
|
| 115 |
+
# Compute dot product (cosine similarity)
|
| 116 |
+
similarities = np.dot(vectors_norm, query_norm)
|
| 117 |
+
|
| 118 |
+
return similarities
|
| 119 |
+
|
| 120 |
+
def _filter_documents(self, indices: List[int], filter_dict: Optional[Dict] = None) -> List[int]:
|
| 121 |
+
"""Filter document indices based on metadata criteria."""
|
| 122 |
+
if not filter_dict:
|
| 123 |
+
return indices
|
| 124 |
+
|
| 125 |
+
filtered = []
|
| 126 |
+
|
| 127 |
+
for idx in indices:
|
| 128 |
+
doc = self.documents[idx]
|
| 129 |
+
metadata = doc["metadata"]
|
| 130 |
+
|
| 131 |
+
# Handle $and operator
|
| 132 |
+
if "$and" in filter_dict:
|
| 133 |
+
all_match = True
|
| 134 |
+
for condition in filter_dict["$and"]:
|
| 135 |
+
for key, value in condition.items():
|
| 136 |
+
if metadata.get(key) != value:
|
| 137 |
+
all_match = False
|
| 138 |
+
break
|
| 139 |
+
if not all_match:
|
| 140 |
+
break
|
| 141 |
+
if all_match:
|
| 142 |
+
filtered.append(idx)
|
| 143 |
+
|
| 144 |
+
# Handle simple key-value filters
|
| 145 |
+
else:
|
| 146 |
+
match = True
|
| 147 |
+
for key, value in filter_dict.items():
|
| 148 |
+
if isinstance(value, dict) and "$eq" in value:
|
| 149 |
+
if metadata.get(key) != value["$eq"]:
|
| 150 |
+
match = False
|
| 151 |
+
break
|
| 152 |
+
elif metadata.get(key) != value:
|
| 153 |
+
match = False
|
| 154 |
+
break
|
| 155 |
+
if match:
|
| 156 |
+
filtered.append(idx)
|
| 157 |
+
|
| 158 |
+
return filtered
|
| 159 |
+
|
| 160 |
+
def query_with_filter(self, query: str, product: str, version: str, k: int = 5) -> List[Document]:
|
| 161 |
+
"""Query with product and version filter."""
|
| 162 |
+
logger.info(f"Querying {product} {version} for: {query}")
|
| 163 |
+
|
| 164 |
+
filter_dict = {"$and": [{"product": product}, {"version": version}]}
|
| 165 |
+
return self._query(query, k, filter_dict)
|
| 166 |
+
|
| 167 |
+
def query_product_all_versions(self, query: str, product: str, k: int = 5) -> List[Document]:
|
| 168 |
+
"""Query across all versions of a product."""
|
| 169 |
+
logger.info(f"Querying all {product} versions for: {query}")
|
| 170 |
+
|
| 171 |
+
filter_dict = {"product": {"$eq": product}}
|
| 172 |
+
return self._query(query, k, filter_dict)
|
| 173 |
+
|
| 174 |
+
def query_all_products(self, query: str, k: int = 5) -> List[Document]:
|
| 175 |
+
"""Query across all products and versions."""
|
| 176 |
+
logger.info(f"Querying all products for: {query}")
|
| 177 |
+
return self._query(query, k, None)
|
| 178 |
+
|
| 179 |
+
def _query(self, query: str, k: int = 5, filter_dict: Optional[Dict] = None) -> List[Document]:
|
| 180 |
+
"""Internal query method."""
|
| 181 |
+
if self.vectors is None or len(self.documents) == 0:
|
| 182 |
+
logger.warning("No documents loaded")
|
| 183 |
+
return []
|
| 184 |
+
|
| 185 |
+
# Get query embedding
|
| 186 |
+
try:
|
| 187 |
+
query_embedding = self.embeddings_model.embed_query(query)
|
| 188 |
+
query_vector = np.array(query_embedding, dtype=np.float32)
|
| 189 |
+
except Exception as e:
|
| 190 |
+
logger.error(f"Error getting query embedding: {e}")
|
| 191 |
+
return []
|
| 192 |
+
|
| 193 |
+
# Compute similarities
|
| 194 |
+
similarities = self._cosine_similarity(query_vector, self.vectors)
|
| 195 |
+
|
| 196 |
+
# Get top k indices
|
| 197 |
+
top_indices = np.argsort(similarities)[::-1] # Sort descending
|
| 198 |
+
|
| 199 |
+
# Apply filters
|
| 200 |
+
if filter_dict:
|
| 201 |
+
top_indices = self._filter_documents(top_indices.tolist(), filter_dict)
|
| 202 |
+
|
| 203 |
+
# Take top k after filtering
|
| 204 |
+
top_indices = top_indices[:k]
|
| 205 |
+
|
| 206 |
+
# Convert to LangChain Document objects
|
| 207 |
+
results = []
|
| 208 |
+
for idx in top_indices:
|
| 209 |
+
doc_data = self.documents[idx]
|
| 210 |
+
doc = Document(
|
| 211 |
+
page_content=doc_data["content"],
|
| 212 |
+
metadata=doc_data["metadata"]
|
| 213 |
+
)
|
| 214 |
+
results.append(doc)
|
| 215 |
+
|
| 216 |
+
logger.info(f"Found {len(results)} documents")
|
| 217 |
+
return results
|
| 218 |
+
|
| 219 |
+
def list_available_versions(self) -> Dict[str, List[str]]:
|
| 220 |
+
"""List all available product versions."""
|
| 221 |
+
if self._available_versions is not None:
|
| 222 |
+
return self._available_versions
|
| 223 |
+
|
| 224 |
+
versions_map = {}
|
| 225 |
+
|
| 226 |
+
for doc in self.documents:
|
| 227 |
+
product = doc["metadata"].get("product", "unknown")
|
| 228 |
+
version = doc["metadata"].get("version", "unknown")
|
| 229 |
+
|
| 230 |
+
if product not in versions_map:
|
| 231 |
+
versions_map[product] = set()
|
| 232 |
+
versions_map[product].add(version)
|
| 233 |
+
|
| 234 |
+
# Convert sets to sorted lists
|
| 235 |
+
self._available_versions = {
|
| 236 |
+
product: sorted(list(versions))
|
| 237 |
+
for product, versions in versions_map.items()
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
return self._available_versions
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# Create a singleton instance
|
| 244 |
+
_db_instance = None
|
| 245 |
+
|
| 246 |
+
def get_simple_vector_db(config=None) -> SimpleVectorDB:
|
| 247 |
+
"""Get or create the singleton vector database instance."""
|
| 248 |
+
global _db_instance
|
| 249 |
+
if _db_instance is None:
|
| 250 |
+
_db_instance = SimpleVectorDB(config)
|
| 251 |
+
return _db_instance
|
py/config.yaml
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Assistant Configuration File
|
| 2 |
+
# This file contains all hyperparameters and settings for the multi-agent system
|
| 3 |
+
|
| 4 |
+
# Model Configuration
|
| 5 |
+
models:
|
| 6 |
+
available:
|
| 7 |
+
- model_id: "gpt-4o-mini"
|
| 8 |
+
display_name: "GPT-4o Mini"
|
| 9 |
+
max_tokens: 128000
|
| 10 |
+
input_cost_per_1m: 0.15 # $0.15 per 1M tokens
|
| 11 |
+
output_cost_per_1m: 0.60 # $0.60 per 1M tokens
|
| 12 |
+
description: "Most cost-efficient model for high-volume tasks"
|
| 13 |
+
default: true
|
| 14 |
+
- model_id: "gpt-4"
|
| 15 |
+
display_name: "GPT-4"
|
| 16 |
+
max_tokens: 8192
|
| 17 |
+
input_cost_per_1m: 30.00 # $30.00 per 1M tokens
|
| 18 |
+
output_cost_per_1m: 60.00 # $60.00 per 1M tokens
|
| 19 |
+
description: "Original GPT-4 with 8k context"
|
| 20 |
+
- model_id: "gpt-4o"
|
| 21 |
+
display_name: "GPT-4o"
|
| 22 |
+
max_tokens: 128000
|
| 23 |
+
input_cost_per_1m: 2.50 # $2.50 per 1M tokens (latest pricing)
|
| 24 |
+
output_cost_per_1m: 10.00 # $10.00 per 1M tokens
|
| 25 |
+
description: "Most capable model with vision capabilities"
|
| 26 |
+
- model_id: "gpt-4o-nano"
|
| 27 |
+
display_name: "GPT-4o Nano"
|
| 28 |
+
max_tokens: 8192
|
| 29 |
+
input_cost_per_1m: 0.10
|
| 30 |
+
output_cost_per_1m: 0.40
|
| 31 |
+
description: "Fastest model (when available)"
|
| 32 |
+
- model_id: "gpt-3.5-turbo"
|
| 33 |
+
display_name: "GPT-3.5 Turbo"
|
| 34 |
+
max_tokens: 16384
|
| 35 |
+
input_cost_per_1m: 0.50 # $0.50 per 1M tokens
|
| 36 |
+
output_cost_per_1m: 1.50 # $1.50 per 1M tokens
|
| 37 |
+
description: "Fast and efficient for simple tasks"
|
| 38 |
+
|
| 39 |
+
# Default model parameters
|
| 40 |
+
default_temperature: 0.0
|
| 41 |
+
default_max_tokens: 4000
|
| 42 |
+
|
| 43 |
+
# Product Configuration
|
| 44 |
+
products:
|
| 45 |
+
available:
|
| 46 |
+
- id: "harmony"
|
| 47 |
+
display_name: "Harmony"
|
| 48 |
+
versions: ["1.8", "1.6", "1.5", "1.2"]
|
| 49 |
+
default_version: "1.8"
|
| 50 |
+
- id: "chorus"
|
| 51 |
+
display_name: "Chorus"
|
| 52 |
+
versions: ["1.1"]
|
| 53 |
+
default_version: "1.1"
|
| 54 |
+
|
| 55 |
+
default_product: "harmony"
|
| 56 |
+
|
| 57 |
+
# RAG (Retrieval-Augmented Generation) Settings
|
| 58 |
+
rag:
|
| 59 |
+
# Number of documents to retrieve for context
|
| 60 |
+
default_k: 6
|
| 61 |
+
|
| 62 |
+
# ChromaDB settings
|
| 63 |
+
collection_name: "documentation"
|
| 64 |
+
embedding_model: "text-embedding-3-small"
|
| 65 |
+
|
| 66 |
+
# Unused RAG parameters (kept for reference)
|
| 67 |
+
# max_k: 15
|
| 68 |
+
# similarity_threshold: 0.7
|
| 69 |
+
# include_metadata: true
|
| 70 |
+
|
| 71 |
+
# Agent Configuration
|
| 72 |
+
agents:
|
| 73 |
+
# Document Reader Agent
|
| 74 |
+
document_reader:
|
| 75 |
+
enabled: true
|
| 76 |
+
display_name: "Document Reader"
|
| 77 |
+
description: "Handles documentation queries with RAG"
|
| 78 |
+
# Tools list not currently used - tools are defined in agent code
|
| 79 |
+
# tools:
|
| 80 |
+
# - search_version_documentation
|
| 81 |
+
# - search_all_versions
|
| 82 |
+
# - list_available_versions
|
| 83 |
+
# - switch_to_profile_settings
|
| 84 |
+
max_iterations: 3
|
| 85 |
+
verbose: true
|
| 86 |
+
|
| 87 |
+
# Profile Settings Agent
|
| 88 |
+
profile_settings:
|
| 89 |
+
enabled: true
|
| 90 |
+
display_name: "Profile Settings"
|
| 91 |
+
description: "Manages user preferences and settings"
|
| 92 |
+
# Tools list not currently used - tools are defined in agent code
|
| 93 |
+
# tools:
|
| 94 |
+
# - view_profile
|
| 95 |
+
# - update_preference
|
| 96 |
+
# - update_notifications
|
| 97 |
+
# - confirm_changes
|
| 98 |
+
# - show_settings_menu
|
| 99 |
+
# - switch_to_document_reader
|
| 100 |
+
max_iterations: 3
|
| 101 |
+
verbose: true
|
| 102 |
+
|
| 103 |
+
# Network Configuration Agent
|
| 104 |
+
network_config:
|
| 105 |
+
enabled: false
|
| 106 |
+
display_name: "Network Configuration"
|
| 107 |
+
description: "Manages network configurations (NAT, VLAN, IP)"
|
| 108 |
+
# Tools list not currently used - tools are defined in agent code
|
| 109 |
+
# tools:
|
| 110 |
+
# - configure_nat
|
| 111 |
+
# - configure_vlan
|
| 112 |
+
# - bulk_create_vlans
|
| 113 |
+
# - configure_ip_address
|
| 114 |
+
# - configure_dhcp
|
| 115 |
+
# - apply_network_template
|
| 116 |
+
# - preview_network_config
|
| 117 |
+
# - list_network_interfaces
|
| 118 |
+
max_iterations: 3
|
| 119 |
+
verbose: true
|
| 120 |
+
|
| 121 |
+
# Subscriber Management Agent
|
| 122 |
+
subscriber_management:
|
| 123 |
+
enabled: false
|
| 124 |
+
display_name: "Subscriber Management"
|
| 125 |
+
description: "Manages subscriber operations and CSV imports"
|
| 126 |
+
# Tools list not currently used - tools are defined in agent code
|
| 127 |
+
# tools:
|
| 128 |
+
# - create_subscriber
|
| 129 |
+
# - create_subscriber_range
|
| 130 |
+
# - import_subscribers_csv
|
| 131 |
+
# - clone_subscriber
|
| 132 |
+
# - update_subscriber
|
| 133 |
+
# - bulk_update_subscribers
|
| 134 |
+
# - toggle_subscribers
|
| 135 |
+
# - generate_csv_template
|
| 136 |
+
# - preview_import
|
| 137 |
+
max_iterations: 3
|
| 138 |
+
verbose: true
|
| 139 |
+
|
| 140 |
+
# System Query Agent
|
| 141 |
+
system_query:
|
| 142 |
+
enabled: false
|
| 143 |
+
display_name: "System Query"
|
| 144 |
+
description: "Provides read-only analytics and insights"
|
| 145 |
+
# Tools list not currently used - tools are defined in agent code
|
| 146 |
+
# tools:
|
| 147 |
+
# - query_radio_status
|
| 148 |
+
# - query_subscriber_sessions
|
| 149 |
+
# - query_network_performance
|
| 150 |
+
# - query_system_logs
|
| 151 |
+
# - generate_analytics_report
|
| 152 |
+
# - query_slice_performance
|
| 153 |
+
# - query_resource_utilization
|
| 154 |
+
# - query_service_health
|
| 155 |
+
# - query_top_talkers
|
| 156 |
+
max_iterations: 3
|
| 157 |
+
verbose: true
|
| 158 |
+
|
| 159 |
+
# Policy & DNN Agent
|
| 160 |
+
policy_dnn:
|
| 161 |
+
enabled: false
|
| 162 |
+
display_name: "Policy & DNN"
|
| 163 |
+
description: "Manages policies, DNNs, and QoS profiles"
|
| 164 |
+
# Tools list not currently used - tools are defined in agent code
|
| 165 |
+
# tools:
|
| 166 |
+
# - create_dnn
|
| 167 |
+
# - update_dnn
|
| 168 |
+
# - assign_dnn_to_subscribers
|
| 169 |
+
# - create_qos_profile
|
| 170 |
+
# - create_policy_rule
|
| 171 |
+
# - apply_policy_template
|
| 172 |
+
# - bulk_update_qos
|
| 173 |
+
# - list_dnns
|
| 174 |
+
# - create_service_profile
|
| 175 |
+
max_iterations: 3
|
| 176 |
+
verbose: true
|
| 177 |
+
|
| 178 |
+
# Default active agent
|
| 179 |
+
default_agent: "document_reader"
|
| 180 |
+
|
| 181 |
+
# Tool Configuration (NOT CURRENTLY USED - Kept for reference)
|
| 182 |
+
# tools:
|
| 183 |
+
# # Maximum number of tool calls per query
|
| 184 |
+
# max_tool_calls: 5
|
| 185 |
+
#
|
| 186 |
+
# # Tool timeout in seconds
|
| 187 |
+
# tool_timeout: 30
|
| 188 |
+
#
|
| 189 |
+
# # Enable/disable specific tools globally
|
| 190 |
+
# enabled_tools:
|
| 191 |
+
# # Document Reader tools
|
| 192 |
+
# - search_version_documentation
|
| 193 |
+
# - search_all_versions
|
| 194 |
+
# - list_available_versions
|
| 195 |
+
# # Profile Settings tools
|
| 196 |
+
# - view_profile
|
| 197 |
+
# - update_preference
|
| 198 |
+
# - update_notifications
|
| 199 |
+
# - confirm_changes
|
| 200 |
+
# - show_settings_menu
|
| 201 |
+
# # Network Configuration tools
|
| 202 |
+
# - configure_nat
|
| 203 |
+
# - configure_vlan
|
| 204 |
+
# - bulk_create_vlans
|
| 205 |
+
# - configure_ip_address
|
| 206 |
+
# - configure_dhcp
|
| 207 |
+
# - apply_network_template
|
| 208 |
+
# - preview_network_config
|
| 209 |
+
# - list_network_interfaces
|
| 210 |
+
# # Subscriber Management tools
|
| 211 |
+
# - create_subscriber
|
| 212 |
+
# - create_subscriber_range
|
| 213 |
+
# - import_subscribers_csv
|
| 214 |
+
# - clone_subscriber
|
| 215 |
+
# - update_subscriber
|
| 216 |
+
# - bulk_update_subscribers
|
| 217 |
+
# - toggle_subscribers
|
| 218 |
+
# - generate_csv_template
|
| 219 |
+
# - preview_import
|
| 220 |
+
# # System Query tools
|
| 221 |
+
# - query_radio_status
|
| 222 |
+
# - query_subscriber_sessions
|
| 223 |
+
# - query_network_performance
|
| 224 |
+
# - query_system_logs
|
| 225 |
+
# - generate_analytics_report
|
| 226 |
+
# - query_slice_performance
|
| 227 |
+
# - query_resource_utilization
|
| 228 |
+
# - query_service_health
|
| 229 |
+
# - query_top_talkers
|
| 230 |
+
# # Policy & DNN tools
|
| 231 |
+
# - create_dnn
|
| 232 |
+
# - update_dnn
|
| 233 |
+
# - assign_dnn_to_subscribers
|
| 234 |
+
# - create_qos_profile
|
| 235 |
+
# - create_policy_rule
|
| 236 |
+
# - apply_policy_template
|
| 237 |
+
# - bulk_update_qos
|
| 238 |
+
# - list_dnns
|
| 239 |
+
# - create_service_profile
|
| 240 |
+
# # Agent switching tools
|
| 241 |
+
# - switch_to_profile_settings
|
| 242 |
+
# - switch_to_document_reader
|
| 243 |
+
# - switch_to_network_config
|
| 244 |
+
# - switch_to_subscriber_management
|
| 245 |
+
# - switch_to_system_query
|
| 246 |
+
# - switch_to_policy_dnn
|
| 247 |
+
|
| 248 |
+
# Conversation Settings
|
| 249 |
+
conversation:
|
| 250 |
+
# SQLite settings
|
| 251 |
+
database_path: "data/chat_history.db"
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# Logging Configuration
|
| 255 |
+
logging:
|
| 256 |
+
level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
| 257 |
+
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 258 |
+
file: "logs/assistant.log"
|
| 259 |
+
console: true
|
py/config_loader.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration loader for the AI Assistant
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import yaml
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, Any, Optional, List
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ConfigLoader:
|
| 15 |
+
"""Loads and manages configuration from YAML file"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, config_path: Optional[str] = None):
|
| 18 |
+
"""Initialize config loader with optional custom path"""
|
| 19 |
+
if config_path is None:
|
| 20 |
+
config_path = Path(__file__).parent / "config.yaml"
|
| 21 |
+
|
| 22 |
+
self.config_path = Path(config_path)
|
| 23 |
+
self.config = self._load_config()
|
| 24 |
+
|
| 25 |
+
# Apply environment variable overrides
|
| 26 |
+
self._apply_env_overrides()
|
| 27 |
+
|
| 28 |
+
def _load_config(self) -> Dict[str, Any]:
|
| 29 |
+
"""Load configuration from YAML file"""
|
| 30 |
+
try:
|
| 31 |
+
with open(self.config_path, 'r') as f:
|
| 32 |
+
config = yaml.safe_load(f)
|
| 33 |
+
logger.info(f"Loaded configuration from {self.config_path}")
|
| 34 |
+
return config
|
| 35 |
+
except FileNotFoundError:
|
| 36 |
+
logger.warning(f"Config file not found at {self.config_path}, using defaults")
|
| 37 |
+
return self._get_default_config()
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error loading config: {e}, using defaults")
|
| 40 |
+
return self._get_default_config()
|
| 41 |
+
|
| 42 |
+
def _get_default_config(self) -> Dict[str, Any]:
|
| 43 |
+
"""Return default configuration if file not found"""
|
| 44 |
+
return {
|
| 45 |
+
"models": {
|
| 46 |
+
"available": [
|
| 47 |
+
{"model_id": "gpt-4o-mini", "display_name": "GPT-4 Omni Mini", "max_tokens": 16384, "default": True},
|
| 48 |
+
{"model_id": "gpt-4o", "display_name": "GPT-4 Omni", "max_tokens": 128000},
|
| 49 |
+
{"model_id": "gpt-4", "display_name": "GPT-4", "max_tokens": 8192},
|
| 50 |
+
{"model_id": "gpt-3.5-turbo", "display_name": "GPT-3.5 Turbo", "max_tokens": 16384}
|
| 51 |
+
],
|
| 52 |
+
"default_temperature": 0.0,
|
| 53 |
+
"default_max_tokens": 4000
|
| 54 |
+
},
|
| 55 |
+
"products": {
|
| 56 |
+
"available": [
|
| 57 |
+
{"id": "harmony", "display_name": "Harmony", "versions": ["1.8", "1.6", "1.5", "1.2"], "default_version": "1.8"},
|
| 58 |
+
{"id": "chorus", "display_name": "Chorus", "versions": ["1.1"], "default_version": "1.1"}
|
| 59 |
+
],
|
| 60 |
+
"default_product": "harmony"
|
| 61 |
+
},
|
| 62 |
+
"rag": {
|
| 63 |
+
"default_k": 5,
|
| 64 |
+
"max_k": 10
|
| 65 |
+
},
|
| 66 |
+
"tools": {
|
| 67 |
+
"max_tool_calls": 5,
|
| 68 |
+
"tool_timeout": 30
|
| 69 |
+
},
|
| 70 |
+
"agents": {
|
| 71 |
+
"default_agent": "document_reader"
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def _apply_env_overrides(self):
|
| 76 |
+
"""Apply environment variable overrides to config"""
|
| 77 |
+
# Example: ASSISTANT_RAG_DEFAULT_K=10 overrides rag.default_k
|
| 78 |
+
prefix = "ASSISTANT_"
|
| 79 |
+
|
| 80 |
+
for key, value in os.environ.items():
|
| 81 |
+
if key.startswith(prefix):
|
| 82 |
+
# Convert ASSISTANT_RAG_DEFAULT_K to rag.default_k
|
| 83 |
+
config_path = key[len(prefix):].lower().replace('_', '.')
|
| 84 |
+
self._set_nested_value(config_path, value)
|
| 85 |
+
|
| 86 |
+
def _set_nested_value(self, path: str, value: str):
|
| 87 |
+
"""Set a nested configuration value using dot notation"""
|
| 88 |
+
keys = path.split('.')
|
| 89 |
+
current = self.config
|
| 90 |
+
|
| 91 |
+
for key in keys[:-1]:
|
| 92 |
+
if key not in current:
|
| 93 |
+
current[key] = {}
|
| 94 |
+
current = current[key]
|
| 95 |
+
|
| 96 |
+
# Try to convert value to appropriate type
|
| 97 |
+
try:
|
| 98 |
+
if value.lower() in ['true', 'false']:
|
| 99 |
+
value = value.lower() == 'true'
|
| 100 |
+
elif value.isdigit():
|
| 101 |
+
value = int(value)
|
| 102 |
+
elif '.' in value and value.replace('.', '').isdigit():
|
| 103 |
+
value = float(value)
|
| 104 |
+
except:
|
| 105 |
+
pass # Keep as string
|
| 106 |
+
|
| 107 |
+
current[keys[-1]] = value
|
| 108 |
+
logger.debug(f"Override config {path} = {value}")
|
| 109 |
+
|
| 110 |
+
def get(self, path: str, default: Any = None) -> Any:
|
| 111 |
+
"""Get configuration value using dot notation"""
|
| 112 |
+
keys = path.split('.')
|
| 113 |
+
current = self.config
|
| 114 |
+
|
| 115 |
+
for key in keys:
|
| 116 |
+
if isinstance(current, dict) and key in current:
|
| 117 |
+
current = current[key]
|
| 118 |
+
else:
|
| 119 |
+
return default
|
| 120 |
+
|
| 121 |
+
return current
|
| 122 |
+
|
| 123 |
+
def get_available_models(self) -> List[Dict[str, Any]]:
|
| 124 |
+
"""Get list of available models"""
|
| 125 |
+
return self.get("models.available", [])
|
| 126 |
+
|
| 127 |
+
def get_model_ids(self) -> List[str]:
|
| 128 |
+
"""Get list of model IDs"""
|
| 129 |
+
return [m["model_id"] for m in self.get_available_models()]
|
| 130 |
+
|
| 131 |
+
def get_default_model(self) -> str:
|
| 132 |
+
"""Get default model ID"""
|
| 133 |
+
models = self.get_available_models()
|
| 134 |
+
for model in models:
|
| 135 |
+
if model.get("default", False):
|
| 136 |
+
return model["model_id"]
|
| 137 |
+
return models[0]["model_id"] if models else "gpt-4o-mini"
|
| 138 |
+
|
| 139 |
+
def get_available_products(self) -> List[Dict[str, Any]]:
|
| 140 |
+
"""Get list of available products"""
|
| 141 |
+
return self.get("products.available", [])
|
| 142 |
+
|
| 143 |
+
def get_product_ids(self) -> List[str]:
|
| 144 |
+
"""Get list of product IDs"""
|
| 145 |
+
return [p["id"] for p in self.get_available_products()]
|
| 146 |
+
|
| 147 |
+
def get_product_versions(self, product_id: str) -> List[str]:
|
| 148 |
+
"""Get versions for a specific product"""
|
| 149 |
+
products = self.get_available_products()
|
| 150 |
+
for product in products:
|
| 151 |
+
if product["id"] == product_id:
|
| 152 |
+
return product.get("versions", [])
|
| 153 |
+
return []
|
| 154 |
+
|
| 155 |
+
def get_default_product(self) -> str:
|
| 156 |
+
"""Get default product ID"""
|
| 157 |
+
return self.get("products.default_product", "harmony")
|
| 158 |
+
|
| 159 |
+
def get_default_version(self, product_id: str) -> str:
|
| 160 |
+
"""Get default version for a product"""
|
| 161 |
+
products = self.get_available_products()
|
| 162 |
+
for product in products:
|
| 163 |
+
if product["id"] == product_id:
|
| 164 |
+
return product.get("default_version", product["versions"][0])
|
| 165 |
+
return "1.0"
|
| 166 |
+
|
| 167 |
+
def get_rag_k(self) -> int:
|
| 168 |
+
"""Get default number of RAG results"""
|
| 169 |
+
return self.get("rag.default_k", 5)
|
| 170 |
+
|
| 171 |
+
def get_max_tool_calls(self) -> int:
|
| 172 |
+
"""Get maximum number of tool calls"""
|
| 173 |
+
return self.get("tools.max_tool_calls", 5)
|
| 174 |
+
|
| 175 |
+
def get_agent_config(self, agent_id: str) -> Dict[str, Any]:
|
| 176 |
+
"""Get configuration for a specific agent"""
|
| 177 |
+
return self.get(f"agents.{agent_id}", {})
|
| 178 |
+
|
| 179 |
+
def is_agent_enabled(self, agent_id: str) -> bool:
|
| 180 |
+
"""Check if an agent is enabled"""
|
| 181 |
+
return self.get(f"agents.{agent_id}.enabled", True)
|
| 182 |
+
|
| 183 |
+
def get_ui_config(self) -> Dict[str, Any]:
|
| 184 |
+
"""Get UI configuration"""
|
| 185 |
+
return self.get("ui", {})
|
| 186 |
+
|
| 187 |
+
def get_logging_config(self) -> Dict[str, Any]:
|
| 188 |
+
"""Get logging configuration"""
|
| 189 |
+
return self.get("logging", {})
|
| 190 |
+
|
| 191 |
+
def reload(self):
|
| 192 |
+
"""Reload configuration from file"""
|
| 193 |
+
self.config = self._load_config()
|
| 194 |
+
self._apply_env_overrides()
|
| 195 |
+
logger.info("Configuration reloaded")
|
| 196 |
+
|
| 197 |
+
def save(self, config_path: Optional[str] = None):
|
| 198 |
+
"""Save current configuration to file"""
|
| 199 |
+
save_path = config_path or self.config_path
|
| 200 |
+
try:
|
| 201 |
+
with open(save_path, 'w') as f:
|
| 202 |
+
yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
|
| 203 |
+
logger.info(f"Configuration saved to {save_path}")
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"Error saving configuration: {e}")
|
| 206 |
+
raise
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# Global config instance
|
| 210 |
+
_config = None
|
| 211 |
+
|
| 212 |
+
def get_config() -> ConfigLoader:
|
| 213 |
+
"""Get global configuration instance"""
|
| 214 |
+
global _config
|
| 215 |
+
if _config is None:
|
| 216 |
+
_config = ConfigLoader()
|
| 217 |
+
return _config
|
py/data/embeddings/chorus_1_1.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
py/data/embeddings/harmony_1_2.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
py/data/embeddings/harmony_1_5.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
py/data/embeddings/harmony_1_6.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
py/data/embeddings/harmony_1_8.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
py/frontend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Frontend package
|
py/frontend/gradio_app.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Peer-to-Peer Agent Frontend for Multi-Agent System
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import logging
|
| 7 |
+
import traceback
|
| 8 |
+
from typing import Dict, List, Tuple, Optional, Any
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import uuid
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path for imports
|
| 14 |
+
import sys
|
| 15 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 16 |
+
|
| 17 |
+
from backend.chat_history_manager import ChatHistoryManager
|
| 18 |
+
from backend.api_gateway import APIGateway
|
| 19 |
+
from config_loader import get_config
|
| 20 |
+
from tools import agent_tools
|
| 21 |
+
|
| 22 |
+
logging.basicConfig(level=logging.INFO)
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class PeerToPeerApp:
|
| 27 |
+
def __init__(self):
|
| 28 |
+
"""Initialize the peer-to-peer application."""
|
| 29 |
+
# Load configuration
|
| 30 |
+
self.config = get_config()
|
| 31 |
+
|
| 32 |
+
# Initialize API Gateway
|
| 33 |
+
self.api_gateway = APIGateway()
|
| 34 |
+
|
| 35 |
+
# Initialize chat history manager
|
| 36 |
+
db_path = self.config.get("conversation.database_path")
|
| 37 |
+
self.history_manager = ChatHistoryManager(db_path)
|
| 38 |
+
|
| 39 |
+
# Track pending confirmations
|
| 40 |
+
self.pending_confirmations = {}
|
| 41 |
+
|
| 42 |
+
# Available versions (hardcoded for now since ChromaDB is in Document Reader)
|
| 43 |
+
self.available_versions = {
|
| 44 |
+
"harmony": ["1.8", "1.6", "1.5", "1.2"],
|
| 45 |
+
"chorus": ["1.1"]
|
| 46 |
+
}
|
| 47 |
+
logger.info(f"Available versions: {self.available_versions}")
|
| 48 |
+
|
| 49 |
+
# Initialize agents using agent_tools singleton registry
|
| 50 |
+
self.current_agent = self.config.get("agents.default_agent", "document_reader")
|
| 51 |
+
|
| 52 |
+
# Store user selections from config
|
| 53 |
+
self.current_model = self.config.get_default_model()
|
| 54 |
+
self.current_product = self.config.get_default_product()
|
| 55 |
+
self.current_version = self.config.get_default_version(self.current_product)
|
| 56 |
+
|
| 57 |
+
# User and conversation management
|
| 58 |
+
self.current_user_id = None
|
| 59 |
+
self.current_conversation_id = None
|
| 60 |
+
|
| 61 |
+
# Initialize default agent
|
| 62 |
+
self._initialize_agents()
|
| 63 |
+
|
| 64 |
+
# Conversation history (for current session)
|
| 65 |
+
self.conversation_history = []
|
| 66 |
+
|
| 67 |
+
def _initialize_agents(self):
|
| 68 |
+
"""Initialize agents with current model using agent_tools singleton registry."""
|
| 69 |
+
from langchain_openai import ChatOpenAI
|
| 70 |
+
|
| 71 |
+
# Create LLM with selected model from config
|
| 72 |
+
llm = ChatOpenAI(
|
| 73 |
+
model=self.current_model,
|
| 74 |
+
temperature=self.config.get("models.default_temperature", 0.0),
|
| 75 |
+
max_tokens=self.config.get("models.default_max_tokens", 4000)
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Initialize all agents in the singleton registry
|
| 79 |
+
agent_tools.initialize_agents(llm, self.api_gateway)
|
| 80 |
+
logger.info("Agent singleton registry initialized")
|
| 81 |
+
|
| 82 |
+
def get_agent_status(self) -> str:
|
| 83 |
+
"""Get current agent status for display."""
|
| 84 |
+
agent_names = {
|
| 85 |
+
"document_reader": "Document Reader",
|
| 86 |
+
"profile_settings": "Profile Settings",
|
| 87 |
+
"network_config": "Network Configuration",
|
| 88 |
+
"subscriber_management": "Subscriber Management",
|
| 89 |
+
"system_query": "System Query",
|
| 90 |
+
"policy_dnn": "Policy & DNN"
|
| 91 |
+
}
|
| 92 |
+
return f"**Active Agent:** {agent_names.get(self.current_agent, self.current_agent)}"
|
| 93 |
+
|
| 94 |
+
def get_product_versions(self, product: str) -> List[str]:
|
| 95 |
+
"""Get available versions for a product."""
|
| 96 |
+
versions = self.available_versions.get(product.lower(), [])
|
| 97 |
+
return sorted(list(versions), reverse=True)
|
| 98 |
+
|
| 99 |
+
def initialize_user(self, user_id: Optional[str] = None) -> str:
|
| 100 |
+
"""Initialize or create a user."""
|
| 101 |
+
self.current_user_id = self.history_manager.create_user(user_id)
|
| 102 |
+
logger.info(f"Initialized user: {self.current_user_id}")
|
| 103 |
+
return self.current_user_id
|
| 104 |
+
|
| 105 |
+
def start_new_conversation(self, title: Optional[str] = None) -> str:
|
| 106 |
+
"""Start a new conversation for the current user."""
|
| 107 |
+
if not self.current_user_id:
|
| 108 |
+
self.initialize_user()
|
| 109 |
+
|
| 110 |
+
# Create new conversation
|
| 111 |
+
self.current_conversation_id = self.history_manager.create_conversation(
|
| 112 |
+
self.current_user_id,
|
| 113 |
+
title,
|
| 114 |
+
metadata={
|
| 115 |
+
"model": self.current_model,
|
| 116 |
+
"product": self.current_product,
|
| 117 |
+
"version": self.current_version
|
| 118 |
+
}
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Clear agent memories and reload history
|
| 122 |
+
self._reload_conversation_history()
|
| 123 |
+
|
| 124 |
+
logger.info(f"Started new conversation: {self.current_conversation_id}")
|
| 125 |
+
return self.current_conversation_id
|
| 126 |
+
|
| 127 |
+
def _reload_conversation_history(self):
|
| 128 |
+
"""Reload conversation history into agent memories."""
|
| 129 |
+
if self.current_conversation_id:
|
| 130 |
+
# Get history from database
|
| 131 |
+
history = self.history_manager.get_formatted_history(
|
| 132 |
+
self.current_conversation_id, format_type="langchain"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Load into all agents from the singleton registry
|
| 136 |
+
for agent_id in agent_tools.AGENT_INFO.keys():
|
| 137 |
+
agent = agent_tools.get_agent(agent_id)
|
| 138 |
+
if agent and hasattr(agent, "memory") and agent.memory:
|
| 139 |
+
agent.memory.chat_memory.messages = history.copy()
|
| 140 |
+
|
| 141 |
+
def update_settings(self, model: str, product: str, version: str) -> str:
|
| 142 |
+
"""Update current settings."""
|
| 143 |
+
# Update settings
|
| 144 |
+
self.current_model = model
|
| 145 |
+
self.current_product = product
|
| 146 |
+
self.current_version = version
|
| 147 |
+
|
| 148 |
+
# Reinitialize agents with new model but preserve history
|
| 149 |
+
self._initialize_agents()
|
| 150 |
+
|
| 151 |
+
# Reload conversation history if we have one
|
| 152 |
+
if self.current_conversation_id:
|
| 153 |
+
self._reload_conversation_history()
|
| 154 |
+
|
| 155 |
+
return f"✅ Settings updated: {model}, {product} {version}"
|
| 156 |
+
|
| 157 |
+
def process_message(self, message: str, history: List[Dict[str, str]],
|
| 158 |
+
model: str, product: str, version: str) -> Tuple[List[Dict[str, str]], str, str]:
|
| 159 |
+
"""Process user message through the current agent."""
|
| 160 |
+
if not message.strip():
|
| 161 |
+
return history, "", self.get_agent_status()
|
| 162 |
+
|
| 163 |
+
# Initialize user and conversation if needed
|
| 164 |
+
if not self.current_user_id:
|
| 165 |
+
self.initialize_user()
|
| 166 |
+
|
| 167 |
+
if not self.current_conversation_id:
|
| 168 |
+
self.start_new_conversation()
|
| 169 |
+
|
| 170 |
+
# Update settings if changed
|
| 171 |
+
if (model != self.current_model or
|
| 172 |
+
product != self.current_product or
|
| 173 |
+
version != self.current_version):
|
| 174 |
+
self.update_settings(model, product, version)
|
| 175 |
+
|
| 176 |
+
# Add user message to history
|
| 177 |
+
history.append({"role": "user", "content": message})
|
| 178 |
+
|
| 179 |
+
# Save user message to database
|
| 180 |
+
self.history_manager.add_message(
|
| 181 |
+
self.current_conversation_id,
|
| 182 |
+
"user",
|
| 183 |
+
message,
|
| 184 |
+
metadata={
|
| 185 |
+
"product": self.current_product,
|
| 186 |
+
"version": self.current_version
|
| 187 |
+
}
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
# Check for manual agent switch
|
| 192 |
+
if message.strip().lower().startswith("/agent"):
|
| 193 |
+
parts = message.strip().split()
|
| 194 |
+
if len(parts) > 1:
|
| 195 |
+
agent_name = parts[1].lower()
|
| 196 |
+
if agent_tools.get_agent(agent_name):
|
| 197 |
+
self.current_agent = agent_name
|
| 198 |
+
response = f"Switched to {agent_name.replace('_', ' ').title()} agent"
|
| 199 |
+
history.append({"role": "assistant", "content": response})
|
| 200 |
+
|
| 201 |
+
# Save switch message and update active agent
|
| 202 |
+
self.history_manager.add_message(
|
| 203 |
+
self.current_conversation_id,
|
| 204 |
+
"assistant",
|
| 205 |
+
response,
|
| 206 |
+
agent_id=self.current_agent
|
| 207 |
+
)
|
| 208 |
+
self.history_manager.update_active_agent(
|
| 209 |
+
self.current_conversation_id,
|
| 210 |
+
self.current_agent
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# Transfer conversation history to new agent
|
| 214 |
+
self._reload_conversation_history()
|
| 215 |
+
|
| 216 |
+
return history, "", self.get_agent_status()
|
| 217 |
+
|
| 218 |
+
# Create context for agents to use
|
| 219 |
+
agent_context = {
|
| 220 |
+
"product": self.current_product,
|
| 221 |
+
"version": self.current_version,
|
| 222 |
+
"model": self.current_model
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
# Run the current agent with the message and context
|
| 226 |
+
logger.info(f"Running {self.current_agent} agent with message: {message[:100]}...")
|
| 227 |
+
result_dict = agent_tools.run_agent(self.current_agent, message, agent_context)
|
| 228 |
+
|
| 229 |
+
# Extract output and check if agent changed
|
| 230 |
+
result = result_dict.get("output", "No response generated")
|
| 231 |
+
new_agent_id = result_dict.get("agent_id", self.current_agent)
|
| 232 |
+
|
| 233 |
+
# If agent changed, update current agent
|
| 234 |
+
if new_agent_id != self.current_agent:
|
| 235 |
+
logger.info(f"Agent switch detected: {self.current_agent} -> {new_agent_id}")
|
| 236 |
+
previous_agent = self.current_agent
|
| 237 |
+
self.current_agent = new_agent_id
|
| 238 |
+
|
| 239 |
+
# Update in database
|
| 240 |
+
if self.current_conversation_id:
|
| 241 |
+
self.history_manager.update_active_agent(
|
| 242 |
+
self.current_conversation_id,
|
| 243 |
+
self.current_agent
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Transfer conversation history to new agent
|
| 247 |
+
self._reload_conversation_history()
|
| 248 |
+
|
| 249 |
+
# The new agent should automatically respond in the next interaction
|
| 250 |
+
|
| 251 |
+
# TODO: Check if agent attempted any write operations
|
| 252 |
+
# If so, create confirmation request through API Gateway
|
| 253 |
+
# Example:
|
| 254 |
+
# if self.current_agent in ["network_config", "subscriber_management", "policy_dnn"]:
|
| 255 |
+
# if "tool_calls" in result_dict and result_dict["tool_calls"]:
|
| 256 |
+
# confirmation_id = str(uuid.uuid4())
|
| 257 |
+
# self.pending_confirmations[confirmation_id] = {
|
| 258 |
+
# "action_type": "configuration_change",
|
| 259 |
+
# "description": result,
|
| 260 |
+
# "agent": self.current_agent,
|
| 261 |
+
# "timestamp": datetime.now()
|
| 262 |
+
# }
|
| 263 |
+
# # Show confirmation dialog
|
| 264 |
+
# # Return special response to trigger confirmation UI
|
| 265 |
+
|
| 266 |
+
# Add agent prefix to the response
|
| 267 |
+
agent_prefixes = {
|
| 268 |
+
"document_reader": "[Document Reader]:",
|
| 269 |
+
"profile_settings": "[Profile Settings]:",
|
| 270 |
+
"network_config": "[Network Config]:",
|
| 271 |
+
"subscriber_management": "[Subscriber Management]:",
|
| 272 |
+
"system_query": "[System Query]:",
|
| 273 |
+
"policy_dnn": "[Policy & DNN]:"
|
| 274 |
+
}
|
| 275 |
+
prefix = agent_prefixes.get(self.current_agent, f"[{self.current_agent}]:")
|
| 276 |
+
result = f"{prefix} {result}"
|
| 277 |
+
|
| 278 |
+
# Update history
|
| 279 |
+
history.append({"role": "assistant", "content": result})
|
| 280 |
+
|
| 281 |
+
# Save assistant response to database
|
| 282 |
+
self.history_manager.add_message(
|
| 283 |
+
self.current_conversation_id,
|
| 284 |
+
"assistant",
|
| 285 |
+
result,
|
| 286 |
+
agent_id=self.current_agent,
|
| 287 |
+
metadata={
|
| 288 |
+
"model": self.current_model,
|
| 289 |
+
"product": self.current_product,
|
| 290 |
+
"version": self.current_version
|
| 291 |
+
}
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Store in conversation history
|
| 295 |
+
self.conversation_history.append({
|
| 296 |
+
"user": message,
|
| 297 |
+
"assistant": result,
|
| 298 |
+
"agent": self.current_agent,
|
| 299 |
+
"product": self.current_product,
|
| 300 |
+
"version": self.current_version,
|
| 301 |
+
"model": self.current_model
|
| 302 |
+
})
|
| 303 |
+
|
| 304 |
+
except Exception as e:
|
| 305 |
+
logger.error(f"Error processing message: {str(e)}")
|
| 306 |
+
error_msg = f"An error occurred: {str(e)}"
|
| 307 |
+
history.append({"role": "assistant", "content": error_msg})
|
| 308 |
+
|
| 309 |
+
# Save error message
|
| 310 |
+
if self.current_conversation_id:
|
| 311 |
+
self.history_manager.add_message(
|
| 312 |
+
self.current_conversation_id,
|
| 313 |
+
"assistant",
|
| 314 |
+
error_msg,
|
| 315 |
+
agent_id=self.current_agent,
|
| 316 |
+
metadata={"error": str(e)}
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
return history, "", self.get_agent_status()
|
| 320 |
+
|
| 321 |
+
def clear_conversation(self) -> Tuple[List[Dict[str, str]], str, str]:
|
| 322 |
+
"""Clear conversation history and start a new conversation."""
|
| 323 |
+
# Start a new conversation
|
| 324 |
+
self.start_new_conversation()
|
| 325 |
+
|
| 326 |
+
# Clear local history
|
| 327 |
+
self.conversation_history.clear()
|
| 328 |
+
|
| 329 |
+
# Reset to default agent
|
| 330 |
+
self.current_agent = "document_reader"
|
| 331 |
+
|
| 332 |
+
return [], f"Started new conversation! ID: {self.current_conversation_id[:8]}...", self.get_agent_status()
|
| 333 |
+
|
| 334 |
+
def get_user_conversations(self) -> List[Dict[str, Any]]:
|
| 335 |
+
"""Get recent conversations for the current user."""
|
| 336 |
+
if not self.current_user_id:
|
| 337 |
+
return []
|
| 338 |
+
|
| 339 |
+
return self.history_manager.get_user_conversations(self.current_user_id)
|
| 340 |
+
|
| 341 |
+
def load_conversation(self, conversation_id: str) -> Tuple[List[Dict[str, str]], str]:
|
| 342 |
+
"""Load a previous conversation."""
|
| 343 |
+
try:
|
| 344 |
+
# Set current conversation
|
| 345 |
+
self.current_conversation_id = conversation_id
|
| 346 |
+
|
| 347 |
+
# Get conversation details
|
| 348 |
+
conv_data = self.history_manager.export_conversation(conversation_id)
|
| 349 |
+
|
| 350 |
+
# Update settings from conversation metadata
|
| 351 |
+
metadata = conv_data.get('metadata', {})
|
| 352 |
+
if metadata:
|
| 353 |
+
self.current_model = metadata.get('model', self.current_model)
|
| 354 |
+
self.current_product = metadata.get('product', self.current_product)
|
| 355 |
+
self.current_version = metadata.get('version', self.current_version)
|
| 356 |
+
|
| 357 |
+
# Reinitialize agents with saved model
|
| 358 |
+
self._initialize_agents()
|
| 359 |
+
|
| 360 |
+
# Set active agent
|
| 361 |
+
self.current_agent = conv_data.get('active_agent', 'document_reader')
|
| 362 |
+
|
| 363 |
+
# Reload history into agents
|
| 364 |
+
self._reload_conversation_history()
|
| 365 |
+
|
| 366 |
+
# Convert messages to Gradio format
|
| 367 |
+
history = [
|
| 368 |
+
{"role": msg["role"], "content": msg["content"]}
|
| 369 |
+
for msg in conv_data["messages"]
|
| 370 |
+
]
|
| 371 |
+
|
| 372 |
+
return history, f"Loaded conversation: {conv_data.get('title', 'Untitled')}"
|
| 373 |
+
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logger.error(f"Error loading conversation: {e}")
|
| 376 |
+
return [], f"Error loading conversation: {str(e)}"
|
| 377 |
+
|
| 378 |
+
def _update_ui_info(self) -> str:
|
| 379 |
+
"""Update the UI info display."""
|
| 380 |
+
info = (
|
| 381 |
+
f"**User:** {self.current_user_id or 'Not initialized'}\n"
|
| 382 |
+
f"**Conversation:** {self.current_conversation_id[:8] + '...' if self.current_conversation_id else 'Not started'}"
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
# Add pending confirmations if any
|
| 386 |
+
if self.pending_confirmations:
|
| 387 |
+
info += f"\n**Pending Confirmations:** {len(self.pending_confirmations)}"
|
| 388 |
+
|
| 389 |
+
return info
|
| 390 |
+
|
| 391 |
+
def handle_confirmation(self, confirmation_id: str, approved: bool) -> str:
|
| 392 |
+
"""Handle user confirmation response"""
|
| 393 |
+
if confirmation_id not in self.pending_confirmations:
|
| 394 |
+
return "No pending confirmation found with that ID."
|
| 395 |
+
|
| 396 |
+
confirmation = self.pending_confirmations[confirmation_id]
|
| 397 |
+
|
| 398 |
+
if approved:
|
| 399 |
+
# TODO: Execute the action through API Gateway
|
| 400 |
+
result = f"Action approved: {confirmation['action_type']}\n"
|
| 401 |
+
result += f"Details: {confirmation['description']}\n"
|
| 402 |
+
result += "NOTE: This would execute through the API Gateway (not implemented)"
|
| 403 |
+
else:
|
| 404 |
+
result = f"Action cancelled: {confirmation['action_type']}"
|
| 405 |
+
|
| 406 |
+
# Remove from pending
|
| 407 |
+
del self.pending_confirmations[confirmation_id]
|
| 408 |
+
|
| 409 |
+
return result
|
| 410 |
+
|
| 411 |
+
def create_interface(self) -> gr.Blocks:
|
| 412 |
+
"""Create the Gradio interface."""
|
| 413 |
+
with gr.Blocks(title="Peer-to-Peer Multi-Agent Assistant", theme=gr.themes.Soft()) as demo:
|
| 414 |
+
gr.Markdown("# Peer-to-Peer Multi-Agent Assistant")
|
| 415 |
+
gr.Markdown("Direct agent interaction with immediate RAG querying for documentation.")
|
| 416 |
+
|
| 417 |
+
with gr.Row():
|
| 418 |
+
with gr.Column(scale=1):
|
| 419 |
+
# Model and document selection
|
| 420 |
+
gr.Markdown("### Settings")
|
| 421 |
+
|
| 422 |
+
# Get model choices from config
|
| 423 |
+
model_choices = [(m["display_name"], m["model_id"]) for m in self.config.get_available_models()]
|
| 424 |
+
model_dropdown = gr.Dropdown(
|
| 425 |
+
choices=model_choices,
|
| 426 |
+
value=self.current_model,
|
| 427 |
+
label="AI Model",
|
| 428 |
+
interactive=True
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
# Get product choices from config
|
| 432 |
+
product_choices = [(p["display_name"], p["id"]) for p in self.config.get_available_products()]
|
| 433 |
+
product_dropdown = gr.Dropdown(
|
| 434 |
+
choices=product_choices,
|
| 435 |
+
value=self.current_product,
|
| 436 |
+
label="Product",
|
| 437 |
+
interactive=True
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
# Get initial versions for default product
|
| 441 |
+
initial_versions = self.get_product_versions(self.current_product)
|
| 442 |
+
version_dropdown = gr.Dropdown(
|
| 443 |
+
choices=initial_versions,
|
| 444 |
+
value=self.current_version if self.current_version in initial_versions else initial_versions[0],
|
| 445 |
+
label="Version",
|
| 446 |
+
interactive=True
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
# Update versions when product changes
|
| 450 |
+
def update_version_choices(product):
|
| 451 |
+
versions = self.get_product_versions(product)
|
| 452 |
+
return gr.update(choices=versions, value=versions[0] if versions else "")
|
| 453 |
+
|
| 454 |
+
product_dropdown.change(
|
| 455 |
+
update_version_choices,
|
| 456 |
+
inputs=[product_dropdown],
|
| 457 |
+
outputs=[version_dropdown]
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Conversation Management
|
| 461 |
+
gr.Markdown("### Conversation")
|
| 462 |
+
|
| 463 |
+
# User info display
|
| 464 |
+
user_info = gr.Markdown(
|
| 465 |
+
f"**User:** {self.current_user_id or 'Not initialized'}\n"
|
| 466 |
+
f"**Conversation:** {self.current_conversation_id[:8] + '...' if self.current_conversation_id else 'Not started'}"
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
# Conversation history dropdown
|
| 470 |
+
with gr.Accordion("Previous Conversations", open=False):
|
| 471 |
+
conversation_list = gr.Dropdown(
|
| 472 |
+
choices=[],
|
| 473 |
+
label="Select a conversation to load",
|
| 474 |
+
interactive=True
|
| 475 |
+
)
|
| 476 |
+
load_conv_btn = gr.Button("Load Selected", size="sm")
|
| 477 |
+
|
| 478 |
+
# Refresh conversations list
|
| 479 |
+
def refresh_conversations():
|
| 480 |
+
if not self.current_user_id:
|
| 481 |
+
return gr.update(choices=[])
|
| 482 |
+
|
| 483 |
+
convs = self.get_user_conversations()
|
| 484 |
+
choices = []
|
| 485 |
+
for conv in convs:
|
| 486 |
+
updated_at = conv.get('updated_at', 'Unknown')
|
| 487 |
+
if isinstance(updated_at, str) and len(updated_at) >= 16:
|
| 488 |
+
updated_at = updated_at[:16]
|
| 489 |
+
title = conv.get('title', 'Untitled')
|
| 490 |
+
msg_count = conv.get('message_count', 0)
|
| 491 |
+
conv_id = conv.get('conversation_id', '')
|
| 492 |
+
choices.append((
|
| 493 |
+
f"{title} ({updated_at}, {msg_count} msgs)",
|
| 494 |
+
conv_id
|
| 495 |
+
))
|
| 496 |
+
return gr.update(choices=choices)
|
| 497 |
+
|
| 498 |
+
# Agent status
|
| 499 |
+
gr.Markdown("### Agent Status")
|
| 500 |
+
agent_status = gr.Markdown(self.get_agent_status())
|
| 501 |
+
|
| 502 |
+
# Agent info
|
| 503 |
+
with gr.Accordion("Agent Information", open=False):
|
| 504 |
+
gr.Markdown("""
|
| 505 |
+
**Available Agents:**
|
| 506 |
+
|
| 507 |
+
**Document Reader**
|
| 508 |
+
- Default agent for documentation queries
|
| 509 |
+
- Automatically searches selected product/version
|
| 510 |
+
|
| 511 |
+
**Profile Settings**
|
| 512 |
+
- Manages user preferences and settings
|
| 513 |
+
|
| 514 |
+
**Network Configuration**
|
| 515 |
+
- Configure NAT, VLANs, IP addresses
|
| 516 |
+
- Handle bulk network operations
|
| 517 |
+
|
| 518 |
+
**Subscriber Management**
|
| 519 |
+
- Create/update subscribers
|
| 520 |
+
- CSV import and bulk operations
|
| 521 |
+
|
| 522 |
+
**System Query**
|
| 523 |
+
- Read-only analytics and insights
|
| 524 |
+
- System health and performance
|
| 525 |
+
|
| 526 |
+
**Policy & DNN**
|
| 527 |
+
- Manage DNNs and QoS profiles
|
| 528 |
+
- Configure policies and services
|
| 529 |
+
|
| 530 |
+
**Tips:**
|
| 531 |
+
- Type `/agent <name>` to manually switch agents
|
| 532 |
+
- All configuration changes require confirmation
|
| 533 |
+
""")
|
| 534 |
+
|
| 535 |
+
# Confirmation Dialog
|
| 536 |
+
with gr.Accordion("Pending Confirmations", open=True, visible=False) as confirmation_accordion:
|
| 537 |
+
gr.Markdown("### Action Requires Confirmation")
|
| 538 |
+
confirmation_text = gr.Markdown("")
|
| 539 |
+
with gr.Row():
|
| 540 |
+
approve_btn = gr.Button("Approve", variant="primary", size="sm")
|
| 541 |
+
deny_btn = gr.Button("Deny", variant="secondary", size="sm")
|
| 542 |
+
confirmation_id_state = gr.State("")
|
| 543 |
+
|
| 544 |
+
# Action buttons
|
| 545 |
+
with gr.Row():
|
| 546 |
+
clear_btn = gr.Button("New Conversation", variant="secondary")
|
| 547 |
+
export_btn = gr.Button("Export Chat", variant="secondary")
|
| 548 |
+
|
| 549 |
+
with gr.Column(scale=3):
|
| 550 |
+
# Chat interface
|
| 551 |
+
chatbot = gr.Chatbot(
|
| 552 |
+
height=500,
|
| 553 |
+
show_label=False,
|
| 554 |
+
elem_id="chatbot",
|
| 555 |
+
bubble_full_width=False,
|
| 556 |
+
type="messages"
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
msg = gr.Textbox(
|
| 560 |
+
label="Message",
|
| 561 |
+
placeholder=f"Ask about {self.current_product.capitalize()} {self.current_version} documentation...",
|
| 562 |
+
lines=2,
|
| 563 |
+
max_lines=10,
|
| 564 |
+
autofocus=True
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
with gr.Row():
|
| 568 |
+
submit = gr.Button("Send", variant="primary", scale=1)
|
| 569 |
+
|
| 570 |
+
# Example queries
|
| 571 |
+
with gr.Row():
|
| 572 |
+
with gr.Column():
|
| 573 |
+
gr.Markdown("**Documentation Examples:**")
|
| 574 |
+
doc_examples = gr.Examples(
|
| 575 |
+
examples=[
|
| 576 |
+
"How do I install this version?",
|
| 577 |
+
"What are the system requirements?",
|
| 578 |
+
"Show me troubleshooting steps",
|
| 579 |
+
"What's new in this version?",
|
| 580 |
+
"Search for configuration options"
|
| 581 |
+
],
|
| 582 |
+
inputs=msg,
|
| 583 |
+
label=""
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
with gr.Column():
|
| 587 |
+
gr.Markdown("**Agent Switching Examples:**")
|
| 588 |
+
agent_examples = gr.Examples(
|
| 589 |
+
examples=[
|
| 590 |
+
"I need to update my settings",
|
| 591 |
+
"Show me my profile preferences",
|
| 592 |
+
"/agent profile_settings",
|
| 593 |
+
"/agent document_reader",
|
| 594 |
+
"Switch to documentation search"
|
| 595 |
+
],
|
| 596 |
+
inputs=msg,
|
| 597 |
+
label=""
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
# Update placeholder when product/version changes
|
| 601 |
+
def update_placeholder(product, version):
|
| 602 |
+
return gr.update(placeholder=f"Ask about {product.capitalize()} {version} documentation...")
|
| 603 |
+
|
| 604 |
+
product_dropdown.change(
|
| 605 |
+
update_placeholder,
|
| 606 |
+
inputs=[product_dropdown, version_dropdown],
|
| 607 |
+
outputs=[msg]
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
version_dropdown.change(
|
| 611 |
+
update_placeholder,
|
| 612 |
+
inputs=[product_dropdown, version_dropdown],
|
| 613 |
+
outputs=[msg]
|
| 614 |
+
)
|
| 615 |
+
|
| 616 |
+
# Event handlers
|
| 617 |
+
msg.submit(
|
| 618 |
+
self.process_message,
|
| 619 |
+
inputs=[msg, chatbot, model_dropdown, product_dropdown, version_dropdown],
|
| 620 |
+
outputs=[chatbot, msg, agent_status]
|
| 621 |
+
).then(
|
| 622 |
+
lambda: self._update_ui_info(),
|
| 623 |
+
outputs=[user_info]
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
submit.click(
|
| 627 |
+
self.process_message,
|
| 628 |
+
inputs=[msg, chatbot, model_dropdown, product_dropdown, version_dropdown],
|
| 629 |
+
outputs=[chatbot, msg, agent_status]
|
| 630 |
+
).then(
|
| 631 |
+
lambda: self._update_ui_info(),
|
| 632 |
+
outputs=[user_info]
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
clear_btn.click(
|
| 636 |
+
self.clear_conversation,
|
| 637 |
+
outputs=[chatbot, msg, agent_status]
|
| 638 |
+
).then(
|
| 639 |
+
lambda: (self._update_ui_info(), refresh_conversations()),
|
| 640 |
+
outputs=[user_info, conversation_list]
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
# Load conversation handler
|
| 644 |
+
def load_selected_conversation(conv_id):
|
| 645 |
+
if conv_id:
|
| 646 |
+
history, status_msg = self.load_conversation(conv_id)
|
| 647 |
+
return history, "", self.get_agent_status(), self._update_ui_info()
|
| 648 |
+
return [], "", self.get_agent_status(), self._update_ui_info()
|
| 649 |
+
|
| 650 |
+
load_conv_btn.click(
|
| 651 |
+
load_selected_conversation,
|
| 652 |
+
inputs=[conversation_list],
|
| 653 |
+
outputs=[chatbot, msg, agent_status, user_info]
|
| 654 |
+
)
|
| 655 |
+
|
| 656 |
+
# Export conversation handler
|
| 657 |
+
def export_current_conversation():
|
| 658 |
+
if self.current_conversation_id:
|
| 659 |
+
try:
|
| 660 |
+
data = self.history_manager.export_conversation(self.current_conversation_id)
|
| 661 |
+
# Convert to downloadable format
|
| 662 |
+
import json
|
| 663 |
+
export_str = json.dumps(data, indent=2)
|
| 664 |
+
return gr.update(value=export_str, visible=True)
|
| 665 |
+
except Exception as e:
|
| 666 |
+
return gr.update(value=f"Error: {str(e)}", visible=True)
|
| 667 |
+
return gr.update(value="No conversation to export", visible=True)
|
| 668 |
+
|
| 669 |
+
# Hidden textbox for export
|
| 670 |
+
export_output = gr.Textbox(visible=False, label="Export Data")
|
| 671 |
+
|
| 672 |
+
export_btn.click(
|
| 673 |
+
export_current_conversation,
|
| 674 |
+
outputs=[export_output]
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
# Confirmation handlers
|
| 678 |
+
def handle_approval():
|
| 679 |
+
"""Handle approval button click"""
|
| 680 |
+
# TODO: Implement actual approval through API Gateway
|
| 681 |
+
return (
|
| 682 |
+
gr.update(visible=False), # Hide accordion
|
| 683 |
+
"", # Clear confirmation text
|
| 684 |
+
"Action approved (not yet implemented)" # Status message
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
def handle_denial():
|
| 688 |
+
"""Handle denial button click"""
|
| 689 |
+
return (
|
| 690 |
+
gr.update(visible=False), # Hide accordion
|
| 691 |
+
"", # Clear confirmation text
|
| 692 |
+
"Action cancelled" # Status message
|
| 693 |
+
)
|
| 694 |
+
|
| 695 |
+
approve_btn.click(
|
| 696 |
+
handle_approval,
|
| 697 |
+
outputs=[confirmation_accordion, confirmation_text, msg]
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
deny_btn.click(
|
| 701 |
+
handle_denial,
|
| 702 |
+
outputs=[confirmation_accordion, confirmation_text, msg]
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
# Refresh conversations on load
|
| 706 |
+
demo.load(
|
| 707 |
+
refresh_conversations,
|
| 708 |
+
outputs=[conversation_list]
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
return demo
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
def create_gradio_interface() -> gr.Blocks:
|
| 715 |
+
"""Create and return the Gradio interface."""
|
| 716 |
+
app = PeerToPeerApp()
|
| 717 |
+
return app.create_interface()
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
# For testing
|
| 721 |
+
if __name__ == "__main__":
|
| 722 |
+
interface = create_gradio_interface()
|
| 723 |
+
interface.launch(share=False, debug=True)
|
py/requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
openai>=1.50.0
|
| 3 |
+
gradio>=4.0.0
|
| 4 |
+
tiktoken>=0.5.0
|
| 5 |
+
|
| 6 |
+
# LangChain (without ChromaDB)
|
| 7 |
+
langchain>=0.1.0
|
| 8 |
+
langchain-openai>=0.0.5
|
| 9 |
+
langchain-community>=0.0.10
|
| 10 |
+
|
| 11 |
+
# Vector operations
|
| 12 |
+
numpy>=1.24.0
|
| 13 |
+
|
| 14 |
+
# Utilities
|
| 15 |
+
python-dotenv>=1.0.0
|
| 16 |
+
packaging>=23.0
|
| 17 |
+
tqdm>=4.65.0
|
| 18 |
+
PyYAML>=6.0
|
| 19 |
+
|
| 20 |
+
# Development dependencies (optional)
|
| 21 |
+
# pytest>=7.0.0
|
| 22 |
+
# black>=23.0.0
|
| 23 |
+
# pylint>=2.17.0
|
py/scripts/migrate_to_chromadb.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Script to migrate JSON embeddings to ChromaDB with metadata
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Dict
|
| 9 |
+
import chromadb
|
| 10 |
+
from chromadb.config import Settings
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO)
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class EmbeddingMigrator:
|
| 18 |
+
def __init__(self, embeddings_dir: Path, chroma_db_path: Path):
|
| 19 |
+
self.embeddings_dir = embeddings_dir
|
| 20 |
+
self.chroma_db_path = chroma_db_path
|
| 21 |
+
|
| 22 |
+
# Initialize ChromaDB with persistent storage
|
| 23 |
+
self.client = chromadb.PersistentClient(
|
| 24 |
+
path=str(chroma_db_path),
|
| 25 |
+
settings=Settings(
|
| 26 |
+
anonymized_telemetry=False,
|
| 27 |
+
allow_reset=True
|
| 28 |
+
)
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def create_collection(self):
|
| 32 |
+
"""Create or get the documentation collection."""
|
| 33 |
+
# Delete existing collection if it exists (for clean migration)
|
| 34 |
+
try:
|
| 35 |
+
self.client.delete_collection("documentation")
|
| 36 |
+
logger.info("Deleted existing collection")
|
| 37 |
+
except:
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
# Create new collection
|
| 41 |
+
self.collection = self.client.create_collection(
|
| 42 |
+
name="documentation",
|
| 43 |
+
metadata={"description": "Technical documentation for Harmony and Chorus products"}
|
| 44 |
+
)
|
| 45 |
+
logger.info("Created new collection: documentation")
|
| 46 |
+
|
| 47 |
+
def migrate_embedding_file(self, file_path: Path) -> int:
|
| 48 |
+
"""Migrate a single embedding JSON file to ChromaDB."""
|
| 49 |
+
logger.info(f"Migrating {file_path.name}...")
|
| 50 |
+
|
| 51 |
+
with open(file_path, 'r') as f:
|
| 52 |
+
data = json.load(f)
|
| 53 |
+
|
| 54 |
+
# Extract metadata from filename
|
| 55 |
+
store_name = file_path.stem # e.g., "harmony_1_8"
|
| 56 |
+
|
| 57 |
+
# Parse product and version
|
| 58 |
+
if store_name == "general_faq":
|
| 59 |
+
product = "general"
|
| 60 |
+
version = "all"
|
| 61 |
+
else:
|
| 62 |
+
parts = store_name.split("_", 1)
|
| 63 |
+
if len(parts) == 2:
|
| 64 |
+
product = parts[0]
|
| 65 |
+
version = parts[1].replace("_", ".")
|
| 66 |
+
else:
|
| 67 |
+
product = "unknown"
|
| 68 |
+
version = "unknown"
|
| 69 |
+
|
| 70 |
+
chunks = data.get("chunks", [])
|
| 71 |
+
|
| 72 |
+
# Prepare batch data
|
| 73 |
+
ids = []
|
| 74 |
+
embeddings = []
|
| 75 |
+
metadatas = []
|
| 76 |
+
documents = []
|
| 77 |
+
|
| 78 |
+
for i, chunk in enumerate(chunks):
|
| 79 |
+
# Generate unique ID
|
| 80 |
+
chunk_id = f"{store_name}_chunk_{i}"
|
| 81 |
+
ids.append(chunk_id)
|
| 82 |
+
|
| 83 |
+
# Extract text and embedding
|
| 84 |
+
text = chunk.get("text", "")
|
| 85 |
+
embedding = chunk.get("embedding", [])
|
| 86 |
+
|
| 87 |
+
documents.append(text)
|
| 88 |
+
embeddings.append(embedding)
|
| 89 |
+
|
| 90 |
+
# Build metadata
|
| 91 |
+
metadata = {
|
| 92 |
+
"product": product,
|
| 93 |
+
"version": version,
|
| 94 |
+
"store_name": store_name,
|
| 95 |
+
"chunk_index": i,
|
| 96 |
+
"chunk_id": chunk_id
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
# Add optional metadata if available
|
| 100 |
+
if "metadata" in chunk:
|
| 101 |
+
chunk_meta = chunk["metadata"]
|
| 102 |
+
metadata.update({
|
| 103 |
+
"source": chunk_meta.get("source", ""),
|
| 104 |
+
"page": chunk_meta.get("page", -1),
|
| 105 |
+
"token_count": chunk_meta.get("token_count", 0)
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
# Add chunk_id from original if available
|
| 109 |
+
if "chunk_id" in chunk:
|
| 110 |
+
metadata["original_chunk_id"] = chunk["chunk_id"]
|
| 111 |
+
|
| 112 |
+
metadatas.append(metadata)
|
| 113 |
+
|
| 114 |
+
# Add to ChromaDB in batches
|
| 115 |
+
batch_size = 100
|
| 116 |
+
total_added = 0
|
| 117 |
+
|
| 118 |
+
for i in range(0, len(ids), batch_size):
|
| 119 |
+
batch_end = min(i + batch_size, len(ids))
|
| 120 |
+
|
| 121 |
+
self.collection.add(
|
| 122 |
+
ids=ids[i:batch_end],
|
| 123 |
+
embeddings=embeddings[i:batch_end],
|
| 124 |
+
metadatas=metadatas[i:batch_end],
|
| 125 |
+
documents=documents[i:batch_end]
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
total_added += (batch_end - i)
|
| 129 |
+
logger.info(f" Added {total_added}/{len(ids)} chunks")
|
| 130 |
+
|
| 131 |
+
return len(ids)
|
| 132 |
+
|
| 133 |
+
def migrate_all(self):
|
| 134 |
+
"""Migrate all embedding files to ChromaDB."""
|
| 135 |
+
self.create_collection()
|
| 136 |
+
|
| 137 |
+
# Find all JSON files
|
| 138 |
+
json_files = list(self.embeddings_dir.glob("*.json"))
|
| 139 |
+
logger.info(f"Found {len(json_files)} embedding files to migrate")
|
| 140 |
+
|
| 141 |
+
total_chunks = 0
|
| 142 |
+
|
| 143 |
+
for file_path in json_files:
|
| 144 |
+
chunks_added = self.migrate_embedding_file(file_path)
|
| 145 |
+
total_chunks += chunks_added
|
| 146 |
+
|
| 147 |
+
logger.info(f"\nMigration complete!")
|
| 148 |
+
logger.info(f"Total chunks migrated: {total_chunks}")
|
| 149 |
+
|
| 150 |
+
# Verify collection
|
| 151 |
+
count = self.collection.count()
|
| 152 |
+
logger.info(f"ChromaDB collection count: {count}")
|
| 153 |
+
|
| 154 |
+
# Test query
|
| 155 |
+
self.test_query()
|
| 156 |
+
|
| 157 |
+
def test_query(self):
|
| 158 |
+
"""Test the migrated data with a sample query."""
|
| 159 |
+
logger.info("\nTesting ChromaDB queries...")
|
| 160 |
+
|
| 161 |
+
# Test 1: Query with product/version filter
|
| 162 |
+
results = self.collection.query(
|
| 163 |
+
query_texts=["How to install Harmony?"],
|
| 164 |
+
n_results=3,
|
| 165 |
+
where={"$and": [{"product": "harmony"}, {"version": "1.8"}]}
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
logger.info(f"Test query 1 returned {len(results['ids'][0])} results")
|
| 169 |
+
if results['ids'][0]:
|
| 170 |
+
logger.info(f" First result metadata: {results['metadatas'][0][0]}")
|
| 171 |
+
|
| 172 |
+
# Test 2: Query across all versions
|
| 173 |
+
results = self.collection.query(
|
| 174 |
+
query_texts=["system requirements"],
|
| 175 |
+
n_results=3,
|
| 176 |
+
where={"product": {"$eq": "harmony"}}
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
logger.info(f"Test query 2 returned {len(results['ids'][0])} results")
|
| 180 |
+
|
| 181 |
+
# Test 3: Get unique products and versions
|
| 182 |
+
all_data = self.collection.get()
|
| 183 |
+
products_versions = set()
|
| 184 |
+
|
| 185 |
+
for metadata in all_data['metadatas']:
|
| 186 |
+
products_versions.add((metadata['product'], metadata['version']))
|
| 187 |
+
|
| 188 |
+
logger.info("\nAvailable products and versions:")
|
| 189 |
+
for product, version in sorted(products_versions):
|
| 190 |
+
logger.info(f" - {product} {version}")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def main():
|
| 194 |
+
"""Run the migration."""
|
| 195 |
+
# Set up paths
|
| 196 |
+
script_dir = Path(__file__).parent
|
| 197 |
+
project_root = script_dir.parent
|
| 198 |
+
embeddings_dir = project_root / "data" / "embeddings"
|
| 199 |
+
chroma_db_path = project_root / "data" / "chroma_db"
|
| 200 |
+
|
| 201 |
+
# Create ChromaDB directory
|
| 202 |
+
chroma_db_path.mkdir(parents=True, exist_ok=True)
|
| 203 |
+
|
| 204 |
+
# Run migration
|
| 205 |
+
migrator = EmbeddingMigrator(embeddings_dir, chroma_db_path)
|
| 206 |
+
migrator.migrate_all()
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
if __name__ == "__main__":
|
| 210 |
+
main()
|
py/tools/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modular Tools System
|
| 2 |
+
|
| 3 |
+
This directory contains the refactored modular tools system for the AI Assistant Multi-Agent System.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The tools have been extracted from individual agents into reusable modules, with clear separation between read and write operations.
|
| 8 |
+
|
| 9 |
+
## Potential Structure
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
tools/
|
| 13 |
+
├── agent_tools.py # Central agent registry and switching tools
|
| 14 |
+
├── document_tools.py # Document search/RAG tools (READ-ONLY)
|
| 15 |
+
├── network_tools.py # Network configuration tools (READ/WRITE)
|
| 16 |
+
├── subscriber_tools.py # Subscriber management tools (READ/WRITE)
|
| 17 |
+
├── system_query_tools.py # Analytics and monitoring tools (READ-ONLY)
|
| 18 |
+
├── policy_dnn_tools.py # Policy and DNN tools (READ/WRITE)
|
| 19 |
+
├── profile_read_tools.py # Profile viewing tools (READ-ONLY)
|
| 20 |
+
└── profile_write_tools.py # Profile modification tools (WRITE)
|
| 21 |
+
```
|
py/tools/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tools package for the AI Assistant Multi-Agent System
|
| 3 |
+
|
| 4 |
+
This package contains modular tools that can be used by different agents.
|
| 5 |
+
Tools are organized by functionality and separated into read/write operations
|
| 6 |
+
where applicable.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from . import (
|
| 10 |
+
agent_tools,
|
| 11 |
+
document_tools,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"agent_tools",
|
| 16 |
+
"document_tools",
|
| 17 |
+
]
|
py/tools/agent_tools.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent tools and information management
|
| 3 |
+
|
| 4 |
+
This module contains:
|
| 5 |
+
- Agent metadata and configuration
|
| 6 |
+
- Agent singleton registry
|
| 7 |
+
- Functions to get available agents
|
| 8 |
+
- Agent switching tool creation
|
| 9 |
+
- Tool conversion utilities for OpenAI function format
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import inspect
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, List, Optional, Callable, Any, get_type_hints
|
| 15 |
+
from langchain.agents import Tool
|
| 16 |
+
from langchain.tools import StructuredTool
|
| 17 |
+
from langchain_openai import ChatOpenAI
|
| 18 |
+
from pydantic import BaseModel, Field
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Agent information dictionary - single source of truth for all agents
|
| 24 |
+
AGENT_INFO = {
|
| 25 |
+
"document_reader": {
|
| 26 |
+
"display_name": "Document Reader",
|
| 27 |
+
"description": "Handles documentation queries with RAG capabilities",
|
| 28 |
+
"tools": {
|
| 29 |
+
"read": ["search_version_documentation", "search_all_versions", "list_available_versions"],
|
| 30 |
+
"write": [] # No write operations
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"profile_settings": {
|
| 34 |
+
"display_name": "Profile Settings",
|
| 35 |
+
"description": "Manages user preferences and settings",
|
| 36 |
+
"tools": {
|
| 37 |
+
"read": ["view_profile", "list_preferences", "view_notification_settings"],
|
| 38 |
+
"write": ["update_preference", "update_notifications", "reset_profile"]
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Global agent singleton registry
|
| 45 |
+
_AGENT_INSTANCES: Dict[str, Any] = {}
|
| 46 |
+
_LLM: Optional[ChatOpenAI] = None
|
| 47 |
+
_API_GATEWAY = None
|
| 48 |
+
_CURRENT_CONTEXT: Dict[str, Any] = {} # Store context for switching tools
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def initialize_agents(llm: ChatOpenAI, api_gateway=None) -> None:
|
| 52 |
+
"""
|
| 53 |
+
Initialize all agent singletons
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
llm: The language model to use for all agents
|
| 57 |
+
api_gateway: Optional API gateway for certain agents
|
| 58 |
+
"""
|
| 59 |
+
global _AGENT_INSTANCES, _LLM, _API_GATEWAY
|
| 60 |
+
|
| 61 |
+
# Avoid circular imports by importing here
|
| 62 |
+
from agents.document_reader import DocumentReaderAgent
|
| 63 |
+
from agents.profile_settings import ProfileSettingsAgent
|
| 64 |
+
|
| 65 |
+
_LLM = llm
|
| 66 |
+
_API_GATEWAY = api_gateway
|
| 67 |
+
|
| 68 |
+
logger.info("Initializing agent singletons...")
|
| 69 |
+
|
| 70 |
+
# Create agent instances (only implemented ones)
|
| 71 |
+
_AGENT_INSTANCES = {
|
| 72 |
+
"document_reader": DocumentReaderAgent(llm),
|
| 73 |
+
"profile_settings": ProfileSettingsAgent(llm),
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
logger.info(f"Initialized {len(_AGENT_INSTANCES)} agents")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_agent(agent_id: str) -> Optional[Any]:
|
| 80 |
+
"""
|
| 81 |
+
Get agent instance by ID
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
agent_id: The agent identifier
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Agent instance or None if not found
|
| 88 |
+
"""
|
| 89 |
+
return _AGENT_INSTANCES.get(agent_id)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def run_agent(agent_id: str, message: str, context: Optional[Dict] = None) -> Dict[str, Any]:
|
| 93 |
+
"""
|
| 94 |
+
Run an agent with a message
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
agent_id: The agent identifier
|
| 98 |
+
message: The message to process
|
| 99 |
+
context: Optional context dictionary
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Agent response dictionary
|
| 103 |
+
"""
|
| 104 |
+
agent = get_agent(agent_id)
|
| 105 |
+
if not agent:
|
| 106 |
+
raise ValueError(f"Unknown agent: {agent_id}")
|
| 107 |
+
|
| 108 |
+
# Store context globally for switching tools to access
|
| 109 |
+
global _CURRENT_CONTEXT
|
| 110 |
+
_CURRENT_CONTEXT = {
|
| 111 |
+
"message": message,
|
| 112 |
+
"context": context or {}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return agent.run(message, context)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_available_agents(exclude_agent_id: Optional[str] = None) -> Dict[str, Dict]:
|
| 119 |
+
"""
|
| 120 |
+
Get all available agents, optionally excluding one
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
exclude_agent_id: Agent ID to exclude from results
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
Dictionary of agent_id -> agent_info
|
| 127 |
+
"""
|
| 128 |
+
return {
|
| 129 |
+
agent_id: info
|
| 130 |
+
for agent_id, info in AGENT_INFO.items()
|
| 131 |
+
if agent_id != exclude_agent_id
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def create_agent_switching_tool(target_agent_id: str, target_agent_name: str) -> Callable:
|
| 136 |
+
"""
|
| 137 |
+
Create a tool function for switching to a specific agent
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
target_agent_id: ID of the agent to switch to
|
| 141 |
+
target_agent_name: Display name of the agent
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
Callable function that performs the agent switch
|
| 145 |
+
"""
|
| 146 |
+
def switch_to_agent(reason: str = "") -> str:
|
| 147 |
+
"""Switch conversation to another agent.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
reason: Brief explanation of why switching (e.g., 'User wants to update profile settings')
|
| 151 |
+
"""
|
| 152 |
+
# Get current context
|
| 153 |
+
global _CURRENT_CONTEXT
|
| 154 |
+
message = _CURRENT_CONTEXT.get("message", "")
|
| 155 |
+
context = _CURRENT_CONTEXT.get("context", {})
|
| 156 |
+
|
| 157 |
+
logger.info(f"Switching from current agent to {target_agent_id}")
|
| 158 |
+
|
| 159 |
+
# Log the transfer
|
| 160 |
+
transfer_msg = f"Transferring to {target_agent_name}"
|
| 161 |
+
if reason:
|
| 162 |
+
transfer_msg += f": {reason}"
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
# Run the target agent with the current message
|
| 166 |
+
result = run_agent(target_agent_id, message, context)
|
| 167 |
+
|
| 168 |
+
# Return a special marker with the target agent's response
|
| 169 |
+
# This tells BaseAgent to update agent_id and use this response
|
| 170 |
+
return f"__SWITCH_AGENT__|{target_agent_id}|{transfer_msg}\n\n{result.get('output', '')}"
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"Error during agent switch: {e}")
|
| 173 |
+
return f"__SWITCH_AGENT__|{target_agent_id}|{transfer_msg}\n\n[Error: Failed to run {target_agent_name} - {str(e)}]"
|
| 174 |
+
|
| 175 |
+
# Set function metadata for LangChain
|
| 176 |
+
switch_to_agent.__name__ = f"switch_to_{target_agent_id}"
|
| 177 |
+
switch_to_agent.__doc__ = f"Transfer conversation to {target_agent_name} agent. Always provide a reason parameter explaining why you're switching (e.g., 'User wants to update profile settings')."
|
| 178 |
+
|
| 179 |
+
return switch_to_agent
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def create_switching_tools_for_agent(current_agent_id: str) -> List[StructuredTool]:
|
| 183 |
+
"""
|
| 184 |
+
Create all agent switching tools for a given agent
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
current_agent_id: ID of the current agent
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
List of StructuredTool objects for switching to other agents
|
| 191 |
+
"""
|
| 192 |
+
tools = []
|
| 193 |
+
other_agents = get_available_agents(exclude_agent_id=current_agent_id)
|
| 194 |
+
|
| 195 |
+
for agent_id, agent_info in other_agents.items():
|
| 196 |
+
# Create a dynamic Pydantic model for this specific agent
|
| 197 |
+
class SwitchToAgentInput(BaseModel):
|
| 198 |
+
reason: str = Field(
|
| 199 |
+
default="",
|
| 200 |
+
description=f"Brief explanation of why switching to {agent_info['display_name']} (e.g., 'User wants to update profile settings')"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# Set the class name dynamically for better debugging
|
| 204 |
+
SwitchToAgentInput.__name__ = f"SwitchTo{agent_id.title().replace('_', '')}Input"
|
| 205 |
+
|
| 206 |
+
switch_func = create_agent_switching_tool(
|
| 207 |
+
agent_id,
|
| 208 |
+
agent_info['display_name']
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
tool = StructuredTool.from_function(
|
| 212 |
+
func=switch_func,
|
| 213 |
+
name=f"switch_to_{agent_id}",
|
| 214 |
+
description=f"Transfer to {agent_info['display_name']} - {agent_info['description']}",
|
| 215 |
+
args_schema=SwitchToAgentInput
|
| 216 |
+
)
|
| 217 |
+
tools.append(tool)
|
| 218 |
+
|
| 219 |
+
return tools
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def get_agent_tools(agent_id: str) -> Dict[str, List[str]]:
|
| 223 |
+
"""
|
| 224 |
+
Get the tools (read and write) for a specific agent
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
agent_id: ID of the agent
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
Dictionary with 'read' and 'write' tool lists
|
| 231 |
+
"""
|
| 232 |
+
if agent_id not in AGENT_INFO:
|
| 233 |
+
raise ValueError(f"Unknown agent ID: {agent_id}")
|
| 234 |
+
|
| 235 |
+
return AGENT_INFO[agent_id]["tools"]
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def is_write_tool(agent_id: str, tool_name: str) -> bool:
|
| 239 |
+
"""
|
| 240 |
+
Check if a tool is a write operation (requires confirmation)
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
agent_id: ID of the agent
|
| 244 |
+
tool_name: Name of the tool
|
| 245 |
+
|
| 246 |
+
Returns:
|
| 247 |
+
True if the tool is a write operation
|
| 248 |
+
"""
|
| 249 |
+
agent_tools = get_agent_tools(agent_id)
|
| 250 |
+
return tool_name in agent_tools.get("write", [])
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def convert_tool_to_openai_function(tool: Any) -> Dict[str, Any]:
|
| 254 |
+
"""
|
| 255 |
+
Convert a LangChain Tool or StructuredTool to OpenAI function format.
|
| 256 |
+
|
| 257 |
+
Handles both Tool and StructuredTool instances appropriately.
|
| 258 |
+
"""
|
| 259 |
+
# Check if this is a StructuredTool with args_schema
|
| 260 |
+
if hasattr(tool, 'args_schema') and tool.args_schema:
|
| 261 |
+
# Use the Pydantic schema directly
|
| 262 |
+
schema = tool.args_schema.schema()
|
| 263 |
+
|
| 264 |
+
# Extract properties and required fields from Pydantic schema
|
| 265 |
+
properties = {}
|
| 266 |
+
required = []
|
| 267 |
+
|
| 268 |
+
for field_name, field_info in schema.get('properties', {}).items():
|
| 269 |
+
properties[field_name] = field_info
|
| 270 |
+
|
| 271 |
+
# Get required fields from schema
|
| 272 |
+
required = schema.get('required', [])
|
| 273 |
+
|
| 274 |
+
return {
|
| 275 |
+
"name": tool.name,
|
| 276 |
+
"description": tool.description,
|
| 277 |
+
"parameters": {
|
| 278 |
+
"type": "object",
|
| 279 |
+
"properties": properties,
|
| 280 |
+
"required": required
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
# Fall back to signature introspection for regular Tool
|
| 285 |
+
func = tool.func
|
| 286 |
+
sig = inspect.signature(func)
|
| 287 |
+
type_hints = get_type_hints(func)
|
| 288 |
+
|
| 289 |
+
# Build properties from function parameters
|
| 290 |
+
properties = {}
|
| 291 |
+
required = []
|
| 292 |
+
|
| 293 |
+
for param_name, param in sig.parameters.items():
|
| 294 |
+
# Skip 'self' parameter if it exists
|
| 295 |
+
if param_name == 'self':
|
| 296 |
+
continue
|
| 297 |
+
|
| 298 |
+
# Determine the type
|
| 299 |
+
param_type = type_hints.get(param_name, type(param.default) if param.default != param.empty else str)
|
| 300 |
+
|
| 301 |
+
# Convert Python types to JSON Schema types
|
| 302 |
+
if param_type == str:
|
| 303 |
+
json_type = "string"
|
| 304 |
+
elif param_type == int:
|
| 305 |
+
json_type = "integer"
|
| 306 |
+
elif param_type == float:
|
| 307 |
+
json_type = "number"
|
| 308 |
+
elif param_type == bool:
|
| 309 |
+
json_type = "boolean"
|
| 310 |
+
elif param_type == list or str(param_type).startswith('typing.List'):
|
| 311 |
+
json_type = "array"
|
| 312 |
+
elif param_type == dict or str(param_type).startswith('typing.Dict'):
|
| 313 |
+
json_type = "object"
|
| 314 |
+
else:
|
| 315 |
+
json_type = "string" # Default fallback
|
| 316 |
+
|
| 317 |
+
# Build property definition
|
| 318 |
+
prop_def = {"type": json_type}
|
| 319 |
+
|
| 320 |
+
# Add description from docstring if available
|
| 321 |
+
if func.__doc__:
|
| 322 |
+
# Simple extraction - could be enhanced with docstring parsing
|
| 323 |
+
prop_def["description"] = f"Parameter: {param_name}"
|
| 324 |
+
|
| 325 |
+
# Add default value if present
|
| 326 |
+
if param.default != param.empty:
|
| 327 |
+
prop_def["default"] = param.default
|
| 328 |
+
else:
|
| 329 |
+
# If no default, it's required
|
| 330 |
+
required.append(param_name)
|
| 331 |
+
|
| 332 |
+
properties[param_name] = prop_def
|
| 333 |
+
|
| 334 |
+
# Return the OpenAI function schema
|
| 335 |
+
return {
|
| 336 |
+
"name": tool.name,
|
| 337 |
+
"description": tool.description,
|
| 338 |
+
"parameters": {
|
| 339 |
+
"type": "object",
|
| 340 |
+
"properties": properties,
|
| 341 |
+
"required": required
|
| 342 |
+
}
|
| 343 |
+
}
|
py/tools/document_tools.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Document tools for searching and querying documentation
|
| 3 |
+
|
| 4 |
+
These are READ-ONLY tools used by the Document Reader agent.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from langchain.agents import Tool
|
| 9 |
+
from langchain.tools import StructuredTool
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
from backend.chromadb_manager import ChromaDBManager
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# Pydantic models for structured tool inputs
|
| 15 |
+
class SearchDocumentationInput(BaseModel):
|
| 16 |
+
"""Input model for searching documentation"""
|
| 17 |
+
query: str = Field(description="The search query")
|
| 18 |
+
product: Optional[str] = Field(None, description="Product name (harmony or chorus). If not specified, searches all products")
|
| 19 |
+
version: Optional[str] = Field(None, description="Product version (e.g., '1.2', '1.8'). If not specified, searches all versions")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ListVersionsInput(BaseModel):
|
| 23 |
+
"""Input model for listing versions"""
|
| 24 |
+
# No parameters needed, but StructuredTool requires a model
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Initialize ChromaDB manager (singleton pattern)
|
| 28 |
+
_db_manager = None
|
| 29 |
+
|
| 30 |
+
def get_db_manager():
|
| 31 |
+
"""Get or create ChromaDB manager instance"""
|
| 32 |
+
global _db_manager
|
| 33 |
+
if _db_manager is None:
|
| 34 |
+
_db_manager = ChromaDBManager()
|
| 35 |
+
return _db_manager
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def search_documentation(query: str, product: Optional[str] = None, version: Optional[str] = None) -> str:
|
| 39 |
+
"""
|
| 40 |
+
Search documentation with flexible filtering
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
query: Search query
|
| 44 |
+
product: Optional product name (harmony or chorus)
|
| 45 |
+
version: Optional product version (e.g., '1.2', '1.8')
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
Search results as formatted string with metadata
|
| 49 |
+
"""
|
| 50 |
+
db_manager = get_db_manager()
|
| 51 |
+
|
| 52 |
+
# Determine which search method to use based on parameters
|
| 53 |
+
if version is not None and product is not None:
|
| 54 |
+
# Search specific version of specific product
|
| 55 |
+
docs = db_manager.query_with_filter(query, product, version, k=5)
|
| 56 |
+
header = f"Results for '{query}' in {product} {version}:"
|
| 57 |
+
elif product is not None:
|
| 58 |
+
# Search all versions of specific product
|
| 59 |
+
docs = db_manager.query_product_all_versions(query, product, k=5)
|
| 60 |
+
header = f"Results for '{query}' across all {product} versions:"
|
| 61 |
+
else:
|
| 62 |
+
# Search across all products and versions
|
| 63 |
+
# Note: We'll need to search each product separately and combine results
|
| 64 |
+
all_docs = []
|
| 65 |
+
for prod in ["harmony", "chorus"]:
|
| 66 |
+
prod_docs = db_manager.query_product_all_versions(query, prod, k=3)
|
| 67 |
+
all_docs.extend(prod_docs)
|
| 68 |
+
docs = all_docs[:5] # Limit total results
|
| 69 |
+
header = f"Results for '{query}' across all products:"
|
| 70 |
+
|
| 71 |
+
if not docs:
|
| 72 |
+
return f"No results found for '{query}'"
|
| 73 |
+
|
| 74 |
+
results = [header]
|
| 75 |
+
metadata_summary = []
|
| 76 |
+
|
| 77 |
+
for i, doc in enumerate(docs, 1):
|
| 78 |
+
# Extract all metadata
|
| 79 |
+
metadata = doc.metadata
|
| 80 |
+
|
| 81 |
+
# Include version info if searching across versions
|
| 82 |
+
if version is None and 'version' in metadata:
|
| 83 |
+
version_info = f"[{metadata.get('product', 'unknown')} {metadata.get('version', 'unknown')}] "
|
| 84 |
+
else:
|
| 85 |
+
version_info = ""
|
| 86 |
+
|
| 87 |
+
# Add main content
|
| 88 |
+
results.append(f"\n{i}. {version_info}{doc.page_content[:500]}...")
|
| 89 |
+
|
| 90 |
+
# Collect metadata for summary
|
| 91 |
+
meta_info = {
|
| 92 |
+
'chunk': i,
|
| 93 |
+
'product': metadata.get('product', 'unknown'),
|
| 94 |
+
'version': metadata.get('version', 'unknown'),
|
| 95 |
+
'document': metadata.get('document', 'unknown'),
|
| 96 |
+
'page': metadata.get('page', 'unknown'),
|
| 97 |
+
'chunk_id': metadata.get('chunk_id', 'unknown')
|
| 98 |
+
}
|
| 99 |
+
metadata_summary.append(meta_info)
|
| 100 |
+
|
| 101 |
+
# Add metadata summary at the end
|
| 102 |
+
results.append("\n\n**Metadata of chunks retrieved:**")
|
| 103 |
+
for meta in metadata_summary:
|
| 104 |
+
results.append(f"- Chunk {meta['chunk']}: {meta['product']} v{meta['version']}, "
|
| 105 |
+
f"{meta['document']} (page {meta['page']})")
|
| 106 |
+
|
| 107 |
+
return "\n".join(results)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def list_available_versions() -> str:
|
| 113 |
+
"""
|
| 114 |
+
List all available product versions
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
List of available products and versions
|
| 118 |
+
"""
|
| 119 |
+
db_manager = get_db_manager()
|
| 120 |
+
versions = db_manager.list_available_versions()
|
| 121 |
+
result = "Available product versions:\n"
|
| 122 |
+
|
| 123 |
+
for product, version_list in versions.items():
|
| 124 |
+
result += f"\n{product.capitalize()}:\n"
|
| 125 |
+
for version in version_list:
|
| 126 |
+
result += f" - {version}\n"
|
| 127 |
+
|
| 128 |
+
return result
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# Tool creation functions for agents to use
|
| 132 |
+
def search_documentation_tool() -> StructuredTool:
|
| 133 |
+
"""Create a StructuredTool object for search_documentation"""
|
| 134 |
+
return StructuredTool.from_function(
|
| 135 |
+
func=search_documentation,
|
| 136 |
+
name="search_documentation",
|
| 137 |
+
description="Search technical documentation with flexible filtering by product and version",
|
| 138 |
+
args_schema=SearchDocumentationInput
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def list_available_versions_tool() -> StructuredTool:
|
| 143 |
+
"""Create a StructuredTool object for list_available_versions"""
|
| 144 |
+
return StructuredTool.from_function(
|
| 145 |
+
func=list_available_versions,
|
| 146 |
+
name="list_available_versions",
|
| 147 |
+
description="List all available product versions in the documentation",
|
| 148 |
+
args_schema=ListVersionsInput
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
py/tools/profile_read_tools.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#TODO
|
| 2 |
+
pass
|
py/tools/profile_write_tools.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#TODO
|
| 2 |
+
pass
|
requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
openai>=1.50.0
|
| 3 |
+
gradio>=4.0.0
|
| 4 |
+
tiktoken>=0.5.0
|
| 5 |
+
|
| 6 |
+
# LangChain (without ChromaDB)
|
| 7 |
+
langchain>=0.1.0
|
| 8 |
+
langchain-openai>=0.0.5
|
| 9 |
+
langchain-community>=0.0.10
|
| 10 |
+
|
| 11 |
+
# Vector operations
|
| 12 |
+
numpy>=1.24.0
|
| 13 |
+
|
| 14 |
+
# Utilities
|
| 15 |
+
python-dotenv>=1.0.0
|
| 16 |
+
packaging>=23.0
|
| 17 |
+
tqdm>=4.65.0
|
| 18 |
+
PyYAML>=6.0
|
| 19 |
+
|
| 20 |
+
# Development dependencies (optional)
|
| 21 |
+
# pytest>=7.0.0
|
| 22 |
+
# black>=23.0.0
|
| 23 |
+
# pylint>=2.17.0
|