Spaces:
Running
Running
File size: 18,803 Bytes
20a01ef 4fe90b3 20a01ef 4fe90b3 20a01ef |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 |
import os
import gradio as gr
# Import necessary LlamaIndex components
from llama_index.indices.managed.llama_cloud import (
LlamaCloudIndex,
LlamaCloudCompositeRetriever,
)
from llama_index.core import Settings
from llama_index.llms.nebius import NebiusLLM
from llama_cloud.types import CompositeRetrievalMode
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.chat_engine import CondensePlusContextChatEngine
from llama_index.core.chat_engine.types import (
AgentChatResponse,
) # Import for type hinting agent response
from llama_index.core.schema import (
NodeWithScore,
) # Import for type hinting source_nodes
# Phoenix/OpenInference imports
from phoenix.otel import register
from openinference.instrumentation.llama_index import LlamaIndexInstrumentor
# --- Configuration ---
# Replace with your actual LlamaCloud Project Name
LLAMA_CLOUD_PROJECT_NAME = "CustomerSupportProject"
# Configure NebiusLLM
# Ensure NEBIUS_API_KEY is set in your environment variables
Settings.llm = NebiusLLM(
model="meta-llama/Meta-Llama-3.1-405B-Instruct",
temperature=0
)
print(f"[INFO] Configured LLM: {Settings.llm.model}")
# Configure LlamaTrace (Arize Phoenix)
PHOENIX_PROJECT_NAME = os.environ.get("PHOENIX_PROJECT_NAME")
PHOENIX_API_KEY = os.environ.get("PHOENIX_API_KEY")
if PHOENIX_PROJECT_NAME and PHOENIX_API_KEY:
os.environ["PHOENIX_CLIENT_HEADERS"] = f"api_key={PHOENIX_API_KEY}"
tracer_provider = register(
project_name=PHOENIX_PROJECT_NAME,
endpoint="https://app.phoenix.arize.com/v1/traces",
auto_instrument=True,
)
LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)
print("[INFO] LlamaIndex tracing configured for LlamaTrace (Arize Phoenix).")
else:
print(
"[INFO] PHOENIX_PROJECT_NAME or PHOENIX_API_KEY not set. LlamaTrace (Arize Phoenix) not configured."
)
# --- Assume LlamaCloud Indices are pre-created ---
# In a real scenario, you would have uploaded your documents to these indices
# via LlamaCloud UI or API. Here, we connect to existing indices.
print("[INFO] Connecting to LlamaCloud Indices...")
try:
product_manuals_index = LlamaCloudIndex(
name="ProductManuals",
project_name=LLAMA_CLOUD_PROJECT_NAME,
)
faq_general_info_index = LlamaCloudIndex(
name="FAQGeneralInfo",
project_name=LLAMA_CLOUD_PROJECT_NAME,
)
billing_policy_index = LlamaCloudIndex(
name="BillingPolicy",
project_name=LLAMA_CLOUD_PROJECT_NAME,
)
company_intro_slides_index = LlamaCloudIndex(
name="CompanyIntroductionSlides",
project_name=LLAMA_CLOUD_PROJECT_NAME,
)
print("[INFO] Successfully connected to LlamaCloud Indices.")
except Exception as e:
print(
f"[ERROR] Error connecting to LlamaCloud Indices. Please ensure they exist and API key is correct: {e}"
)
print(
"[INFO] Exiting. Please create your indices on LlamaCloud and set environment variables."
)
exit() # Exit if indices cannot be connected, as the rest of the code depends on them
# --- Create LlamaCloudCompositeRetriever for Agentic Routing ---
print("[INFO] Creating LlamaCloudCompositeRetriever...")
composite_retriever = LlamaCloudCompositeRetriever(
name="Customer Support Retriever",
project_name=LLAMA_CLOUD_PROJECT_NAME,
create_if_not_exists=True,
mode=CompositeRetrievalMode.ROUTING, # Enable intelligent routing
rerank_top_n=2, # Rerank and return top 2 results from all queried indices
)
# Add indices to the composite retriever with descriptive descriptions
# These descriptions are crucial for the agent's routing decisions.
print("[INFO] Adding sub-indices to the composite retriever with descriptions...")
composite_retriever.add_index(
product_manuals_index,
description="Information source for detailed product features, technical specifications, troubleshooting steps, and usage guides for various products.",
)
composite_retriever.add_index(
faq_general_info_index,
description="Contains common questions and answers, general company policies, public announcements, and basic information about services.",
)
composite_retriever.add_index(
billing_policy_index,
description="Provides information related to pricing, subscriptions, invoices, payment methods, and refund policies.",
)
composite_retriever.add_index(
company_intro_slides_index,
description="Contains presentations that provide an overview of the company, its mission, leadership, and key information for new employees, partners, or investors.",
)
print("[INFO] Sub-indices added.")
# --- Create CondensePlusContextChatEngine ---
memory = ChatMemoryBuffer.from_defaults(token_limit=4096)
chat_engine = CondensePlusContextChatEngine.from_defaults(
retriever=composite_retriever,
memory=memory,
system_prompt=(
"""
You are a Smart Customer Support Triage Agent.
Always be polite and friendly.
Provide accurate answers from product manuals, FAQs, and billing policies by intelligently routing queries to the most relevant knowledge base.
Provide accurate, precise, and useful information directly.
Never refer to or mention your information sources (e.g., "the manual says", "from the document").
State facts authoritatively.
When asked about file-specific details like the author, creation date, or last modification date, retrieve this information from the document's metadata if available in the provided context.
"""
),
verbose=True,
)
print("[INFO] ChatEngine initialized.")
# --- Gradio Chat UI ---
def initial_submit(message: str, history: list):
"""
Handles the immediate UI update after user submits a message.
Adds user message to history and shows a loading state for retriever info.
"""
# Append user message in the 'messages' format
history.append({"role": "user", "content": message})
# Return updated history, clear input box, show loading for retriever info, and the original message
# outputs=[chatbot, msg, retriever_output, user_message_state]
return history, "", "Retrieving relevant information...", message
def get_agent_response_and_retriever_info(message_from_state: str, history: list):
"""
Processes the LLM response and extracts retriever information.
This function is called AFTER initial_submit, so history already contains user's message.
"""
retriever_output_text = "Error: Could not retrieve information."
try:
# Call the chat engine to get the response
response: AgentChatResponse = chat_engine.chat(message_from_state)
# AgentChatResponse.response holds the actual LLM generated text: `response=str(response)`
# Append the assistant's response in the 'messages' format
history.append({"role": "assistant", "content": response.response})
# Prepare the retriever information for the new textbox
check_retriever_text = []
# Safely attempt to get condensed_question
condensed_question = "Condensed question not explicitly exposed by chat engine."
# `chat` method returns `sources=[context_source]` within `AgentChatResponse`
if hasattr(response, "sources") and response.sources is not None:
context_source = response.sources[0]
if (
hasattr(context_source, "raw_input")
and context_source.raw_input is not None
and "message" in context_source.raw_input
):
condensed_question = context_source.raw_input["message"]
check_retriever_text.append(f"Condensed question: {condensed_question}")
check_retriever_text.append("==============================")
# Safely get source_nodes. Ensure it's iterable.
nodes: list[NodeWithScore] = (
response.source_nodes
if hasattr(response, "source_nodes") and response.source_nodes is not None
else []
)
if nodes:
for i, node in enumerate(nodes):
# Safely access node metadata and attributes
metadata = (
node.metadata
if hasattr(node, "metadata") and node.metadata is not None
else {}
)
score = (
node.score
if hasattr(node, "score") and node.score is not None
else "N/A"
)
file_name = metadata.get("file_name", "N/A")
page_info = ""
# Add page number for .pptx files
if file_name.lower().endswith(".pptx"):
page_label = metadata.get("page_label")
if page_label:
page_info = f" p.{page_label}"
node_block = f"""\
[Node {i + 1}]
Index: {metadata.get("retriever_pipeline_name", "N/A")}
File: {file_name}{page_info}
Score: {score}
=============================="""
check_retriever_text.append(node_block)
else:
check_retriever_text.append("No source nodes found for this query.")
retriever_output_text = "\n".join(check_retriever_text)
# Return updated history and the retriever text
return history, retriever_output_text
except Exception as e:
# Log the full error for debugging
import traceback
print(f"Error in get_agent_response_and_retriever_info: {e}")
traceback.print_exc()
# Append a generic error message from the assistant
history.append(
{
"role": "assistant",
"content": "I'm sorry, I encountered an error while processing your request. Please try again.",
}
)
# Only return the detailed error in the retriever info box
retriever_output_text = f"Error generating retriever info: {e}"
return history, retriever_output_text
# Markdown text for the application and chatbot welcoming message
description_text = """
Hello! I'm your Smart Customer Support Triage Agent. I can answer questions about our product manuals, FAQs, and billing policies. Ask me anything!
Explore the documents in `./data` directory for sample knowledge base π
"""
# Markdown text for `./data` folder structure
knowledge_base_md = """
### π Sample Knowledge Base ([click to explore!](https://huggingface.co/spaces/Agents-MCP-Hackathon/cs-agent/tree/main/data))
```
./data/
βββ ProductManuals/
β βββ product_manuals_metadata.csv
β βββ product_manuals.pdf
β βββ task_automation_setup.pdf
β βββ collaboration_tools_overview.pdf
βββ FAQGeneralInfo/
β βββ faqs_general_metadata.csv
β βββ faqs_general.pdf
β βββ remote_work_best_practices_faq.pdf
β βββ sustainability_initiatives_info.pdf
βββ BillingPolicy/
β βββ billing_policies_metadata.csv
β βββ billing_policies.pdf
β βββ multi_user_discount_guide.pdf
β βββ late_payment_policy.pdf
β βββ late_payment_policy_v2.pdf
βββ CompanyIntroductionSlides/
βββ company_introduction_slides_metadata.csv
βββ TechSolve_Introduction.pptx
```
"""
# Create a Gradio `Blocks` layout to structure the application
print("[INFO] Launching Gradio interface...")
with gr.Blocks(theme=gr.themes.Ocean()) as demo:
# Custom CSS
demo.css = """
.monospace-font textarea {
font-family: monospace; /* monospace font for better readability of structured text */
}
.center-title { /* centering the title */
text-align: center;
}
"""
# `State` component to hold the user's message between chained function calls
user_message_state = gr.State(value="")
# Retriever info begin message
retriever_info_begin_msg = "Retriever information will appear here after each query."
# Center-aligned title
gr.Markdown("# π¬ Smart Customer Support Triage Agent", elem_classes="center-title")
gr.Markdown(description_text)
with gr.Row():
with gr.Column(scale=2): # Main chat area
chatbot = gr.Chatbot(
label="Chat History",
height=500,
show_copy_button=True,
resizable=True,
avatar_images=(None, "./logo.png"),
type="messages",
value=[
{"role": "assistant", "content": description_text}
], # Welcoming message
)
msg = gr.Textbox( # User input
placeholder="Type your message here...", lines=1, container=False
)
with gr.Row():
send_button = gr.Button("Send")
clear_button = gr.ClearButton([msg, chatbot])
with gr.Column(scale=1): # Retriever info area
retriever_output = gr.Textbox(
label="Agentic Retrieval & Smart Routing",
interactive=False,
lines=28,
show_copy_button=True,
autoscroll=False,
elem_classes="monospace-font",
value=retriever_info_begin_msg,
)
# New row for Examples and Sample Knowledge Base Tree Diagram
with gr.Row():
with gr.Column(scale=1): # Examples column (left)
gr.Markdown("### π£οΈ Example Questions")
# Store the Examples component in a variable to access its `load_input_event`
examples_component = gr.Examples(
examples_per_page=8,
examples=[
["Help! No response from the app, I can't do anything. What should I do? Who can I contact?"],
["I got an $200 invoice outstanding for 45 days. How much is the late charge?"],
["Who is the author of the product manual and when is the last modified date?"],
["Who are the founders of this company? What are their backgrounds?"],
["Is your company environmentally friendly?"],
["What are the procedures to set up task automation?"],
["If I sign up for an annual 'Pro' subscription today and receive the 10% discount, but then decide to cancel after 20 days because the software isn't working for me, what exact amount would I be refunded, considering the 14-day refund policy for annual plans?"],
["I have a question specifically about the 'Sustainable Software Design' aspect mentioned in your sustainability initiatives, which email address should I use for support, [email protected] or [email protected], and what kind of technical detail can I expect in a response?"],
["How can your software help team collaboration?"],
["What is your latest late payment policy?"],
["If I enable auto-pay to avoid late payments, but my payment method on file expires, will TechSolve send me a notification before the payment fails and potentially incurs late fees?"],
["In shared workspaces, when multiple users are co-editing a document, how does the system handle concurrent edits to the exact same line of text by different users, and what mechanism is in place to prevent data loss or conflicts?"],
["The refund policy states that annual subscriptions can be refunded within 14 days. Does this '14 days' refer to 14 calendar days or 14 business days from the purchase date?"],
["Your multi-user discount guide states that discounts apply to accounts with 5+ users. If my team currently has 4 users and I add a 5th user mid-billing cycle, will the 10% discount be applied immediately to all 5 users, or only at the next billing cycle, and how would the prorated amount for the new user be calculated?"],
],
inputs=[msg], # This tells examples to populate the 'msg' textbox
)
with gr.Column(scale=1): # Knowledge Base column (right)
gr.Markdown(knowledge_base_md)
# Define the interaction for sending messages (via Enter key in textbox)
submit_event = msg.submit( # Step 1: Immediate UI update (updated history, clear input box, show loading for retriever info, and the original message)
fn=initial_submit,
inputs=[msg, chatbot],
outputs=[chatbot, msg, retriever_output, user_message_state],
queue=False,
).then( # Step 2: Call LLM and update agent response and detailed retriever info
fn=get_agent_response_and_retriever_info,
inputs=[user_message_state, chatbot],
outputs=[chatbot, retriever_output],
queue=True, # Allow queuing for potentially long LLM calls
)
# Define the interaction for send button click
send_button.click(
fn=initial_submit,
inputs=[msg, chatbot],
outputs=[chatbot, msg, retriever_output, user_message_state],
queue=False,
).then(
fn=get_agent_response_and_retriever_info,
inputs=[user_message_state, chatbot],
outputs=[chatbot, retriever_output],
queue=True,
)
# Define the interaction for example questions click
examples_component.load_input_event.then(
fn=initial_submit,
inputs=[msg, chatbot], # 'msg' will have been populated by the example
outputs=[chatbot, msg, retriever_output, user_message_state],
queue=False,
).then(
fn=get_agent_response_and_retriever_info,
inputs=[user_message_state, chatbot],
outputs=[chatbot, retriever_output],
queue=True,
)
# Define the interaction for clearing all outputs
clear_button.click(
fn=lambda: (
[],
"",
retriever_info_begin_msg,
"",
),
inputs=[],
outputs=[chatbot, msg, retriever_output, user_message_state],
queue=False,
)
# DeepLinkButton for sharing current conversation
gr.DeepLinkButton()
# Privacy notice and additional info at the bottom
gr.Markdown(
"""
_\*By using this chat, you agree that conversations may be recorded for improvement and evaluation. DO NOT disclose any privacy information in the conversation._
_\*This space is dedicated to the [Gradio Agents & MCP Hackathon 2025](https://huggingface.co/Agents-MCP-Hackathon) submission. Future updates will be available in my personal space: [karenwky/cs-agent](https://huggingface.co/spaces/karenwky/cs-agent)._
"""
)
# Launch the interface
if __name__ == "__main__":
demo.launch(show_error=True) |