chrizefan commited on
Commit
fe52ef9
·
verified ·
1 Parent(s): f06951a

Upload folder using huggingface_hub

Browse files
.github/workflows/update_space.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - develop
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v2
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v2
18
+ with:
19
+ python-version: '3.10'
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install -r requirements.txt
25
+ pip install huggingface_hub
26
+
27
+ - name: Deploy to Space
28
+ env:
29
+ HUGGINGFACE_TOKEN: ${{ secrets.HUGGINGFACE_TOKEN }}
30
+ run: |
31
+ python -c '
32
+ from huggingface_hub import HfApi, create_repo
33
+ api = HfApi()
34
+ create_repo("chrizefan/digichat", token="${{ secrets.HUGGINGFACE_TOKEN }}", repo_type="space", space_sdk="gradio", exist_ok=True)
35
+ api.upload_folder(
36
+ folder_path=".",
37
+ repo_id="chrizefan/digichat",
38
+ repo_type="space",
39
+ token="${{ secrets.HUGGINGFACE_TOKEN }}",
40
+ ignore_patterns=["__pycache__", "*.pyc", ".env", ".git", ".gitignore", ".conda"]
41
+ )
42
+ '
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
.huggingface/config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "default_branch": "develop",
3
+ "sdk": "gradio",
4
+ "sdk_version": "5.23.0",
5
+ "build": {
6
+ "builder": "gradio",
7
+ "build_command": "pip install -r requirements.txt"
8
+ },
9
+ "runtime": {
10
+ "python_version": "3.10"
11
+ }
12
+ }
README.md CHANGED
@@ -1,12 +1,93 @@
1
  ---
2
- title: Digichat
3
- emoji:
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.27.0
8
- app_file: app.py
9
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: digichat
3
+ app_file: app.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.27.0
6
+ hf_oauth: true
7
+ ---
8
+
9
+ # DigiChat
10
+
11
+ DigiChat is an advanced, modular chatbot framework designed for flexible, multi-agent conversational AI. It supports streaming responses, file processing, integration with Azure AI Search and vector indexes, and orchestration of specialized agents for complex tasks.
12
+
13
+ ## Features
14
+
15
+ - **Multi-Agent Architecture:**
16
+ Supports multiple agent types, including:
17
+ - **BaseAgent:** Standard LLM-based conversation.
18
+ - **DataAgent:** Handles queries involving external data sources.
19
+ - **SearchAgent:** Integrates with Azure AI Search and vector indexes for semantic search.
20
+ - **OrchestratorAgent:** Routes and coordinates between specialized agents as tools.
21
+
22
+ - **Streaming Responses:**
23
+ Real-time, token-by-token streaming of model outputs for responsive user experience.
24
+
25
+ - **File Upload and Processing:**
26
+ Users can upload files (text, images, etc.), which are processed and incorporated into the conversation context.
27
+
28
+ - **Flexible Model Configuration:**
29
+ Easily switch between different LLM engines and adjust parameters like temperature.
30
+
31
+ - **Azure AI Search Integration:**
32
+ Seamlessly query Azure Search and vector indexes for retrieval-augmented generation.
33
+
34
+ - **Extensible Tooling:**
35
+ Add new tools and data sources via configuration, enabling custom workflows.
36
+
37
+ - **History Management:**
38
+ Maintains conversation history and supports advanced prompt engineering.
39
+
40
+ - **Robust Error Handling:**
41
+ Gracefully handles errors in streaming, file processing, and agent orchestration.
42
+
43
+ - **Logging and Debugging:**
44
+ Built-in logging for monitoring and debugging agent behavior.
45
+
46
+ ## Usage
47
+
48
+ 1. **Start the Chatbot:**
49
+ Launch the Gradio interface or integrate DigiChat into your own application.
50
+
51
+ 2. **Interact:**
52
+ - Send messages and receive streaming responses.
53
+ - Upload files for context-aware answers.
54
+ - Leverage advanced search and data retrieval via specialized agents.
55
+
56
+ 3. **Configure:**
57
+ - Adjust agent configurations, add tools, or connect new data sources as needed.
58
+
59
+ ## Architecture
60
+
61
+ See [`architecture.puml`](architecture.puml) for a detailed PlantUML diagram of the system's class structure and relationships.
62
+
63
+ ## Requirements
64
+
65
+ - Python 3.10+
66
+ - Gradio
67
+ - Azure SDKs (for Search integration)
68
+ - Other dependencies as listed in `requirements.txt`
69
+
70
+ ## Setup
71
+
72
+ 1. Clone the repository
73
+ 2. Install dependencies:
74
+ ```bash
75
+ pip install -r requirements.txt
76
+ ```
77
+ 3. Set up your environment variables in `.env`:
78
+ ```
79
+ OPENAI_API_KEY=your_api_key_here
80
+ XAI_API_KEY=your_xai_key_here
81
+ ```
82
+ 4. Run the application:
83
+ ```bash
84
+ python app.py
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT License
90
+
91
  ---
92
 
93
+ For more details, see the code and comments in each module.
agent_config.py ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List
2
+ import os
3
+ import json
4
+ import datetime
5
+ from abc import ABC, abstractmethod
6
+
7
+ class AgentConfig(ABC):
8
+ """Base configuration class for agents"""
9
+
10
+ def __init__(self):
11
+ self.data_sources: Dict[str, Dict[str, Any]] = {}
12
+ self.tools: List[Dict[str, Any]] = []
13
+ self.system_prompts: Dict[str, str] = {}
14
+ self.model_config: Dict[str, Any] = {
15
+ "model": "grok-2-latest",
16
+ "temperature": 0.5,
17
+ "max_iterations": 10
18
+ }
19
+ self.azure_index: Dict[str, Any] = {}
20
+ self.tool_agent_map: Dict[str, str] = {} # tool name -> agent type
21
+
22
+ @abstractmethod
23
+ def setup_data_sources(self) -> None:
24
+ """Setup data source configurations"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def setup_tools(self) -> None:
29
+ """Setup available tools"""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def setup_system_prompts(self) -> None:
34
+ """Setup system prompts"""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def setup_azure_index(self) -> None:
39
+ """Setup search indexes configurations"""
40
+ pass
41
+
42
+ def setup_model_config(self, model: str = "grok-2-latest", temperature: float = 0.5, max_iterations: int = 10) -> None:
43
+ """Setup model configuration"""
44
+ self.model_config = {
45
+ "model": model,
46
+ "temperature": temperature,
47
+ "max_iterations": max_iterations
48
+ }
49
+
50
+ def get_data_sources(self) -> Dict[str, Dict[str, Any]]:
51
+ """Get all data source configurations"""
52
+ return self.data_sources
53
+
54
+ def get_azure_index(self) -> Dict[str, Any]:
55
+ """Get all search index configurations"""
56
+ return self.azure_index
57
+
58
+ def get_tools(self) -> List[Dict[str, Any]]:
59
+ """Get the list of available tools"""
60
+ return self.tools
61
+
62
+ def get_system_prompt(self, agent_type: str) -> str:
63
+ """Get the system prompt for a specific agent type"""
64
+ if agent_type not in self.system_prompts:
65
+ return None
66
+ return self.system_prompts[agent_type]
67
+
68
+ def get_model_config(self) -> Dict[str, Any]:
69
+ """Get the model configuration"""
70
+ return self.model_config
71
+
72
+ def to_json(self) -> str:
73
+ """Convert the configuration to a JSON string"""
74
+ config = {
75
+ "tools": self.tools,
76
+ "system_prompts": self.system_prompts,
77
+ "model_config": self.model_config,
78
+ "data_sources": self.data_sources,
79
+ "azure_index": self.azure_index
80
+ }
81
+ return json.dumps(config, indent=2)
82
+
83
+ def get_azure_index(self) -> Dict[str, Any]:
84
+ """Get the Azure Search configuration"""
85
+ return self.azure_index
86
+
87
+ def get_tool_agent_map(self) -> Dict[str, str]:
88
+ """Get the mapping from tool name to agent type"""
89
+ return self.tool_agent_map
90
+
91
+ class BaseAgentConfig(AgentConfig):
92
+ """Standard configuration class for basic language model agents"""
93
+
94
+ def __init__(self, model: str = "grok-2-latest", temperature: float = 0.5):
95
+ super().__init__()
96
+ self.model = model
97
+ self.setup_model_config(model, temperature)
98
+ self.setup_data_sources()
99
+ self.setup_tools()
100
+ self.setup_system_prompts()
101
+ self.setup_azure_index()
102
+
103
+ def setup_data_sources(self) -> None:
104
+ """Basic agent has no data sources"""
105
+ self.data_sources = {}
106
+
107
+ def setup_tools(self) -> None:
108
+ """Basic agent has no tools"""
109
+ self.tools = []
110
+
111
+ def setup_system_prompts(self) -> None:
112
+ """Setup standard system prompt"""
113
+ self.system_prompts = {
114
+ "default": "You are a helpful AI assistant."
115
+ }
116
+
117
+ def setup_azure_index(self) -> None:
118
+ """Basic agent has no search indexes"""
119
+ self.azure_index = {}
120
+
121
+ class SITAASAgentConfig(BaseAgentConfig):
122
+ """Configuration class for the SITAAS agent"""
123
+
124
+ def __init__(self, model: str = "gpt-4o-mini", temperature: float = 0.0):
125
+ super().__init__(model, temperature)
126
+ self.setup_tool_agent_map()
127
+
128
+ def setup_data_sources(self) -> None:
129
+ """Setup SITAAS data source configurations"""
130
+ self.data_sources = {
131
+ "preview_metadata_agent": {
132
+ "type": "azure_search",
133
+ "parameters": {
134
+ "endpoint": os.getenv("SEARCH_ENDPOINT"),
135
+ "index_name": "azureblob-index",
136
+ "semantic_configuration": "default",
137
+ "query_type": "simple",
138
+ "fields_mapping": {},
139
+ "in_scope": True,
140
+ "filter": None,
141
+ "strictness": 1,
142
+ "top_n_documents": 20,
143
+ "authentication": {
144
+ "type": "api_key",
145
+ "key": os.getenv("SEARCH_KEY"),
146
+ }
147
+ }
148
+ },
149
+ "preview_content_agent": {
150
+ "type": "azure_search",
151
+ "parameters": {
152
+ "endpoint": os.getenv("SEARCH_ENDPOINT"),
153
+ "index_name": "vector-1745685878660",
154
+ "semantic_configuration": "default",
155
+ "query_type": "vector_simple_hybrid",
156
+ "fields_mapping": {},
157
+ "in_scope": True,
158
+ "filter": None,
159
+ "strictness": 1,
160
+ "top_n_documents": 20,
161
+ "authentication": {
162
+ "type": "api_key",
163
+ "key": os.getenv("SEARCH_KEY"),
164
+ },
165
+ "embedding_dependency": {
166
+ "type": "deployment_name",
167
+ "deployment_name": "text-embedding-ada-002"
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ def setup_tools(self) -> None:
174
+ """Setup SITAAS available tools"""
175
+ self.tools = [
176
+ {
177
+ "type": "function",
178
+ "function": {
179
+ "name": "search_metadata",
180
+ "description": "Retrieve metadata documents from Azure Cognitive Search using Lucene syntax. Useful when more filtering or exploration of metadata is needed.",
181
+ "parameters": {
182
+ "type": "object",
183
+ "properties": {
184
+ "search_text": {
185
+ "type": "string",
186
+ "description": "Lucene syntax query to retrieve metadata for documents.",
187
+ "default": "*",
188
+ },
189
+ "num_results": {
190
+ "type": "integer",
191
+ "description": "Number of metadata records to retrieve.",
192
+ "minimum": 1,
193
+ "maximum": 1000
194
+ },
195
+ "filter": {
196
+ "type": "string",
197
+ "description": "Optional OData filter expression to apply to the search.",
198
+ },
199
+ "orderby": {
200
+ "type": "string",
201
+ "description": "Optional OData orderby expression to sort the results.",
202
+ },
203
+ "select": {
204
+ "type": "string",
205
+ "description": "Optional OData select expression to limit the fields returned in the results.",
206
+ "default": "Id,Owner,Name,WebUrl,CreatedDataTime,CreatedBy,LastModifiedDate,LastModifiedBy"
207
+ },
208
+ },
209
+ "required": ["search_text"],
210
+ "additionalProperties": False
211
+ }
212
+ }
213
+ },
214
+ {
215
+ "type": "function",
216
+ "function": {
217
+ "name": "search_content",
218
+ "description": "Similarity search on document chunks in a vector index. Ideal for high-recall document content analysis.",
219
+ "parameters": {
220
+ "type": "object",
221
+ "properties": {
222
+ "search_text": {
223
+ "type": "string",
224
+ "description": "Semantic query for content-based document retrieval.",
225
+ "default": "*",
226
+ },
227
+ "num_results": {
228
+ "type": "integer",
229
+ "description": "Number of document chunks to retrieve.",
230
+ "minimum": 1,
231
+ "maximum": 1000
232
+ },
233
+ "filter": {
234
+ "type": "string",
235
+ "description": "Optional OData filter expression to apply to the search.",
236
+ },
237
+ "orderby": {
238
+ "type": "string",
239
+ "description": "Optional OData orderby expression to sort the results.",
240
+ },
241
+ "select": {
242
+ "type": "string",
243
+ "description": "Optional OData select expression to limit the fields returned in the results.",
244
+ "default": "title,chunk_id,chunk, Id, Owner, Name"
245
+ },
246
+ },
247
+ "required": ["search_text"],
248
+ "additionalProperties": False
249
+ }
250
+ }
251
+ },
252
+ # {
253
+ # "type": "function",
254
+ # "function": {
255
+ # "name": "generate_final_response",
256
+ # "description": "Generate a final synthesized answer using the selected documents and an optional formatting instruction.",
257
+ # "parameters": {
258
+ # "type": "object",
259
+ # "properties": {
260
+ # "format_instruction": {
261
+ # "type": "string",
262
+ # "description": "Optional formatting guide for the final output (e.g., summary, markdown, table, Q&A, etc.)."
263
+ # }
264
+ # },
265
+ # "additionalProperties": False
266
+ # }
267
+ # }
268
+ # }
269
+ ]
270
+
271
+ def setup_azure_index(self) -> None:
272
+ """Setup SITAAS search indexes configurations"""
273
+ self.azure_index = {
274
+ "search_endpoint": os.getenv("SEARCH_ENDPOINT"),
275
+ "search_key": os.getenv("SEARCH_KEY"),
276
+ "search_index_name": "azureblob-index",
277
+ "vector_index_name": "vector-1745685878660",
278
+ "embeddings_deployment": "text-embedding-ada-002",
279
+ "azure_openai_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"),
280
+ "azure_openai_key": os.getenv("AZURE_OPENAI_API_KEY")
281
+ }
282
+
283
+ def setup_system_prompts(self) -> None:
284
+ """Setup SITAAS system prompts"""
285
+
286
+ self.system_prompts = {
287
+ "orchestrator": f"""
288
+ You are an intelligent orchestration agent designed to fulfill complex user queries by coordinating across multiple specialized document search tools and output generation utilities.
289
+
290
+ You are plugged into a database and have access to powerful search tools. If the user's request is ambiguous or missing critical information, you may ask a clarifying question, but do not overdo it—prefer to take action and use your tools to retrieve information whenever possible.
291
+
292
+ Only ask the user for more details if it is truly necessary to proceed. Otherwise, attempt to fulfill the request using the available tools and data.
293
+
294
+ You are an agent – please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved.
295
+
296
+ If you are not sure about file content or document structure pertaining to the user’s request, use your tools to retrieve information. Do NOT guess or make up an answer.
297
+
298
+ You MUST plan before each function call and reflect on the outcomes of previous function calls, but do not delay action by repeatedly asking for clarification.
299
+
300
+ **Date**: Today is {datetime.datetime.now().strftime("%Y-%m-%d")}.
301
+
302
+ ---
303
+
304
+ ### 🧠 Planning & Reflection
305
+ Before each tool call, briefly **plan** out your next action.
306
+ After each tool call, **reflect** on what was retrieved and adjust your next step accordingly.
307
+ Do not simply chain tool calls without commentary or thinking.
308
+
309
+ ### 🔧 Tool-Calling Reminder
310
+ Use your tools to gather information instead of guessing.
311
+ Each tool serves a purpose—select them wisely based on the query type.
312
+
313
+ ### 🔍 Search Strategy
314
+ 1. **Scoping Phase**:
315
+ - Start by calling search tools (such as preview or metadata tools) with a small number of results (e.g., 5–10).
316
+ - Use these initial results to understand the structure, quality, and relevance of the data.
317
+ - Analyze the returned results to identify useful filters, patterns, or query refinements.
318
+ - If results are ambiguous or insufficient, adjust your search parameters (e.g., keywords, filters, or sort order) and try again with a small result set.
319
+
320
+ 2. **Refinement Phase**:
321
+ - Based on insights from the scoping phase, refine your search parameters to target the most relevant data.
322
+ - Continue using small result sets until you are confident that your search parameters are well-tuned and will yield high-quality results.
323
+
324
+ 3. **Comprehensive Search Phase**:
325
+ - Once you have refined your search parameters and are confident in their effectiveness, perform a comprehensive search without a strict result limit (or with a much higher limit).
326
+ - Retrieve all relevant documents or content needed to fully address the user's query.
327
+
328
+ 4. **Final Output**:
329
+ - Once you’ve gathered sufficient content, use the `generate_final_response` tool to produce a human-readable output.
330
+ - If the user specified a desired output format (e.g., "summary", "bullet points", "explanatory"), pass that to the response tool.
331
+
332
+ ---
333
+
334
+ ### 🧩 Metadata Fields Available
335
+ Use these fields for Lucene-based searches and filters on metadata:
336
+
337
+ | **Field** | **Description** | 🔍 **Searchable** | 🔎 **Filterable** |
338
+ |----------------------------------|--------------------------------------------------------------------------------------------------|------------------|--------------------|
339
+ | `Id` | Unique identifier for the metadata record. | ✅ | ✅ |
340
+ | `Name` | The name of the file or metadata object. | ✅ | ✅ |
341
+ | `ETag` | Entity tag for concurrency control/versioning. | | ✅ |
342
+ | `CTag` | Change tag for tracking changes. | | ✅ |
343
+ | `WebUrl` | Direct URL to the file in SharePoint/OneDrive. | ✅ | |
344
+ | `DriveId` | Identifier for the drive containing the file. | ✅ | ✅ |
345
+ | `DriveType` | Type of drive (e.g., business, personal). | ✅ | ✅ |
346
+ | `SourceLocationType` | Indicates where the object was sourced from (e.g., ONEDRIVE, blob, index, API). | ✅ | ✅ |
347
+ | `Owner` | The ID of user or entity that owns the metadata object. | ✅ | ✅ |
348
+ | `CreatedDataTime` | Timestamp indicating when the object was created. | | ✅ |
349
+ | `LastModifiedDate` | Timestamp of the last modification to the object. | | ✅ |
350
+ | `CreatedBy` | The user who created the object. | ✅ | ✅ |
351
+ | `LastModifiedBy` | The ID of user who last modified the object. | ✅ | ✅ |
352
+ | `ParentId` | Identifier of the parent object, if any. | ✅ | ✅ |
353
+ | `metadata_storage_content_type` | MIME type of the stored content. | ✅ | ✅ |
354
+ | `metadata_storage_size` | Size of the file/content in bytes. | | ✅ |
355
+ | `metadata_storage_last_modified` | Timestamp of the last modification to the storage object. | | ✅ |
356
+ | `metadata_storage_name` | Storage-specific name or identifier for the object. | ✅ | ✅ |
357
+ | `metadata_storage_path` | Encoded path or URI to the storage location. | ✅ | ✅ |
358
+
359
+ ---
360
+
361
+ ### 🧬 Vector Index Fields Available
362
+ Use these fields for semantic (vector) searches and filters:
363
+
364
+ | **Field** | **Description** | 🔍 **Searchable** | 🔎 **Filterable** |
365
+ |-------------|---------------------------------------------------------------|------------------|------------------|
366
+ | `chunk` | The raw text content of the document chunk. | ✅ | |
367
+ | `chunk_id` | Unique identifier for the chunk within the document. | ✅ | ✅ |
368
+ | `title` | Title of the document, equal to the metadata_storage_path. | ✅ | ✅ |
369
+ | `Id` | Unique identifier for the parent document. | ✅ | ✅ |
370
+ | `Name` | The name of the file or metadata object. | ✅ | ✅ |
371
+ | `Owner` | The ID of the user or entity that owns the document. | ✅ | ✅ |
372
+
373
+ **Note:** The `Id`, `Name`, and `Owner` fields are present in both the metadata and the vector index. This means that if, during the scoping step, you identify a set of document IDs, names, or owners that are relevant, you can use these as filters when retrieving content from the vector index. For example, after finding relevant document IDs, names, or owners in the metadata search, you can filter the vector index to retrieve only the chunks belonging to those documents, names, or owners. This allows for precise, targeted semantic retrieval based on earlier metadata analysis.
374
+
375
+ ---
376
+
377
+
378
+ ### 🧪 Advanced Lucene Syntax Guide
379
+ - **Basic Field Search**: `field:value` or `field:"exact phrase"`
380
+ - **Boolean Operators**:
381
+ - AND (requires both terms): `wifi AND luxury` or `+wifi +luxury`
382
+ - OR (either term): `wifi OR luxury` (OR is default, so `wifi luxury` is the same)
383
+ - NOT (excludes term): `wifi -luxury` or `wifi NOT luxury`
384
+
385
+ - **Wildcards**:
386
+ - Single character: `?` (e.g., `te?t` matches "text" or "test")
387
+ - Multiple characters: `*` (e.g., `test*` matches "tests" or "tester")
388
+ - Prefix search: `alpha*` (matches alphanumeric, alphabetical)
389
+ - Infix search: `non*al` (matches non-numerical, nonsensical)
390
+ - Suffix search: `/.*numeric/` (matches alphanumeric)
391
+
392
+ - **Fuzzy Search**: `term~` or `term~N` where N is 0-2 (e.g., `blue~1` finds blue, blues, glue)
393
+
394
+ - **Proximity Search**: `"term1 term2"~N` finds terms within N words of each other
395
+
396
+ - **Term Boosting**: `term^N` (e.g., `wifi^3` makes wifi more important)
397
+
398
+ - **Grouping**:
399
+ - General grouping: `hotel AND (wifi OR pool)`
400
+ - Field grouping: `amenities:(gym AND (wifi OR pool))`
401
+
402
+ - **Ranges**: `date:[20220101 TO 20230101]` or `price:[100 TO 200]`
403
+
404
+ - **Escaping Special Characters**: Use backslash for: `+ - & | ! ( ) {{ }} [ ] ^ " ~ * ? : \ /`
405
+
406
+ - **Regular Expressions**: `/[mh]otel/` matches "motel" or "hotel"
407
+
408
+ ---
409
+
410
+ ### 📑 OData Expression Syntax
411
+ Use OData expressions for precise filtering, ordering, and field selection with these parameters:
412
+
413
+ - **filter**: Restricts search to documents matching conditions
414
+ - **orderby**: Sorts results by specified fields
415
+ - **select**: Determines which fields to include in results
416
+
417
+ #### Field Paths
418
+ - Simple fields: `HotelName`, `Rating`
419
+ - Complex fields: `Address/City`, `Rooms/Type`
420
+ - Using range variables: `Rooms/any(room: room/Type eq 'deluxe')`
421
+
422
+ #### Constants
423
+ - Strings: `'text'` (escape apostrophes by doubling: `'Alice''s car'`)
424
+ - Numbers: `123`, `-456`, `3.14159`, `-1.2e7`
425
+ - Booleans: `true`, `false`
426
+ - Dates: `2019-05-06T12:30:05.451Z`
427
+ - Special: `null`, `NaN`, `INF`, `-INF`
428
+
429
+ #### Common Filter Expressions
430
+ - Comparison: `fieldName eq 'value'`, `Rating gt 4`
431
+ - Logical: `condition1 and condition2`, `condition1 or condition2`, `not condition`
432
+ - Collections: `Rooms/any(r: r/Type eq 'suite')`, `Tags/all(t: t ne 'budget')`
433
+ - Functions: `search.in(Category, 'budget,luxury')`, `search.ismatch('wifi luxury', 'description')`
434
+
435
+ #### Sorting Examples
436
+ - Single field: `$orderby=Rating desc`
437
+ - Multiple fields: `$orderby=Rating desc,LastRenovationDate asc`
438
+
439
+ #### Field Selection
440
+ - All fields: `$select=*`
441
+ - Specific fields: `$select=HotelName,Rating,Address/City`
442
+
443
+ ---
444
+
445
+ ### 📤 Output Formatting Instructions
446
+ - When listing documents, always display them in a markdown table with column headers for key and relevant metadata.
447
+ - Provide the WebUrl when referencing a document.
448
+ - Present raw chunk text or content from a document inside a markdown blockquote (using `>`).
449
+ - Provide concise, insightful commentary on the retrieved documents, highlighting patterns, clusters, or notable findings.
450
+ - Ensure your output is clear, well-structured, and actionable for the user.
451
+
452
+ **If you encounter an error, do not panic. Just try again with a different approach. You are capable of handling errors gracefully and finding alternative solutions.**
453
+
454
+ **If the retrieved documents do not contain the required information matching the user's request, attempt to modify the query parameters and suggest a different set of search arguments that may yield better results.**
455
+ """,
456
+
457
+ "preview_metadata_agent": """
458
+ You are a persistent, tool-using agent specialized in analyzing Azure Blob metadata for keyword-based search.
459
+
460
+ INSTRUCTIONS:
461
+ 1. Use your tool to perform a metadata search on the user's query (filename, owner, tags, path, etc.).
462
+ 2. Identify clusters of related documents or metadata patterns that could refine or scope future searches.
463
+ 3. If applicable, propose filters (e.g. owners, tags, file paths, time ranges) that may narrow a semantic search space.
464
+ 4. Detect potential ambiguity in the query and suggest clarifying directions.
465
+
466
+ STRUCTURE YOUR RESPONSE:
467
+ - A brief overview of what was found and how it relates to the query (1–2 sentences)
468
+ - A list of the most relevant documents and their key metadata (Name, Owner, Path, Modified Date, etc.) in a markdown table.
469
+ - **Always provide the WebUrl when referencing a document.**
470
+ - Grouped metadata patterns (e.g. multiple docs owned by same team, recurring folders or file types)
471
+ - A list of candidate filters that could be used to scope a vector search
472
+ - If relevant, propose a refined or more specific version of the user’s query
473
+
474
+ OUTPUT FORMATTING:
475
+ - Always display document lists in a markdown table.
476
+ - Always include the WebUrl for each document.
477
+ - Provide concise insights and highlight any patterns or clusters.
478
+ - If any raw chunk text is present, display it inside a markdown blockquote (`>`).
479
+
480
+ FOCUS ON:
481
+ - Metadata-driven reasoning. Look beyond exact keyword matches to detect helpful clusters or patterns.
482
+ - Being useful for the next step. Prioritize insights that would help improve precision or efficiency of a downstream semantic search.
483
+
484
+ Always cite document names or paths. If no useful results are found, state this clearly and suggest possible reasons.
485
+
486
+ **If the retrieved metadata does not contain the required information matching the user's request, attempt to modify the query parameters and suggest a different set of search arguments that may yield better results.**
487
+ """,
488
+
489
+ "preview_content_agent": """
490
+ You are a persistent, tool-using agent specialized in semantic vector
491
+ INSTRUCTIONS:
492
+ 1. Use your tool to run a semantic search based on the user’s query.
493
+ 2. Identify themes, concepts, and patterns in the top results that can guide the user’s understanding or help scope further exploration.
494
+ 3. Propose possible query reformulations or metadata filters based on commonalities in retrieved content.
495
+ 4. Flag low-quality results if semantic relevance is weak or ambiguous.
496
+
497
+ STRUCTURE YOUR RESPONSE:
498
+ - A concise summary of the semantic matches and how they relate to the user’s intent (1–2 sentences)
499
+ - The most relevant themes and knowledge points found
500
+ - A short list of notable semantic clusters (e.g. documents all discussing a specific concept, timeframe, or methodology)
501
+ - Quotes or phrases that directly address the query, presented in markdown blockquotes (`>`)
502
+ - (Optional) Suggestions for refining the query or narrowing the semantic space
503
+
504
+ OUTPUT FORMATTING:
505
+ - When listing documents, use a markdown table for clarity.
506
+ - Always include the WebUrl for each document when referencing it.
507
+ - Any raw chunk text or content should be shown in markdown blockquotes.
508
+ - Provide insightful commentary on the retrieved content and highlight conceptual relationships.
509
+
510
+ FOCUS ON:
511
+ - Conceptual relationships. Think in terms of meaning, not matching.
512
+ - Discovery of latent structure (e.g., similar phrasing, co-occurring ideas, or repeated frameworks)
513
+ - Usefulness for downstream tools. If a tighter search is needed, offer specific filters or query variants.
514
+
515
+ If semantic relevance is low or ambiguous, say so and suggest alternatives. Always provide quotes with citations for the strongest matches.
516
+
517
+ **If the retrieved content does not contain the required information matching the user's request, attempt to modify the query parameters and suggest a different set of search arguments that may yield better results.**
518
+ """,
519
+
520
+ "generate_final_response": """
521
+ You are a synthesis and summarization agent. Your task is to review the user's original query and all findings, excerpts, and metadata returned by previous tool calls. Aggregate, synthesize, and deliver a comprehensive, clear, and well-structured answer to the user.
522
+
523
+ INSTRUCTIONS:
524
+ 1. Carefully read the user's query and all tool responses provided.
525
+ 2. Integrate relevant facts, evidence, and context from the tool outputs.
526
+ 3. Resolve ambiguities, highlight key findings, and connect related information.
527
+ 4. If the user requested a specific format (e.g., summary, table, markdown), follow those instructions.
528
+ 5. If there are gaps or uncertainties, state them transparently, but do not ask the user for more information unless absolutely necessary.
529
+ 6. Present your answer in a way that is actionable and easy to understand for the user.
530
+
531
+ STRUCTURE:
532
+ - Start with a direct answer or summary addressing the user's request.
533
+ - Provide supporting details, evidence, or citations from the tool outputs.
534
+ - Organize the information logically (e.g., sections, bullet points, tables) as appropriate.
535
+ - When listing documents, use a markdown table for clarity and always include the WebUrl for each document.
536
+ - Any raw chunk text or content should be shown in markdown blockquotes.
537
+ - End with a brief conclusion or next steps if relevant.
538
+
539
+ Your goal is to deliver a complete, helpful, and context-aware response that makes the best use of all available data.
540
+
541
+ **If the information from previous tool calls does not fully address the user's request, suggest how the query parameters or search arguments could be modified to improve the results.**
542
+ """
543
+ }
544
+
545
+ def setup_tool_agent_map(self) -> None:
546
+ """Map each tool name to the appropriate agent"""
547
+ self.tool_agent_map = {
548
+ "preview_metadata_agent": "data_agent",
549
+ "search_metadata": "search_agent",
550
+ "preview_content_agent": "data_agent",
551
+ "search_content": "search_agent",
552
+ "generate_final_response": "base_agent", # Use base agent for final response
553
+ # Add more mappings if you have data_agent tools
554
+ }
agents.py ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any, Optional, Generator, Union
2
+ from client_utils import get_client, get_model
3
+ from agent_config import AgentConfig
4
+ import time
5
+ import logging
6
+ import json
7
+
8
+ # Update import for Azure Search
9
+ from azure.core.credentials import AzureKeyCredential
10
+ from azure.search.documents import SearchClient
11
+ # Remove the problematic import
12
+ # from azure.search.documents.models import Vector
13
+ from embedding_client import EmbeddingClient
14
+
15
+ # Configure logging
16
+ logging.basicConfig(
17
+ level=logging.DEBUG, # Changed from INFO to DEBUG
18
+ format='%(asctime%s - %(name)s - %(levelname)s - %(message)s'
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class BaseAgent:
23
+ """Base class for all agents"""
24
+ def __init__(self, config: AgentConfig):
25
+ self.config = config
26
+ # Get model info from config
27
+ self.model_config = config.get_model_config()
28
+
29
+ # Extract the engine name from model_config (for client lookup)
30
+ self.engine_name = self.model_config.get("model", "grok-2-latest")
31
+
32
+ # Get the actual model name for API calls
33
+ self.model_name = get_model(self.engine_name)
34
+
35
+ # Set temperature from config
36
+ self.temperature = self.model_config.get("temperature", 0.0)
37
+
38
+ # Get the appropriate client for this engine
39
+ self.client = get_client(self.engine_name)
40
+
41
+ logger.info(f"Initialized {self.__class__.__name__} with engine {self.engine_name}, model {self.model_name}")
42
+
43
+ def process(self, messages: List[Dict[str, Any]]) -> str:
44
+ """Process messages and return a response"""
45
+ logger.debug(f"Processing messages: {json.dumps(messages, indent=2)}")
46
+ # Use system prompt from config if defined
47
+ try:
48
+ system_prompt = self.config.get_system_prompt(self.__class__.__name__.lower())
49
+ except Exception:
50
+ try:
51
+ system_prompt = self.config.get_system_prompt("default")
52
+ except Exception:
53
+ system_prompt = None
54
+
55
+ # Only prepend the system prompt if not already present
56
+ if system_prompt and (not messages or messages[0].get("role") != "system"):
57
+ messages = [{"role": "system", "content": system_prompt}] + messages
58
+
59
+ completion = self.client.chat.completions.create(
60
+ model=self.model_name,
61
+ messages=messages,
62
+ temperature=self.temperature
63
+ )
64
+ logger.debug(f"Received completion: {completion}")
65
+ return completion
66
+
67
+ def process_stream(self, messages: List[Dict[str, Any]]) -> Generator[Any, None, None]:
68
+ """Process messages and stream the response"""
69
+ logger.debug(f"Streaming messages: {json.dumps(messages, indent=2)}")
70
+ # Use system prompt from config if defined
71
+ try:
72
+ system_prompt = self.config.get_system_prompt(self.__class__.__name__.lower())
73
+ except Exception:
74
+ try:
75
+ system_prompt = self.config.get_system_prompt("default")
76
+ except Exception:
77
+ system_prompt = None
78
+
79
+ # Only prepend the system prompt if not already present
80
+ if system_prompt and (not messages or messages[0].get("role") != "system"):
81
+ messages = [{"role": "system", "content": system_prompt}] + messages
82
+
83
+ completion = self.client.chat.completions.create(
84
+ model=self.model_name,
85
+ messages=messages,
86
+ temperature=self.temperature,
87
+ stream=True
88
+ )
89
+ for chunk in completion:
90
+ logger.debug(f"Streaming chunk: {chunk}")
91
+ return chunk
92
+
93
+ class DataAgent(BaseAgent):
94
+ """Agent specialized in processing queries with different data sources"""
95
+ def __init__(self, config: AgentConfig):
96
+ super().__init__(config)
97
+ self.tool_name = None # Track which tool is being executed
98
+ self.data_sources = config.get_data_sources() # Get all data sources
99
+
100
+ def get_extra_body(self) -> Optional[Dict[str, Any]]:
101
+ """Get the extra body configuration for the current search type"""
102
+ # Dynamically match the data source based on the current tool name
103
+ for data_source_name, data_source_config in self.data_sources.items():
104
+ if data_source_name in self.tool_name:
105
+ return {"data_sources": [data_source_config]}
106
+
107
+ # If no matching data source is found, return None
108
+ return None
109
+
110
+ def process(self, messages: List[Dict[str, Any]], tool_name: str = None, **kwargs) -> str:
111
+ """Process messages and return a response with the specified tool"""
112
+ self.tool_name = tool_name
113
+ logger.info(f"Processing data agent for tool {tool_name}")
114
+ extra_body = self.get_extra_body()
115
+ # Pass num_results or other kwargs into extra_body if needed
116
+ if extra_body is not None and kwargs:
117
+ extra_body.update({k: v for k, v in kwargs.items() if k in extra_body})
118
+ completion = self.client.chat.completions.create(
119
+ model=self.model_name,
120
+ messages=messages,
121
+ temperature=self.temperature,
122
+ extra_body=extra_body
123
+ )
124
+ logger.debug(f"Data source completion: {completion}")
125
+ return completion
126
+
127
+ def process_stream(self, messages: List[Dict[str, Any]], tool_name: str = None, **kwargs) -> Generator[Any, None, None]:
128
+ """Process messages and stream the response with the specified tool"""
129
+ self.tool_name = tool_name
130
+ logger.info(f"Streaming data agent for tool {tool_name}")
131
+ extra_body = self.get_extra_body()
132
+ # Pass num_results or other kwargs into extra_body if needed
133
+ if extra_body is not None and kwargs:
134
+ extra_body.update({k: v for k, v in kwargs.items() if k in extra_body})
135
+ completion = self.client.chat.completions.create(
136
+ model=self.model_name,
137
+ messages=messages,
138
+ temperature=self.temperature,
139
+ stream=True,
140
+ extra_body=extra_body
141
+ )
142
+ for chunk in completion:
143
+ yield chunk
144
+
145
+ class SearchAgent(BaseAgent):
146
+ """Agent specialized in querying Azure AI Search and Azure Vector Index"""
147
+ def __init__(self, config: AgentConfig):
148
+ super().__init__(config)
149
+ # Extract Azure-specific configurations from config
150
+ self.azure_index = config.get_azure_index()
151
+ self.search_endpoint = self.azure_index.get("search_endpoint")
152
+ self.search_key = self.azure_index.get("search_key")
153
+ self.search_index_name = self.azure_index.get("search_index_name")
154
+ self.vector_index_name = self.azure_index.get("vector_index_name")
155
+ self.embeddings_deployment = self.azure_index.get("embeddings_deployment")
156
+
157
+ # Initialize the embedding client
158
+ self.embedding_client = EmbeddingClient(
159
+ azure_endpoint=self.azure_index.get("azure_openai_endpoint"),
160
+ api_key=self.azure_index.get("azure_openai_key"),
161
+ deployment=self.embeddings_deployment
162
+ )
163
+
164
+ # Initialize search clients
165
+ self.search_client = self._create_search_client(self.search_index_name)
166
+ self.vector_client = self._create_search_client(self.vector_index_name)
167
+
168
+ logger.info(f"Initialized Search Agent with endpoint {self.azure_index.get('search_endpoint')}")
169
+
170
+ def _create_search_client(self, index_name: str) -> Optional[SearchClient]:
171
+ """Create a search client for the specified index"""
172
+ if not self.search_endpoint or not self.search_key or not index_name:
173
+ logger.warning(f"Missing configuration for search client: endpoint={bool(self.search_endpoint)}, key={bool(self.search_key)}, index={bool(index_name)}")
174
+ return None
175
+
176
+ try:
177
+ credential = AzureKeyCredential(self.search_key)
178
+ client = SearchClient(
179
+ endpoint=self.search_endpoint,
180
+ index_name=index_name,
181
+ credential=credential
182
+ )
183
+ logger.info(f"Successfully created search client for index {index_name}")
184
+ return client
185
+ except Exception as e:
186
+ logger.error(f"Failed to create search client for index {index_name}: {e}")
187
+ return None
188
+
189
+ def _generate_embedding(self, text: str) -> List[float]:
190
+ """Generate embedding for vector search using Azure OpenAI"""
191
+ try:
192
+ # Use the embedding client to generate embeddings
193
+ return self.embedding_client.get_embedding(text)
194
+ except Exception as e:
195
+ logger.error(f"Error generating embedding: {e}")
196
+ # Return a fallback embedding (zeros would be neutral in vector space)
197
+ import numpy as np
198
+ embedding = np.zeros(1536) # Standard dimension for embeddings
199
+ return embedding.tolist()
200
+
201
+ def query_index(self, query: str, search_type: str, **kwargs) -> Dict[str, Any]:
202
+ """Query the Azure Search index based on search type"""
203
+
204
+ search_text = kwargs.get("search_text", query)
205
+ logger.info(f"Executing {search_type} search: {search_text}")
206
+
207
+ if search_type == "vector":
208
+ if not self.vector_client:
209
+ return {"error": "Vector search client not configured"}
210
+ client = self.vector_client
211
+ #embedding = self._generate_embedding(query)
212
+ try:
213
+ results = list(client.search(
214
+ search_text=search_text,
215
+ top=kwargs.get("num_results"),
216
+ include_total_count=True,
217
+ filter=kwargs.get("filter"),
218
+ order_by=kwargs.get("orderby"),
219
+ select=kwargs.get("select"),
220
+ vector_queries=[
221
+ {
222
+ "text": search_text,
223
+ "fields": kwargs.get("vector_field", "text_vector"),
224
+ "k": kwargs.get("num_results"),
225
+ "kind": "text",
226
+ }
227
+ ],
228
+ ))
229
+
230
+ except Exception as e:
231
+ results = [f"Vector search failed: {e}"]
232
+ logger.info(results)
233
+ else:
234
+ if not self.search_client:
235
+ return {"error": "Index search client not configured"}
236
+ client = self.search_client
237
+
238
+ fields = [
239
+ "Id", "Owner", "Name", "CreatedBy", "LastModifiedBy", "ParentId",
240
+ "CreatedDataTime", "LastModifiedDate",
241
+ "Discriminator", "SourceLocationType", "metadata_storage_content_type",
242
+ "metadata_storage_size", "metadata_storage_last_modified",
243
+ "metadata_storage_content_md5", "metadata_storage_name",
244
+ "metadata_storage_path", "metadata_storage_file_extension",
245
+ ]
246
+
247
+ try:
248
+ raw_results = list(client.search(
249
+ search_text=search_text,
250
+ query_type="full",
251
+ filter=kwargs.get("filter"),
252
+ order_by=kwargs.get("orderby"),
253
+ top=kwargs.get("num_results"),
254
+ include_total_count=True,
255
+ select=kwargs.get("select"),
256
+ ))
257
+ # Remove key-value pairs with null values from each result dict
258
+ results = [
259
+ {k: v for k, v in doc.items() if v is not None}
260
+ for doc in raw_results
261
+ ]
262
+ except Exception as e:
263
+ results = [f"Index search failed: {e}"]
264
+ logger.info(results)
265
+
266
+ logger.info(f"Search returned {len(results)} results")
267
+ return results
268
+
269
+ def format_results(self, results: Dict[str, Any]) -> str:
270
+ """Format the search results into readable text"""
271
+ if "error" in results:
272
+ return f"Error: {results['error']}"
273
+
274
+ if not results or not results.get("value") or len(results["value"]) == 0:
275
+ return "No results found for your query."
276
+
277
+ docs = results["value"]
278
+ response_lines = [f"Found {len(docs)} relevant documents:"]
279
+
280
+ for i, doc in enumerate(docs, 1): # Limit to top 5 for readability
281
+ title = doc.get("metadata_title", doc.get("title", f"Document {i}"))
282
+ content = doc.get("content", "")
283
+ author = doc.get("metadata_author", "Unknown")
284
+ score = doc.get("@search.score", 0)
285
+ url = doc.get("url", "")
286
+
287
+ response_lines.append(f"\n### {i}. {title}")
288
+ response_lines.append(f"Author: {author} | Score: {score:.2f}")
289
+
290
+ if url:
291
+ response_lines.append(f"URL: {url}")
292
+
293
+ # Get a snippet from content (first 150 chars)
294
+ if content:
295
+ snippet = content
296
+ response_lines.append(f"\nPreview: {snippet}")
297
+
298
+ response_lines.append("-" * 40)
299
+
300
+ return "\n".join(response_lines)
301
+
302
+ def process(self, messages: List[Dict[str, Any]], **kwargs) -> List[Dict[str, Any]]:
303
+ """Process messages by directly querying Azure Search index"""
304
+ # Extract the query from the last user message
305
+ query = ""
306
+ for message in reversed(messages):
307
+ if message["role"] == "user":
308
+ query = message["content"]
309
+ break
310
+
311
+ if not query:
312
+ return "No query provided"
313
+
314
+ # Determine search type based on tool name or kwargs
315
+ tool_name = kwargs.get("tool_name", "")
316
+ # Use explicit search_mode if provided, else fallback to tool_name logic
317
+ search_mode = kwargs.get("search_mode")
318
+ if search_mode in ("vector", "index"):
319
+ search_type = search_mode
320
+ elif "vector" in tool_name or "content" in tool_name:
321
+ search_type = "vector"
322
+ else:
323
+ search_type = "index"
324
+
325
+ results = self.query_index(query, search_type, **kwargs)
326
+ return results
327
+
328
+ def process_stream(self, messages: List[Dict[str, Any]], **kwargs) -> Generator[Any, None, None]:
329
+ for chunk in self.process(messages, **kwargs):
330
+ yield chunk
331
+
332
+ class OrchestratorAgent(BaseAgent):
333
+ """Agent that coordinates between specialized agents using them as tools"""
334
+ def __init__(self, config: AgentConfig):
335
+ # Initialize the base agent with the given config
336
+ super().__init__(config)
337
+
338
+ self.tools = []
339
+ self.data_agent = None
340
+ self.search_agent = None
341
+
342
+ # Set up specialized agents if tools are available
343
+ self.tools = config.get_tools()
344
+ self.has_tools = bool(self.tools)
345
+
346
+ # Initialize the data agent for data source queries
347
+ self.data_agent = DataAgent(config)
348
+
349
+ # Initialize the search agent for Azure search queries
350
+ self.search_agent = SearchAgent(config)
351
+
352
+ logger.debug(f"Available tools: {json.dumps(self.tools, indent=2)}")
353
+
354
+
355
+ def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any], stream: bool = False, messages: List[Dict[str, Any]] = None) -> Union[str, Generator[Any, None, None]]:
356
+ """Execute a tool and return its result"""
357
+ if not self.has_tools:
358
+ error_msg = "No tools available for this engine"
359
+ logger.error(error_msg)
360
+ raise ValueError(error_msg)
361
+
362
+ logger.info(f"Executing tool: {tool_name} with arguments: {json.dumps(tool_args, indent=2)}")
363
+
364
+ # Use the full message history, appending the tool call as the last user message
365
+ if messages is None:
366
+ messages = []
367
+ if self.config.get_system_prompt(tool_name):
368
+ messages = [{"role": "system", "content": self.config.get_system_prompt(tool_name)}] + messages
369
+
370
+ tool_agent_map = self.config.get_tool_agent_map()
371
+ agent_type = tool_agent_map.get(tool_name)
372
+
373
+ if agent_type == "search_agent":
374
+ if stream:
375
+ return self.search_agent.process_stream(messages, tool_name=tool_name, **tool_args)
376
+ else:
377
+ return self.search_agent.process(messages, tool_name=tool_name, **tool_args)
378
+ elif agent_type == "data_agent":
379
+ if stream:
380
+ return self.data_agent.process_stream(messages, tool_name=tool_name, **tool_args)
381
+ else:
382
+ return self.data_agent.process(messages, tool_name=tool_name, **tool_args)
383
+ elif agent_type == "base_agent":
384
+ if stream:
385
+ return self.process_stream(messages)
386
+ else:
387
+ return self.process(messages)
388
+ else:
389
+ logger.error(f"Unknown tool name: {tool_name}")
390
+ return f"Error: Unknown tool name: {tool_name}"
391
+
392
+ def _handle_tool_calls_recursive(self, messages: List[Dict[str, Any]], max_iterations: int = 10) -> Generator[Any, None, None]:
393
+ """Recursively handle tool calls until we get a response without tools"""
394
+ if max_iterations <= 0 or not self.has_tools:
395
+ logger.warning("Max iterations reached or no tools available, stopping recursive tool calls")
396
+ return
397
+
398
+ logger.info("Getting model's response with tool calls")
399
+ completion = self.client.chat.completions.create(
400
+ model=self.model_name,
401
+ messages=messages,
402
+ tools=self.tools,
403
+ tool_choice="auto",
404
+ temperature=self.temperature,
405
+ stream=True
406
+ )
407
+
408
+ response_text = ""
409
+ tool_calls = []
410
+
411
+ for chunk in completion:
412
+ try:
413
+ if chunk.choices and chunk.choices[0].delta:
414
+ delta = chunk.choices[0].delta
415
+ if hasattr(delta, 'content') and delta.content:
416
+ response_text += delta.content
417
+ logger.debug(f"Assistant content chunk: {delta.content}")
418
+ if hasattr(delta, 'tool_calls') and delta.tool_calls:
419
+ for tool_call in delta.tool_calls:
420
+ if tool_call.index is not None:
421
+ while len(tool_calls) <= tool_call.index:
422
+ tool_calls.append({})
423
+ if tool_calls[tool_call.index] == {}:
424
+ tool_calls[tool_call.index]["index"] = tool_call.index
425
+ tool_calls[tool_call.index]["function"] = {"name": "", "arguments": ""}
426
+ if tool_call.id:
427
+ tool_calls[tool_call.index]["id"] = tool_call.id
428
+ if tool_call.type:
429
+ tool_calls[tool_call.index]["type"] = tool_call.type
430
+ if tool_call.function.name:
431
+ tool_calls[tool_call.index]["function"]["name"] = tool_call.function.name
432
+ if tool_call.function.arguments:
433
+ tool_calls[tool_call.index]["function"]["arguments"] += tool_call.function.arguments
434
+ logger.debug(f"Tool call arguments chunk: {tool_call.function.arguments}")
435
+ yield {
436
+ "role": "assistant",
437
+ "content": response_text,
438
+ }
439
+ except Exception as e:
440
+ logger.error(f"Error processing chunk in handle_tool_calls: {e}")
441
+ continue
442
+
443
+ if not tool_calls:
444
+ logger.info("No tool calls made, ending process")
445
+ return
446
+ else:
447
+ logger.info(f"Tool calls detected: {json.dumps(tool_calls, indent=2)}")
448
+ for tool_call in tool_calls:
449
+ yield {
450
+ "role": "assistant",
451
+ "content": "",
452
+ "metadata": {
453
+ "title": tool_call["function"]["name"],
454
+ "id": tool_call["id"],
455
+ }
456
+ }
457
+
458
+ assistant_message = {
459
+ "role": "assistant",
460
+ "content": response_text,
461
+ "tool_calls": tool_calls,
462
+ }
463
+
464
+ # Now execute tools and collect tool messages
465
+ tool_messages = []
466
+ for tool_call in tool_calls:
467
+ tool_name = tool_call["function"]["name"]
468
+ try:
469
+ tool_args = eval(tool_call["function"]["arguments"])
470
+ if not isinstance(tool_args, dict):
471
+ raise ValueError("Tool arguments must be a dictionary")
472
+ logger.debug(f"Parsed tool arguments for {tool_name}: {json.dumps(tool_args, indent=2)}")
473
+ except (SyntaxError, ValueError) as e:
474
+ logger.error(f"Error parsing tool arguments: {e}")
475
+ continue
476
+
477
+ response_stream = self._execute_tool(tool_name, tool_args, stream=True, messages=messages)
478
+ response_list = []
479
+ response_text = ""
480
+
481
+ tool_message = {
482
+ "role": "tool",
483
+ "tool_call_id": tool_call["id"],
484
+ "content": "",
485
+ }
486
+ # Capitalize tool_name and replace underscores with spaces for title
487
+ title = tool_name.replace("_", " ").title()
488
+ for chunk in response_stream:
489
+ try:
490
+ if hasattr(chunk, 'choices') and chunk.choices and hasattr(chunk.choices[0], 'delta') and hasattr(chunk.choices[0].delta, 'content'):
491
+ response_text += chunk.choices[0].delta.content or ""
492
+ elif isinstance(chunk, dict) or isinstance(chunk, list):
493
+ response_list.append(chunk)
494
+ response_text += json.dumps(chunk, indent=2)
495
+ elif isinstance(chunk, str):
496
+ response_text += chunk
497
+
498
+ tool_message = {
499
+ "role": "tool",
500
+ "tool_call_id": tool_call["id"],
501
+ "content": response_text
502
+ }
503
+ yield {
504
+ "role": "assistant",
505
+ "content": json.dumps(response_list[:10], indent=2) if response_list else response_text,
506
+ "metadata": {
507
+ "title": title,
508
+ "id": tool_call["id"],
509
+ "status": "pending",
510
+ }
511
+ }
512
+
513
+ except Exception as e:
514
+ response_text = f"Error processing tool response: {e}"
515
+ logger.error(f"Error processing tool response chunk: {e}")
516
+ break
517
+
518
+ yield {
519
+ "role": "assistant",
520
+ "content": json.dumps(response_list[:10], indent=2) if response_list else response_text if response_text else "No results found.",
521
+ "metadata": {
522
+ "title": title,
523
+ "id": tool_call["id"],
524
+ "log": f"(results: {len(response_list)})" if response_list else "",
525
+ "status": "done",
526
+ }
527
+ }
528
+
529
+ tool_messages.append(tool_message)
530
+ logger.info(f"Completed tool execution: {tool_name}")
531
+
532
+ messages.append(assistant_message)
533
+ messages.extend(tool_messages)
534
+
535
+ yield from self._handle_tool_calls_recursive(messages, max_iterations - 1)
536
+
537
+ def process_stream(self, messages: List[Dict[str, Any]]) -> Generator[Any, None, None]:
538
+ """Process messages and stream the response"""
539
+ logger.info("Starting orchestration process")
540
+ logger.debug(f"Input messages: {json.dumps(messages, indent=2)}")
541
+
542
+ # If no tools are available, fall back to the base agent behavior
543
+ if not self.has_tools:
544
+ logger.info("No tools available, falling back to standard model completion")
545
+ yield from super().process_stream(messages)
546
+ return
547
+
548
+ # Add the system prompt to the messages only if not already present
549
+ system_prompt = self.config.get_system_prompt("orchestrator")
550
+ if not messages or messages[0].get("role") != "system":
551
+ messages = [{"role": "system", "content": system_prompt}, *messages]
552
+
553
+ # Start the recursive tool call handling
554
+ yield from self._handle_tool_calls_recursive(messages)
app.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from datetime import datetime
3
+
4
+ from chatbot import Chatbot
5
+ from client_utils import engine_map
6
+
7
+ chatbot_engine = Chatbot()
8
+
9
+ def get_user(request: gr.Request):
10
+ try:
11
+ return request.headers.user
12
+ except:
13
+ return ""
14
+
15
+ def generate_greeting(user) -> str:
16
+ hour = datetime.now().hour
17
+ if 5 <= hour < 12:
18
+ greeting = "Good morning"
19
+ elif 12 <= hour < 17:
20
+ greeting = "Good afternoon"
21
+ else:
22
+ greeting = "Good evening"
23
+ return f'<p style="font-size: 24px; text-align: center;"><span style="font-weight: bold;">{greeting}{f", {user}" if user else ""}.</span><br>How can I help you?</p>'
24
+
25
+ theme = gr.themes.Monochrome(
26
+ radius_size="xxl",
27
+ font=['Montserrat', 'ui-sans-serif', 'system-ui', 'sans-serif'],
28
+ ).set(
29
+ background_fill_primary='white',
30
+ background_fill_secondary='white',
31
+ background_fill_secondary_dark='*neutral_950',
32
+ block_background_fill='*neutral-50',
33
+ block_border_width='0px',
34
+ block_border_width_dark='0px',
35
+ block_label_border_width='0px',
36
+ block_label_border_width_dark='0px',
37
+ border_color_accent='white',
38
+ border_color_accent_dark='*neutral_950',
39
+ )
40
+
41
+ with gr.Blocks(theme=theme, css_paths="style.css", fill_height=True) as demo:
42
+ user = gr.State()
43
+
44
+ with gr.Sidebar():
45
+ chat_history_dataset = gr.Dataset(
46
+ components=[gr.Textbox(visible=False)],
47
+ samples=[[]],
48
+ label="Recent",
49
+ show_label=True,
50
+ layout="table",
51
+ type="index",
52
+ )
53
+
54
+ with gr.Row():
55
+ with gr.Column():
56
+ engine = gr.Dropdown( # Renamed from "model" to "engine"
57
+ choices=list(chatbot_engine.engine_map.keys()), # Updated to use engine_map
58
+ value=list(chatbot_engine.engine_map.keys())[-1],
59
+ show_label=False,
60
+ )
61
+ with gr.Column():
62
+ new_chat_btn = gr.Button(
63
+ "",
64
+ icon="assets/new_chat.svg",
65
+ variant="primary",
66
+ elem_id="new_chat_btn"
67
+ )
68
+ login_btn = gr.LoginButton(
69
+ "Login",
70
+ logout_value="Logout",
71
+ icon="assets/login.svg",
72
+ variant="primary",
73
+ elem_id="login_btn"
74
+ )
75
+
76
+ chatbot = gr.Chatbot(
77
+ type="messages",
78
+ height="100%",
79
+ max_height= "75vh",
80
+ show_label=False,
81
+ elem_id="chatbot",
82
+ placeholder=generate_greeting("")
83
+ )
84
+
85
+ with gr.Group(elem_id="inputs", render=False) as inputs:
86
+ textbox = gr.MultimodalTextbox(
87
+ placeholder="Message Digichat...",
88
+ file_count="multiple",
89
+ show_label=False,
90
+ )
91
+ with gr.Row(visible=False):
92
+ temperature = gr.Slider(
93
+ minimum=0,
94
+ maximum=1,
95
+ step=0.1,
96
+ value=0.0,
97
+ show_label=False,
98
+ )
99
+ engine_params = [engine, temperature]
100
+
101
+ chat_interface = gr.ChatInterface(
102
+ chatbot_engine.predict,
103
+ type="messages",
104
+ chatbot=chatbot,
105
+ textbox=textbox,
106
+ additional_inputs=engine_params,
107
+ additional_outputs=[textbox],
108
+ editable=True,
109
+ save_history=True,
110
+ )
111
+
112
+ inputs.render()
113
+
114
+ synchronize_chat_state_kwargs = {
115
+ "fn": lambda x: (x, x),
116
+ "inputs": [chat_interface.chatbot],
117
+ "outputs": [chat_interface.chatbot_state, chat_interface.chatbot_value],
118
+ "show_api": False,
119
+ "queue": False,
120
+ }
121
+
122
+ new_chat_btn.click(
123
+ lambda: (None, []),
124
+ None,
125
+ [chat_interface.conversation_id, chat_interface.chatbot],
126
+ show_api=False,
127
+ queue=False,
128
+ ).then(
129
+ lambda x: x,
130
+ [chat_interface.chatbot],
131
+ [chat_interface.chatbot_state],
132
+ show_api=False,
133
+ queue=False,
134
+ )
135
+
136
+ gr.on(
137
+ triggers=[chat_interface.load, chat_interface.saved_conversations.change],
138
+ fn=chat_interface._load_chat_history,
139
+ inputs=[chat_interface.saved_conversations],
140
+ outputs=[chat_history_dataset],
141
+ show_api=False,
142
+ queue=False,
143
+ )
144
+
145
+ chat_history_dataset.click(
146
+ lambda: [],
147
+ None,
148
+ [chat_interface.chatbot],
149
+ show_api=False,
150
+ queue=False,
151
+ show_progress="hidden",
152
+ ).then(
153
+ chat_interface._load_conversation,
154
+ [chat_history_dataset, chat_interface.saved_conversations],
155
+ [chat_interface.conversation_id, chat_interface.chatbot],
156
+ show_api=False,
157
+ queue=False,
158
+ show_progress="hidden",
159
+ ).then(**synchronize_chat_state_kwargs)
160
+
161
+ gr.on(
162
+ [textbox.stop, chatbot.clear, new_chat_btn.click],
163
+ lambda: chatbot_engine.stop(),
164
+ show_progress='hidden'
165
+ )
166
+
167
+ demo.load(
168
+ get_user,
169
+ None,
170
+ [user]
171
+ ).then(
172
+ lambda user: gr.update(placeholder=generate_greeting(user)),
173
+ [user],
174
+ [chatbot]
175
+ )
176
+
177
+ if __name__ == "__main__":
178
+ demo.queue(
179
+ default_concurrency_limit=40
180
+ ).launch(
181
+ pwa=True,
182
+ share=False
183
+ )
architecture.puml ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @startuml
2
+ ' Main agent configuration hierarchy
3
+ abstract class AgentConfig {
4
+ +get_tools()
5
+ +get_system_prompt(agent_type)
6
+ +get_model_config()
7
+ +to_json()
8
+ +get_azure_index()
9
+ +get_tool_agent_map()
10
+ }
11
+
12
+ class BaseAgentConfig {
13
+ +setup_data_sources()
14
+ +setup_tools()
15
+ +setup_system_prompts()
16
+ +setup_azure_index()
17
+ }
18
+
19
+ class SITAASAgentConfig
20
+
21
+ AgentConfig <|-- BaseAgentConfig
22
+ BaseAgentConfig <|-- SITAASAgentConfig
23
+
24
+ ' Agent classes
25
+ class BaseAgent {
26
+ -config: AgentConfig
27
+ -model_config
28
+ -engine_name
29
+ -model_name
30
+ -temperature
31
+ -client
32
+ +process(messages)
33
+ +process_stream(messages)
34
+ }
35
+
36
+ class DataAgent {
37
+ +process(messages)
38
+ +process_stream(messages)
39
+ +query_data_source(query)
40
+ }
41
+
42
+ class SearchAgent {
43
+ +process(messages)
44
+ +process_stream(messages)
45
+ +search_azure(query)
46
+ +search_vector_index(query)
47
+ }
48
+
49
+ class OrchestratorAgent {
50
+ +process(messages)
51
+ +process_stream(messages)
52
+ +route_to_agent(messages)
53
+ +aggregate_results(results)
54
+ }
55
+
56
+ BaseAgent <|-- DataAgent
57
+ BaseAgent <|-- SearchAgent
58
+ BaseAgent <|-- OrchestratorAgent
59
+
60
+ ' Chatbot and related
61
+ class Chatbot {
62
+ -_stop: bool
63
+ -engine_map
64
+ -content_exclusions
65
+ +process_file(files)
66
+ +process_message(message)
67
+ +predict(message, history, engine, temperature)
68
+ +stop()
69
+ }
70
+
71
+ ' File processing
72
+ class FileProcessorFactory {
73
+ +get_processor(file_path)
74
+ }
75
+
76
+ ' Utilities and generators
77
+ class client_utils
78
+ class generators {
79
+ +chat_completion(messages, engine, temperature)
80
+ +chat_completion_stream(messages, engine, temperature)
81
+ }
82
+
83
+ ' Relationships
84
+ Chatbot ..> FileProcessorFactory : uses
85
+ Chatbot ..> generators : uses
86
+ generators ..> BaseAgent : creates
87
+ generators ..> OrchestratorAgent : creates
88
+ BaseAgent ..> AgentConfig : uses
89
+
90
+ @enduml
assets/login.svg ADDED
assets/new_chat.svg ADDED
chatbot.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import os
4
+ from typing import List, Dict, Any, Union, Generator
5
+
6
+ from generators import chat_completion_stream
7
+ from client_utils import engine_map
8
+ from file_processors import FileProcessorFactory
9
+ import logging
10
+ import time
11
+
12
+ # Configure logging first
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ # Then get a logger
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class Chatbot:
21
+ def __init__(self):
22
+ """Initialize a new chatbot with history management."""
23
+ self._stop = False # Flag to interrupt streaming
24
+
25
+ self.engine_map = engine_map
26
+ self.content_exclusions = ["<|eos|>"]
27
+
28
+ def process_file(self, files: Union[List[str], str]) -> Union[str, List[Dict[str, Any]]]:
29
+ """Process uploaded files and return their contents as JSON."""
30
+ if isinstance(files, str):
31
+ files = [files]
32
+
33
+ file_contents = []
34
+ for file_path in files:
35
+ try:
36
+ processor = FileProcessorFactory.get_processor(file_path)
37
+ file_contents.append(processor.process(file_path))
38
+ except Exception as e:
39
+ file_contents.append(f"Error processing file {os.path.basename(file_path)}: {str(e)}\n")
40
+
41
+ return json.dumps(file_contents)
42
+
43
+ def process_message(self, message: Union[str, dict]) -> dict:
44
+ """Convert various message formats into a standardized message dictionary."""
45
+ if isinstance(message, dict):
46
+ if message.get("text") and message.get("files"):
47
+ return {
48
+ "role": "user",
49
+ "content": f'{message["text"]}\n\nFile Upload(s):\n{self.process_file(message["files"])}'
50
+ }
51
+ elif message.get("text"):
52
+ return {
53
+ "role": "user",
54
+ "content": message["text"]
55
+ }
56
+ elif message.get("files"):
57
+ return {
58
+ "role": "user",
59
+ "content": f'File Upload(s):\n{self.process_file(message["files"])}'
60
+ }
61
+ elif message.get("content") and message.get("role"):
62
+ if isinstance(message["content"], tuple):
63
+ return {
64
+ "role": message["role"],
65
+ "content": f'File Upload(s):\n{self.process_file(message["content"][0])}'
66
+ }
67
+ elif isinstance(message["content"], str):
68
+ return {
69
+ "role": message["role"],
70
+ "content": message["content"]
71
+ }
72
+ elif isinstance(message, str):
73
+ if message:
74
+ return {
75
+ "role": "user",
76
+ "content": message,
77
+ }
78
+ return
79
+
80
+ def predict(self, message: Union[str, dict], history: List[Dict[str, Any]], engine: str, temperature: float) -> Generator:
81
+ """Generate a streaming response for the given message and conversation history."""
82
+ self._stop = False # Reset stop flag
83
+
84
+ yield "", gr.update(interactive=False, submit_btn=False, stop_btn=True)
85
+
86
+ history.append(message)
87
+
88
+ # Process all messages and update the list
89
+ history = [msg for msg in (self.process_message(msg) for msg in history) if msg is not None]
90
+
91
+ response = []
92
+ streamed_content = "" # Accumulate the assistant's response here
93
+
94
+ def fade_html(text: str, fade_count: int = 200) -> str:
95
+ """
96
+ Fade all characters if text is shorter than fade_count.
97
+ Otherwise, fade only the last fade_count characters.
98
+ """
99
+ if not text:
100
+ return text
101
+ text_len = len(text)
102
+ if text_len <= fade_count:
103
+ # Fade all characters
104
+ fade_spans = ''
105
+ for i, c in enumerate(text):
106
+ opacity = 1.0 - (i / max(text_len - 1, 1))
107
+ fade_spans += f'<span style="opacity:{opacity:.2f};transition:opacity 0.2s">{c}</span>'
108
+ return fade_spans
109
+ else:
110
+ base = text[:-fade_count]
111
+ fades = text[-fade_count:]
112
+ fade_spans = ''
113
+ for i, c in enumerate(fades):
114
+ opacity = 1.0 - (i / (fade_count - 1))
115
+ fade_spans += f'<span style="opacity:{opacity:.2f};transition:opacity 0.2s">{c}</span>'
116
+ return base + fade_spans
117
+
118
+ for chunk in chat_completion_stream(history, engine=engine, temperature=temperature):
119
+ logger.debug("Chunk received: %s", chunk)
120
+ if self._stop:
121
+ break
122
+
123
+ try:
124
+ content = chunk.get("content", "")
125
+ if chunk.get("metadata"):
126
+ # Do not stream metadata contents, just update/append as before
127
+ new_metadata = True
128
+ for i, resp in enumerate(response):
129
+ if resp.get("metadata", {}).get("id") == chunk["metadata"]["id"]:
130
+ response[i]["content"] = content
131
+ response[i]["metadata"] = chunk["metadata"]
132
+ new_metadata = False
133
+ break
134
+ if new_metadata:
135
+ response.append({
136
+ "role": "assistant",
137
+ "content": content,
138
+ "metadata": chunk["metadata"]
139
+ })
140
+ # Do not stream char-by-char for metadata
141
+ # yield response, gr.skip()
142
+ else:
143
+ # Stream each character in the latest assistant message (no metadata)
144
+ if not response or response[-1].get("metadata"):
145
+ response.append({
146
+ "role": "assistant",
147
+ "content": content,
148
+ })
149
+ streamed_content = "" # Reset for new message
150
+ else:
151
+ response[-1]["content"] = content
152
+ latest = response[-1]
153
+ prev_len = len(streamed_content)
154
+ new_content = latest.get("content", "")
155
+ for c in new_content[prev_len:]:
156
+ if self._stop:
157
+ break
158
+ streamed_content += c
159
+ # Only fade the last fade_count characters of the current message
160
+ latest["content"] = fade_html(streamed_content)
161
+ yield response, gr.skip()
162
+ time.sleep(0.003)
163
+ except Exception as e:
164
+ logger.info(f"Error processing chunk from chat completion: {e}")
165
+ continue
166
+
167
+ yield response, gr.update(interactive=True, submit_btn=True, stop_btn=False)
168
+
169
+ def stop(self):
170
+ """Interrupt the currently streaming response."""
171
+ self._stop = True
client_utils.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, Any
3
+ from dotenv import load_dotenv
4
+ from openai import OpenAI, AzureOpenAI
5
+
6
+ load_dotenv()
7
+
8
+ # Initialize clients only if their required environment variables are present
9
+ engine_map = {}
10
+
11
+ # Import here to avoid circular imports - will be populated in code below
12
+ from agent_config import BaseAgentConfig, SITAASAgentConfig
13
+
14
+ # Azure OpenAI client setup
15
+ if all(os.getenv(var) for var in ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_VERSION"]):
16
+ azure_client = AzureOpenAI(
17
+ api_key=os.getenv("AZURE_OPENAI_API_KEY"),
18
+ azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
19
+ api_version=os.getenv("AZURE_OPENAI_API_VERSION")
20
+ )
21
+ engine_map.update({
22
+ "azure-gpt-4o-mini": {
23
+ "client": azure_client,
24
+ "model": "gpt-4o-mini",
25
+ "config": BaseAgentConfig("azure-gpt-4o-mini")
26
+ },
27
+ "azure-gpt-4o": {
28
+ "client": azure_client,
29
+ "model": "gpt-4o",
30
+ "config": BaseAgentConfig("azure-gpt-4o")
31
+ },
32
+ })
33
+
34
+ # OpenAI client setup
35
+ if os.getenv("OPENAI_API_KEY"):
36
+ openai_client = OpenAI(
37
+ api_key=os.getenv("OPENAI_API_KEY"),
38
+ )
39
+ engine_map.update({
40
+ "openai-gpt-4o-mini": {
41
+ "client": openai_client,
42
+ "model": "gpt-4o-mini",
43
+ "config": BaseAgentConfig("openai-gpt-4o-mini")
44
+ },
45
+ "openai-gpt-4o": {
46
+ "client": openai_client,
47
+ "model": "gpt-4o",
48
+ "config": BaseAgentConfig("openai-gpt-4o")
49
+ },
50
+ })
51
+
52
+ # x.ai client setup
53
+ if os.getenv("XAI_API_KEY"):
54
+ xai_client = OpenAI(
55
+ api_key=os.getenv("XAI_API_KEY"),
56
+ base_url="https://api.x.ai/v1",
57
+ )
58
+ engine_map["grok-2-latest"] = {
59
+ "client": xai_client,
60
+ "model": "grok-2-latest",
61
+ "config": BaseAgentConfig("grok-2-latest")
62
+ }
63
+
64
+ # Add the SITAAS agent to the engine_map if required env vars are available
65
+ if all(os.getenv(var) for var in ["SEARCH_ENDPOINT", "SEARCH_KEY"]):
66
+ engine_map["sitaas-agent"] = {
67
+ "client": azure_client,
68
+ "model": "gpt-4.1-mini",
69
+ "config": SITAASAgentConfig("gpt-4.1-mini") # Use the SITAAS agent configuration
70
+ }
71
+
72
+ def get_client(engine: str) -> OpenAI:
73
+ """Get the appropriate client for the given engine"""
74
+ try:
75
+ return engine_map[engine]["client"]
76
+ except KeyError:
77
+ raise ValueError(f"Unsupported engine in get_client: {engine}")
78
+
79
+ def get_model(engine: str) -> str:
80
+ """Get the model name for the given engine"""
81
+ try:
82
+ return engine_map[engine]["model"]
83
+ except KeyError:
84
+ raise ValueError(f"Unsupported engine in get_model: {engine}")
85
+
86
+ def get_config(engine: str) -> Any:
87
+ """Get the agent configuration for the given engine"""
88
+ try:
89
+ return engine_map[engine]["config"]
90
+ except KeyError:
91
+ raise ValueError(f"Unsupported engine in get_config: {engine}")
embedding_client.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Client for handling embeddings for vector search"""
2
+
3
+ import os
4
+ import logging
5
+ from typing import List, Optional
6
+ import numpy as np
7
+ from openai import AzureOpenAI
8
+
9
+ # Configure logging
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
13
+ )
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class EmbeddingClient:
17
+ """Client for generating embeddings using Azure OpenAI"""
18
+
19
+ def __init__(self, azure_endpoint: str, api_key: str, deployment: str, api_version: str = "2023-05-15"):
20
+ """Initialize the embedding client"""
21
+ self.azure_endpoint = azure_endpoint
22
+ self.api_key = api_key
23
+ self.deployment = deployment
24
+ self.api_version = api_version
25
+
26
+ # Initialize client
27
+ self.client = None
28
+ if self.azure_endpoint and self.api_key and self.deployment:
29
+ try:
30
+ self.client = AzureOpenAI(
31
+ api_key=self.api_key,
32
+ api_version=self.api_version,
33
+ azure_endpoint=self.azure_endpoint
34
+ )
35
+ logger.info(f"Initialized embedding client with deployment {self.deployment}")
36
+ except Exception as e:
37
+ logger.error(f"Failed to initialize Azure OpenAI client: {e}")
38
+ self.client = None
39
+ else:
40
+ logger.warning("Missing configuration for embedding client")
41
+
42
+ def get_embedding(self, text: str) -> List[float]:
43
+ """Generate embedding for the given text"""
44
+ if not self.client:
45
+ logger.warning("No embedding client available, falling back to mock embedding")
46
+ return self._get_mock_embedding()
47
+
48
+ try:
49
+ # Truncate text if needed
50
+ max_chars = 32000 # Approximate limit
51
+ if len(text) > max_chars:
52
+ text = text[:max_chars]
53
+ logger.warning(f"Text truncated to {max_chars} characters")
54
+
55
+ # Call the embeddings API
56
+ response = self.client.embeddings.create(
57
+ input=text,
58
+ model=self.deployment
59
+ )
60
+
61
+ # Extract the embedding
62
+ embedding = response.data[0].embedding
63
+ logger.info(f"Successfully generated embedding of dimension {len(embedding)}")
64
+ return embedding
65
+ except Exception as e:
66
+ logger.error(f"Error generating embedding: {e}")
67
+ return self._get_mock_embedding()
68
+
69
+ def _get_mock_embedding(self) -> List[float]:
70
+ """Generate a mock embedding for fallback"""
71
+ logger.warning("Using mock embedding - this is not suitable for production use")
72
+ # Generate a normalized random vector
73
+ embedding = np.random.normal(size=1536) # Standard dimension for text-embedding-ada-002
74
+ embedding = embedding / np.linalg.norm(embedding)
75
+ return embedding.tolist()
environment.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: digichat
2
+ channels:
3
+ - conda-forge
4
+ - pytorch
5
+ - defaults
6
+ dependencies:
7
+ - python=3.10
8
+ - pip
9
+ - pytorch
10
+ - torchvision
11
+ - pip:
12
+ - gradio==5.27.0
13
+ - pandas
14
+ - openai
15
+ - tiktoken
16
+ - python-docx
17
+ - PyPDF2
18
+ - pillow
19
+ - transformers
file_processors.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ from typing import Dict, Any
4
+ import PyPDF2
5
+ from docx import Document
6
+ import pandas as pd
7
+ import nbformat
8
+
9
+ class BaseProcessor:
10
+ """Base class for all file processors"""
11
+ @classmethod
12
+ def process(cls, file_path: str) -> Dict[str, Any]:
13
+ raise NotImplementedError("Subclasses must implement process method")
14
+
15
+ class PDFProcessor(BaseProcessor):
16
+ @classmethod
17
+ def process(cls, file_path: str) -> Dict[str, Any]:
18
+ with open(file_path, 'rb') as file:
19
+ pdf_reader = PyPDF2.PdfReader(file)
20
+ pages = []
21
+ for n, page in enumerate(pdf_reader.pages):
22
+ pages.append({"page": n, "content": page.extract_text()})
23
+ return {
24
+ "content": pages,
25
+ "metadata": {
26
+ "total_pages": len(pages),
27
+ "file_type": "PDF"
28
+ }
29
+ }
30
+
31
+ class DocxProcessor(BaseProcessor):
32
+ @classmethod
33
+ def process(cls, file_path: str) -> Dict[str, Any]:
34
+ doc = Document(file_path)
35
+ paragraphs = []
36
+
37
+ for n, paragraph in enumerate(doc.paragraphs, 1):
38
+ if paragraph.text.strip():
39
+ paragraphs.append({
40
+ "paragraph": n,
41
+ "content": paragraph.text
42
+ })
43
+
44
+ return {
45
+ "content": paragraphs,
46
+ "metadata": {
47
+ "total_paragraphs": len(paragraphs),
48
+ "file_type": "DOCX"
49
+ }
50
+ }
51
+
52
+ class ImageProcessor(BaseProcessor):
53
+ @classmethod
54
+ def process(cls, file_path: str) -> Dict[str, Any]:
55
+ try:
56
+ with open(file_path, 'rb') as file:
57
+ content = base64.b64encode(file.read()).decode('ascii')
58
+ return {
59
+ "content": content,
60
+ "metadata": {
61
+ "file_type": "IMAGE",
62
+ "encoding": "base64"
63
+ }
64
+ }
65
+ except Exception as e:
66
+ raise Exception(f"Error processing image: {str(e)}")
67
+
68
+ class JSONProcessor(BaseProcessor):
69
+ @classmethod
70
+ def process(cls, file_path: str) -> Dict[str, Any]:
71
+ with open(file_path, 'r') as file:
72
+ try:
73
+ data = json.load(file)
74
+ return {
75
+ "content": data,
76
+ "metadata": {
77
+ "file_type": "JSON",
78
+ "is_valid": True
79
+ }
80
+ }
81
+ except json.JSONDecodeError as e:
82
+ return {
83
+ "content": None,
84
+ "metadata": {
85
+ "file_type": "JSON",
86
+ "is_valid": False,
87
+ "error": str(e)
88
+ }
89
+ }
90
+
91
+ class TextProcessor(BaseProcessor):
92
+ @classmethod
93
+ def process(cls, file_path: str) -> Dict[str, Any]:
94
+ with open(file_path, 'r') as file:
95
+ content = file.read()
96
+ return {
97
+ "content": content,
98
+ "metadata": {
99
+ "file_type": "TEXT",
100
+ "encoding": "utf-8"
101
+ }
102
+ }
103
+
104
+ class CSVProcessor(BaseProcessor):
105
+ @classmethod
106
+ def process(cls, file_path: str) -> Dict[str, Any]:
107
+ try:
108
+ df = pd.read_csv(file_path)
109
+ return {
110
+ "content": df.to_dict(orient='records'),
111
+ "metadata": {
112
+ "file_type": "CSV",
113
+ "rows": len(df),
114
+ "columns": len(df.columns),
115
+ "column_names": list(df.columns),
116
+ "summary": {
117
+ "first_few_rows": df.head().to_dict(orient='records'),
118
+ "statistics": df.describe().to_dict()
119
+ }
120
+ }
121
+ }
122
+ except Exception as e:
123
+ return {
124
+ "content": None,
125
+ "metadata": {
126
+ "file_type": "CSV",
127
+ "error": str(e)
128
+ }
129
+ }
130
+
131
+ class ExcelProcessor(BaseProcessor):
132
+ @classmethod
133
+ def process(cls, file_path: str) -> Dict[str, Any]:
134
+ try:
135
+ # Get sheet names first
136
+ xl = pd.ExcelFile(file_path)
137
+ sheets = xl.sheet_names
138
+
139
+ # Read the first sheet by default
140
+ df = pd.read_excel(file_path, sheet_name=sheets[0])
141
+
142
+ # Read all sheets
143
+ all_sheets = {}
144
+ for sheet in sheets:
145
+ all_sheets[sheet] = pd.read_excel(file_path, sheet_name=sheet).to_dict(orient='records')
146
+
147
+ return {
148
+ "content": all_sheets,
149
+ "metadata": {
150
+ "file_type": "EXCEL",
151
+ "sheets": sheets,
152
+ "current_sheet": {
153
+ "name": sheets[0],
154
+ "rows": len(df),
155
+ "columns": len(df.columns),
156
+ "column_names": list(df.columns),
157
+ "summary": {
158
+ "first_few_rows": df.head().to_dict(orient='records'),
159
+ "statistics": df.describe().to_dict()
160
+ }
161
+ }
162
+ }
163
+ }
164
+ except Exception as e:
165
+ return {
166
+ "content": None,
167
+ "metadata": {
168
+ "file_type": "EXCEL",
169
+ "error": str(e)
170
+ }
171
+ }
172
+
173
+ class PythonProcessor(BaseProcessor):
174
+ @classmethod
175
+ def process(cls, file_path: str) -> Dict[str, Any]:
176
+ with open(file_path, 'r') as file:
177
+ content = file.read()
178
+ return {
179
+ "content": content,
180
+ "metadata": {
181
+ "file_type": "PYTHON",
182
+ "encoding": "utf-8"
183
+ }
184
+ }
185
+
186
+ class JupyterNotebookProcessor(BaseProcessor):
187
+ @classmethod
188
+ def process(cls, file_path: str) -> Dict[str, Any]:
189
+ try:
190
+ with open(file_path, 'r') as file:
191
+ nb = nbformat.read(file, as_version=4)
192
+ content = []
193
+ for cell in nb.cells:
194
+ cell_info = {
195
+ "type": cell.cell_type,
196
+ "content": cell.source
197
+ }
198
+ if cell.cell_type == "code" and cell.outputs:
199
+ cell_info["outputs"] = [str(output) for output in cell.outputs]
200
+ content.append(cell_info)
201
+
202
+ return {
203
+ "content": content,
204
+ "metadata": {
205
+ "file_type": "JUPYTER_NOTEBOOK",
206
+ "total_cells": len(content),
207
+ "cell_types": list(set(cell["type"] for cell in content))
208
+ }
209
+ }
210
+ except Exception as e:
211
+ return {
212
+ "content": None,
213
+ "metadata": {
214
+ "file_type": "JUPYTER_NOTEBOOK",
215
+ "error": str(e)
216
+ }
217
+ }
218
+
219
+ class SVGProcessor(BaseProcessor):
220
+ @classmethod
221
+ def process(cls, file_path: str) -> Dict[str, Any]:
222
+ try:
223
+ with open(file_path, 'r') as file:
224
+ return {
225
+ "content": file.read(),
226
+ "metadata": {
227
+ "file_type": "SVG",
228
+ "encoding": "utf-8"
229
+ }
230
+ }
231
+ except Exception as e:
232
+ return {
233
+ "content": None,
234
+ "metadata": {
235
+ "file_type": "SVG",
236
+ "error": str(e)
237
+ }
238
+ }
239
+
240
+ class JavaScriptProcessor(BaseProcessor):
241
+ @classmethod
242
+ def process(cls, file_path: str) -> Dict[str, Any]:
243
+ with open(file_path, 'r') as file:
244
+ content = file.read()
245
+ return {
246
+ "content": content,
247
+ "metadata": {
248
+ "file_type": "JAVASCRIPT",
249
+ "encoding": "utf-8"
250
+ }
251
+ }
252
+
253
+ class HTMLProcessor(BaseProcessor):
254
+ @classmethod
255
+ def process(cls, file_path: str) -> Dict[str, Any]:
256
+ with open(file_path, 'r') as file:
257
+ content = file.read()
258
+ return {
259
+ "content": content,
260
+ "metadata": {
261
+ "file_type": "HTML",
262
+ "encoding": "utf-8"
263
+ }
264
+ }
265
+
266
+ class CSSProcessor(BaseProcessor):
267
+ @classmethod
268
+ def process(cls, file_path: str) -> Dict[str, Any]:
269
+ with open(file_path, 'r') as file:
270
+ content = file.read()
271
+ return {
272
+ "content": content,
273
+ "metadata": {
274
+ "file_type": "CSS",
275
+ "encoding": "utf-8"
276
+ }
277
+ }
278
+
279
+ class JavaProcessor(BaseProcessor):
280
+ @classmethod
281
+ def process(cls, file_path: str) -> Dict[str, Any]:
282
+ with open(file_path, 'r') as file:
283
+ content = file.read()
284
+ return {
285
+ "content": content,
286
+ "metadata": {
287
+ "file_type": "JAVA",
288
+ "encoding": "utf-8"
289
+ }
290
+ }
291
+
292
+ class CppProcessor(BaseProcessor):
293
+ @classmethod
294
+ def process(cls, file_path: str) -> Dict[str, Any]:
295
+ with open(file_path, 'r') as file:
296
+ content = file.read()
297
+ return {
298
+ "content": content,
299
+ "metadata": {
300
+ "file_type": "CPP",
301
+ "encoding": "utf-8"
302
+ }
303
+ }
304
+
305
+ class HeaderProcessor(BaseProcessor):
306
+ @classmethod
307
+ def process(cls, file_path: str) -> Dict[str, Any]:
308
+ with open(file_path, 'r') as file:
309
+ content = file.read()
310
+ return {
311
+ "content": content,
312
+ "metadata": {
313
+ "file_type": "HEADER",
314
+ "encoding": "utf-8"
315
+ }
316
+ }
317
+
318
+ class ShellScriptProcessor(BaseProcessor):
319
+ @classmethod
320
+ def process(cls, file_path: str) -> Dict[str, Any]:
321
+ with open(file_path, 'r') as file:
322
+ content = file.read()
323
+ return {
324
+ "content": content,
325
+ "metadata": {
326
+ "file_type": "SHELL_SCRIPT",
327
+ "encoding": "utf-8"
328
+ }
329
+ }
330
+
331
+ class CodeFileProcessor(BaseProcessor):
332
+ """Universal processor for code files that just need to be read as text"""
333
+ @classmethod
334
+ def process(cls, file_path: str) -> Dict[str, Any]:
335
+ with open(file_path, 'r') as file:
336
+ content = file.read()
337
+ ext = file_path[file_path.rfind('.'):].upper()[1:] # Remove the dot and capitalize
338
+ return {
339
+ "content": content,
340
+ "metadata": {
341
+ "file_type": ext,
342
+ "encoding": "utf-8"
343
+ }
344
+ }
345
+
346
+ class FileProcessorFactory:
347
+ """Factory class to get the appropriate processor for a file type"""
348
+ _processors = {
349
+ '.pdf': PDFProcessor,
350
+ '.docx': DocxProcessor,
351
+ '.png': ImageProcessor,
352
+ '.jpg': ImageProcessor,
353
+ '.jpeg': ImageProcessor,
354
+ '.json': JSONProcessor,
355
+ '.txt': TextProcessor,
356
+ '.csv': CSVProcessor,
357
+ '.xlsx': ExcelProcessor,
358
+ '.xls': ExcelProcessor,
359
+ '.py': PythonProcessor,
360
+ '.ipynb': JupyterNotebookProcessor,
361
+ '.svg': SVGProcessor,
362
+ # Universal code file processor for various file types
363
+ '.js': CodeFileProcessor,
364
+ '.html': CodeFileProcessor,
365
+ '.htm': CodeFileProcessor,
366
+ '.css': CodeFileProcessor,
367
+ '.java': CodeFileProcessor,
368
+ '.cpp': CodeFileProcessor,
369
+ '.cc': CodeFileProcessor,
370
+ '.cxx': CodeFileProcessor,
371
+ '.h': CodeFileProcessor,
372
+ '.hpp': CodeFileProcessor,
373
+ '.sh': CodeFileProcessor,
374
+ '.bash': CodeFileProcessor
375
+ }
376
+
377
+ @classmethod
378
+ def get_processor(cls, file_path: str) -> BaseProcessor:
379
+ ext = file_path[file_path.rfind('.'):].lower()
380
+ processor = cls._processors.get(ext)
381
+ if not processor:
382
+ raise ValueError(f"Unsupported file type: {ext}")
383
+ return processor
generators.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Dict, Any, Generator
3
+ import importlib
4
+ import threading
5
+ import queue
6
+
7
+ from dotenv import load_dotenv
8
+ from client_utils import get_client, get_model, get_config, engine_map
9
+
10
+ load_dotenv()
11
+
12
+ def get_agent(agent_type: str, *args, **kwargs):
13
+ """Retrieve an agent configuration based on the agent type from engine_map."""
14
+ try:
15
+ if agent_type.lower() in engine_map:
16
+ return engine_map[agent_type.lower()]["config"]
17
+ raise ValueError(f"Unsupported agent type: {agent_type}")
18
+ except KeyError:
19
+ raise ValueError(f"Unsupported agent type: {agent_type}")
20
+
21
+ def chat_completion(messages: List[Dict[str, Any]], engine: str = "grok-2-latest", temperature: float = 0.5) -> str:
22
+ # Dynamic import to avoid circular import
23
+ from agents import BaseAgent, OrchestratorAgent
24
+
25
+ # Get the appropriate config
26
+ config = get_config(engine)
27
+
28
+ # Override temperature if specified
29
+ config.setup_model_config(engine, temperature)
30
+
31
+ # Use orchestrator if the config has tools available
32
+ if len(config.get_tools()) > 0:
33
+ agent = OrchestratorAgent(config)
34
+ else:
35
+ agent = BaseAgent(config)
36
+
37
+ result_queue = queue.Queue()
38
+
39
+ def run_process():
40
+ try:
41
+ result = agent.process(messages)
42
+ result_queue.put(result)
43
+ except Exception as e:
44
+ result_queue.put(e)
45
+
46
+ t = threading.Thread(target=run_process)
47
+ t.start()
48
+ t.join()
49
+ result = result_queue.get()
50
+ if isinstance(result, Exception):
51
+ raise result
52
+ return result
53
+
54
+ def chat_completion_stream(messages: List[Dict[str, Any]], engine: str = "grok-2-latest", temperature: float = 0.5) -> Generator[Any, None, None]:
55
+ """Stream chat completions based on the selected engine or agent."""
56
+ try:
57
+ # Dynamic import to avoid circular import
58
+ from agents import BaseAgent, OrchestratorAgent
59
+
60
+ # Get the appropriate config
61
+ config = get_config(engine)
62
+
63
+ # Override temperature if specified
64
+ config.setup_model_config(engine, temperature)
65
+
66
+ # Use orchestrator if the config has tools available
67
+ if len(config.get_tools()) > 0:
68
+ agent = OrchestratorAgent(config)
69
+ stream_fn = agent.process_stream
70
+ else:
71
+ agent = BaseAgent(config)
72
+ stream_fn = agent.process_stream
73
+
74
+ q = queue.Queue()
75
+ sentinel = object()
76
+
77
+ def run_stream():
78
+ try:
79
+ for chunk in stream_fn(messages):
80
+ q.put(chunk)
81
+ except Exception as e:
82
+ q.put(e)
83
+ finally:
84
+ q.put(sentinel)
85
+
86
+ t = threading.Thread(target=run_stream)
87
+ t.start()
88
+ while True:
89
+ item = q.get()
90
+ if item is sentinel:
91
+ break
92
+ if isinstance(item, Exception):
93
+ raise item
94
+ yield item
95
+
96
+ except Exception as e:
97
+ # In case of any error, yield a special error chunk that our handler can process
98
+ print(f"Error in chat_completion_stream: {e}")
99
+ error_msg = f"Error: {str(e)}"
100
+ # Create a minimal mock chunk with the error message
101
+ yield {"choices": [{"delta": {"content": error_msg}}]}
requirements.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio[oauth]==5.24.0
2
+ python-dotenv>=1.0.0
3
+ pandas>=2.0.0
4
+ openai>=1.0.0
5
+ tiktoken>=0.5.0
6
+
7
+ # For document processing
8
+ python-docx>=0.8.11
9
+ PyPDF2>=3.0.0
10
+ pillow>=10.0.0
11
+ pytesseract>=0.3.10
12
+ torch>=2.0.0
13
+ nbformat>=5.9.0
14
+ python-multipart>=0.0.6
15
+
16
+ # Azure packages
17
+ requests>=2.28.0
18
+ azure-core>=1.29.4
19
+ azure-identity>=1.15.0
20
+ # Specify newer version that supports vector search
21
+ azure-search-documents>=11.4.0b6
22
+ azure-ai-formrecognizer>=3.2.0
23
+ azure-storage-blob>=12.17.0
24
+
25
+ # For embeddings and vector operations
26
+ numpy>=1.20.0
27
+ scikit-learn>=1.0.0
style.css ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ div#component-7, div#component-10 {
2
+ flex: 0;
3
+ flex-wrap: nowrap;
4
+ flex-grow: 0;
5
+ flex-shrink: 0;
6
+ flex-direction: row;
7
+ }
8
+
9
+ div#component-7 {
10
+ justify-content: flex-start;
11
+ }
12
+
13
+ div#component-10 {
14
+ justify-content: flex-end;
15
+ }
16
+
17
+ .wrap.svelte-1hfxrpf {
18
+ background: none;
19
+ width: fit-content;
20
+ }
21
+
22
+ div#component-8, .form.svelte-633qhp {
23
+ background: none;
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ button#new_chat_btn, button#login_btn {
29
+ min-width: min-content;
30
+ width: fit-content;
31
+ }
32
+
33
+ .button-icon {
34
+ width: var(--text-xxl);
35
+ height: var(--text-xxl);
36
+ }
37
+
38
+ div#component-0 {
39
+ height: 90vh;
40
+ }
41
+
42
+ div#component-27, div#component-33 {
43
+ display: none;
44
+ }
45
+
46
+ .icon-wrap.svelte-1hfxrpf {
47
+ right: 0;
48
+ }
49
+
50
+ input.border-none.svelte-1hfxrpf {
51
+ font-size: medium;
52
+ font-weight: bold;
53
+ }
54
+
55
+ .label.svelte-p5q82i {
56
+ margin-bottom: var(--size-5)
57
+ }
58
+ textarea.edit-textarea {
59
+ border: none;
60
+ }
61
+
62
+ div#component-32 {
63
+ gap: 0;
64
+ max-height: 70vh;
65
+ }
66
+
67
+ div#chatbot {
68
+ max-width: min(80%, 900px);
69
+ align-self: center;
70
+ }
71
+ div#inputs {
72
+ max-width: min(80%, 700px);
73
+ min-height: fit-content;
74
+ align-self: center;
75
+ }
76
+ .styler.svelte-1nguped {
77
+ background: var(--block-background-fill);
78
+ }
79
+ .form.svelte-633qhp {
80
+ gap: 0;
81
+ box-shadow: none;
82
+ }
83
+
84
+ .bot.svelte-yaaj3.message {
85
+ border: none;
86
+ }
87
+ .styler.svelte-1nguped {
88
+ gap: 0px;
89
+ }
90
+
91
+ .table-wrap.svelte-p5q82i {
92
+ border: none;
93
+ }
94
+ thead {
95
+ display: none;
96
+ }