|
|
import os |
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
from llama_index.core.schema import ( |
|
|
NodeWithScore, |
|
|
) |
|
|
|
|
|
|
|
|
from phoenix.otel import register |
|
|
from openinference.instrumentation.llama_index import LlamaIndexInstrumentor |
|
|
|
|
|
|
|
|
|
|
|
LLAMA_CLOUD_PROJECT_NAME = "CustomerSupportProject" |
|
|
|
|
|
|
|
|
|
|
|
Settings.llm = NebiusLLM( |
|
|
model="meta-llama/Meta-Llama-3.1-405B-Instruct", |
|
|
temperature=0 |
|
|
) |
|
|
print(f"[INFO] Configured LLM: {Settings.llm.model}") |
|
|
|
|
|
|
|
|
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." |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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, |
|
|
rerank_top_n=2, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
history.append({"role": "user", "content": message}) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
response: AgentChatResponse = chat_engine.chat(message_from_state) |
|
|
|
|
|
|
|
|
|
|
|
history.append({"role": "assistant", "content": response.response}) |
|
|
|
|
|
|
|
|
check_retriever_text = [] |
|
|
|
|
|
|
|
|
condensed_question = "Condensed question not explicitly exposed by chat engine." |
|
|
|
|
|
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("==============================") |
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
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 = "" |
|
|
|
|
|
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 history, retriever_output_text |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
import traceback |
|
|
|
|
|
print(f"Error in get_agent_response_and_retriever_info: {e}") |
|
|
traceback.print_exc() |
|
|
|
|
|
|
|
|
history.append( |
|
|
{ |
|
|
"role": "assistant", |
|
|
"content": "I'm sorry, I encountered an error while processing your request. Please try again.", |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
retriever_output_text = f"Error generating retriever info: {e}" |
|
|
return history, retriever_output_text |
|
|
|
|
|
|
|
|
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 📑 |
|
|
""" |
|
|
|
|
|
|
|
|
knowledge_base_md = """ |
|
|
### 📁 Sample Knowledge Base ([click to explore!](https://huggingface.co/spaces/karenwky/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 |
|
|
``` |
|
|
""" |
|
|
|
|
|
|
|
|
print("[INFO] Launching Gradio interface...") |
|
|
with gr.Blocks(theme=gr.themes.Ocean()) as demo: |
|
|
|
|
|
demo.css = """ |
|
|
.monospace-font textarea { |
|
|
font-family: monospace; /* monospace font for better readability of structured text */ |
|
|
} |
|
|
.center-title { /* centering the title */ |
|
|
text-align: center; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
user_message_state = gr.State(value="") |
|
|
|
|
|
|
|
|
retriever_info_begin_msg = "Retriever information will appear here after each query." |
|
|
|
|
|
|
|
|
gr.Markdown("# 💬 Smart Customer Support Triage Agent", elem_classes="center-title") |
|
|
gr.Markdown(description_text) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=2): |
|
|
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} |
|
|
], |
|
|
) |
|
|
msg = gr.Textbox( |
|
|
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_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, |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 🗣️ Example Questions") |
|
|
|
|
|
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, sustainability@techsolve.com or support@techsolve.com, 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], |
|
|
) |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown(knowledge_base_md) |
|
|
|
|
|
|
|
|
submit_event = msg.submit( |
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
examples_component.load_input_event.then( |
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
clear_button.click( |
|
|
fn=lambda: ( |
|
|
[], |
|
|
"", |
|
|
retriever_info_begin_msg, |
|
|
"", |
|
|
), |
|
|
inputs=[], |
|
|
outputs=[chatbot, msg, retriever_output, user_message_state], |
|
|
queue=False, |
|
|
) |
|
|
|
|
|
|
|
|
gr.DeepLinkButton() |
|
|
|
|
|
|
|
|
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._" |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(show_error=True) |