Spaces:
Running
Running
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) |