karenwky commited on
Commit
20a01ef
Β·
verified Β·
1 Parent(s): 986416d

Upload app.py and requirements.txt

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