nothingworry commited on
Commit
fe818bb
Β·
1 Parent(s): db5de26

feat: restrict Editor role to Document Ingestion tab only in Gradio UI

Browse files
Files changed (1) hide show
  1. app.py +707 -267
app.py CHANGED
@@ -15,8 +15,28 @@ except ImportError:
15
 
16
  BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://localhost:8000")
17
 
 
 
 
18
 
19
- def chat_with_agent(message, tenant_id, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  """
21
  Send a message to the backend MCP agent and return the response.
22
  Uses streaming for real-time word-by-word updates.
@@ -55,12 +75,19 @@ def chat_with_agent(message, tenant_id, history):
55
  "temperature": 0.0
56
  }
57
 
 
 
 
 
 
 
 
58
  try:
59
  # Make streaming request
60
  response = requests.post(
61
  backend_url,
62
  json=payload,
63
- headers={"Content-Type": "application/json"},
64
  stream=True,
65
  timeout=120
66
  )
@@ -144,6 +171,7 @@ def chat_with_agent(message, tenant_id, history):
144
 
145
  def ingest_document(
146
  tenant_id: str,
 
147
  source_type: str,
148
  content: str,
149
  document_url: str,
@@ -153,6 +181,9 @@ def ingest_document(
153
  ):
154
  if not tenant_id or not tenant_id.strip():
155
  return "❗ Tenant ID is required to ingest documents."
 
 
 
156
 
157
  tenant_id = tenant_id.strip()
158
 
@@ -187,10 +218,15 @@ def ingest_document(
187
  }
188
 
189
  try:
 
 
 
 
 
190
  response = requests.post(
191
  f"{BACKEND_BASE_URL}/rag/ingest-document",
192
  json=payload,
193
- headers={"Content-Type": "application/json"},
194
  timeout=60
195
  )
196
  if response.status_code == 200:
@@ -205,11 +241,14 @@ def ingest_document(
205
  return f"❌ Unexpected error during ingestion: {exc}"
206
 
207
 
208
- def ingest_file(tenant_id: str, file_obj):
209
  if not tenant_id or not tenant_id.strip():
210
  return "❗ Tenant ID is required to ingest files."
211
  if file_obj is None:
212
  return "❗ Please select a file to upload."
 
 
 
213
 
214
  tenant_id = tenant_id.strip()
215
 
@@ -221,10 +260,14 @@ def ingest_file(tenant_id: str, file_obj):
221
  files = {
222
  "file": (file_path.name, file_bytes, "application/octet-stream")
223
  }
 
 
 
 
224
  response = requests.post(
225
  f"{BACKEND_BASE_URL}/rag/ingest-file",
226
  files=files,
227
- headers={"x-tenant-id": tenant_id},
228
  timeout=120
229
  )
230
  if response.status_code == 200:
@@ -245,15 +288,19 @@ def _format_rules_table(rules: list[str]) -> list[list]:
245
  return [[idx + 1, rule] for idx, rule in enumerate(rules)]
246
 
247
 
248
- def fetch_admin_rules(tenant_id: str) -> tuple[str, list[list]]:
249
  if not tenant_id or not tenant_id.strip():
250
  return "❗ Tenant ID is required.", []
251
 
252
  tenant_id = tenant_id.strip()
253
  try:
 
 
 
 
254
  response = requests.get(
255
  f"{BACKEND_BASE_URL}/admin/rules",
256
- headers={"x-tenant-id": tenant_id},
257
  timeout=30
258
  )
259
  if response.status_code == 200:
@@ -335,11 +382,14 @@ def extract_rules_from_file(file_path) -> str:
335
  return f"❌ Error reading file: {str(e)}"
336
 
337
 
338
- def add_admin_rules(tenant_id: str, rules_text: str) -> str:
339
  if not tenant_id or not tenant_id.strip():
340
  return "❗ Tenant ID is required."
341
  if not rules_text or not rules_text.strip():
342
  return "❗ Provide at least one rule to upload."
 
 
 
343
 
344
  tenant_id = tenant_id.strip()
345
  # Filter out comment lines (starting with #) and empty lines
@@ -362,10 +412,14 @@ def add_admin_rules(tenant_id: str, rules_text: str) -> str:
362
  if total_rules == 1:
363
  # Single rule - use regular endpoint
364
  try:
 
 
 
 
365
  resp = requests.post(
366
  f"{BACKEND_BASE_URL}/admin/rules",
367
  params={"rule": rules[0], "enhance": "true"},
368
- headers={"x-tenant-id": tenant_id},
369
  timeout=30
370
  )
371
  if resp.status_code == 200:
@@ -392,10 +446,14 @@ def add_admin_rules(tenant_id: str, rules_text: str) -> str:
392
  total_chunks = (total_rules + CHUNK_SIZE - 1) // CHUNK_SIZE
393
 
394
  try:
 
 
 
 
395
  resp = requests.post(
396
  f"{BACKEND_BASE_URL}/admin/rules/bulk",
397
  json={"rules": chunk},
398
- headers={"x-tenant-id": tenant_id},
399
  params={"enhance": "true"},
400
  timeout=45 # Timeout per chunk (5 rules)
401
  )
@@ -428,19 +486,26 @@ def add_admin_rules(tenant_id: str, rules_text: str) -> str:
428
  return "\n\n".join(summary) if summary else "No rules were added."
429
 
430
 
431
- def delete_admin_rule(tenant_id: str, rule: str) -> str:
432
  if not tenant_id or not tenant_id.strip():
433
  return "❗ Tenant ID is required."
434
  if not rule or not rule.strip():
435
  return "❗ Provide the exact rule text to delete."
 
 
 
436
 
437
  tenant_id = tenant_id.strip()
438
  rule = rule.strip()
439
 
440
  try:
 
 
 
 
441
  resp = requests.delete(
442
  f"{BACKEND_BASE_URL}/admin/rules/{rule}",
443
- headers={"x-tenant-id": tenant_id},
444
  timeout=15
445
  )
446
  if resp.status_code == 200:
@@ -454,7 +519,7 @@ def delete_admin_rule(tenant_id: str, rule: str) -> str:
454
  return f"❌ Unexpected error: {exc}"
455
 
456
 
457
- def add_rules_from_file(tenant_id: str, file_path):
458
  """
459
  Extract rules from uploaded file and add them.
460
  """
@@ -477,31 +542,38 @@ def add_rules_from_file(tenant_id: str, file_path):
477
  return "❗ No text could be extracted from the file.", summary, rows
478
 
479
  # Add rules from extracted text
480
- status = add_admin_rules(tenant_id, extracted_text)
481
- summary, rows = fetch_admin_rules(tenant_id)
482
  return status, summary, rows
483
 
484
 
485
- def add_rules_and_refresh(tenant_id: str, rules_text: str):
486
- status = add_admin_rules(tenant_id, rules_text)
487
- summary, rows = fetch_admin_rules(tenant_id)
488
  return status, summary, rows
489
 
490
 
491
- def delete_rule_and_refresh(tenant_id: str, rule: str):
492
- status = delete_admin_rule(tenant_id, rule)
493
- summary, rows = fetch_admin_rules(tenant_id)
494
  return status, summary, rows
495
 
496
 
497
- def fetch_admin_analytics(tenant_id: str):
498
  """Fetch analytics data and return formatted results with visualizations."""
499
  if not tenant_id or not tenant_id.strip():
500
  error_msg = "❗ Tenant ID is required to view analytics."
501
  return error_msg, {}, None, None, None, None
 
 
 
 
502
 
503
  tenant_id = tenant_id.strip()
504
- headers = {"x-tenant-id": tenant_id}
 
 
 
505
 
506
  overview_data = {}
507
  tool_usage_data = {}
@@ -776,7 +848,7 @@ def fetch_admin_analytics(tenant_id: str):
776
  return summary_text, tool_usage, tool_chart, latency_chart, rag_chart, error_msg
777
 
778
 
779
- def list_documents(tenant_id: str, limit: int = 1000, offset: int = 0):
780
  """
781
  List all documents for a tenant.
782
  Returns a tuple of (status_message, documents_list, total_count, stats_dict, chart_fig).
@@ -786,10 +858,14 @@ def list_documents(tenant_id: str, limit: int = 1000, offset: int = 0):
786
 
787
  tenant_id = tenant_id.strip()
788
  try:
 
 
 
 
789
  response = requests.get(
790
  f"{BACKEND_BASE_URL}/rag/list",
791
  params={"tenant_id": tenant_id, "limit": limit, "offset": offset},
792
- headers={"x-tenant-id": tenant_id},
793
  timeout=30
794
  )
795
 
@@ -890,7 +966,7 @@ def list_documents(tenant_id: str, limit: int = 1000, offset: int = 0):
890
  return f"❌ Unexpected error: {exc}", [], 0, {}, None
891
 
892
 
893
- def delete_document(tenant_id: str, document_id: int):
894
  """Delete a specific document by ID."""
895
  if not tenant_id or not tenant_id.strip():
896
  return "❗ Tenant ID is required."
@@ -898,12 +974,19 @@ def delete_document(tenant_id: str, document_id: int):
898
  if not document_id or document_id <= 0:
899
  return "❗ Invalid document ID."
900
 
 
 
 
901
  tenant_id = tenant_id.strip()
902
  try:
 
 
 
 
903
  response = requests.delete(
904
  f"{BACKEND_BASE_URL}/rag/delete/{document_id}",
905
  params={"tenant_id": tenant_id},
906
- headers={"x-tenant-id": tenant_id},
907
  timeout=30
908
  )
909
 
@@ -923,17 +1006,25 @@ def delete_document(tenant_id: str, document_id: int):
923
  return f"❌ Unexpected error: {exc}"
924
 
925
 
926
- def delete_all_documents(tenant_id: str):
927
  """Delete all documents for a tenant."""
928
  if not tenant_id or not tenant_id.strip():
929
  return "❗ Tenant ID is required."
930
 
931
  tenant_id = tenant_id.strip()
 
 
 
 
932
  try:
 
 
 
 
933
  response = requests.delete(
934
  f"{BACKEND_BASE_URL}/rag/delete-all",
935
  params={"tenant_id": tenant_id},
936
- headers={"x-tenant-id": tenant_id},
937
  timeout=60
938
  )
939
 
@@ -953,7 +1044,7 @@ def delete_all_documents(tenant_id: str):
953
  return f"❌ Unexpected error: {exc}"
954
 
955
 
956
- def search_knowledge_base(tenant_id: str, query: str):
957
  """Search the knowledge base using RAG semantic search."""
958
  if not tenant_id or not tenant_id.strip():
959
  return "❗ Tenant ID is required.", []
@@ -965,10 +1056,15 @@ def search_knowledge_base(tenant_id: str, query: str):
965
  query = query.strip()
966
 
967
  try:
 
 
 
 
 
968
  response = requests.post(
969
  f"{BACKEND_BASE_URL}/rag/search",
970
  json={"tenant_id": tenant_id, "query": query, "threshold": 0.3},
971
- headers={"x-tenant-id": tenant_id, "Content-Type": "application/json"},
972
  timeout=30
973
  )
974
 
@@ -1002,115 +1098,294 @@ def search_knowledge_base(tenant_id: str, query: str):
1002
  # Create Gradio interface
1003
  with gr.Blocks(
1004
  title="IntegraChat β€” MCP Autonomous Agent",
1005
- theme=gr.themes.Soft(),
 
 
 
 
 
1006
  css="""
1007
- .stat-card {
1008
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  padding: 20px;
1010
  border-radius: 12px;
 
 
 
 
 
 
 
 
 
1011
  color: white;
1012
  text-align: center;
1013
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1014
- transition: transform 0.2s;
 
1015
  }
1016
  .stat-card:hover {
1017
- transform: translateY(-2px);
1018
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
1019
  }
1020
  .stat-card h3 {
1021
- margin: 0 0 10px 0;
1022
  font-size: 14px;
1023
- opacity: 0.9;
 
 
 
1024
  }
1025
  .stat-card strong {
1026
- font-size: 24px;
1027
- font-weight: bold;
 
 
1028
  }
 
 
1029
  .summary-box {
1030
- background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
1031
- padding: 24px;
1032
- border-radius: 12px;
1033
- border: 2px solid #374151;
1034
  max-height: 500px;
1035
  overflow-y: auto;
1036
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
1037
- color: #f9fafb;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1038
  }
1039
  .summary-box h3, .summary-box h4 {
1040
  margin-top: 0;
1041
- margin-bottom: 12px;
1042
  color: #ffffff;
1043
  font-weight: 600;
1044
  }
1045
  .summary-box h4 {
1046
- color: #e5e7eb;
1047
  font-size: 16px;
1048
- margin-top: 20px;
1049
- margin-bottom: 10px;
1050
  }
1051
  .summary-box p {
1052
- color: #f3f4f6;
1053
- margin: 8px 0;
1054
- line-height: 1.6;
1055
  }
1056
  .summary-box ul {
1057
- margin: 10px 0;
1058
- padding-left: 24px;
1059
- color: #f3f4f6;
1060
  }
1061
  .summary-box li {
1062
- margin: 8px 0;
1063
- color: #f3f4f6;
1064
- line-height: 1.6;
1065
  }
1066
  .summary-box code {
1067
- background-color: #000000;
1068
- color: #00ff00;
1069
- padding: 2px 6px;
1070
- border-radius: 4px;
1071
- font-family: 'Courier New', monospace;
1072
  font-size: 13px;
1073
- border: 1px solid #374151;
1074
  }
1075
  .summary-box hr {
1076
  border: none;
1077
- border-top: 1px solid #4b5563;
1078
- margin: 16px 0;
1079
  }
1080
  .summary-box strong {
1081
  color: #ffffff;
 
1082
  }
 
 
1083
  .chart-title {
1084
- margin-bottom: 8px;
1085
  margin-top: 0;
1086
  font-weight: 600;
1087
- color: #1f2937;
1088
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  }
1090
  """
1091
  ) as demo:
1092
- gr.Markdown(
1093
- """
1094
- # πŸ€– IntegraChat β€” MCP Autonomous Agent
1095
-
1096
- **Enterprise-grade AI with autonomous agents, secure multi-tenant RAG, real-time web search, and governance.**
1097
-
1098
- Enter your Tenant ID to chat with the MCP-powered agent or ingest documents into the enterprise knowledge base.
1099
- """
1100
- )
 
 
 
 
 
 
 
 
 
 
 
1101
 
1102
- tenant_id_input = gr.Textbox(
1103
- label="Tenant ID",
1104
- placeholder="Enter your tenant ID (e.g., tenant123)",
1105
- value="",
1106
- interactive=True
1107
- )
 
 
 
 
 
 
 
 
 
 
 
1108
 
1109
  with gr.Tabs():
1110
  with gr.Tab("Chat"):
1111
- with gr.Row():
1112
- with gr.Column(scale=2):
1113
- chatbot = gr.Chatbot(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  label="Chat with Agent",
1115
  height=500,
1116
  show_label=True,
@@ -1131,24 +1406,29 @@ with gr.Blocks(
1131
  with gr.Column(scale=1):
1132
  gr.Markdown(
1133
  """
 
1134
  ### πŸ“ Chat Instructions
1135
- 1. Enter your **Tenant ID** above
1136
  2. Ask a question or give a task to the agent
1137
  3. The MCP agent will automatically select tools (RAG, Web, etc.)
1138
 
1139
- ### βš™οΈ Backend Configuration
1140
- The agent connects to the FastAPI backend at `http://localhost:8000/agent/message`
 
 
 
 
1141
  """
1142
  )
1143
 
1144
  # Event handlers for chat tab with streaming
1145
- def send_message(message, tenant_id, history):
1146
  # Clear message input immediately
1147
  message_input_value = ""
1148
  # Use streaming function which yields updates
1149
  # Gradio will automatically handle the generator and update UI in real-time
1150
  try:
1151
- for updated_history in chat_with_agent(message, tenant_id, history):
1152
  yield updated_history, message_input_value
1153
  except Exception as e:
1154
  # Fallback if streaming fails
@@ -1158,24 +1438,44 @@ with gr.Blocks(
1158
 
1159
  send_button.click(
1160
  fn=send_message,
1161
- inputs=[message_input, tenant_id_input, chatbot],
1162
  outputs=[chatbot, message_input]
1163
  )
1164
 
1165
  message_input.submit(
1166
  fn=send_message,
1167
- inputs=[message_input, tenant_id_input, chatbot],
1168
  outputs=[chatbot, message_input]
1169
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1170
 
1171
- with gr.Tab("Document Ingestion"):
1172
  gr.Markdown(
1173
  """
 
1174
  ### πŸ“š Knowledge Base Ingestion
1175
  Ingest documents so the MCP agent can reference tenant-private knowledge.
1176
 
1177
- - **Raw text / URLs:** Use the fields below.
1178
- - **Files (PDF, DOCX, TXT, MD):** Use the file upload section.
 
 
 
 
 
1179
  """
1180
  )
1181
 
@@ -1213,6 +1513,7 @@ with gr.Blocks(
1213
 
1214
  def handle_ingest_document(
1215
  tenant_id,
 
1216
  mode,
1217
  content,
1218
  doc_url,
@@ -1223,6 +1524,7 @@ with gr.Blocks(
1223
  source_type = "raw_text" if mode == "Raw Text" else "url"
1224
  result = ingest_document(
1225
  tenant_id=tenant_id,
 
1226
  source_type=source_type,
1227
  content=content,
1228
  document_url=doc_url,
@@ -1239,6 +1541,7 @@ with gr.Blocks(
1239
  fn=handle_ingest_document,
1240
  inputs=[
1241
  tenant_id_input,
 
1242
  ingestion_mode,
1243
  doc_content,
1244
  document_url,
@@ -1257,8 +1560,8 @@ with gr.Blocks(
1257
  )
1258
  ingest_file_button = gr.Button("Upload & Ingest File", visible=False)
1259
 
1260
- def handle_file_ingestion(tenant_id, file_obj):
1261
- result = ingest_file(tenant_id, file_obj)
1262
  # Add note about refreshing Knowledge Base Library
1263
  if "βœ…" in result:
1264
  result += "\n\nπŸ’‘ **Tip:** Go to the 'Knowledge Base Library' tab to view your ingested documents."
@@ -1266,7 +1569,7 @@ with gr.Blocks(
1266
 
1267
  ingest_file_button.click(
1268
  fn=handle_file_ingestion,
1269
- inputs=[tenant_id_input, file_upload],
1270
  outputs=document_status
1271
  )
1272
 
@@ -1300,16 +1603,38 @@ with gr.Blocks(
1300
  ]
1301
  )
1302
 
1303
- with gr.Tab("Knowledge Base Library"):
1304
- gr.Markdown(
 
1305
  """
1306
- ### πŸ“š Knowledge Base Library
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307
  View, search, and manage all ingested documents for your tenant with visual analytics.
1308
 
 
1309
  - **πŸ“Š Statistics:** View document counts, types, and distribution
1310
  - **πŸ” Search:** Use semantic search to find relevant documents
1311
  - **πŸ”½ Filter:** Filter documents by type (text, PDF, FAQ, link)
1312
- - **πŸ—‘οΈ Delete:** Remove individual documents or delete all at once
 
1313
  """
1314
  )
1315
 
@@ -1367,8 +1692,9 @@ with gr.Blocks(
1367
  wrap=True
1368
  )
1369
 
1370
- # Delete Section
1371
- with gr.Row():
 
1372
  kb_delete_id = gr.Number(
1373
  label="Delete Document by ID",
1374
  value=None,
@@ -1380,8 +1706,16 @@ with gr.Blocks(
1380
 
1381
  kb_delete_status = gr.Markdown("")
1382
 
1383
- def refresh_documents(tenant_id, filter_type="all"):
1384
- status, docs, total, stats, chart_fig = list_documents(tenant_id)
 
 
 
 
 
 
 
 
1385
 
1386
  # Filter documents by type if not "all"
1387
  if filter_type != "all" and docs:
@@ -1405,28 +1739,28 @@ with gr.Blocks(
1405
  avg_length_md, chart_fig
1406
  )
1407
 
1408
- def filter_documents(tenant_id, filter_type):
1409
- return refresh_documents(tenant_id, filter_type)
1410
 
1411
- def search_kb(tenant_id, query):
1412
- status, results = search_knowledge_base(tenant_id, query)
1413
  return status, results
1414
 
1415
- def delete_doc(tenant_id, doc_id):
1416
  if doc_id is None or doc_id <= 0:
1417
  return "❗ Please enter a valid document ID.", "", "", "", "", "", "", "", None
1418
- result = delete_document(tenant_id, int(doc_id))
1419
  # Refresh document list after deletion
1420
- return (result, *refresh_documents(tenant_id, "all"))
1421
 
1422
- def delete_all_docs(tenant_id):
1423
- result = delete_all_documents(tenant_id)
1424
  # Refresh document list after deletion
1425
- return (result, *refresh_documents(tenant_id, "all"))
1426
 
1427
  kb_refresh_button.click(
1428
  fn=refresh_documents,
1429
- inputs=[tenant_id_input, kb_filter_type],
1430
  outputs=[
1431
  kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
1432
  kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
@@ -1435,7 +1769,7 @@ with gr.Blocks(
1435
 
1436
  kb_filter_type.change(
1437
  fn=filter_documents,
1438
- inputs=[tenant_id_input, kb_filter_type],
1439
  outputs=[
1440
  kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
1441
  kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
@@ -1444,19 +1778,19 @@ with gr.Blocks(
1444
 
1445
  kb_search_button.click(
1446
  fn=search_kb,
1447
- inputs=[tenant_id_input, kb_search_query],
1448
  outputs=[kb_search_status, kb_search_results]
1449
  )
1450
 
1451
  kb_search_query.submit(
1452
  fn=search_kb,
1453
- inputs=[tenant_id_input, kb_search_query],
1454
  outputs=[kb_search_status, kb_search_results]
1455
  )
1456
 
1457
  kb_delete_button.click(
1458
  fn=delete_doc,
1459
- inputs=[tenant_id_input, kb_delete_id],
1460
  outputs=[
1461
  kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
1462
  kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
@@ -1465,167 +1799,252 @@ with gr.Blocks(
1465
 
1466
  kb_delete_all_button.click(
1467
  fn=delete_all_docs,
1468
- inputs=[tenant_id_input],
1469
  outputs=[
1470
  kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
1471
  kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
1472
  ]
1473
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1474
 
1475
- with gr.Tab("Admin Analytics"):
1476
- gr.Markdown(
1477
- """
1478
- # πŸ“Š Admin Analytics Dashboard
1479
-
1480
- Comprehensive tenant-level analytics with visual insights, performance metrics, and detailed tool usage statistics.
1481
  """
 
 
 
 
 
 
 
 
 
 
 
1482
  )
1483
 
1484
- # Refresh Button at Top
1485
- with gr.Row():
1486
- analytics_refresh = gr.Button("πŸ”„ Fetch Analytics Snapshot", variant="primary", size="lg")
1487
- gr.Markdown("")
1488
-
1489
- # Statistics Cards
1490
- gr.Markdown("### πŸ“ˆ Key Metrics")
1491
- with gr.Row():
1492
- analytics_total_queries = gr.Markdown("### πŸ“Š Total Queries\n**0**", elem_classes=["stat-card"])
1493
- analytics_active_users = gr.Markdown("### πŸ‘₯ Active Users\n**0**", elem_classes=["stat-card"])
1494
- analytics_redflags = gr.Markdown("### 🚩 Red Flags\n**0**", elem_classes=["stat-card"])
1495
- analytics_rag_searches = gr.Markdown("### πŸ” RAG Searches\n**0**", elem_classes=["stat-card"])
1496
-
1497
- # Charts Section
1498
- gr.Markdown("### πŸ“Š Performance Charts")
1499
- with gr.Row():
1500
- with gr.Column(scale=1):
1501
- gr.Markdown("#### πŸ“ˆ Tool Usage Count", elem_classes=["chart-title"])
1502
- analytics_tool_chart = gr.Plot(label="", show_label=False)
1503
- with gr.Column(scale=1):
1504
- gr.Markdown("#### ⚑ Average Tool Latency", elem_classes=["chart-title"])
1505
- analytics_latency_chart = gr.Plot(label="", show_label=False)
1506
 
1507
- # RAG Quality and Summary Section
1508
- with gr.Row():
1509
- with gr.Column(scale=1):
1510
- gr.Markdown("#### πŸ” RAG Quality Metrics", elem_classes=["chart-title"])
1511
- analytics_rag_chart = gr.Plot(label="", show_label=False)
 
 
 
 
 
 
 
1512
 
1513
- with gr.Column(scale=1):
1514
- gr.Markdown("### πŸ“‹ Analytics Summary")
1515
- analytics_summary = gr.Markdown(
1516
- "πŸ‘‰ Click **Fetch Analytics Snapshot** to load data.",
1517
- elem_classes=["summary-box"]
1518
- )
1519
-
1520
- # Tool Usage Details Table
1521
- gr.Markdown("### πŸ”§ Detailed Tool Usage")
1522
- analytics_tool_table = gr.Dataframe(
1523
- headers=["Tool", "Count", "Avg Latency (ms)", "Success", "Errors", "Total Tokens"],
1524
- datatype=["str", "number", "number", "number", "number", "number"],
1525
- interactive=False,
1526
- label="",
1527
- wrap=True
1528
- )
1529
-
1530
- analytics_error = gr.Markdown("", visible=False)
1531
-
1532
- def format_analytics(tenant_id):
1533
- summary, tool_usage, tool_chart, latency_chart, rag_chart, error = fetch_admin_analytics(tenant_id)
1534
 
1535
- if error:
1536
- return (
1537
- error, "", "", "", "", None, None, None, []
1538
- )
 
 
 
 
 
1539
 
1540
- # Extract overview data - fetch_admin_analytics already fetched it, but we need it again for cards
1541
- overview_data = {}
1542
- try:
1543
- resp = requests.get(
1544
- f"{BACKEND_BASE_URL}/analytics/overview",
1545
- headers={"x-tenant-id": tenant_id},
1546
- timeout=30
1547
- )
1548
- if resp.status_code == 200:
1549
- data = resp.json()
1550
- # The API returns {"overview": {...}} or direct overview object
1551
- overview_data = data.get("overview", data) if isinstance(data, dict) else {}
1552
- # Debug: print to see what we're getting
1553
- print(f"DEBUG: Overview data keys: {overview_data.keys() if isinstance(overview_data, dict) else 'Not a dict'}")
1554
- except Exception as e:
1555
- print(f"Error fetching overview: {e}")
1556
- pass
1557
 
1558
- # Extract values with proper fallbacks - handle both nested and flat structures
1559
- if isinstance(overview_data, dict):
1560
- total_queries = overview_data.get("total_queries", 0)
1561
- active_users = overview_data.get("active_users", 0)
1562
- redflag_count = overview_data.get("redflag_count", 0)
1563
- rag_quality = overview_data.get("rag_quality", {})
1564
- rag_searches = rag_quality.get("total_searches", 0) if isinstance(rag_quality, dict) else 0
1565
- else:
1566
- total_queries = 0
1567
- active_users = 0
1568
- redflag_count = 0
1569
- rag_quality = {}
1570
- rag_searches = 0
1571
 
1572
- # Format statistics cards
1573
- queries_md = f"### πŸ“Š Total Queries\n**{total_queries}**"
1574
- users_md = f"### πŸ‘₯ Active Users\n**{active_users}**"
1575
- redflags_md = f"### 🚩 Red Flags\n**{redflag_count}**"
1576
- rag_md = f"### πŸ” RAG Searches\n**{rag_searches}**"
1577
 
1578
- # Format tool usage table
1579
- tool_table_data = []
1580
- for tool_name, stats in tool_usage.items():
1581
- tool_table_data.append({
1582
- "Tool": tool_name.replace(".", " ").title(),
1583
- "Count": stats.get("count", 0),
1584
- "Avg Latency (ms)": round(stats.get("avg_latency_ms", 0), 2),
1585
- "Success": stats.get("success_count", 0),
1586
- "Errors": stats.get("error_count", 0),
1587
- "Total Tokens": stats.get("total_tokens", 0)
1588
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1589
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1590
  return (
1591
- summary, queries_md, users_md, redflags_md, rag_md,
1592
- tool_chart, latency_chart, rag_chart, tool_table_data
1593
  )
1594
 
1595
- analytics_refresh.click(
1596
- fn=format_analytics,
1597
- inputs=[tenant_id_input],
1598
- outputs=[
1599
- analytics_summary,
1600
- analytics_total_queries,
1601
- analytics_active_users,
1602
- analytics_redflags,
1603
- analytics_rag_searches,
1604
- analytics_tool_chart,
1605
- analytics_latency_chart,
1606
- analytics_rag_chart,
1607
- analytics_tool_table
1608
- ]
1609
  )
 
1610
 
1611
- with gr.Tab("Admin Rules & Compliance"):
1612
- gr.Markdown(
1613
- """
1614
- ### πŸ›‘οΈ Admin Rules & Regulations
1615
- Upload or manage tenant-specific governance rules (red-flag patterns, compliance policies, etc.).
1616
-
1617
- **Upload Methods:**
1618
- - **Text Input:** Enter one rule per line in the text box
1619
- - **File Upload:** Upload rules from TXT, PDF, DOC, or DOCX files
1620
-
1621
- **Features:**
1622
- - Rules are automatically enhanced by LLM (identifies edge cases, improves patterns)
1623
- - Comment lines (starting with #) are automatically ignored
1624
- - Use the delete box to remove an exact rule
1625
- - Refresh anytime to view the latest rule set
1626
  """
 
 
 
 
 
 
 
 
 
 
 
1627
  )
1628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1629
  rules_summary = gr.Markdown("πŸ‘‰ Click **Refresh Rules** to see existing entries.")
1630
  rules_table = gr.Dataframe(
1631
  headers=["#", "Rule"],
@@ -1665,32 +2084,53 @@ with gr.Blocks(
1665
 
1666
  refresh_rules_button.click(
1667
  fn=fetch_admin_rules,
1668
- inputs=[tenant_id_input],
1669
  outputs=[rules_summary, rules_table]
1670
  )
1671
 
1672
  upload_rules_button.click(
1673
  fn=add_rules_and_refresh,
1674
- inputs=[tenant_id_input, rules_input],
1675
  outputs=[rules_status, rules_summary, rules_table]
1676
  )
1677
 
1678
  upload_file_button.click(
1679
  fn=add_rules_from_file,
1680
- inputs=[tenant_id_input, rules_file_upload],
1681
  outputs=[rules_status, rules_summary, rules_table]
1682
  )
1683
 
1684
  delete_rule_button.click(
1685
  fn=delete_rule_and_refresh,
1686
- inputs=[tenant_id_input, delete_rule_input],
1687
  outputs=[rules_status, rules_summary, rules_table]
1688
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1689
 
1690
  gr.Markdown(
1691
  """
1692
- ---
1693
- **Built with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) for the MCP Hackathon**
 
 
 
 
 
 
1694
  """
1695
  )
1696
 
 
15
 
16
  BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://localhost:8000")
17
 
18
+ # Role-based access control permissions
19
+ VALID_ROLES = ["viewer", "editor", "admin", "owner"]
20
+ DEFAULT_ROLE = "viewer"
21
 
22
+ def can_manage_rules(role: str) -> bool:
23
+ """Check if role can manage rules (admin/owner only)."""
24
+ return role in ["admin", "owner"]
25
+
26
+ def can_ingest_documents(role: str) -> bool:
27
+ """Check if role can ingest documents (editor/admin/owner)."""
28
+ return role in ["editor", "admin", "owner"]
29
+
30
+ def can_delete_documents(role: str) -> bool:
31
+ """Check if role can delete documents (admin/owner only)."""
32
+ return role in ["admin", "owner"]
33
+
34
+ def can_view_analytics(role: str) -> bool:
35
+ """Check if role can view analytics (admin/owner only)."""
36
+ return role in ["admin", "owner"]
37
+
38
+
39
+ def chat_with_agent(message, tenant_id, role, history):
40
  """
41
  Send a message to the backend MCP agent and return the response.
42
  Uses streaming for real-time word-by-word updates.
 
75
  "temperature": 0.0
76
  }
77
 
78
+ # Prepare headers with role
79
+ headers = {
80
+ "Content-Type": "application/json",
81
+ "x-tenant-id": tenant_id.strip(),
82
+ "x-user-role": role if role else DEFAULT_ROLE
83
+ }
84
+
85
  try:
86
  # Make streaming request
87
  response = requests.post(
88
  backend_url,
89
  json=payload,
90
+ headers=headers,
91
  stream=True,
92
  timeout=120
93
  )
 
171
 
172
  def ingest_document(
173
  tenant_id: str,
174
+ role: str,
175
  source_type: str,
176
  content: str,
177
  document_url: str,
 
181
  ):
182
  if not tenant_id or not tenant_id.strip():
183
  return "❗ Tenant ID is required to ingest documents."
184
+
185
+ if not can_ingest_documents(role):
186
+ return "❌ Access Denied: You need Editor, Admin, or Owner role to ingest documents."
187
 
188
  tenant_id = tenant_id.strip()
189
 
 
218
  }
219
 
220
  try:
221
+ headers = {
222
+ "Content-Type": "application/json",
223
+ "x-tenant-id": tenant_id,
224
+ "x-user-role": role if role else DEFAULT_ROLE
225
+ }
226
  response = requests.post(
227
  f"{BACKEND_BASE_URL}/rag/ingest-document",
228
  json=payload,
229
+ headers=headers,
230
  timeout=60
231
  )
232
  if response.status_code == 200:
 
241
  return f"❌ Unexpected error during ingestion: {exc}"
242
 
243
 
244
+ def ingest_file(tenant_id: str, role: str, file_obj):
245
  if not tenant_id or not tenant_id.strip():
246
  return "❗ Tenant ID is required to ingest files."
247
  if file_obj is None:
248
  return "❗ Please select a file to upload."
249
+
250
+ if not can_ingest_documents(role):
251
+ return "❌ Access Denied: You need Editor, Admin, or Owner role to ingest files."
252
 
253
  tenant_id = tenant_id.strip()
254
 
 
260
  files = {
261
  "file": (file_path.name, file_bytes, "application/octet-stream")
262
  }
263
+ headers = {
264
+ "x-tenant-id": tenant_id,
265
+ "x-user-role": role if role else DEFAULT_ROLE
266
+ }
267
  response = requests.post(
268
  f"{BACKEND_BASE_URL}/rag/ingest-file",
269
  files=files,
270
+ headers=headers,
271
  timeout=120
272
  )
273
  if response.status_code == 200:
 
288
  return [[idx + 1, rule] for idx, rule in enumerate(rules)]
289
 
290
 
291
+ def fetch_admin_rules(tenant_id: str, role: str) -> tuple[str, list[list]]:
292
  if not tenant_id or not tenant_id.strip():
293
  return "❗ Tenant ID is required.", []
294
 
295
  tenant_id = tenant_id.strip()
296
  try:
297
+ headers = {
298
+ "x-tenant-id": tenant_id,
299
+ "x-user-role": role if role else DEFAULT_ROLE
300
+ }
301
  response = requests.get(
302
  f"{BACKEND_BASE_URL}/admin/rules",
303
+ headers=headers,
304
  timeout=30
305
  )
306
  if response.status_code == 200:
 
382
  return f"❌ Error reading file: {str(e)}"
383
 
384
 
385
+ def add_admin_rules(tenant_id: str, role: str, rules_text: str) -> str:
386
  if not tenant_id or not tenant_id.strip():
387
  return "❗ Tenant ID is required."
388
  if not rules_text or not rules_text.strip():
389
  return "❗ Provide at least one rule to upload."
390
+
391
+ if not can_manage_rules(role):
392
+ return "❌ Access Denied: You need Admin or Owner role to manage rules."
393
 
394
  tenant_id = tenant_id.strip()
395
  # Filter out comment lines (starting with #) and empty lines
 
412
  if total_rules == 1:
413
  # Single rule - use regular endpoint
414
  try:
415
+ headers = {
416
+ "x-tenant-id": tenant_id,
417
+ "x-user-role": role if role else DEFAULT_ROLE
418
+ }
419
  resp = requests.post(
420
  f"{BACKEND_BASE_URL}/admin/rules",
421
  params={"rule": rules[0], "enhance": "true"},
422
+ headers=headers,
423
  timeout=30
424
  )
425
  if resp.status_code == 200:
 
446
  total_chunks = (total_rules + CHUNK_SIZE - 1) // CHUNK_SIZE
447
 
448
  try:
449
+ headers = {
450
+ "x-tenant-id": tenant_id,
451
+ "x-user-role": role if role else DEFAULT_ROLE
452
+ }
453
  resp = requests.post(
454
  f"{BACKEND_BASE_URL}/admin/rules/bulk",
455
  json={"rules": chunk},
456
+ headers=headers,
457
  params={"enhance": "true"},
458
  timeout=45 # Timeout per chunk (5 rules)
459
  )
 
486
  return "\n\n".join(summary) if summary else "No rules were added."
487
 
488
 
489
+ def delete_admin_rule(tenant_id: str, role: str, rule: str) -> str:
490
  if not tenant_id or not tenant_id.strip():
491
  return "❗ Tenant ID is required."
492
  if not rule or not rule.strip():
493
  return "❗ Provide the exact rule text to delete."
494
+
495
+ if not can_manage_rules(role):
496
+ return "❌ Access Denied: You need Admin or Owner role to delete rules."
497
 
498
  tenant_id = tenant_id.strip()
499
  rule = rule.strip()
500
 
501
  try:
502
+ headers = {
503
+ "x-tenant-id": tenant_id,
504
+ "x-user-role": role if role else DEFAULT_ROLE
505
+ }
506
  resp = requests.delete(
507
  f"{BACKEND_BASE_URL}/admin/rules/{rule}",
508
+ headers=headers,
509
  timeout=15
510
  )
511
  if resp.status_code == 200:
 
519
  return f"❌ Unexpected error: {exc}"
520
 
521
 
522
+ def add_rules_from_file(tenant_id: str, role: str, file_path):
523
  """
524
  Extract rules from uploaded file and add them.
525
  """
 
542
  return "❗ No text could be extracted from the file.", summary, rows
543
 
544
  # Add rules from extracted text
545
+ status = add_admin_rules(tenant_id, role, extracted_text)
546
+ summary, rows = fetch_admin_rules(tenant_id, role)
547
  return status, summary, rows
548
 
549
 
550
+ def add_rules_and_refresh(tenant_id: str, role: str, rules_text: str):
551
+ status = add_admin_rules(tenant_id, role, rules_text)
552
+ summary, rows = fetch_admin_rules(tenant_id, role)
553
  return status, summary, rows
554
 
555
 
556
+ def delete_rule_and_refresh(tenant_id: str, role: str, rule: str):
557
+ status = delete_admin_rule(tenant_id, role, rule)
558
+ summary, rows = fetch_admin_rules(tenant_id, role)
559
  return status, summary, rows
560
 
561
 
562
+ def fetch_admin_analytics(tenant_id: str, role: str):
563
  """Fetch analytics data and return formatted results with visualizations."""
564
  if not tenant_id or not tenant_id.strip():
565
  error_msg = "❗ Tenant ID is required to view analytics."
566
  return error_msg, {}, None, None, None, None
567
+
568
+ if not can_view_analytics(role):
569
+ error_msg = "❌ Access Denied: You need Admin or Owner role to view analytics."
570
+ return error_msg, {}, None, None, None, None
571
 
572
  tenant_id = tenant_id.strip()
573
+ headers = {
574
+ "x-tenant-id": tenant_id,
575
+ "x-user-role": role if role else DEFAULT_ROLE
576
+ }
577
 
578
  overview_data = {}
579
  tool_usage_data = {}
 
848
  return summary_text, tool_usage, tool_chart, latency_chart, rag_chart, error_msg
849
 
850
 
851
+ def list_documents(tenant_id: str, role: str, limit: int = 1000, offset: int = 0):
852
  """
853
  List all documents for a tenant.
854
  Returns a tuple of (status_message, documents_list, total_count, stats_dict, chart_fig).
 
858
 
859
  tenant_id = tenant_id.strip()
860
  try:
861
+ headers = {
862
+ "x-tenant-id": tenant_id,
863
+ "x-user-role": role if role else DEFAULT_ROLE
864
+ }
865
  response = requests.get(
866
  f"{BACKEND_BASE_URL}/rag/list",
867
  params={"tenant_id": tenant_id, "limit": limit, "offset": offset},
868
+ headers=headers,
869
  timeout=30
870
  )
871
 
 
966
  return f"❌ Unexpected error: {exc}", [], 0, {}, None
967
 
968
 
969
+ def delete_document(tenant_id: str, role: str, document_id: int):
970
  """Delete a specific document by ID."""
971
  if not tenant_id or not tenant_id.strip():
972
  return "❗ Tenant ID is required."
 
974
  if not document_id or document_id <= 0:
975
  return "❗ Invalid document ID."
976
 
977
+ if not can_delete_documents(role):
978
+ return "❌ Access Denied: You need Admin or Owner role to delete documents."
979
+
980
  tenant_id = tenant_id.strip()
981
  try:
982
+ headers = {
983
+ "x-tenant-id": tenant_id,
984
+ "x-user-role": role if role else DEFAULT_ROLE
985
+ }
986
  response = requests.delete(
987
  f"{BACKEND_BASE_URL}/rag/delete/{document_id}",
988
  params={"tenant_id": tenant_id},
989
+ headers=headers,
990
  timeout=30
991
  )
992
 
 
1006
  return f"❌ Unexpected error: {exc}"
1007
 
1008
 
1009
+ def delete_all_documents(tenant_id: str, role: str):
1010
  """Delete all documents for a tenant."""
1011
  if not tenant_id or not tenant_id.strip():
1012
  return "❗ Tenant ID is required."
1013
 
1014
  tenant_id = tenant_id.strip()
1015
+
1016
+ if not can_delete_documents(role):
1017
+ return "❌ Access Denied: You need Admin or Owner role to delete documents."
1018
+
1019
  try:
1020
+ headers = {
1021
+ "x-tenant-id": tenant_id,
1022
+ "x-user-role": role if role else DEFAULT_ROLE
1023
+ }
1024
  response = requests.delete(
1025
  f"{BACKEND_BASE_URL}/rag/delete-all",
1026
  params={"tenant_id": tenant_id},
1027
+ headers=headers,
1028
  timeout=60
1029
  )
1030
 
 
1044
  return f"❌ Unexpected error: {exc}"
1045
 
1046
 
1047
+ def search_knowledge_base(tenant_id: str, role: str, query: str):
1048
  """Search the knowledge base using RAG semantic search."""
1049
  if not tenant_id or not tenant_id.strip():
1050
  return "❗ Tenant ID is required.", []
 
1056
  query = query.strip()
1057
 
1058
  try:
1059
+ headers = {
1060
+ "x-tenant-id": tenant_id,
1061
+ "x-user-role": role if role else DEFAULT_ROLE,
1062
+ "Content-Type": "application/json"
1063
+ }
1064
  response = requests.post(
1065
  f"{BACKEND_BASE_URL}/rag/search",
1066
  json={"tenant_id": tenant_id, "query": query, "threshold": 0.3},
1067
+ headers=headers,
1068
  timeout=30
1069
  )
1070
 
 
1098
  # Create Gradio interface
1099
  with gr.Blocks(
1100
  title="IntegraChat β€” MCP Autonomous Agent",
1101
+ theme=gr.themes.Soft(
1102
+ primary_hue="cyan",
1103
+ secondary_hue="blue",
1104
+ neutral_hue="slate",
1105
+ font=("Inter", "system-ui", "sans-serif")
1106
+ ),
1107
  css="""
1108
+ /* Global improvements */
1109
+ .gradio-container {
1110
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
1111
+ }
1112
+
1113
+ /* Header styling */
1114
+ .header-section {
1115
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
1116
+ padding: 32px 24px;
1117
+ border-radius: 16px;
1118
+ margin-bottom: 24px;
1119
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
1120
+ border: 1px solid rgba(148, 163, 184, 0.1);
1121
+ }
1122
+
1123
+ .header-section h1 {
1124
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
1125
+ -webkit-background-clip: text;
1126
+ -webkit-text-fill-color: transparent;
1127
+ background-clip: text;
1128
+ font-size: 2.5rem;
1129
+ font-weight: 700;
1130
+ margin-bottom: 12px;
1131
+ }
1132
+
1133
+ /* Input fields styling */
1134
+ .input-container {
1135
+ background: rgba(255, 255, 255, 0.05);
1136
  padding: 20px;
1137
  border-radius: 12px;
1138
+ border: 1px solid rgba(148, 163, 184, 0.2);
1139
+ backdrop-filter: blur(10px);
1140
+ }
1141
+
1142
+ /* Stat cards with better gradients */
1143
+ .stat-card {
1144
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1145
+ padding: 24px;
1146
+ border-radius: 16px;
1147
  color: white;
1148
  text-align: center;
1149
+ box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
1150
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1151
+ border: 1px solid rgba(255, 255, 255, 0.1);
1152
  }
1153
  .stat-card:hover {
1154
+ transform: translateY(-4px) scale(1.02);
1155
+ box-shadow: 0 12px 32px rgba(102, 126, 234, 0.4);
1156
  }
1157
  .stat-card h3 {
1158
+ margin: 0 0 12px 0;
1159
  font-size: 14px;
1160
+ opacity: 0.95;
1161
+ font-weight: 500;
1162
+ letter-spacing: 0.5px;
1163
+ text-transform: uppercase;
1164
  }
1165
  .stat-card strong {
1166
+ font-size: 32px;
1167
+ font-weight: 700;
1168
+ display: block;
1169
+ margin-top: 8px;
1170
  }
1171
+
1172
+ /* Enhanced summary box */
1173
  .summary-box {
1174
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
1175
+ padding: 28px;
1176
+ border-radius: 16px;
1177
+ border: 1px solid rgba(148, 163, 184, 0.2);
1178
  max-height: 500px;
1179
  overflow-y: auto;
1180
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1181
+ color: #f1f5f9;
1182
+ backdrop-filter: blur(10px);
1183
+ }
1184
+ .summary-box::-webkit-scrollbar {
1185
+ width: 8px;
1186
+ }
1187
+ .summary-box::-webkit-scrollbar-track {
1188
+ background: rgba(255, 255, 255, 0.05);
1189
+ border-radius: 4px;
1190
+ }
1191
+ .summary-box::-webkit-scrollbar-thumb {
1192
+ background: rgba(148, 163, 184, 0.3);
1193
+ border-radius: 4px;
1194
+ }
1195
+ .summary-box::-webkit-scrollbar-thumb:hover {
1196
+ background: rgba(148, 163, 184, 0.5);
1197
  }
1198
  .summary-box h3, .summary-box h4 {
1199
  margin-top: 0;
1200
+ margin-bottom: 16px;
1201
  color: #ffffff;
1202
  font-weight: 600;
1203
  }
1204
  .summary-box h4 {
1205
+ color: #e2e8f0;
1206
  font-size: 16px;
1207
+ margin-top: 24px;
1208
+ margin-bottom: 12px;
1209
  }
1210
  .summary-box p {
1211
+ color: #f1f5f9;
1212
+ margin: 10px 0;
1213
+ line-height: 1.7;
1214
  }
1215
  .summary-box ul {
1216
+ margin: 12px 0;
1217
+ padding-left: 28px;
1218
+ color: #f1f5f9;
1219
  }
1220
  .summary-box li {
1221
+ margin: 10px 0;
1222
+ color: #f1f5f9;
1223
+ line-height: 1.7;
1224
  }
1225
  .summary-box code {
1226
+ background-color: rgba(0, 0, 0, 0.4);
1227
+ color: #00ff88;
1228
+ padding: 3px 8px;
1229
+ border-radius: 6px;
1230
+ font-family: 'Fira Code', 'Courier New', monospace;
1231
  font-size: 13px;
1232
+ border: 1px solid rgba(148, 163, 184, 0.2);
1233
  }
1234
  .summary-box hr {
1235
  border: none;
1236
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
1237
+ margin: 20px 0;
1238
  }
1239
  .summary-box strong {
1240
  color: #ffffff;
1241
+ font-weight: 600;
1242
  }
1243
+
1244
+ /* Chart titles */
1245
  .chart-title {
1246
+ margin-bottom: 12px;
1247
  margin-top: 0;
1248
  font-weight: 600;
1249
+ color: #1e293b;
1250
  text-align: center;
1251
+ font-size: 18px;
1252
+ }
1253
+
1254
+ /* Button enhancements */
1255
+ button.primary {
1256
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
1257
+ border: none;
1258
+ box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
1259
+ transition: all 0.3s ease;
1260
+ }
1261
+ button.primary:hover {
1262
+ transform: translateY(-2px);
1263
+ box-shadow: 0 6px 16px rgba(6, 182, 212, 0.4);
1264
+ }
1265
+
1266
+ /* Tab styling */
1267
+ .tab-nav {
1268
+ border-bottom: 2px solid rgba(148, 163, 184, 0.1);
1269
+ }
1270
+
1271
+ /* Role badge styling */
1272
+ .role-badge {
1273
+ display: inline-block;
1274
+ padding: 6px 12px;
1275
+ border-radius: 20px;
1276
+ font-size: 12px;
1277
+ font-weight: 600;
1278
+ text-transform: uppercase;
1279
+ letter-spacing: 0.5px;
1280
+ }
1281
+ .role-viewer {
1282
+ background: linear-gradient(135deg, #64748b 0%, #475569 100%);
1283
+ color: white;
1284
+ }
1285
+ .role-editor {
1286
+ background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
1287
+ color: white;
1288
+ }
1289
+ .role-admin {
1290
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
1291
+ color: white;
1292
+ }
1293
+ .role-owner {
1294
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
1295
+ color: white;
1296
+ }
1297
+
1298
+ /* Improved input styling */
1299
+ input[type="text"], textarea, select {
1300
+ border-radius: 10px !important;
1301
+ border: 2px solid rgba(148, 163, 184, 0.2) !important;
1302
+ transition: all 0.3s ease !important;
1303
+ }
1304
+ input[type="text"]:focus, textarea:focus, select:focus {
1305
+ border-color: #06b6d4 !important;
1306
+ box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1) !important;
1307
+ }
1308
+
1309
+ /* Card styling for sections */
1310
+ .section-card {
1311
+ background: rgba(255, 255, 255, 0.02);
1312
+ padding: 24px;
1313
+ border-radius: 16px;
1314
+ border: 1px solid rgba(148, 163, 184, 0.1);
1315
+ margin-bottom: 20px;
1316
+ backdrop-filter: blur(10px);
1317
+ }
1318
+
1319
+ /* Chatbot styling */
1320
+ .chatbot {
1321
+ border-radius: 12px !important;
1322
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
1323
  }
1324
  """
1325
  ) as demo:
1326
+ with gr.Column(elem_classes=["header-section"]):
1327
+ gr.Markdown(
1328
+ """
1329
+ # πŸ€– IntegraChat β€” MCP Autonomous Agent
1330
+
1331
+ **Enterprise-grade AI with autonomous agents, secure multi-tenant RAG, real-time web search, and governance.**
1332
+ """
1333
+ )
1334
+ gr.Markdown(
1335
+ """
1336
+ <div style="background: rgba(6, 182, 212, 0.1); padding: 16px; border-radius: 10px; border-left: 4px solid #06b6d4; margin-top: 16px;">
1337
+ <strong>πŸ” Role-Based Access Control:</strong> Features are automatically shown/hidden based on your role:
1338
+ <ul style="margin: 8px 0 0 0; padding-left: 24px;">
1339
+ <li><strong>πŸ‘€ Viewer:</strong> Chat only</li>
1340
+ <li><strong>✏️ Editor:</strong> Chat + Document Ingestion (no delete)</li>
1341
+ <li><strong>πŸ›‘οΈ Admin/Owner:</strong> Full access to all features</li>
1342
+ </ul>
1343
+ </div>
1344
+ """
1345
+ )
1346
 
1347
+ with gr.Row(elem_classes=["input-container"]):
1348
+ tenant_id_input = gr.Textbox(
1349
+ label="🏒 Tenant ID",
1350
+ placeholder="Enter your tenant ID (e.g., tenant123)",
1351
+ value="",
1352
+ interactive=True,
1353
+ scale=2,
1354
+ info="Required for all operations"
1355
+ )
1356
+ role_input = gr.Dropdown(
1357
+ label="πŸ‘€ User Role",
1358
+ choices=VALID_ROLES,
1359
+ value=DEFAULT_ROLE,
1360
+ interactive=True,
1361
+ scale=1,
1362
+ info="Select your role to access appropriate features"
1363
+ )
1364
 
1365
  with gr.Tabs():
1366
  with gr.Tab("Chat"):
1367
+ # Access denied for Editor role - Editor should only see Document Ingestion
1368
+ chat_access_denied = gr.Markdown(
1369
+ """
1370
+ <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%); padding: 40px; border-radius: 16px; border: 2px solid rgba(239, 68, 68, 0.4); text-align: center; margin: 20px 0;">
1371
+ <h2 style="color: #fca5a5; margin-bottom: 16px;">πŸ”’ Access Denied</h2>
1372
+ <p style="color: #f1f5f9; font-size: 16px; margin-bottom: 12px;">
1373
+ <strong>Editor role can only access Document Ingestion.</strong>
1374
+ </p>
1375
+ <p style="color: #cbd5e1; font-size: 14px;">
1376
+ Please switch to Owner or Admin role to access Chat functionality, or go to the Document Ingestion tab.
1377
+ </p>
1378
+ </div>
1379
+ """,
1380
+ visible=False
1381
+ )
1382
+
1383
+ chat_content = gr.Column(visible=True)
1384
+
1385
+ with chat_content:
1386
+ with gr.Row():
1387
+ with gr.Column(scale=2):
1388
+ chatbot = gr.Chatbot(
1389
  label="Chat with Agent",
1390
  height=500,
1391
  show_label=True,
 
1406
  with gr.Column(scale=1):
1407
  gr.Markdown(
1408
  """
1409
+ <div style="background: linear-gradient(135deg, rgba(6, 182, 212, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%); padding: 20px; border-radius: 12px; border: 1px solid rgba(6, 182, 212, 0.2);">
1410
  ### πŸ“ Chat Instructions
1411
+ 1. Enter your **Tenant ID** and **Role** above
1412
  2. Ask a question or give a task to the agent
1413
  3. The MCP agent will automatically select tools (RAG, Web, etc.)
1414
 
1415
+ ### ⚑ Features
1416
+ - ✨ Real-time streaming responses
1417
+ - 🧠 Multi-step planning & reasoning
1418
+ - πŸ” Automatic tool selection
1419
+ - πŸ’Ύ Conversation memory
1420
+ </div>
1421
  """
1422
  )
1423
 
1424
  # Event handlers for chat tab with streaming
1425
+ def send_message(message, tenant_id, role, history):
1426
  # Clear message input immediately
1427
  message_input_value = ""
1428
  # Use streaming function which yields updates
1429
  # Gradio will automatically handle the generator and update UI in real-time
1430
  try:
1431
+ for updated_history in chat_with_agent(message, tenant_id, role, history):
1432
  yield updated_history, message_input_value
1433
  except Exception as e:
1434
  # Fallback if streaming fails
 
1438
 
1439
  send_button.click(
1440
  fn=send_message,
1441
+ inputs=[message_input, tenant_id_input, role_input, chatbot],
1442
  outputs=[chatbot, message_input]
1443
  )
1444
 
1445
  message_input.submit(
1446
  fn=send_message,
1447
+ inputs=[message_input, tenant_id_input, role_input, chatbot],
1448
  outputs=[chatbot, message_input]
1449
  )
1450
+
1451
+ # Function to update Chat tab visibility based on role (Editor sees access denied)
1452
+ def update_chat_visibility(role):
1453
+ is_editor = role == "editor"
1454
+ return (
1455
+ gr.update(visible=is_editor), # Access denied message for Editor
1456
+ gr.update(visible=not is_editor), # Chat content for Owner/Admin
1457
+ )
1458
+
1459
+ role_input.change(
1460
+ fn=update_chat_visibility,
1461
+ inputs=[role_input],
1462
+ outputs=[chat_access_denied, chat_content]
1463
+ )
1464
 
1465
+ with gr.Tab("πŸ“š Document Ingestion"):
1466
  gr.Markdown(
1467
  """
1468
+ <div style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); padding: 20px; border-radius: 12px; border: 1px solid rgba(16, 185, 129, 0.2); margin-bottom: 20px;">
1469
  ### πŸ“š Knowledge Base Ingestion
1470
  Ingest documents so the MCP agent can reference tenant-private knowledge.
1471
 
1472
+ **πŸ“„ Supported Formats:**
1473
+ - **Raw text / URLs:** Use the fields below
1474
+ - **Files:** PDF, DOCX, TXT, Markdown
1475
+ - **Metadata:** Optional JSON metadata for better organization
1476
+
1477
+ **⚠️ Note:** Editor role and above can ingest. Admin/Owner can delete.
1478
+ </div>
1479
  """
1480
  )
1481
 
 
1513
 
1514
  def handle_ingest_document(
1515
  tenant_id,
1516
+ role,
1517
  mode,
1518
  content,
1519
  doc_url,
 
1524
  source_type = "raw_text" if mode == "Raw Text" else "url"
1525
  result = ingest_document(
1526
  tenant_id=tenant_id,
1527
+ role=role,
1528
  source_type=source_type,
1529
  content=content,
1530
  document_url=doc_url,
 
1541
  fn=handle_ingest_document,
1542
  inputs=[
1543
  tenant_id_input,
1544
+ role_input,
1545
  ingestion_mode,
1546
  doc_content,
1547
  document_url,
 
1560
  )
1561
  ingest_file_button = gr.Button("Upload & Ingest File", visible=False)
1562
 
1563
+ def handle_file_ingestion(tenant_id, role, file_obj):
1564
+ result = ingest_file(tenant_id, role, file_obj)
1565
  # Add note about refreshing Knowledge Base Library
1566
  if "βœ…" in result:
1567
  result += "\n\nπŸ’‘ **Tip:** Go to the 'Knowledge Base Library' tab to view your ingested documents."
 
1569
 
1570
  ingest_file_button.click(
1571
  fn=handle_file_ingestion,
1572
+ inputs=[tenant_id_input, role_input, file_upload],
1573
  outputs=document_status
1574
  )
1575
 
 
1603
  ]
1604
  )
1605
 
1606
+ with gr.Tab("πŸ“– Knowledge Base Library"):
1607
+ # Access denied for Editor role
1608
+ kb_access_denied = gr.Markdown(
1609
  """
1610
+ <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%); padding: 40px; border-radius: 16px; border: 2px solid rgba(239, 68, 68, 0.4); text-align: center; margin: 20px 0;">
1611
+ <h2 style="color: #fca5a5; margin-bottom: 16px;">πŸ”’ Access Denied</h2>
1612
+ <p style="color: #f1f5f9; font-size: 16px; margin-bottom: 12px;">
1613
+ <strong>Editor role can only access Document Ingestion.</strong>
1614
+ </p>
1615
+ <p style="color: #cbd5e1; font-size: 14px;">
1616
+ Please switch to Owner or Admin role to access Knowledge Base Library.
1617
+ </p>
1618
+ </div>
1619
+ """,
1620
+ visible=False
1621
+ )
1622
+
1623
+ kb_library_content = gr.Column(visible=True)
1624
+
1625
+ with kb_library_content:
1626
+ gr.Markdown(
1627
+ """
1628
+ <div style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(124, 58, 237, 0.1) 100%); padding: 20px; border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2); margin-bottom: 20px;">
1629
+ ### πŸ“– Knowledge Base Library
1630
  View, search, and manage all ingested documents for your tenant with visual analytics.
1631
 
1632
+ **Features:**
1633
  - **πŸ“Š Statistics:** View document counts, types, and distribution
1634
  - **πŸ” Search:** Use semantic search to find relevant documents
1635
  - **πŸ”½ Filter:** Filter documents by type (text, PDF, FAQ, link)
1636
+ - **πŸ—‘οΈ Delete:** Remove individual documents or delete all at once (Admin/Owner only)
1637
+ </div>
1638
  """
1639
  )
1640
 
 
1692
  wrap=True
1693
  )
1694
 
1695
+ # Delete Section (Admin/Owner only)
1696
+ kb_delete_section = gr.Row()
1697
+ with kb_delete_section:
1698
  kb_delete_id = gr.Number(
1699
  label="Delete Document by ID",
1700
  value=None,
 
1706
 
1707
  kb_delete_status = gr.Markdown("")
1708
 
1709
+ # Function to update KB tab visibility based on role
1710
+ def update_kb_visibility(role):
1711
+ can_delete = can_delete_documents(role)
1712
+ return (
1713
+ gr.update(visible=can_delete), # Delete all button
1714
+ gr.update(visible=can_delete), # Delete section
1715
+ )
1716
+
1717
+ def refresh_documents(tenant_id, role, filter_type="all"):
1718
+ status, docs, total, stats, chart_fig = list_documents(tenant_id, role)
1719
 
1720
  # Filter documents by type if not "all"
1721
  if filter_type != "all" and docs:
 
1739
  avg_length_md, chart_fig
1740
  )
1741
 
1742
+ def filter_documents(tenant_id, role, filter_type):
1743
+ return refresh_documents(tenant_id, role, filter_type)
1744
 
1745
+ def search_kb(tenant_id, role, query):
1746
+ status, results = search_knowledge_base(tenant_id, role, query)
1747
  return status, results
1748
 
1749
+ def delete_doc(tenant_id, role, doc_id):
1750
  if doc_id is None or doc_id <= 0:
1751
  return "❗ Please enter a valid document ID.", "", "", "", "", "", "", "", None
1752
+ result = delete_document(tenant_id, role, int(doc_id))
1753
  # Refresh document list after deletion
1754
+ return (result, *refresh_documents(tenant_id, role, "all"))
1755
 
1756
+ def delete_all_docs(tenant_id, role):
1757
+ result = delete_all_documents(tenant_id, role)
1758
  # Refresh document list after deletion
1759
+ return (result, *refresh_documents(tenant_id, role, "all"))
1760
 
1761
  kb_refresh_button.click(
1762
  fn=refresh_documents,
1763
+ inputs=[tenant_id_input, role_input, kb_filter_type],
1764
  outputs=[
1765
  kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
1766
  kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
 
1769
 
1770
  kb_filter_type.change(
1771
  fn=filter_documents,
1772
+ inputs=[tenant_id_input, role_input, kb_filter_type],
1773
  outputs=[
1774
  kb_status, kb_documents_table, kb_total_docs, kb_text_docs,
1775
  kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
 
1778
 
1779
  kb_search_button.click(
1780
  fn=search_kb,
1781
+ inputs=[tenant_id_input, role_input, kb_search_query],
1782
  outputs=[kb_search_status, kb_search_results]
1783
  )
1784
 
1785
  kb_search_query.submit(
1786
  fn=search_kb,
1787
+ inputs=[tenant_id_input, role_input, kb_search_query],
1788
  outputs=[kb_search_status, kb_search_results]
1789
  )
1790
 
1791
  kb_delete_button.click(
1792
  fn=delete_doc,
1793
+ inputs=[tenant_id_input, role_input, kb_delete_id],
1794
  outputs=[
1795
  kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
1796
  kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
 
1799
 
1800
  kb_delete_all_button.click(
1801
  fn=delete_all_docs,
1802
+ inputs=[tenant_id_input, role_input],
1803
  outputs=[
1804
  kb_delete_status, kb_status, kb_documents_table, kb_total_docs,
1805
  kb_text_docs, kb_pdf_docs, kb_faq_docs, kb_link_docs, kb_avg_length, kb_chart
1806
  ]
1807
  )
1808
+
1809
+ # Update visibility when role changes
1810
+ def update_kb_full_visibility(role):
1811
+ is_editor = role == "editor"
1812
+ can_delete = can_delete_documents(role)
1813
+ return (
1814
+ gr.update(visible=is_editor), # Access denied for Editor
1815
+ gr.update(visible=not is_editor), # KB content for Owner/Admin
1816
+ gr.update(visible=can_delete), # Delete all button
1817
+ gr.update(visible=can_delete), # Delete section
1818
+ )
1819
+
1820
+ role_input.change(
1821
+ fn=update_kb_full_visibility,
1822
+ inputs=[role_input],
1823
+ outputs=[kb_access_denied, kb_library_content, kb_delete_all_button, kb_delete_section]
1824
+ )
1825
 
1826
+ with gr.Tab("πŸ“Š Admin Analytics"):
1827
+ # Access denied message for non-admin/owner roles
1828
+ analytics_access_denied = gr.Markdown(
 
 
 
1829
  """
1830
+ <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%); padding: 40px; border-radius: 16px; border: 2px solid rgba(239, 68, 68, 0.4); text-align: center; margin: 20px 0;">
1831
+ <h2 style="color: #fca5a5; margin-bottom: 16px;">πŸ”’ Access Denied</h2>
1832
+ <p style="color: #f1f5f9; font-size: 16px; margin-bottom: 12px;">
1833
+ <strong>You need Admin or Owner role to access Analytics Dashboard.</strong>
1834
+ </p>
1835
+ <p style="color: #cbd5e1; font-size: 14px;">
1836
+ Analytics features are restricted to administrative roles for data security and privacy.
1837
+ </p>
1838
+ </div>
1839
+ """,
1840
+ visible=False
1841
  )
1842
 
1843
+ # Analytics content (visible for admin/owner)
1844
+ analytics_content = gr.Column(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1845
 
1846
+ with analytics_content:
1847
+ gr.Markdown(
1848
+ """
1849
+ <div style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%); padding: 24px; border-radius: 12px; border: 1px solid rgba(245, 158, 11, 0.2); margin-bottom: 20px;">
1850
+ # πŸ“Š Admin Analytics Dashboard
1851
+
1852
+ Comprehensive tenant-level analytics with visual insights, performance metrics, and detailed tool usage statistics.
1853
+
1854
+ **πŸ”’ Access:** Admin and Owner roles only
1855
+ </div>
1856
+ """
1857
+ )
1858
 
1859
+ # Refresh Button at Top
1860
+ with gr.Row():
1861
+ analytics_refresh = gr.Button("πŸ”„ Fetch Analytics Snapshot", variant="primary", size="lg")
1862
+ gr.Markdown("")
1863
+
1864
+ # Statistics Cards
1865
+ gr.Markdown("### πŸ“ˆ Key Metrics")
1866
+ with gr.Row():
1867
+ analytics_total_queries = gr.Markdown("### πŸ“Š Total Queries\n**0**", elem_classes=["stat-card"])
1868
+ analytics_active_users = gr.Markdown("### πŸ‘₯ Active Users\n**0**", elem_classes=["stat-card"])
1869
+ analytics_redflags = gr.Markdown("### 🚩 Red Flags\n**0**", elem_classes=["stat-card"])
1870
+ analytics_rag_searches = gr.Markdown("### πŸ” RAG Searches\n**0**", elem_classes=["stat-card"])
 
 
 
 
 
 
 
 
 
1871
 
1872
+ # Charts Section
1873
+ gr.Markdown("### πŸ“Š Performance Charts")
1874
+ with gr.Row():
1875
+ with gr.Column(scale=1):
1876
+ gr.Markdown("#### πŸ“ˆ Tool Usage Count", elem_classes=["chart-title"])
1877
+ analytics_tool_chart = gr.Plot(label="", show_label=False)
1878
+ with gr.Column(scale=1):
1879
+ gr.Markdown("#### ⚑ Average Tool Latency", elem_classes=["chart-title"])
1880
+ analytics_latency_chart = gr.Plot(label="", show_label=False)
1881
 
1882
+ # RAG Quality and Summary Section
1883
+ with gr.Row():
1884
+ with gr.Column(scale=1):
1885
+ gr.Markdown("#### πŸ” RAG Quality Metrics", elem_classes=["chart-title"])
1886
+ analytics_rag_chart = gr.Plot(label="", show_label=False)
1887
+
1888
+ with gr.Column(scale=1):
1889
+ gr.Markdown("### πŸ“‹ Analytics Summary")
1890
+ analytics_summary = gr.Markdown(
1891
+ "πŸ‘‰ Click **Fetch Analytics Snapshot** to load data.",
1892
+ elem_classes=["summary-box"]
1893
+ )
 
 
 
 
 
1894
 
1895
+ # Tool Usage Details Table
1896
+ gr.Markdown("### πŸ”§ Detailed Tool Usage")
1897
+ analytics_tool_table = gr.Dataframe(
1898
+ headers=["Tool", "Count", "Avg Latency (ms)", "Success", "Errors", "Total Tokens"],
1899
+ datatype=["str", "number", "number", "number", "number", "number"],
1900
+ interactive=False,
1901
+ label="",
1902
+ wrap=True
1903
+ )
 
 
 
 
1904
 
1905
+ analytics_error = gr.Markdown("", visible=False)
 
 
 
 
1906
 
1907
+ def format_analytics(tenant_id, role):
1908
+ summary, tool_usage, tool_chart, latency_chart, rag_chart, error = fetch_admin_analytics(tenant_id, role)
1909
+
1910
+ if error:
1911
+ return (
1912
+ error, "", "", "", "", None, None, None, []
1913
+ )
1914
+
1915
+ # Extract overview data - fetch_admin_analytics already fetched it, but we need it again for cards
1916
+ overview_data = {}
1917
+ try:
1918
+ headers = {
1919
+ "x-tenant-id": tenant_id,
1920
+ "x-user-role": role if role else DEFAULT_ROLE
1921
+ }
1922
+ resp = requests.get(
1923
+ f"{BACKEND_BASE_URL}/analytics/overview",
1924
+ headers=headers,
1925
+ timeout=30
1926
+ )
1927
+ if resp.status_code == 200:
1928
+ data = resp.json()
1929
+ # The API returns {"overview": {...}} or direct overview object
1930
+ overview_data = data.get("overview", data) if isinstance(data, dict) else {}
1931
+ # Debug: print to see what we're getting
1932
+ print(f"DEBUG: Overview data keys: {overview_data.keys() if isinstance(overview_data, dict) else 'Not a dict'}")
1933
+ except Exception as e:
1934
+ print(f"Error fetching overview: {e}")
1935
+ pass
1936
+
1937
+ # Extract values with proper fallbacks - handle both nested and flat structures
1938
+ if isinstance(overview_data, dict):
1939
+ total_queries = overview_data.get("total_queries", 0)
1940
+ active_users = overview_data.get("active_users", 0)
1941
+ redflag_count = overview_data.get("redflag_count", 0)
1942
+ rag_quality = overview_data.get("rag_quality", {})
1943
+ rag_searches = rag_quality.get("total_searches", 0) if isinstance(rag_quality, dict) else 0
1944
+ else:
1945
+ total_queries = 0
1946
+ active_users = 0
1947
+ redflag_count = 0
1948
+ rag_quality = {}
1949
+ rag_searches = 0
1950
+
1951
+ # Format statistics cards
1952
+ queries_md = f"### πŸ“Š Total Queries\n**{total_queries}**"
1953
+ users_md = f"### πŸ‘₯ Active Users\n**{active_users}**"
1954
+ redflags_md = f"### 🚩 Red Flags\n**{redflag_count}**"
1955
+ rag_md = f"### πŸ” RAG Searches\n**{rag_searches}**"
1956
+
1957
+ # Format tool usage table
1958
+ tool_table_data = []
1959
+ for tool_name, stats in tool_usage.items():
1960
+ tool_table_data.append({
1961
+ "Tool": tool_name.replace(".", " ").title(),
1962
+ "Count": stats.get("count", 0),
1963
+ "Avg Latency (ms)": round(stats.get("avg_latency_ms", 0), 2),
1964
+ "Success": stats.get("success_count", 0),
1965
+ "Errors": stats.get("error_count", 0),
1966
+ "Total Tokens": stats.get("total_tokens", 0)
1967
+ })
1968
+
1969
+ return (
1970
+ summary, queries_md, users_md, redflags_md, rag_md,
1971
+ tool_chart, latency_chart, rag_chart, tool_table_data
1972
+ )
1973
 
1974
+ analytics_refresh.click(
1975
+ fn=format_analytics,
1976
+ inputs=[tenant_id_input, role_input],
1977
+ outputs=[
1978
+ analytics_summary,
1979
+ analytics_total_queries,
1980
+ analytics_active_users,
1981
+ analytics_redflags,
1982
+ analytics_rag_searches,
1983
+ analytics_tool_chart,
1984
+ analytics_latency_chart,
1985
+ analytics_rag_chart,
1986
+ analytics_tool_table
1987
+ ]
1988
+ )
1989
+
1990
+ # Function to update Analytics tab visibility based on role (Editor sees access denied)
1991
+ def update_analytics_visibility(role):
1992
+ is_editor = role == "editor"
1993
+ has_access = can_view_analytics(role)
1994
  return (
1995
+ gr.update(visible=is_editor or not has_access), # Access denied for Editor or non-admin
1996
+ gr.update(visible=has_access and not is_editor), # Analytics content for Admin/Owner only
1997
  )
1998
 
1999
+ # Update visibility when role changes
2000
+ role_input.change(
2001
+ fn=update_analytics_visibility,
2002
+ inputs=[role_input],
2003
+ outputs=[analytics_access_denied, analytics_content]
 
 
 
 
 
 
 
 
 
2004
  )
2005
+
2006
 
2007
+ with gr.Tab("πŸ›‘οΈ Admin Rules & Compliance"):
2008
+ # Access denied for Editor role
2009
+ rules_access_denied = gr.Markdown(
 
 
 
 
 
 
 
 
 
 
 
 
2010
  """
2011
+ <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%); padding: 40px; border-radius: 16px; border: 2px solid rgba(239, 68, 68, 0.4); text-align: center; margin: 20px 0;">
2012
+ <h2 style="color: #fca5a5; margin-bottom: 16px;">πŸ”’ Access Denied</h2>
2013
+ <p style="color: #f1f5f9; font-size: 16px; margin-bottom: 12px;">
2014
+ <strong>Editor role can only access Document Ingestion.</strong>
2015
+ </p>
2016
+ <p style="color: #cbd5e1; font-size: 14px;">
2017
+ Admin Rules & Compliance is restricted to Admin and Owner roles only.
2018
+ </p>
2019
+ </div>
2020
+ """,
2021
+ visible=False
2022
  )
2023
 
2024
+ rules_content = gr.Column(visible=True)
2025
+
2026
+ with rules_content:
2027
+ gr.Markdown(
2028
+ """
2029
+ <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%); padding: 20px; border-radius: 12px; border: 1px solid rgba(239, 68, 68, 0.2); margin-bottom: 20px;">
2030
+ ### πŸ›‘οΈ Admin Rules & Regulations
2031
+ Upload or manage tenant-specific governance rules (red-flag patterns, compliance policies, etc.).
2032
+
2033
+ **πŸ“€ Upload Methods:**
2034
+ - **Text Input:** Enter one rule per line in the text box
2035
+ - **File Upload:** Upload rules from TXT, PDF, DOC, or DOCX files
2036
+
2037
+ **✨ Features:**
2038
+ - πŸ€– Rules are automatically enhanced by LLM (identifies edge cases, improves patterns)
2039
+ - πŸ’¬ Comment lines (starting with #) are automatically ignored
2040
+ - πŸ—‘οΈ Use the delete box to remove an exact rule
2041
+ - πŸ”„ Refresh anytime to view the latest rule set
2042
+
2043
+ **πŸ”’ Access:** Admin and Owner roles only
2044
+ </div>
2045
+ """
2046
+ )
2047
+
2048
  rules_summary = gr.Markdown("πŸ‘‰ Click **Refresh Rules** to see existing entries.")
2049
  rules_table = gr.Dataframe(
2050
  headers=["#", "Rule"],
 
2084
 
2085
  refresh_rules_button.click(
2086
  fn=fetch_admin_rules,
2087
+ inputs=[tenant_id_input, role_input],
2088
  outputs=[rules_summary, rules_table]
2089
  )
2090
 
2091
  upload_rules_button.click(
2092
  fn=add_rules_and_refresh,
2093
+ inputs=[tenant_id_input, role_input, rules_input],
2094
  outputs=[rules_status, rules_summary, rules_table]
2095
  )
2096
 
2097
  upload_file_button.click(
2098
  fn=add_rules_from_file,
2099
+ inputs=[tenant_id_input, role_input, rules_file_upload],
2100
  outputs=[rules_status, rules_summary, rules_table]
2101
  )
2102
 
2103
  delete_rule_button.click(
2104
  fn=delete_rule_and_refresh,
2105
+ inputs=[tenant_id_input, role_input, delete_rule_input],
2106
  outputs=[rules_status, rules_summary, rules_table]
2107
  )
2108
+
2109
+ # Function to update Admin Rules tab visibility based on role
2110
+ def update_rules_visibility(role):
2111
+ is_editor = role == "editor"
2112
+ has_access = can_manage_rules(role)
2113
+ return (
2114
+ gr.update(visible=is_editor or not has_access), # Access denied for Editor or non-admin
2115
+ gr.update(visible=has_access and not is_editor), # Rules content for Admin/Owner only
2116
+ )
2117
+
2118
+ role_input.change(
2119
+ fn=update_rules_visibility,
2120
+ inputs=[role_input],
2121
+ outputs=[rules_access_denied, rules_content]
2122
+ )
2123
 
2124
  gr.Markdown(
2125
  """
2126
+ <div style="margin-top: 40px; padding: 24px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.5) 0%, rgba(30, 41, 59, 0.5) 100%); border-radius: 12px; border: 1px solid rgba(148, 163, 184, 0.1); text-align: center;">
2127
+ <p style="margin: 0; color: #94a3b8; font-size: 14px;">
2128
+ Built with ❀️ using <a href="https://modelcontextprotocol.io/" target="_blank" style="color: #06b6d4; text-decoration: none; font-weight: 600;">Model Context Protocol (MCP)</a>
2129
+ </p>
2130
+ <p style="margin: 8px 0 0 0; color: #64748b; font-size: 12px;">
2131
+ Enterprise-Grade MCP Autonomous Agent Platform
2132
+ </p>
2133
+ </div>
2134
  """
2135
  )
2136