Upload folder using huggingface_hub
Browse files- .github/workflows/update_space.yml +42 -0
- .gradio/certificate.pem +31 -0
- .huggingface/config.json +12 -0
- README.md +88 -7
- agent_config.py +554 -0
- agents.py +554 -0
- app.py +183 -0
- architecture.puml +90 -0
- assets/login.svg +16 -0
- assets/new_chat.svg +16 -0
- chatbot.py +171 -0
- client_utils.py +91 -0
- embedding_client.py +75 -0
- environment.yml +19 -0
- file_processors.py +383 -0
- generators.py +101 -0
- requirements.txt +27 -0
- style.css +96 -0
.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:
|
| 3 |
-
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.27.0
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
| 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 |
+
}
|