Spaces:
Sleeping
Sleeping
Commit
Β·
b6650bb
1
Parent(s):
ebb22ed
imporvement in the UI in app.py
Browse files- README.md +45 -5
- app.py +900 -42
- backend/README.md +22 -0
- frontend/README.md +2 -0
- requirements.txt +2 -1
README.md
CHANGED
|
@@ -75,12 +75,31 @@ This platform showcases how MCP can power intelligent, governed, multi-tenant AI
|
|
| 75 |
|
| 76 |
The Gradio UI exposes four tabs once you launch `app.py`:
|
| 77 |
|
| 78 |
-
1. **Chat** β enter your Tenant ID, ask questions, and see multi-tool MCP responses.
|
|
|
|
| 79 |
2. **Document Ingestion** β toggle between Raw Text, URL, or File Upload to populate the tenant RAG index. View and manage your ingested documents with delete functionality.
|
| 80 |
-
3. **Admin Analytics** β click "Fetch Analytics Snapshot" to view overview/tool-usage/red-flag/activity metrics.
|
| 81 |
-
4. **Admin Rules & Compliance** β upload/delete governance rules that are stored via the backend `/admin/rules` API.
|
| 82 |
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
### Frontend (Next.js) Operator Console
|
| 86 |
|
|
@@ -234,7 +253,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
| 234 |
## Technical Stack
|
| 235 |
|
| 236 |
- **Backend**: FastAPI with async/await for high-performance MCP orchestration
|
| 237 |
-
- **Frontend**: Gradio interface + Next.js operator console
|
|
|
|
| 238 |
- **LLM Integration**: Ollama (local) or Groq (cloud) via configurable backend
|
| 239 |
- **Vector Store**: pgvector (via Supabase) or SQLite embeddings
|
| 240 |
- **Analytics**: SQLite with indexed queries for fast analytics
|
|
@@ -261,10 +281,30 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
| 261 |
- **Response wrapping**: Standardized response format with automatic unwrapping in clients
|
| 262 |
- **Error handling**: Comprehensive error responses with detailed messages for debugging
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
## Acknowledgments
|
| 265 |
|
| 266 |
- Built with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
|
| 267 |
- Powered by [Gradio](https://gradio.app/) for the interface
|
|
|
|
| 268 |
- Backend built with [FastAPI](https://fastapi.tiangolo.com/)
|
| 269 |
- Analytics and governance features inspired by enterprise AI platform requirements
|
| 270 |
|
|
|
|
| 75 |
|
| 76 |
The Gradio UI exposes four tabs once you launch `app.py`:
|
| 77 |
|
| 78 |
+
1. **Chat** β enter your Tenant ID, ask questions, and see multi-tool MCP responses with autonomous tool orchestration.
|
| 79 |
+
|
| 80 |
2. **Document Ingestion** β toggle between Raw Text, URL, or File Upload to populate the tenant RAG index. View and manage your ingested documents with delete functionality.
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
3. **Knowledge Base Library** β comprehensive document management interface with:
|
| 83 |
+
- **Statistics Dashboard**: Visual cards showing total documents, document types (Text, PDF, FAQ, Link), and average length
|
| 84 |
+
- **Interactive Charts**: Plotly pie chart displaying document type distribution
|
| 85 |
+
- **Semantic Search**: Search your knowledge base with relevance scoring
|
| 86 |
+
- **Type Filtering**: Filter documents by type (all, text, pdf, faq, link)
|
| 87 |
+
- **Document Management**: View all documents in a table with preview, delete individual documents, or delete all at once
|
| 88 |
+
- **Auto-refresh**: Document lists automatically update after ingestion or deletion
|
| 89 |
+
|
| 90 |
+
4. **Admin Analytics** β comprehensive analytics dashboard with visualizations:
|
| 91 |
+
- **Statistics Cards**: Total queries, active users, red flags, and RAG searches
|
| 92 |
+
- **Interactive Bar Charts**:
|
| 93 |
+
- Tool Usage Count (RAG, Web, Admin tools)
|
| 94 |
+
- Average Tool Latency (performance metrics)
|
| 95 |
+
- RAG Quality Metrics (hits, scores, recall indicators)
|
| 96 |
+
- **Tool Usage Table**: Detailed breakdown of tool performance with counts, latency, success/error rates, and token usage
|
| 97 |
+
- **Formatted Summary**: Key metrics displayed in an easy-to-read format
|
| 98 |
+
- Click "π Fetch Analytics Snapshot" to load the latest data
|
| 99 |
+
|
| 100 |
+
5. **Admin Rules & Compliance** β upload/delete governance rules that are stored via the backend `/admin/rules` API.
|
| 101 |
+
|
| 102 |
+
**Tip:** Every action requires a tenant ID. The tenant ID is now managed centrally and persists across page refreshes. The Knowledge Base Library and Admin Analytics tabs feature beautiful, modern UI with dark theme styling and interactive Plotly visualizations.
|
| 103 |
|
| 104 |
### Frontend (Next.js) Operator Console
|
| 105 |
|
|
|
|
| 253 |
## Technical Stack
|
| 254 |
|
| 255 |
- **Backend**: FastAPI with async/await for high-performance MCP orchestration
|
| 256 |
+
- **Frontend**: Gradio interface with Plotly visualizations + Next.js operator console
|
| 257 |
+
- **UI Libraries**: Plotly for interactive charts, Gradio for web interface
|
| 258 |
- **LLM Integration**: Ollama (local) or Groq (cloud) via configurable backend
|
| 259 |
- **Vector Store**: pgvector (via Supabase) or SQLite embeddings
|
| 260 |
- **Analytics**: SQLite with indexed queries for fast analytics
|
|
|
|
| 281 |
- **Response wrapping**: Standardized response format with automatic unwrapping in clients
|
| 282 |
- **Error handling**: Comprehensive error responses with detailed messages for debugging
|
| 283 |
|
| 284 |
+
## UI Features
|
| 285 |
+
|
| 286 |
+
### Knowledge Base Library
|
| 287 |
+
- **Visual Statistics**: Real-time document counts and type distribution
|
| 288 |
+
- **Interactive Charts**: Plotly pie charts for document type visualization
|
| 289 |
+
- **Advanced Search**: Semantic search across all ingested documents with relevance scoring
|
| 290 |
+
- **Smart Filtering**: Filter by document type (text, PDF, FAQ, link)
|
| 291 |
+
- **Bulk Operations**: Delete individual documents or all documents at once
|
| 292 |
+
- **Auto-refresh**: Lists automatically update after operations
|
| 293 |
+
|
| 294 |
+
### Admin Analytics Dashboard
|
| 295 |
+
- **Statistics Cards**: Key metrics displayed in visually appealing cards with icons
|
| 296 |
+
- **Tool Usage Visualization**: Bar charts showing tool invocation counts and performance
|
| 297 |
+
- **Latency Metrics**: Visual representation of tool response times
|
| 298 |
+
- **RAG Quality Analysis**: Charts displaying search quality metrics (hits, scores, recall)
|
| 299 |
+
- **Detailed Tables**: Comprehensive tool usage breakdown with success/error rates
|
| 300 |
+
- **Dark Theme**: Modern UI with dark background and white text for better readability
|
| 301 |
+
- **Real-time Updates**: Fetch latest analytics data with a single click
|
| 302 |
+
|
| 303 |
## Acknowledgments
|
| 304 |
|
| 305 |
- Built with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
|
| 306 |
- Powered by [Gradio](https://gradio.app/) for the interface
|
| 307 |
+
- Visualizations created with [Plotly](https://plotly.com/python/)
|
| 308 |
- Backend built with [FastAPI](https://fastapi.tiangolo.com/)
|
| 309 |
- Analytics and governance features inspired by enterprise AI platform requirements
|
| 310 |
|
app.py
CHANGED
|
@@ -3,6 +3,15 @@ import requests
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://localhost:8000")
|
| 8 |
|
|
@@ -295,46 +304,601 @@ def delete_rule_and_refresh(tenant_id: str, rule: str):
|
|
| 295 |
return status, summary, rows
|
| 296 |
|
| 297 |
|
| 298 |
-
def fetch_admin_analytics(tenant_id: str)
|
|
|
|
| 299 |
if not tenant_id or not tenant_id.strip():
|
| 300 |
-
|
|
|
|
| 301 |
|
| 302 |
tenant_id = tenant_id.strip()
|
| 303 |
headers = {"x-tenant-id": tenant_id}
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
try:
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
|
| 336 |
# Create Gradio interface
|
| 337 |
-
with gr.Blocks(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
gr.Markdown(
|
| 339 |
"""
|
| 340 |
# π€ IntegraChat β MCP Autonomous Agent
|
|
@@ -457,7 +1021,7 @@ with gr.Blocks(title="IntegraChat β MCP Autonomous Agent", theme=gr.themes.Sof
|
|
| 457 |
metadata
|
| 458 |
):
|
| 459 |
source_type = "raw_text" if mode == "Raw Text" else "url"
|
| 460 |
-
|
| 461 |
tenant_id=tenant_id,
|
| 462 |
source_type=source_type,
|
| 463 |
content=content,
|
|
@@ -466,6 +1030,10 @@ with gr.Blocks(title="IntegraChat β MCP Autonomous Agent", theme=gr.themes.Sof
|
|
| 466 |
doc_id=doc_id_value,
|
| 467 |
metadata_json=metadata
|
| 468 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
ingest_doc_button.click(
|
| 471 |
fn=handle_ingest_document,
|
|
@@ -490,7 +1058,11 @@ with gr.Blocks(title="IntegraChat β MCP Autonomous Agent", theme=gr.themes.Sof
|
|
| 490 |
ingest_file_button = gr.Button("Upload & Ingest File", visible=False)
|
| 491 |
|
| 492 |
def handle_file_ingestion(tenant_id, file_obj):
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
ingest_file_button.click(
|
| 496 |
fn=handle_file_ingestion,
|
|
@@ -528,26 +1100,312 @@ with gr.Blocks(title="IntegraChat β MCP Autonomous Agent", theme=gr.themes.Sof
|
|
| 528 |
]
|
| 529 |
)
|
| 530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
with gr.Tab("Admin Analytics"):
|
| 532 |
gr.Markdown(
|
| 533 |
"""
|
| 534 |
-
|
| 535 |
-
Review tenant-level analytics generated by the IntegraChat backend.
|
| 536 |
|
| 537 |
-
-
|
| 538 |
-
- **Tool Usage:** How often RAG, Web, and Admin tools are invoked.
|
| 539 |
-
- **Red Flags:** Recent governance events for this tenant.
|
| 540 |
-
- **Activity:** Summary of tenant activity metrics.
|
| 541 |
"""
|
| 542 |
)
|
| 543 |
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
analytics_refresh.click(
|
| 548 |
-
fn=
|
| 549 |
inputs=[tenant_id_input],
|
| 550 |
-
outputs=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
)
|
| 552 |
|
| 553 |
with gr.Tab("Admin Rules & Compliance"):
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
+
from collections import Counter
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
import plotly.graph_objects as go
|
| 11 |
+
PLOTLY_AVAILABLE = True
|
| 12 |
+
except ImportError:
|
| 13 |
+
PLOTLY_AVAILABLE = False
|
| 14 |
+
go = None
|
| 15 |
|
| 16 |
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://localhost:8000")
|
| 17 |
|
|
|
|
| 304 |
return status, summary, rows
|
| 305 |
|
| 306 |
|
| 307 |
+
def fetch_admin_analytics(tenant_id: str):
|
| 308 |
+
"""Fetch analytics data and return formatted results with visualizations."""
|
| 309 |
if not tenant_id or not tenant_id.strip():
|
| 310 |
+
error_msg = "β Tenant ID is required to view analytics."
|
| 311 |
+
return error_msg, {}, None, None, None, None
|
| 312 |
|
| 313 |
tenant_id = tenant_id.strip()
|
| 314 |
headers = {"x-tenant-id": tenant_id}
|
| 315 |
+
|
| 316 |
+
overview_data = {}
|
| 317 |
+
tool_usage_data = {}
|
| 318 |
+
redflags_data = {}
|
| 319 |
+
activity_data = {}
|
| 320 |
+
error_msg = None
|
| 321 |
+
|
| 322 |
+
# Fetch Overview
|
| 323 |
+
try:
|
| 324 |
+
resp = requests.get(
|
| 325 |
+
f"{BACKEND_BASE_URL}/analytics/overview",
|
| 326 |
+
headers=headers,
|
| 327 |
+
timeout=30
|
| 328 |
+
)
|
| 329 |
+
if resp.status_code == 200:
|
| 330 |
+
overview_data = resp.json()
|
| 331 |
+
else:
|
| 332 |
+
error_msg = f"β Error fetching overview: {resp.status_code}"
|
| 333 |
+
except Exception as e:
|
| 334 |
+
error_msg = f"β Error: {str(e)}"
|
| 335 |
+
|
| 336 |
+
# Fetch Tool Usage
|
| 337 |
+
try:
|
| 338 |
+
resp = requests.get(
|
| 339 |
+
f"{BACKEND_BASE_URL}/analytics/tool-usage",
|
| 340 |
+
headers=headers,
|
| 341 |
+
timeout=30
|
| 342 |
+
)
|
| 343 |
+
if resp.status_code == 200:
|
| 344 |
+
tool_usage_data = resp.json()
|
| 345 |
+
except Exception:
|
| 346 |
+
pass
|
| 347 |
+
|
| 348 |
+
# Fetch Red Flags
|
| 349 |
+
try:
|
| 350 |
+
resp = requests.get(
|
| 351 |
+
f"{BACKEND_BASE_URL}/analytics/redflags",
|
| 352 |
+
headers=headers,
|
| 353 |
+
timeout=30
|
| 354 |
+
)
|
| 355 |
+
if resp.status_code == 200:
|
| 356 |
+
redflags_data = resp.json()
|
| 357 |
+
except Exception:
|
| 358 |
+
pass
|
| 359 |
+
|
| 360 |
+
# Fetch Activity
|
| 361 |
+
try:
|
| 362 |
+
resp = requests.get(
|
| 363 |
+
f"{BACKEND_BASE_URL}/analytics/activity",
|
| 364 |
+
headers=headers,
|
| 365 |
+
timeout=30
|
| 366 |
+
)
|
| 367 |
+
if resp.status_code == 200:
|
| 368 |
+
activity_data = resp.json()
|
| 369 |
+
except Exception:
|
| 370 |
+
pass
|
| 371 |
+
|
| 372 |
+
# Extract data for visualizations
|
| 373 |
+
overview = overview_data.get("overview", {})
|
| 374 |
+
tool_usage = overview.get("tool_usage", tool_usage_data.get("tool_usage", {}))
|
| 375 |
+
rag_quality = overview.get("rag_quality", {})
|
| 376 |
+
|
| 377 |
+
# Create tool usage bar chart
|
| 378 |
+
tool_chart = None
|
| 379 |
+
if tool_usage and PLOTLY_AVAILABLE:
|
| 380 |
try:
|
| 381 |
+
tools = []
|
| 382 |
+
counts = []
|
| 383 |
+
latencies = []
|
| 384 |
+
colors_list = []
|
| 385 |
+
|
| 386 |
+
color_map = {
|
| 387 |
+
"rag": "#3b82f6",
|
| 388 |
+
"rag.search": "#2563eb",
|
| 389 |
+
"rag.ingest": "#1d4ed8",
|
| 390 |
+
"rag.list": "#1e40af",
|
| 391 |
+
"web.search": "#06b6d4",
|
| 392 |
+
"admin": "#a855f7",
|
| 393 |
+
"llm": "#10b981"
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
for tool_name, stats in tool_usage.items():
|
| 397 |
+
tools.append(tool_name.replace(".", " ").title())
|
| 398 |
+
counts.append(stats.get("count", 0))
|
| 399 |
+
latencies.append(stats.get("avg_latency_ms", 0))
|
| 400 |
+
colors_list.append(color_map.get(tool_name, "#6b7280"))
|
| 401 |
+
|
| 402 |
+
if tools:
|
| 403 |
+
fig = go.Figure()
|
| 404 |
+
fig.add_trace(go.Bar(
|
| 405 |
+
x=tools,
|
| 406 |
+
y=counts,
|
| 407 |
+
name="Usage Count",
|
| 408 |
+
marker_color=colors_list,
|
| 409 |
+
text=counts,
|
| 410 |
+
textposition='outside',
|
| 411 |
+
hovertemplate='<b>%{x}</b><br>Count: %{y}<br><extra></extra>'
|
| 412 |
+
))
|
| 413 |
+
fig.update_layout(
|
| 414 |
+
title={
|
| 415 |
+
"text": "Tool Usage Count",
|
| 416 |
+
"x": 0.5,
|
| 417 |
+
"xanchor": "center",
|
| 418 |
+
"font": {"size": 16, "color": "#1f2937"}
|
| 419 |
+
},
|
| 420 |
+
xaxis_title="Tool",
|
| 421 |
+
yaxis_title="Count",
|
| 422 |
+
height=380,
|
| 423 |
+
showlegend=False,
|
| 424 |
+
margin=dict(l=50, r=20, t=60, b=50),
|
| 425 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 426 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 427 |
+
font=dict(color="#374151", size=12),
|
| 428 |
+
xaxis=dict(gridcolor="rgba(0,0,0,0.1)"),
|
| 429 |
+
yaxis=dict(gridcolor="rgba(0,0,0,0.1)")
|
| 430 |
+
)
|
| 431 |
+
tool_chart = fig
|
| 432 |
+
except Exception:
|
| 433 |
+
tool_chart = None
|
| 434 |
+
|
| 435 |
+
# Create latency chart
|
| 436 |
+
latency_chart = None
|
| 437 |
+
if tool_usage and PLOTLY_AVAILABLE:
|
| 438 |
+
try:
|
| 439 |
+
tools = []
|
| 440 |
+
latencies = []
|
| 441 |
+
colors_list = []
|
| 442 |
+
|
| 443 |
+
color_map = {
|
| 444 |
+
"rag": "#3b82f6",
|
| 445 |
+
"rag.search": "#2563eb",
|
| 446 |
+
"rag.ingest": "#1d4ed8",
|
| 447 |
+
"rag.list": "#1e40af",
|
| 448 |
+
"web.search": "#06b6d4",
|
| 449 |
+
"admin": "#a855f7",
|
| 450 |
+
"llm": "#10b981"
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
for tool_name, stats in tool_usage.items():
|
| 454 |
+
avg_latency = stats.get("avg_latency_ms", 0)
|
| 455 |
+
if avg_latency > 0:
|
| 456 |
+
tools.append(tool_name.replace(".", " ").title())
|
| 457 |
+
latencies.append(round(avg_latency, 2))
|
| 458 |
+
colors_list.append(color_map.get(tool_name, "#6b7280"))
|
| 459 |
+
|
| 460 |
+
if tools:
|
| 461 |
+
fig = go.Figure()
|
| 462 |
+
fig.add_trace(go.Bar(
|
| 463 |
+
x=tools,
|
| 464 |
+
y=latencies,
|
| 465 |
+
name="Avg Latency (ms)",
|
| 466 |
+
marker_color=colors_list,
|
| 467 |
+
text=[f"{l}ms" for l in latencies],
|
| 468 |
+
textposition='outside',
|
| 469 |
+
hovertemplate='<b>%{x}</b><br>Avg Latency: %{y}ms<extra></extra>'
|
| 470 |
+
))
|
| 471 |
+
fig.update_layout(
|
| 472 |
+
title={
|
| 473 |
+
"text": "Average Tool Latency",
|
| 474 |
+
"x": 0.5,
|
| 475 |
+
"xanchor": "center",
|
| 476 |
+
"font": {"size": 16, "color": "#1f2937"}
|
| 477 |
+
},
|
| 478 |
+
xaxis_title="Tool",
|
| 479 |
+
yaxis_title="Latency (ms)",
|
| 480 |
+
height=380,
|
| 481 |
+
showlegend=False,
|
| 482 |
+
margin=dict(l=50, r=20, t=60, b=50),
|
| 483 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 484 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 485 |
+
font=dict(color="#374151", size=12),
|
| 486 |
+
xaxis=dict(gridcolor="rgba(0,0,0,0.1)"),
|
| 487 |
+
yaxis=dict(gridcolor="rgba(0,0,0,0.1)")
|
| 488 |
+
)
|
| 489 |
+
latency_chart = fig
|
| 490 |
+
except Exception:
|
| 491 |
+
latency_chart = None
|
| 492 |
+
|
| 493 |
+
# Create RAG quality metrics visualization
|
| 494 |
+
rag_chart = None
|
| 495 |
+
if rag_quality and PLOTLY_AVAILABLE:
|
| 496 |
+
try:
|
| 497 |
+
metrics = ["Avg Hits", "Avg Score", "Avg Top Score"]
|
| 498 |
+
values = [
|
| 499 |
+
rag_quality.get("avg_hits_per_search", 0),
|
| 500 |
+
rag_quality.get("avg_score", 0) * 100, # Convert to percentage
|
| 501 |
+
rag_quality.get("avg_top_score", 0) * 100
|
| 502 |
+
]
|
| 503 |
+
|
| 504 |
+
fig = go.Figure(data=[go.Bar(
|
| 505 |
+
x=metrics,
|
| 506 |
+
y=values,
|
| 507 |
+
marker_color=["#3b82f6", "#10b981", "#f59e0b"],
|
| 508 |
+
text=[f"{v:.2f}" for v in values],
|
| 509 |
+
textposition='outside',
|
| 510 |
+
hovertemplate='<b>%{x}</b><br>Value: %{y:.2f}<extra></extra>'
|
| 511 |
+
)])
|
| 512 |
+
fig.update_layout(
|
| 513 |
+
title={
|
| 514 |
+
"text": "RAG Quality Metrics",
|
| 515 |
+
"x": 0.5,
|
| 516 |
+
"xanchor": "center",
|
| 517 |
+
"font": {"size": 16, "color": "#1f2937"}
|
| 518 |
+
},
|
| 519 |
+
xaxis_title="Metric",
|
| 520 |
+
yaxis_title="Value",
|
| 521 |
+
height=350,
|
| 522 |
+
showlegend=False,
|
| 523 |
+
margin=dict(l=50, r=20, t=60, b=50),
|
| 524 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 525 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 526 |
+
font=dict(color="#374151", size=12),
|
| 527 |
+
xaxis=dict(gridcolor="rgba(0,0,0,0.1)"),
|
| 528 |
+
yaxis=dict(gridcolor="rgba(0,0,0,0.1)")
|
| 529 |
)
|
| 530 |
+
rag_chart = fig
|
| 531 |
+
except Exception:
|
| 532 |
+
rag_chart = None
|
| 533 |
+
|
| 534 |
+
# Format summary text
|
| 535 |
+
total_queries = overview.get("total_queries", activity_data.get("activity", {}).get("total_queries", 0))
|
| 536 |
+
active_users = overview.get("active_users", activity_data.get("activity", {}).get("active_users", 0))
|
| 537 |
+
redflag_count = overview.get("redflag_count", redflags_data.get("count", 0))
|
| 538 |
+
last_query = overview.get("last_query", activity_data.get("activity", {}).get("last_query"))
|
| 539 |
+
|
| 540 |
+
# Calculate total tool usage
|
| 541 |
+
total_tool_calls = sum(stats.get("count", 0) for stats in tool_usage.values())
|
| 542 |
+
total_success = sum(stats.get("success_count", 0) for stats in tool_usage.values())
|
| 543 |
+
total_errors = sum(stats.get("error_count", 0) for stats in tool_usage.values())
|
| 544 |
+
|
| 545 |
+
success_rate = (total_success / total_tool_calls * 100) if total_tool_calls > 0 else 0
|
| 546 |
+
|
| 547 |
+
summary_text = f"""
|
| 548 |
+
#### π Activity Metrics
|
| 549 |
+
- **Total Queries:** `{total_queries}`
|
| 550 |
+
- **Active Users:** `{active_users}`
|
| 551 |
+
- **Red Flags:** `{redflag_count}`
|
| 552 |
+
- **Last Query:** `{last_query if last_query else "N/A"}`
|
| 553 |
+
|
| 554 |
+
---
|
| 555 |
+
|
| 556 |
+
#### π§ Tool Usage Overview
|
| 557 |
+
- **Total Tool Calls:** `{total_tool_calls}`
|
| 558 |
+
- **Successful Calls:** `{total_success}` β
|
| 559 |
+
- **Failed Calls:** `{total_errors}` {'β οΈ' if total_errors > 0 else ''}
|
| 560 |
+
- **Success Rate:** `{success_rate:.1f}%` {'π’' if success_rate >= 95 else 'π‘' if success_rate >= 80 else 'π΄'}
|
| 561 |
+
|
| 562 |
+
---
|
| 563 |
+
|
| 564 |
+
#### π RAG Quality Metrics
|
| 565 |
+
- **Total Searches:** `{rag_quality.get("total_searches", 0)}`
|
| 566 |
+
- **Avg Hits per Search:** `{rag_quality.get("avg_hits_per_search", 0):.2f}`
|
| 567 |
+
- **Avg Relevance Score:** `{rag_quality.get("avg_score", 0):.3f}`
|
| 568 |
+
- **Avg Top Score:** `{rag_quality.get("avg_top_score", 0):.3f}`
|
| 569 |
+
- **Avg Search Latency:** `{rag_quality.get("avg_latency_ms", 0):.2f}ms`
|
| 570 |
+
|
| 571 |
+
---
|
| 572 |
+
|
| 573 |
+
#### π Tool Breakdown
|
| 574 |
+
"""
|
| 575 |
+
|
| 576 |
+
# Add individual tool stats to summary
|
| 577 |
+
for tool_name, stats in sorted(tool_usage.items(), key=lambda x: x[1].get("count", 0), reverse=True):
|
| 578 |
+
tool_display = tool_name.replace(".", " ").title()
|
| 579 |
+
count = stats.get("count", 0)
|
| 580 |
+
latency = stats.get("avg_latency_ms", 0)
|
| 581 |
+
success = stats.get("success_count", 0)
|
| 582 |
+
errors = stats.get("error_count", 0)
|
| 583 |
+
status_icon = "β
" if errors == 0 else "β οΈ"
|
| 584 |
+
summary_text += f"- **{tool_display}** {status_icon}<br> β {count} calls β’ {latency:.1f}ms avg β’ {success} success β’ {errors} errors\n"
|
| 585 |
+
|
| 586 |
+
return summary_text, tool_usage, tool_chart, latency_chart, rag_chart, error_msg
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def list_documents(tenant_id: str, limit: int = 1000, offset: int = 0):
|
| 590 |
+
"""
|
| 591 |
+
List all documents for a tenant.
|
| 592 |
+
Returns a tuple of (status_message, documents_list, total_count, stats_dict, chart_fig).
|
| 593 |
+
"""
|
| 594 |
+
if not tenant_id or not tenant_id.strip():
|
| 595 |
+
return "β Tenant ID is required.", [], 0, {}, None
|
| 596 |
+
|
| 597 |
+
tenant_id = tenant_id.strip()
|
| 598 |
+
try:
|
| 599 |
+
response = requests.get(
|
| 600 |
+
f"{BACKEND_BASE_URL}/rag/list",
|
| 601 |
+
params={"tenant_id": tenant_id, "limit": limit, "offset": offset},
|
| 602 |
+
headers={"x-tenant-id": tenant_id},
|
| 603 |
+
timeout=30
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
if response.status_code == 200:
|
| 607 |
+
data = response.json()
|
| 608 |
+
documents = data.get("documents", [])
|
| 609 |
+
total = data.get("total", 0)
|
| 610 |
+
|
| 611 |
+
# Format documents for display and collect stats
|
| 612 |
+
formatted_docs = []
|
| 613 |
+
type_counts = Counter()
|
| 614 |
+
total_length = 0
|
| 615 |
+
|
| 616 |
+
for doc in documents:
|
| 617 |
+
doc_id = doc.get("id", "N/A")
|
| 618 |
+
text = doc.get("text", "")
|
| 619 |
+
created_at = doc.get("created_at", "")
|
| 620 |
+
preview = text[:200] + "..." if len(text) > 200 else text
|
| 621 |
+
|
| 622 |
+
# Detect document type
|
| 623 |
+
text_lower = text.lower()
|
| 624 |
+
if "http://" in text_lower or "https://" in text_lower or "www." in text_lower:
|
| 625 |
+
doc_type = "link"
|
| 626 |
+
elif any(x in text_lower for x in ["q:", "question:", "faq", "frequently asked"]):
|
| 627 |
+
doc_type = "faq"
|
| 628 |
+
elif ".pdf" in text_lower or "pdf document" in text_lower:
|
| 629 |
+
doc_type = "pdf"
|
| 630 |
+
else:
|
| 631 |
+
doc_type = "text"
|
| 632 |
+
|
| 633 |
+
type_counts[doc_type] += 1
|
| 634 |
+
total_length += len(text)
|
| 635 |
+
|
| 636 |
+
formatted_docs.append({
|
| 637 |
+
"ID": doc_id,
|
| 638 |
+
"Type": doc_type,
|
| 639 |
+
"Preview": preview,
|
| 640 |
+
"Length": len(text),
|
| 641 |
+
"Created": created_at[:10] if created_at else "N/A"
|
| 642 |
+
})
|
| 643 |
+
|
| 644 |
+
# Create statistics dictionary
|
| 645 |
+
stats = {
|
| 646 |
+
"total": total,
|
| 647 |
+
"types": dict(type_counts),
|
| 648 |
+
"avg_length": total_length // total if total > 0 else 0,
|
| 649 |
+
"total_chars": total_length
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
# Create pie chart for document types
|
| 653 |
+
chart_fig = None
|
| 654 |
+
if type_counts and PLOTLY_AVAILABLE:
|
| 655 |
+
try:
|
| 656 |
+
labels = list(type_counts.keys())
|
| 657 |
+
values = list(type_counts.values())
|
| 658 |
+
colors = {
|
| 659 |
+
"text": "#3b82f6", # blue
|
| 660 |
+
"pdf": "#ef4444", # red
|
| 661 |
+
"faq": "#a855f7", # purple
|
| 662 |
+
"link": "#06b6d4" # cyan
|
| 663 |
+
}
|
| 664 |
+
chart_colors = [colors.get(label, "#6b7280") for label in labels]
|
| 665 |
+
|
| 666 |
+
fig = go.Figure(data=[go.Pie(
|
| 667 |
+
labels=labels,
|
| 668 |
+
values=values,
|
| 669 |
+
hole=0.4,
|
| 670 |
+
marker=dict(colors=chart_colors),
|
| 671 |
+
textinfo='label+percent+value',
|
| 672 |
+
textfont=dict(size=12),
|
| 673 |
+
hovertemplate='<b>%{label}</b><br>Count: %{value}<br>Percentage: %{percent}<extra></extra>'
|
| 674 |
+
)])
|
| 675 |
+
fig.update_layout(
|
| 676 |
+
title={
|
| 677 |
+
"text": "Document Type Distribution",
|
| 678 |
+
"x": 0.5,
|
| 679 |
+
"xanchor": "center",
|
| 680 |
+
"font": {"size": 16}
|
| 681 |
+
},
|
| 682 |
+
height=400,
|
| 683 |
+
showlegend=True,
|
| 684 |
+
margin=dict(l=20, r=20, t=50, b=20)
|
| 685 |
+
)
|
| 686 |
+
chart_fig = fig
|
| 687 |
+
except Exception:
|
| 688 |
+
chart_fig = None
|
| 689 |
+
|
| 690 |
+
status = f"β
Found {total} document(s)"
|
| 691 |
+
return status, formatted_docs, total, stats, chart_fig
|
| 692 |
+
else:
|
| 693 |
+
error_msg = f"β Error {response.status_code}: {response.text}"
|
| 694 |
+
return error_msg, [], 0, {}, None
|
| 695 |
+
except requests.exceptions.ConnectionError:
|
| 696 |
+
return "β Could not reach backend. Ensure the FastAPI server is running.", [], 0, {}, None
|
| 697 |
+
except requests.exceptions.Timeout:
|
| 698 |
+
return "β±οΈ Request timed out. Please try again.", [], 0, {}, None
|
| 699 |
+
except Exception as exc:
|
| 700 |
+
return f"β Unexpected error: {exc}", [], 0, {}, None
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def delete_document(tenant_id: str, document_id: int):
|
| 704 |
+
"""Delete a specific document by ID."""
|
| 705 |
+
if not tenant_id or not tenant_id.strip():
|
| 706 |
+
return "β Tenant ID is required."
|
| 707 |
+
|
| 708 |
+
if not document_id or document_id <= 0:
|
| 709 |
+
return "β Invalid document ID."
|
| 710 |
+
|
| 711 |
+
tenant_id = tenant_id.strip()
|
| 712 |
+
try:
|
| 713 |
+
response = requests.delete(
|
| 714 |
+
f"{BACKEND_BASE_URL}/rag/delete/{document_id}",
|
| 715 |
+
params={"tenant_id": tenant_id},
|
| 716 |
+
headers={"x-tenant-id": tenant_id},
|
| 717 |
+
timeout=30
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
if response.status_code == 200:
|
| 721 |
+
return f"β
Document {document_id} deleted successfully."
|
| 722 |
+
elif response.status_code == 404:
|
| 723 |
+
return f"β Document {document_id} not found or access denied."
|
| 724 |
+
else:
|
| 725 |
+
error_data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
| 726 |
+
error_msg = error_data.get("detail", error_data.get("error", response.text))
|
| 727 |
+
return f"β Error {response.status_code}: {error_msg}"
|
| 728 |
+
except requests.exceptions.ConnectionError:
|
| 729 |
+
return "β Could not reach backend. Ensure the FastAPI server is running."
|
| 730 |
+
except requests.exceptions.Timeout:
|
| 731 |
+
return "β±οΈ Request timed out. Please try again."
|
| 732 |
+
except Exception as exc:
|
| 733 |
+
return f"β Unexpected error: {exc}"
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
def delete_all_documents(tenant_id: str):
|
| 737 |
+
"""Delete all documents for a tenant."""
|
| 738 |
+
if not tenant_id or not tenant_id.strip():
|
| 739 |
+
return "β Tenant ID is required."
|
| 740 |
+
|
| 741 |
+
tenant_id = tenant_id.strip()
|
| 742 |
+
try:
|
| 743 |
+
response = requests.delete(
|
| 744 |
+
f"{BACKEND_BASE_URL}/rag/delete-all",
|
| 745 |
+
params={"tenant_id": tenant_id},
|
| 746 |
+
headers={"x-tenant-id": tenant_id},
|
| 747 |
+
timeout=60
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
if response.status_code == 200:
|
| 751 |
+
data = response.json()
|
| 752 |
+
deleted_count = data.get("deleted_count", 0)
|
| 753 |
+
return f"β
Deleted {deleted_count} document(s) successfully."
|
| 754 |
+
else:
|
| 755 |
+
error_data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
| 756 |
+
error_msg = error_data.get("detail", error_data.get("error", response.text))
|
| 757 |
+
return f"β Error {response.status_code}: {error_msg}"
|
| 758 |
+
except requests.exceptions.ConnectionError:
|
| 759 |
+
return "β Could not reach backend. Ensure the FastAPI server is running."
|
| 760 |
+
except requests.exceptions.Timeout:
|
| 761 |
+
return "β±οΈ Request timed out. Please try again."
|
| 762 |
+
except Exception as exc:
|
| 763 |
+
return f"β Unexpected error: {exc}"
|
| 764 |
+
|
| 765 |
|
| 766 |
+
def search_knowledge_base(tenant_id: str, query: str):
|
| 767 |
+
"""Search the knowledge base using RAG semantic search."""
|
| 768 |
+
if not tenant_id or not tenant_id.strip():
|
| 769 |
+
return "β Tenant ID is required.", []
|
| 770 |
+
|
| 771 |
+
if not query or not query.strip():
|
| 772 |
+
return "β Please enter a search query.", []
|
| 773 |
+
|
| 774 |
+
tenant_id = tenant_id.strip()
|
| 775 |
+
query = query.strip()
|
| 776 |
+
|
| 777 |
+
try:
|
| 778 |
+
response = requests.post(
|
| 779 |
+
f"{BACKEND_BASE_URL}/rag/search",
|
| 780 |
+
json={"tenant_id": tenant_id, "query": query, "threshold": 0.3},
|
| 781 |
+
headers={"x-tenant-id": tenant_id, "Content-Type": "application/json"},
|
| 782 |
+
timeout=30
|
| 783 |
+
)
|
| 784 |
+
|
| 785 |
+
if response.status_code == 200:
|
| 786 |
+
data = response.json()
|
| 787 |
+
results = data.get("results", [])
|
| 788 |
+
|
| 789 |
+
formatted_results = []
|
| 790 |
+
for idx, result in enumerate(results, 1):
|
| 791 |
+
text = result.get("text", "")
|
| 792 |
+
relevance = result.get("relevance", result.get("score", 0.0))
|
| 793 |
+
formatted_results.append({
|
| 794 |
+
"Rank": idx,
|
| 795 |
+
"Text": text[:300] + "..." if len(text) > 300 else text,
|
| 796 |
+
"Relevance": f"{relevance:.3f}" if relevance else "N/A"
|
| 797 |
+
})
|
| 798 |
+
|
| 799 |
+
status = f"β
Found {len(results)} result(s) for '{query}'"
|
| 800 |
+
return status, formatted_results
|
| 801 |
+
else:
|
| 802 |
+
error_msg = f"β Error {response.status_code}: {response.text}"
|
| 803 |
+
return error_msg, []
|
| 804 |
+
except requests.exceptions.ConnectionError:
|
| 805 |
+
return "β Could not reach backend. Ensure the FastAPI server is running.", []
|
| 806 |
+
except requests.exceptions.Timeout:
|
| 807 |
+
return "β±οΈ Request timed out. Please try again.", []
|
| 808 |
+
except Exception as exc:
|
| 809 |
+
return f"β Unexpected error: {exc}", []
|
| 810 |
|
| 811 |
|
| 812 |
# Create Gradio interface
|
| 813 |
+
with gr.Blocks(
|
| 814 |
+
title="IntegraChat β MCP Autonomous Agent",
|
| 815 |
+
theme=gr.themes.Soft(),
|
| 816 |
+
css="""
|
| 817 |
+
.stat-card {
|
| 818 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 819 |
+
padding: 20px;
|
| 820 |
+
border-radius: 12px;
|
| 821 |
+
color: white;
|
| 822 |
+
text-align: center;
|
| 823 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 824 |
+
transition: transform 0.2s;
|
| 825 |
+
}
|
| 826 |
+
.stat-card:hover {
|
| 827 |
+
transform: translateY(-2px);
|
| 828 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
| 829 |
+
}
|
| 830 |
+
.stat-card h3 {
|
| 831 |
+
margin: 0 0 10px 0;
|
| 832 |
+
font-size: 14px;
|
| 833 |
+
opacity: 0.9;
|
| 834 |
+
}
|
| 835 |
+
.stat-card strong {
|
| 836 |
+
font-size: 24px;
|
| 837 |
+
font-weight: bold;
|
| 838 |
+
}
|
| 839 |
+
.summary-box {
|
| 840 |
+
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
| 841 |
+
padding: 24px;
|
| 842 |
+
border-radius: 12px;
|
| 843 |
+
border: 2px solid #374151;
|
| 844 |
+
max-height: 500px;
|
| 845 |
+
overflow-y: auto;
|
| 846 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
| 847 |
+
color: #f9fafb;
|
| 848 |
+
}
|
| 849 |
+
.summary-box h3, .summary-box h4 {
|
| 850 |
+
margin-top: 0;
|
| 851 |
+
margin-bottom: 12px;
|
| 852 |
+
color: #ffffff;
|
| 853 |
+
font-weight: 600;
|
| 854 |
+
}
|
| 855 |
+
.summary-box h4 {
|
| 856 |
+
color: #e5e7eb;
|
| 857 |
+
font-size: 16px;
|
| 858 |
+
margin-top: 20px;
|
| 859 |
+
margin-bottom: 10px;
|
| 860 |
+
}
|
| 861 |
+
.summary-box p {
|
| 862 |
+
color: #f3f4f6;
|
| 863 |
+
margin: 8px 0;
|
| 864 |
+
line-height: 1.6;
|
| 865 |
+
}
|
| 866 |
+
.summary-box ul {
|
| 867 |
+
margin: 10px 0;
|
| 868 |
+
padding-left: 24px;
|
| 869 |
+
color: #f3f4f6;
|
| 870 |
+
}
|
| 871 |
+
.summary-box li {
|
| 872 |
+
margin: 8px 0;
|
| 873 |
+
color: #f3f4f6;
|
| 874 |
+
line-height: 1.6;
|
| 875 |
+
}
|
| 876 |
+
.summary-box code {
|
| 877 |
+
background-color: #000000;
|
| 878 |
+
color: #00ff00;
|
| 879 |
+
padding: 2px 6px;
|
| 880 |
+
border-radius: 4px;
|
| 881 |
+
font-family: 'Courier New', monospace;
|
| 882 |
+
font-size: 13px;
|
| 883 |
+
border: 1px solid #374151;
|
| 884 |
+
}
|
| 885 |
+
.summary-box hr {
|
| 886 |
+
border: none;
|
| 887 |
+
border-top: 1px solid #4b5563;
|
| 888 |
+
margin: 16px 0;
|
| 889 |
+
}
|
| 890 |
+
.summary-box strong {
|
| 891 |
+
color: #ffffff;
|
| 892 |
+
}
|
| 893 |
+
.chart-title {
|
| 894 |
+
margin-bottom: 8px;
|
| 895 |
+
margin-top: 0;
|
| 896 |
+
font-weight: 600;
|
| 897 |
+
color: #1f2937;
|
| 898 |
+
text-align: center;
|
| 899 |
+
}
|
| 900 |
+
"""
|
| 901 |
+
) as demo:
|
| 902 |
gr.Markdown(
|
| 903 |
"""
|
| 904 |
# π€ IntegraChat β MCP Autonomous Agent
|
|
|
|
| 1021 |
metadata
|
| 1022 |
):
|
| 1023 |
source_type = "raw_text" if mode == "Raw Text" else "url"
|
| 1024 |
+
result = ingest_document(
|
| 1025 |
tenant_id=tenant_id,
|
| 1026 |
source_type=source_type,
|
| 1027 |
content=content,
|
|
|
|
| 1030 |
doc_id=doc_id_value,
|
| 1031 |
metadata_json=metadata
|
| 1032 |
)
|
| 1033 |
+
# Add note about refreshing Knowledge Base Library
|
| 1034 |
+
if "β
" in result:
|
| 1035 |
+
result += "\n\nπ‘ **Tip:** Go to the 'Knowledge Base Library' tab to view your ingested documents."
|
| 1036 |
+
return result
|
| 1037 |
|
| 1038 |
ingest_doc_button.click(
|
| 1039 |
fn=handle_ingest_document,
|
|
|
|
| 1058 |
ingest_file_button = gr.Button("Upload & Ingest File", visible=False)
|
| 1059 |
|
| 1060 |
def handle_file_ingestion(tenant_id, file_obj):
|
| 1061 |
+
result = ingest_file(tenant_id, file_obj)
|
| 1062 |
+
# Add note about refreshing Knowledge Base Library
|
| 1063 |
+
if "β
" in result:
|
| 1064 |
+
result += "\n\nπ‘ **Tip:** Go to the 'Knowledge Base Library' tab to view your ingested documents."
|
| 1065 |
+
return result
|
| 1066 |
|
| 1067 |
ingest_file_button.click(
|
| 1068 |
fn=handle_file_ingestion,
|
|
|
|
| 1100 |
]
|
| 1101 |
)
|
| 1102 |
|
| 1103 |
+
with gr.Tab("Knowledge Base Library"):
|
| 1104 |
+
gr.Markdown(
|
| 1105 |
+
"""
|
| 1106 |
+
### π Knowledge Base Library
|
| 1107 |
+
View, search, and manage all ingested documents for your tenant with visual analytics.
|
| 1108 |
+
|
| 1109 |
+
- **π Statistics:** View document counts, types, and distribution
|
| 1110 |
+
- **π Search:** Use semantic search to find relevant documents
|
| 1111 |
+
- **π½ Filter:** Filter documents by type (text, PDF, FAQ, link)
|
| 1112 |
+
- **ποΈ Delete:** Remove individual documents or delete all at once
|
| 1113 |
+
"""
|
| 1114 |
+
)
|
| 1115 |
+
|
| 1116 |
+
# Statistics Section
|
| 1117 |
+
with gr.Row():
|
| 1118 |
+
kb_total_docs = gr.Markdown("### π Total Documents\n**0**", elem_classes=["stat-card"])
|
| 1119 |
+
kb_text_docs = gr.Markdown("### π Text Documents\n**0**", elem_classes=["stat-card"])
|
| 1120 |
+
kb_pdf_docs = gr.Markdown("### π PDF Documents\n**0**", elem_classes=["stat-card"])
|
| 1121 |
+
kb_faq_docs = gr.Markdown("### β FAQ Documents\n**0**", elem_classes=["stat-card"])
|
| 1122 |
+
kb_link_docs = gr.Markdown("### π Link Documents\n**0**", elem_classes=["stat-card"])
|
| 1123 |
+
|
| 1124 |
+
# Chart and Search Section
|
| 1125 |
+
with gr.Row():
|
| 1126 |
+
with gr.Column(scale=1):
|
| 1127 |
+
kb_chart = gr.Plot(label="Document Type Distribution", show_label=True)
|
| 1128 |
+
kb_refresh_button = gr.Button("π Refresh Documents", variant="primary", size="lg")
|
| 1129 |
+
kb_delete_all_button = gr.Button("ποΈ Delete All Documents", variant="stop")
|
| 1130 |
+
|
| 1131 |
+
with gr.Column(scale=1):
|
| 1132 |
+
kb_search_query = gr.Textbox(
|
| 1133 |
+
label="π Search Knowledge Base",
|
| 1134 |
+
placeholder="Enter a search query (e.g., 'admin', 'policy', 'FAQ')...",
|
| 1135 |
+
show_label=True
|
| 1136 |
+
)
|
| 1137 |
+
kb_search_button = gr.Button("Search", variant="primary")
|
| 1138 |
+
kb_search_status = gr.Markdown("")
|
| 1139 |
+
kb_search_results = gr.Dataframe(
|
| 1140 |
+
headers=["Rank", "Text", "Relevance"],
|
| 1141 |
+
datatype=["number", "str", "str"],
|
| 1142 |
+
interactive=False,
|
| 1143 |
+
label="Search Results",
|
| 1144 |
+
wrap=True
|
| 1145 |
+
)
|
| 1146 |
+
|
| 1147 |
+
# Status and Filter Section
|
| 1148 |
+
kb_status = gr.Markdown("π Click **Refresh Documents** to load your knowledge base.")
|
| 1149 |
+
|
| 1150 |
+
with gr.Row():
|
| 1151 |
+
with gr.Column(scale=2):
|
| 1152 |
+
kb_filter_type = gr.Radio(
|
| 1153 |
+
["all", "text", "pdf", "faq", "link"],
|
| 1154 |
+
value="all",
|
| 1155 |
+
label="Filter by Type",
|
| 1156 |
+
info="Filter documents by detected type"
|
| 1157 |
+
)
|
| 1158 |
+
with gr.Column(scale=1):
|
| 1159 |
+
kb_avg_length = gr.Markdown("**Average Length:** 0 characters")
|
| 1160 |
+
|
| 1161 |
+
# Documents Table
|
| 1162 |
+
kb_documents_table = gr.Dataframe(
|
| 1163 |
+
headers=["ID", "Type", "Preview", "Length", "Created"],
|
| 1164 |
+
datatype=["number", "str", "str", "number", "str"],
|
| 1165 |
+
interactive=False,
|
| 1166 |
+
label="Documents",
|
| 1167 |
+
wrap=True
|
| 1168 |
+
)
|
| 1169 |
+
|
| 1170 |
+
# Delete Section
|
| 1171 |
+
with gr.Row():
|
| 1172 |
+
kb_delete_id = gr.Number(
|
| 1173 |
+
label="Delete Document by ID",
|
| 1174 |
+
value=None,
|
| 1175 |
+
precision=0,
|
| 1176 |
+
info="Enter document ID to delete",
|
| 1177 |
+
scale=3
|
| 1178 |
+
)
|
| 1179 |
+
kb_delete_button = gr.Button("Delete Document", variant="stop", scale=1)
|
| 1180 |
+
|
| 1181 |
+
kb_delete_status = gr.Markdown("")
|
| 1182 |
+
|
| 1183 |
+
def refresh_documents(tenant_id, filter_type="all"):
|
| 1184 |
+
status, docs, total, stats, chart_fig = list_documents(tenant_id)
|
| 1185 |
+
|
| 1186 |
+
# Filter documents by type if not "all"
|
| 1187 |
+
if filter_type != "all" and docs:
|
| 1188 |
+
filtered_docs = [doc for doc in docs if doc.get("Type", "").lower() == filter_type.lower()]
|
| 1189 |
+
docs = filtered_docs
|
| 1190 |
+
status = f"β
Found {len(docs)} {filter_type} document(s) (out of {total} total)"
|
| 1191 |
+
|
| 1192 |
+
# Update statistics cards
|
| 1193 |
+
type_counts = stats.get("types", {})
|
| 1194 |
+
total_md = f"### π Total Documents\n**{total}**"
|
| 1195 |
+
text_md = f"### π Text Documents\n**{type_counts.get('text', 0)}**"
|
| 1196 |
+
pdf_md = f"### π PDF Documents\n**{type_counts.get('pdf', 0)}**"
|
| 1197 |
+
faq_md = f"### β FAQ Documents\n**{type_counts.get('faq', 0)}**"
|
| 1198 |
+
link_md = f"### π Link Documents\n**{type_counts.get('link', 0)}**"
|
| 1199 |
+
avg_length_md = f"**Average Length:** {stats.get('avg_length', 0):,} characters"
|
| 1200 |
+
|
| 1201 |
+
status_msg = f"{status}\n\n**Total Documents:** {total} | **Total Characters:** {stats.get('total_chars', 0):,}"
|
| 1202 |
+
|
| 1203 |
+
return (
|
| 1204 |
+
status_msg, docs, total_md, text_md, pdf_md, faq_md, link_md,
|
| 1205 |
+
avg_length_md, chart_fig
|
| 1206 |
+
)
|
| 1207 |
+
|
| 1208 |
+
def filter_documents(tenant_id, filter_type):
|
| 1209 |
+
return refresh_documents(tenant_id, filter_type)
|
| 1210 |
+
|
| 1211 |
+
def search_kb(tenant_id, query):
|
| 1212 |
+
status, results = search_knowledge_base(tenant_id, query)
|
| 1213 |
+
return status, results
|
| 1214 |
+
|
| 1215 |
+
def delete_doc(tenant_id, doc_id):
|
| 1216 |
+
if doc_id is None or doc_id <= 0:
|
| 1217 |
+
return "β Please enter a valid document ID.", "", "", "", "", "", "", "", None
|
| 1218 |
+
result = delete_document(tenant_id, int(doc_id))
|
| 1219 |
+
# Refresh document list after deletion
|
| 1220 |
+
return (result, *refresh_documents(tenant_id, "all"))
|
| 1221 |
+
|
| 1222 |
+
def delete_all_docs(tenant_id):
|
| 1223 |
+
result = delete_all_documents(tenant_id)
|
| 1224 |
+
# Refresh document list after deletion
|
| 1225 |
+
return (result, *refresh_documents(tenant_id, "all"))
|
| 1226 |
+
|
| 1227 |
+
kb_refresh_button.click(
|
| 1228 |
+
fn=refresh_documents,
|
| 1229 |
+
inputs=[tenant_id_input, kb_filter_type],
|
| 1230 |
+
outputs=[
|
| 1231 |
+
kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
|
| 1232 |
+
kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
|
| 1233 |
+
]
|
| 1234 |
+
)
|
| 1235 |
+
|
| 1236 |
+
kb_filter_type.change(
|
| 1237 |
+
fn=filter_documents,
|
| 1238 |
+
inputs=[tenant_id_input, kb_filter_type],
|
| 1239 |
+
outputs=[
|
| 1240 |
+
kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
|
| 1241 |
+
kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
|
| 1242 |
+
]
|
| 1243 |
+
)
|
| 1244 |
+
|
| 1245 |
+
kb_search_button.click(
|
| 1246 |
+
fn=search_kb,
|
| 1247 |
+
inputs=[tenant_id_input, kb_search_query],
|
| 1248 |
+
outputs=[kb_search_status, kb_search_results]
|
| 1249 |
+
)
|
| 1250 |
+
|
| 1251 |
+
kb_search_query.submit(
|
| 1252 |
+
fn=search_kb,
|
| 1253 |
+
inputs=[tenant_id_input, kb_search_query],
|
| 1254 |
+
outputs=[kb_search_status, kb_search_results]
|
| 1255 |
+
)
|
| 1256 |
+
|
| 1257 |
+
kb_delete_button.click(
|
| 1258 |
+
fn=delete_doc,
|
| 1259 |
+
inputs=[tenant_id_input, kb_delete_id],
|
| 1260 |
+
outputs=[
|
| 1261 |
+
kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
|
| 1262 |
+
kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
|
| 1263 |
+
]
|
| 1264 |
+
)
|
| 1265 |
+
|
| 1266 |
+
kb_delete_all_button.click(
|
| 1267 |
+
fn=delete_all_docs,
|
| 1268 |
+
inputs=[tenant_id_input],
|
| 1269 |
+
outputs=[
|
| 1270 |
+
kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
|
| 1271 |
+
kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
|
| 1272 |
+
]
|
| 1273 |
+
)
|
| 1274 |
+
|
| 1275 |
with gr.Tab("Admin Analytics"):
|
| 1276 |
gr.Markdown(
|
| 1277 |
"""
|
| 1278 |
+
# π Admin Analytics Dashboard
|
|
|
|
| 1279 |
|
| 1280 |
+
Comprehensive tenant-level analytics with visual insights, performance metrics, and detailed tool usage statistics.
|
|
|
|
|
|
|
|
|
|
| 1281 |
"""
|
| 1282 |
)
|
| 1283 |
|
| 1284 |
+
# Refresh Button at Top
|
| 1285 |
+
with gr.Row():
|
| 1286 |
+
analytics_refresh = gr.Button("π Fetch Analytics Snapshot", variant="primary", size="lg")
|
| 1287 |
+
gr.Markdown("")
|
| 1288 |
+
|
| 1289 |
+
# Statistics Cards
|
| 1290 |
+
gr.Markdown("### π Key Metrics")
|
| 1291 |
+
with gr.Row():
|
| 1292 |
+
analytics_total_queries = gr.Markdown("### π Total Queries\n**0**", elem_classes=["stat-card"])
|
| 1293 |
+
analytics_active_users = gr.Markdown("### π₯ Active Users\n**0**", elem_classes=["stat-card"])
|
| 1294 |
+
analytics_redflags = gr.Markdown("### π© Red Flags\n**0**", elem_classes=["stat-card"])
|
| 1295 |
+
analytics_rag_searches = gr.Markdown("### π RAG Searches\n**0**", elem_classes=["stat-card"])
|
| 1296 |
+
|
| 1297 |
+
# Charts Section
|
| 1298 |
+
gr.Markdown("### π Performance Charts")
|
| 1299 |
+
with gr.Row():
|
| 1300 |
+
with gr.Column(scale=1):
|
| 1301 |
+
gr.Markdown("#### π Tool Usage Count", elem_classes=["chart-title"])
|
| 1302 |
+
analytics_tool_chart = gr.Plot(label="", show_label=False)
|
| 1303 |
+
with gr.Column(scale=1):
|
| 1304 |
+
gr.Markdown("#### β‘ Average Tool Latency", elem_classes=["chart-title"])
|
| 1305 |
+
analytics_latency_chart = gr.Plot(label="", show_label=False)
|
| 1306 |
+
|
| 1307 |
+
# RAG Quality and Summary Section
|
| 1308 |
+
with gr.Row():
|
| 1309 |
+
with gr.Column(scale=1):
|
| 1310 |
+
gr.Markdown("#### π RAG Quality Metrics", elem_classes=["chart-title"])
|
| 1311 |
+
analytics_rag_chart = gr.Plot(label="", show_label=False)
|
| 1312 |
+
|
| 1313 |
+
with gr.Column(scale=1):
|
| 1314 |
+
gr.Markdown("### π Analytics Summary")
|
| 1315 |
+
analytics_summary = gr.Markdown(
|
| 1316 |
+
"π Click **Fetch Analytics Snapshot** to load data.",
|
| 1317 |
+
elem_classes=["summary-box"]
|
| 1318 |
+
)
|
| 1319 |
+
|
| 1320 |
+
# Tool Usage Details Table
|
| 1321 |
+
gr.Markdown("### π§ Detailed Tool Usage")
|
| 1322 |
+
analytics_tool_table = gr.Dataframe(
|
| 1323 |
+
headers=["Tool", "Count", "Avg Latency (ms)", "Success", "Errors", "Total Tokens"],
|
| 1324 |
+
datatype=["str", "number", "number", "number", "number", "number"],
|
| 1325 |
+
interactive=False,
|
| 1326 |
+
label="",
|
| 1327 |
+
wrap=True
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
analytics_error = gr.Markdown("", visible=False)
|
| 1331 |
+
|
| 1332 |
+
def format_analytics(tenant_id):
|
| 1333 |
+
summary, tool_usage, tool_chart, latency_chart, rag_chart, error = fetch_admin_analytics(tenant_id)
|
| 1334 |
+
|
| 1335 |
+
if error:
|
| 1336 |
+
return (
|
| 1337 |
+
error, "", "", "", "", None, None, None, []
|
| 1338 |
+
)
|
| 1339 |
+
|
| 1340 |
+
# Extract overview data - fetch_admin_analytics already fetched it, but we need it again for cards
|
| 1341 |
+
overview_data = {}
|
| 1342 |
+
try:
|
| 1343 |
+
resp = requests.get(
|
| 1344 |
+
f"{BACKEND_BASE_URL}/analytics/overview",
|
| 1345 |
+
headers={"x-tenant-id": tenant_id},
|
| 1346 |
+
timeout=30
|
| 1347 |
+
)
|
| 1348 |
+
if resp.status_code == 200:
|
| 1349 |
+
data = resp.json()
|
| 1350 |
+
# The API returns {"overview": {...}} or direct overview object
|
| 1351 |
+
overview_data = data.get("overview", data) if isinstance(data, dict) else {}
|
| 1352 |
+
# Debug: print to see what we're getting
|
| 1353 |
+
print(f"DEBUG: Overview data keys: {overview_data.keys() if isinstance(overview_data, dict) else 'Not a dict'}")
|
| 1354 |
+
except Exception as e:
|
| 1355 |
+
print(f"Error fetching overview: {e}")
|
| 1356 |
+
pass
|
| 1357 |
+
|
| 1358 |
+
# Extract values with proper fallbacks - handle both nested and flat structures
|
| 1359 |
+
if isinstance(overview_data, dict):
|
| 1360 |
+
total_queries = overview_data.get("total_queries", 0)
|
| 1361 |
+
active_users = overview_data.get("active_users", 0)
|
| 1362 |
+
redflag_count = overview_data.get("redflag_count", 0)
|
| 1363 |
+
rag_quality = overview_data.get("rag_quality", {})
|
| 1364 |
+
rag_searches = rag_quality.get("total_searches", 0) if isinstance(rag_quality, dict) else 0
|
| 1365 |
+
else:
|
| 1366 |
+
total_queries = 0
|
| 1367 |
+
active_users = 0
|
| 1368 |
+
redflag_count = 0
|
| 1369 |
+
rag_quality = {}
|
| 1370 |
+
rag_searches = 0
|
| 1371 |
+
|
| 1372 |
+
# Format statistics cards
|
| 1373 |
+
queries_md = f"### π Total Queries\n**{total_queries}**"
|
| 1374 |
+
users_md = f"### π₯ Active Users\n**{active_users}**"
|
| 1375 |
+
redflags_md = f"### π© Red Flags\n**{redflag_count}**"
|
| 1376 |
+
rag_md = f"### π RAG Searches\n**{rag_searches}**"
|
| 1377 |
+
|
| 1378 |
+
# Format tool usage table
|
| 1379 |
+
tool_table_data = []
|
| 1380 |
+
for tool_name, stats in tool_usage.items():
|
| 1381 |
+
tool_table_data.append({
|
| 1382 |
+
"Tool": tool_name.replace(".", " ").title(),
|
| 1383 |
+
"Count": stats.get("count", 0),
|
| 1384 |
+
"Avg Latency (ms)": round(stats.get("avg_latency_ms", 0), 2),
|
| 1385 |
+
"Success": stats.get("success_count", 0),
|
| 1386 |
+
"Errors": stats.get("error_count", 0),
|
| 1387 |
+
"Total Tokens": stats.get("total_tokens", 0)
|
| 1388 |
+
})
|
| 1389 |
+
|
| 1390 |
+
return (
|
| 1391 |
+
summary, queries_md, users_md, redflags_md, rag_md,
|
| 1392 |
+
tool_chart, latency_chart, rag_chart, tool_table_data
|
| 1393 |
+
)
|
| 1394 |
|
| 1395 |
analytics_refresh.click(
|
| 1396 |
+
fn=format_analytics,
|
| 1397 |
inputs=[tenant_id_input],
|
| 1398 |
+
outputs=[
|
| 1399 |
+
analytics_summary,
|
| 1400 |
+
analytics_total_queries,
|
| 1401 |
+
analytics_active_users,
|
| 1402 |
+
analytics_redflags,
|
| 1403 |
+
analytics_rag_searches,
|
| 1404 |
+
analytics_tool_chart,
|
| 1405 |
+
analytics_latency_chart,
|
| 1406 |
+
analytics_rag_chart,
|
| 1407 |
+
analytics_tool_table
|
| 1408 |
+
]
|
| 1409 |
)
|
| 1410 |
|
| 1411 |
with gr.Tab("Admin Rules & Compliance"):
|
backend/README.md
CHANGED
|
@@ -116,6 +116,28 @@ Use the helper scripts in the repo root when validating backend changes:
|
|
| 116 |
- **Enhanced tool selection** automatically triggers RAG for admin questions, fact lookups ("who is", "what is"), and internal knowledge queries
|
| 117 |
- **Response unwrapping** in MCP client ensures orchestrator receives properly formatted results for tool scoring and prompt building
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
## Environment Variables (excerpt)
|
| 120 |
|
| 121 |
Defined in `env.example`:
|
|
|
|
| 116 |
- **Enhanced tool selection** automatically triggers RAG for admin questions, fact lookups ("who is", "what is"), and internal knowledge queries
|
| 117 |
- **Response unwrapping** in MCP client ensures orchestrator receives properly formatted results for tool scoring and prompt building
|
| 118 |
|
| 119 |
+
### UI Enhancements (app.py)
|
| 120 |
+
- **Knowledge Base Library Tab**:
|
| 121 |
+
- Statistics cards showing document counts by type
|
| 122 |
+
- Interactive Plotly pie chart for document type distribution
|
| 123 |
+
- Semantic search with relevance scoring
|
| 124 |
+
- Type filtering (text, PDF, FAQ, link)
|
| 125 |
+
- Document management with preview and deletion
|
| 126 |
+
- Auto-refresh after operations
|
| 127 |
+
|
| 128 |
+
- **Admin Analytics Tab**:
|
| 129 |
+
- Statistics cards for key metrics (queries, users, red flags, RAG searches)
|
| 130 |
+
- Interactive Plotly bar charts for tool usage, latency, and RAG quality
|
| 131 |
+
- Detailed tool usage table with performance metrics
|
| 132 |
+
- Formatted summary with dark theme styling
|
| 133 |
+
- Real-time data fetching and visualization
|
| 134 |
+
|
| 135 |
+
- **Modern UI/UX**:
|
| 136 |
+
- Dark theme with white text for better readability
|
| 137 |
+
- Custom CSS styling for cards and charts
|
| 138 |
+
- Improved error handling and status messages
|
| 139 |
+
- Responsive layout with proper component scaling
|
| 140 |
+
|
| 141 |
## Environment Variables (excerpt)
|
| 142 |
|
| 143 |
Defined in `env.example`:
|
frontend/README.md
CHANGED
|
@@ -10,6 +10,8 @@ It provides a polished operator console with:
|
|
| 10 |
- **Document ingestion UI** for uploading PDF, DOCX, TXT files or raw text
|
| 11 |
- **Feature grid** showcasing platform capabilities
|
| 12 |
|
|
|
|
|
|
|
| 13 |
## Running Locally
|
| 14 |
|
| 15 |
```bash
|
|
|
|
| 10 |
- **Document ingestion UI** for uploading PDF, DOCX, TXT files or raw text
|
| 11 |
- **Feature grid** showcasing platform capabilities
|
| 12 |
|
| 13 |
+
**Note:** IntegraChat also includes a Gradio-based UI (`app.py`) with interactive visualizations, statistics cards, and Plotly charts. See the root `README.md` for details on running the Gradio interface.
|
| 14 |
+
|
| 15 |
## Running Locally
|
| 16 |
|
| 17 |
```bash
|
requirements.txt
CHANGED
|
@@ -13,4 +13,5 @@ PyPDF2
|
|
| 13 |
python-docx
|
| 14 |
python-multipart
|
| 15 |
gradio>=4.0.0
|
| 16 |
-
requests>=2.31.0
|
|
|
|
|
|
| 13 |
python-docx
|
| 14 |
python-multipart
|
| 15 |
gradio>=4.0.0
|
| 16 |
+
requests>=2.31.0
|
| 17 |
+
plotly>=5.0.0
|