KheemDH commited on
Commit
1a2c3c5
·
1 Parent(s): ea3e1ef

Add application file

Browse files
Files changed (1) hide show
  1. app.py +171 -0
app.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from dotenv import load_dotenv
4
+ from typing import Dict, TypedDict
5
+
6
+ import gradio as gr
7
+
8
+ from langgraph.graph import StateGraph, END
9
+ from langchain_core.prompts import ChatPromptTemplate
10
+ from langchain_openai import ChatOpenAI
11
+ from browser_use import Agent
12
+
13
+ # Load environment variables (including OPENAI_API_KEY) from .env
14
+ load_dotenv()
15
+
16
+ # Define a TypedDict to hold state information.
17
+ class State(TypedDict):
18
+ query: str
19
+ category: str
20
+ sentiment: str
21
+ response: str
22
+
23
+ # Initialize our language models.
24
+ # We use llm_standard for normal tasks and llm_browser for browser-based tasks.
25
+ llm_standard = ChatOpenAI(temperature=0)
26
+ llm_browser = ChatOpenAI(model="gpt-4o", temperature=0)
27
+
28
+ # Node functions for our workflow.
29
+ def categorize(state: State) -> State:
30
+ prompt = ChatPromptTemplate.from_template(
31
+ "Categorize the following customer query into one of these categories: "
32
+ "Technical, Billing, General. Query: {query}"
33
+ )
34
+ chain = prompt | llm_standard
35
+ category = chain.invoke({"query": state["query"]}).content.strip()
36
+ return {"category": category}
37
+
38
+ def analyze_sentiment(state: State) -> State:
39
+ prompt = ChatPromptTemplate.from_template(
40
+ "Analyze the sentiment of the following customer query. "
41
+ "Respond with either 'Positive', 'Neutral', or 'Negative'. Query: {query}"
42
+ )
43
+ chain = prompt | llm_standard
44
+ sentiment = chain.invoke({"query": state["query"]}).content.strip()
45
+ return {"sentiment": sentiment}
46
+
47
+ def handle_technical(state: State) -> State:
48
+ prompt = ChatPromptTemplate.from_template(
49
+ "Provide a technical support response to the following query: {query}"
50
+ )
51
+ chain = prompt | llm_standard
52
+ response = chain.invoke({"query": state["query"]}).content.strip()
53
+ return {"response": response}
54
+
55
+ def handle_billing(state: State) -> State:
56
+ prompt = ChatPromptTemplate.from_template(
57
+ "Provide a billing support response to the following query: {query}"
58
+ )
59
+ chain = prompt | llm_standard
60
+ response = chain.invoke({"query": state["query"]}).content.strip()
61
+ return {"response": response}
62
+
63
+ async def run_browser_agent(task: str) -> str:
64
+ # Run the browser-use agent asynchronously.
65
+ agent = Agent(task=task, llm=llm_browser)
66
+ result = await agent.run()
67
+ return result
68
+
69
+ def handle_general(state: State) -> State:
70
+ """
71
+ For general queries, we use the browser agent to consult online resources.
72
+ We call the async function with asyncio.run and then extract only the final answer.
73
+ """
74
+ task = (
75
+ "You are a customer support agent that consults online sources. "
76
+ f"Provide a detailed, informed response to this customer query: {state['query']}"
77
+ )
78
+ result = asyncio.run(run_browser_agent(task))
79
+ final_text = ""
80
+
81
+ if isinstance(result, str):
82
+ final_text = result.strip()
83
+ elif hasattr(result, "all_results"):
84
+ # Iterate over the list of ActionResults to extract the final done answer
85
+ for action in result.all_results:
86
+ # Check if the action is marked as done and has extracted content
87
+ if action.get("is_done") and action.get("extracted_content"):
88
+ final_text = action.get("extracted_content").strip()
89
+ # Fallback in case no done action is found
90
+ if not final_text:
91
+ final_text = str(result).strip()
92
+ else:
93
+ final_text = str(result).strip()
94
+
95
+ return {"response": final_text}
96
+
97
+ def escalate(state: State) -> State:
98
+ return {"response": "This query has been escalated to a human agent due to negative sentiment."}
99
+
100
+ def route_query(state: State) -> str:
101
+ """Determine which node to route to based on sentiment and category."""
102
+ if state["sentiment"].lower() == "negative":
103
+ return "escalate"
104
+ elif state["category"].lower() == "technical":
105
+ return "handle_technical"
106
+ elif state["category"].lower() == "billing":
107
+ return "handle_billing"
108
+ else:
109
+ return "handle_general"
110
+
111
+ # Create the workflow graph.
112
+ workflow = StateGraph(State)
113
+ workflow.add_node("categorize", categorize)
114
+ workflow.add_node("analyze_sentiment", analyze_sentiment)
115
+ workflow.add_node("handle_technical", handle_technical)
116
+ workflow.add_node("handle_billing", handle_billing)
117
+ workflow.add_node("handle_general", handle_general)
118
+ workflow.add_node("escalate", escalate)
119
+
120
+ workflow.add_edge("categorize", "analyze_sentiment")
121
+ workflow.add_conditional_edges(
122
+ "analyze_sentiment",
123
+ route_query,
124
+ {
125
+ "handle_technical": "handle_technical",
126
+ "handle_billing": "handle_billing",
127
+ "handle_general": "handle_general",
128
+ "escalate": "escalate"
129
+ }
130
+ )
131
+ workflow.add_edge("handle_technical", END)
132
+ workflow.add_edge("handle_billing", END)
133
+ workflow.add_edge("handle_general", END)
134
+ workflow.add_edge("escalate", END)
135
+ workflow.set_entry_point("categorize")
136
+ app = workflow.compile()
137
+
138
+ def run_customer_support(query: str, api_key: str) -> str:
139
+ """
140
+ Process the customer query through the workflow.
141
+ Use the provided API key, or if none is given, fall back to the .env value.
142
+ Only the final answer is returned.
143
+ """
144
+ # If no API key is provided in the UI, try to read it from the environment.
145
+ if not api_key.strip():
146
+ api_key = os.getenv("OPENAI_API_KEY", "")
147
+ if not api_key:
148
+ return "Please provide a valid OpenAI API key."
149
+ os.environ["OPENAI_API_KEY"] = api_key
150
+ results = app.invoke({"query": query})
151
+ # Return only the final answer (the response part)
152
+ return results.get("response", "No response generated.")
153
+
154
+ # Build the Gradio UI.
155
+ with gr.Blocks(title="Customer Support Agent with Browser Use") as demo:
156
+ gr.Markdown("# Customer Support Agent with Browser Use")
157
+ gr.Markdown("This agent categorizes customer queries and uses a browser-based agent to provide informed answers.")
158
+
159
+ with gr.Row():
160
+ with gr.Column():
161
+ # The API key textbox (if left empty, the app will try to use the .env key)
162
+ api_key_input = gr.Textbox(label="OpenAI API Key", type="password", placeholder="sk-...", value="")
163
+ query_input = gr.Textbox(label="Customer Query", placeholder="Enter your query here...", lines=3)
164
+ submit_btn = gr.Button("Submit Query")
165
+ with gr.Column():
166
+ output_box = gr.Textbox(label="Agent Response", lines=10, interactive=False)
167
+
168
+ submit_btn.click(fn=run_customer_support, inputs=[query_input, api_key_input], outputs=output_box)
169
+
170
+ # Launch the Gradio interface.
171
+ demo.launch(share=True)