Julian Vanecek commited on
Commit
bb80caa
·
0 Parent(s):

Initial commit: AI Assistant Multi-Agent System for HuggingFace Spaces

Browse files
.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