nothingworry commited on
Commit
b6650bb
Β·
1 Parent(s): ebb22ed

imporvement in the UI in app.py

Browse files
Files changed (5) hide show
  1. README.md +45 -5
  2. app.py +900 -42
  3. backend/README.md +22 -0
  4. frontend/README.md +2 -0
  5. 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
- **Tip:** Every action requires a tenant ID. The tenant ID is now managed centrally and persists across page refreshes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) -> str:
 
299
  if not tenant_id or not tenant_id.strip():
300
- return "❗ Tenant ID is required to view analytics."
 
301
 
302
  tenant_id = tenant_id.strip()
303
  headers = {"x-tenant-id": tenant_id}
304
- sections = []
305
-
306
- endpoints = [
307
- ("Overview", "/analytics/overview"),
308
- ("Tool Usage", "/analytics/tool-usage"),
309
- ("Red Flags", "/analytics/redflags"),
310
- ("Activity", "/analytics/activity"),
311
- ]
312
-
313
- for label, path in endpoints:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  try:
315
- resp = requests.get(
316
- f"{BACKEND_BASE_URL}{path}",
317
- headers=headers,
318
- timeout=30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  )
320
- if resp.status_code == 200:
321
- data = resp.json()
322
- pretty = json.dumps(data, indent=2)
323
- sections.append(f"### {label}\n```json\n{pretty}\n```")
324
- else:
325
- sections.append(f"### {label}\n❌ Error {resp.status_code}: {resp.text}")
326
- except requests.exceptions.ConnectionError:
327
- sections.append(f"### {label}\n❌ Could not reach backend. Is the FastAPI server running?")
328
- except requests.exceptions.Timeout:
329
- sections.append(f"### {label}\n⏱️ Request timed out. Please try again.")
330
- except Exception as exc:
331
- sections.append(f"### {label}\n❌ Unexpected error: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- return "\n\n".join(sections) if sections else "No analytics available."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
 
336
  # Create Gradio interface
337
- with gr.Blocks(title="IntegraChat β€” MCP Autonomous Agent", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return ingest_document(
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
- return ingest_file(tenant_id, file_obj)
 
 
 
 
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
- ### πŸ“Š Admin Analytics
535
- Review tenant-level analytics generated by the IntegraChat backend.
536
 
537
- - **Overview:** Total queries, active users, red-flag count.
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
- analytics_refresh = gr.Button("Fetch Analytics Snapshot", variant="primary")
545
- analytics_output = gr.Markdown("πŸ‘‰ Click the button to load analytics for the current tenant.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
  analytics_refresh.click(
548
- fn=fetch_admin_analytics,
549
  inputs=[tenant_id_input],
550
- outputs=analytics_output
 
 
 
 
 
 
 
 
 
 
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