shan gao commited on
Commit
a948051
·
1 Parent(s): 6a294f5

Add application file

Browse files
Files changed (2) hide show
  1. app.py +184 -100
  2. requirement.txt +3 -1
app.py CHANGED
@@ -1,8 +1,8 @@
1
-
2
  import os
3
  import gradio as gr
4
  import datasets
5
  from typing import List, Tuple
 
6
 
7
  # LangChain / LangGraph imports
8
  # from langchain_core.documents import Document
@@ -11,7 +11,7 @@ from langchain_community.tools import DuckDuckGoSearchRun
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
  from langchain.tools import Tool
14
- from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, ToolMessage
15
  from typing import TypedDict, Annotated
16
  from langgraph.graph.message import add_messages
17
  from langgraph.prebuilt import ToolNode
@@ -19,105 +19,131 @@ from langgraph.graph import START, StateGraph
19
  from langgraph.prebuilt import tools_condition
20
  from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
21
 
22
- def set_token_hfhub(value):
 
 
 
 
 
 
23
  os.environ["HF_TOKEN"] = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- # ==============================
26
- # 1) Build the same agent (Alfred)
27
- # ==============================
28
-
29
- # Load the dataset and make Documents
30
- guest_dataset = datasets.load_dataset("agents-course/unit3-invitees", split="train")
31
- docs = [
32
- Document(
33
- page_content="\n".join(
34
- [
35
- f"Name: {guest['name']}",
36
- f"Relation: {guest['relation']}",
37
- f"Description: {guest['description']}",
38
- f"Email: {guest['email']}",
39
- ]
40
- ),
41
- metadata={"name": guest["name"]},
42
  )
43
- for guest in guest_dataset
44
- ]
45
-
46
- # Embeddings & Vectorstore retriever
47
- embeddings = HuggingFaceEmbeddings(
48
- model_name="sentence-transformers/all-MiniLM-L6-v2",
49
- encode_kwargs={"normalize_embeddings": True},
50
- )
51
- vectorstore = FAISS.from_documents(docs, embeddings)
52
- retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
53
-
54
- # Guest info tool
55
  def extract_text(query: str) -> str:
56
  """Retrieves detailed information about gala guests based on their name or relation."""
 
57
  results = retriever.invoke(query)
58
  if results:
59
  return "\n\n".join([doc.page_content for doc in results])
60
  else:
61
  return "No matching guest information found."
62
 
63
- guest_info_tool = Tool(
64
- name="guest_info_retriever",
65
- func=extract_text,
66
- description="Retrieves detailed information about gala guests based on their name or relation.",
67
- )
68
-
69
- # Web search tool
70
- search_tool = DuckDuckGoSearchRun()
71
 
72
- # LLM endpoint (reads token from env var or Python var fallback)
73
- # hf_token = os.getenv("HF_TOKEN")
74
- hf_token = os.environ["HF_TOKEN"]
75
-
76
- if not hf_token:
77
- raise RuntimeError(
78
- "HUGGINGFACEHUB_API_TOKEN is not set. Please export it before running the app."
79
  )
 
 
 
80
 
81
- llm = HuggingFaceEndpoint(
82
- repo_id="Qwen/Qwen2.5-Coder-32B-Instruct",
83
- huggingfacehub_api_token=hf_token,
84
- )
85
- chat = ChatHuggingFace(llm=llm, verbose=True)
86
- tools = [guest_info_tool, search_tool]
87
- chat_with_tools = chat.bind_tools(tools)
88
 
89
- # Agent state & node
90
  class AgentState(TypedDict):
91
  messages: Annotated[List[AnyMessage], add_messages]
92
 
93
- def assistant(state: AgentState):
94
- # Produce one assistant message (may include a tool call)
95
- return {"messages": [chat_with_tools.invoke(state["messages"])]}
96
 
97
- # Graph wiring
98
- builder = StateGraph(AgentState)
99
- builder.add_node("assistant", assistant)
100
- builder.add_node("tools", ToolNode(tools))
101
- builder.add_edge(START, "assistant")
102
- builder.add_conditional_edges("assistant", tools_condition)
103
- builder.add_edge("tools", "assistant")
104
- alfred = builder.compile()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- # ======================================
107
- # 2) Helper functions for the Gradio UI
108
- # ======================================
109
 
110
  def _msg_content_to_str(msg: AnyMessage) -> str:
111
  """
112
  Coerce LangChain message content (which might contain tool call structures)
113
  into displayable text for the Chatbot.
114
  """
115
- # Most often, content is a string already
116
  content = getattr(msg, "content", "")
117
  if isinstance(content, str):
118
  return content
119
-
120
- # If it's a list of parts (e.g., tool call traces), join any text parts
121
  if isinstance(content, list):
122
  texts = []
123
  for part in content:
@@ -126,27 +152,63 @@ def _msg_content_to_str(msg: AnyMessage) -> str:
126
  elif isinstance(part, str):
127
  texts.append(part)
128
  return "\n".join(texts) if texts else str(content)
129
-
130
- # Fallback
131
  return str(content)
132
 
 
133
  def startup_state() -> List[AnyMessage]:
134
  """Start with an empty conversation."""
135
  return []
136
 
137
- # Gradio expects chatbot history as List[Tuple[str, str]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  def submit_user_message(
139
  user_text: str,
140
- chat_history: List[Tuple[str, str]],
141
  agent_messages: List[AnyMessage],
 
142
  ):
143
  """
144
  1) Append HumanMessage to agent state
145
- 2) Run Alfred
146
  3) Extract last AIMessage and append to chat_history
147
  """
148
  if not user_text or user_text.strip() == "":
149
- return gr.update(), chat_history, agent_messages
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  # Step 1: add HumanMessage to state
152
  agent_messages = list(agent_messages or [])
@@ -155,10 +217,9 @@ def submit_user_message(
155
  # Step 2: run the graph
156
  out = alfred.invoke({"messages": agent_messages})
157
 
158
- # The graph returns a new messages list *including* the latest assistant/tool steps.
159
- # We use the last AIMessage as the displayed reply.
160
  new_msgs: List[AnyMessage] = out["messages"]
161
- agent_messages = new_msgs # keep full state for the next turn
162
 
163
  # Find the last assistant message to show in the UI
164
  ai_text = ""
@@ -167,21 +228,22 @@ def submit_user_message(
167
  ai_text = _msg_content_to_str(m)
168
  break
169
  if not ai_text:
170
- # fallback: in rare cases of only tool messages, show a generic note
171
  ai_text = "I processed your request using my tools."
172
 
173
  chat_history = list(chat_history or [])
174
  chat_history.append({"role": "user", "content": user_text})
175
  chat_history.append({"role": "assistant", "content": ai_text})
176
- return "", chat_history, agent_messages
 
177
 
178
- def clear_chat():
179
- """Reset the Gradio UI and agent state."""
180
- return [], startup_state()
181
 
182
- # ========================
183
- # 3) Gradio App UI layout
184
- # ========================
 
185
 
186
  with gr.Blocks(title="Alfred — LangGraph Agent") as demo:
187
  gr.Markdown(
@@ -190,12 +252,15 @@ with gr.Blocks(title="Alfred — LangGraph Agent") as demo:
190
  Ask questions and Alfred will respond, using:
191
  - a vector search tool over the guest list
192
  - DuckDuckGo web search
 
 
193
  """
194
  )
195
 
196
- with gr.Row():
197
  token1 = gr.Textbox(
198
- label="Your hf token",
 
199
  autofocus=True,
200
  scale=2,
201
  )
@@ -206,29 +271,48 @@ with gr.Blocks(title="Alfred — LangGraph Agent") as demo:
206
  type="messages",
207
  height=500,
208
  show_copy_button=True,
209
- avatar_images=(None, None), # customize if you like
210
  )
211
 
212
  with gr.Row():
213
  txt = gr.Textbox(
214
  label="Your message",
215
  placeholder="Ask anything…",
216
- autofocus=True,
217
  scale=4,
218
  )
219
  send_btn = gr.Button("Send", variant="primary", scale=1)
220
  clear_btn = gr.Button("Clear")
221
 
222
- # Hidden state: the agent’s full message list (LangChain messages)
223
- agent_state = gr.State(startup_state())
 
224
 
225
  # Wire up events
226
- token1.submit(set_token_hfhub, [token1])
227
- txt.submit(submit_user_message, [txt, chatbot, agent_state], [txt, chatbot, agent_state])
228
- send_btn.click(submit_user_message, [txt, chatbot, agent_state], [txt, chatbot, agent_state])
229
- clear_btn.click(clear_chat, outputs=[chatbot, agent_state])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  # Entry point
232
  if __name__ == "__main__":
233
  # You can tweak server_name/port as needed
234
- demo.launch()
 
 
1
  import os
2
  import gradio as gr
3
  import datasets
4
  from typing import List, Tuple
5
+ from functools import lru_cache
6
 
7
  # LangChain / LangGraph imports
8
  # from langchain_core.documents import Document
 
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
  from langchain.tools import Tool
14
+ from langchain_core.messages import AnyMessage, HumanMessage, AIMessage
15
  from typing import TypedDict, Annotated
16
  from langgraph.graph.message import add_messages
17
  from langgraph.prebuilt import ToolNode
 
19
  from langgraph.prebuilt import tools_condition
20
  from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
21
 
22
+ ##############################
23
+ # Token management (supports both env var names)
24
+ ##############################
25
+
26
+ def set_token_hfhub(value: str):
27
+ """Update both common env var names for HF tokens."""
28
+ value = (value or "").strip()
29
  os.environ["HF_TOKEN"] = value
30
+ os.environ["HUGGINGFACEHUB_API_TOKEN"] = value
31
+
32
+
33
+ def _get_token_from_env() -> str:
34
+ return (
35
+ os.getenv("HF_TOKEN")
36
+ or os.getenv("HUGGINGFACEHUB_API_TOKEN")
37
+ or ""
38
+ ).strip()
39
+
40
+
41
+ ##############################
42
+ # 1) Lazy data + retriever build
43
+ ##############################
44
+
45
+ @lru_cache(maxsize=1)
46
+ def build_retriever():
47
+ """Load dataset, embed, and return a retriever. Cached after first call."""
48
+ guest_dataset = datasets.load_dataset("agents-course/unit3-invitees", split="train")
49
+ docs = [
50
+ Document(
51
+ page_content="\n".join(
52
+ [
53
+ f"Name: {guest['name']}",
54
+ f"Relation: {guest['relation']}",
55
+ f"Description: {guest['description']}",
56
+ f"Email: {guest['email']}",
57
+ ]
58
+ ),
59
+ metadata={"name": guest["name"]},
60
+ )
61
+ for guest in guest_dataset
62
+ ]
63
 
64
+ embeddings = HuggingFaceEmbeddings(
65
+ model_name="sentence-transformers/all-MiniLM-L6-v2",
66
+ encode_kwargs={"normalize_embeddings": True},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  )
68
+ vectorstore = FAISS.from_documents(docs, embeddings)
69
+ retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
70
+ return retriever
71
+
72
+
73
+ ##############################
74
+ # 2) Tools (use lazy retriever)
75
+ ##############################
76
+
 
 
 
77
  def extract_text(query: str) -> str:
78
  """Retrieves detailed information about gala guests based on their name or relation."""
79
+ retriever = build_retriever()
80
  results = retriever.invoke(query)
81
  if results:
82
  return "\n\n".join([doc.page_content for doc in results])
83
  else:
84
  return "No matching guest information found."
85
 
 
 
 
 
 
 
 
 
86
 
87
+ def get_tools():
88
+ guest_info_tool = Tool(
89
+ name="guest_info_retriever",
90
+ func=extract_text,
91
+ description="Retrieves detailed information about gala guests based on their name or relation.",
 
 
92
  )
93
+ search_tool = DuckDuckGoSearchRun()
94
+ return [guest_info_tool, search_tool]
95
+
96
 
97
+ ##############################
98
+ # 3) Lazy LLM/chat + graph builders
99
+ ##############################
 
 
 
 
100
 
 
101
  class AgentState(TypedDict):
102
  messages: Annotated[List[AnyMessage], add_messages]
103
 
 
 
 
104
 
105
+ def build_chat(hf_token: str):
106
+ if not hf_token:
107
+ raise RuntimeError(
108
+ "No HF token provided. Enter it in the textbox first."
109
+ )
110
+ llm = HuggingFaceEndpoint(
111
+ repo_id="Qwen/Qwen2.5-Coder-32B-Instruct",
112
+ huggingfacehub_api_token=hf_token,
113
+ )
114
+ return ChatHuggingFace(llm=llm, verbose=True)
115
+
116
+
117
+ def build_agent(chat: ChatHuggingFace):
118
+ tools = get_tools()
119
+ chat_with_tools = chat.bind_tools(tools)
120
+
121
+ def assistant(state: AgentState):
122
+ # Produce one assistant message (may include a tool call)
123
+ return {"messages": [chat_with_tools.invoke(state["messages"])]}
124
+
125
+ builder = StateGraph(AgentState)
126
+ builder.add_node("assistant", assistant)
127
+ builder.add_node("tools", ToolNode(tools))
128
+ builder.add_edge(START, "assistant")
129
+ builder.add_conditional_edges("assistant", tools_condition)
130
+ builder.add_edge("tools", "assistant")
131
+ return builder.compile()
132
+
133
+
134
+ ##############################
135
+ # 4) Gradio UI plumbing
136
+ ##############################
137
 
 
 
 
138
 
139
  def _msg_content_to_str(msg: AnyMessage) -> str:
140
  """
141
  Coerce LangChain message content (which might contain tool call structures)
142
  into displayable text for the Chatbot.
143
  """
 
144
  content = getattr(msg, "content", "")
145
  if isinstance(content, str):
146
  return content
 
 
147
  if isinstance(content, list):
148
  texts = []
149
  for part in content:
 
152
  elif isinstance(part, str):
153
  texts.append(part)
154
  return "\n".join(texts) if texts else str(content)
 
 
155
  return str(content)
156
 
157
+
158
  def startup_state() -> List[AnyMessage]:
159
  """Start with an empty conversation."""
160
  return []
161
 
162
+
163
+ # Gradio expects chatbot history as list of {role, content} when type="messages"
164
+
165
+ def setup_runtime(hf_token: str, chatbot, agent_messages, runtime_state):
166
+ """Initialize chat + agent given a token and store in runtime_state."""
167
+ try:
168
+ set_token_hfhub(hf_token)
169
+ chat = build_chat(_get_token_from_env())
170
+ alfred = build_agent(chat)
171
+ runtime_state = {"alfred": alfred}
172
+ system_note = (
173
+ "✅ Token set. You can start chatting now!"
174
+ )
175
+ chatbot = [
176
+ {"role": "assistant", "content": system_note}
177
+ ]
178
+ agent_messages = startup_state()
179
+ return gr.update(), chatbot, agent_messages, runtime_state
180
+ except Exception as e:
181
+ # Surface the error in the chat UI
182
+ err = f"⚠️ Failed to initialize model: {e}"
183
+ chatbot = [{"role": "assistant", "content": err}]
184
+ return gr.update(), chatbot, agent_messages, runtime_state
185
+
186
+
187
  def submit_user_message(
188
  user_text: str,
189
+ chat_history: List[dict],
190
  agent_messages: List[AnyMessage],
191
+ runtime_state: dict,
192
  ):
193
  """
194
  1) Append HumanMessage to agent state
195
+ 2) Run Alfred (lazy-initialized)
196
  3) Extract last AIMessage and append to chat_history
197
  """
198
  if not user_text or user_text.strip() == "":
199
+ return gr.update(), chat_history, agent_messages, runtime_state
200
+
201
+ # Ensure agent is initialized
202
+ alfred = (runtime_state or {}).get("alfred")
203
+ if alfred is None:
204
+ # If there's no agent yet, ask for a token
205
+ note = (
206
+ "🔐 Please enter your Hugging Face token above and press Enter to initialize the model."
207
+ )
208
+ chat_history = list(chat_history or [])
209
+ chat_history.append({"role": "user", "content": user_text})
210
+ chat_history.append({"role": "assistant", "content": note})
211
+ return "", chat_history, agent_messages, runtime_state
212
 
213
  # Step 1: add HumanMessage to state
214
  agent_messages = list(agent_messages or [])
 
217
  # Step 2: run the graph
218
  out = alfred.invoke({"messages": agent_messages})
219
 
220
+ # Graph returns full messages list including assistant/tool steps
 
221
  new_msgs: List[AnyMessage] = out["messages"]
222
+ agent_messages = new_msgs
223
 
224
  # Find the last assistant message to show in the UI
225
  ai_text = ""
 
228
  ai_text = _msg_content_to_str(m)
229
  break
230
  if not ai_text:
 
231
  ai_text = "I processed your request using my tools."
232
 
233
  chat_history = list(chat_history or [])
234
  chat_history.append({"role": "user", "content": user_text})
235
  chat_history.append({"role": "assistant", "content": ai_text})
236
+ return "", chat_history, agent_messages, runtime_state
237
+
238
 
239
+ def clear_chat(runtime_state: dict):
240
+ """Reset the visible chat but keep the initialized agent (if any)."""
241
+ return [], startup_state(), runtime_state
242
 
243
+
244
+ ##############################
245
+ # 5) Gradio App UI layout
246
+ ##############################
247
 
248
  with gr.Blocks(title="Alfred — LangGraph Agent") as demo:
249
  gr.Markdown(
 
252
  Ask questions and Alfred will respond, using:
253
  - a vector search tool over the guest list
254
  - DuckDuckGo web search
255
+
256
+ **Setup:** Paste your Hugging Face token below and press Enter.
257
  """
258
  )
259
 
260
+ with gr.Row():
261
  token1 = gr.Textbox(
262
+ label="Your HF token",
263
+ placeholder="hf_...",
264
  autofocus=True,
265
  scale=2,
266
  )
 
271
  type="messages",
272
  height=500,
273
  show_copy_button=True,
274
+ avatar_images=(None, None),
275
  )
276
 
277
  with gr.Row():
278
  txt = gr.Textbox(
279
  label="Your message",
280
  placeholder="Ask anything…",
281
+ autofocus=False,
282
  scale=4,
283
  )
284
  send_btn = gr.Button("Send", variant="primary", scale=1)
285
  clear_btn = gr.Button("Clear")
286
 
287
+ # Hidden states
288
+ agent_state = gr.State(startup_state()) # LangChain messages
289
+ runtime_state = gr.State({"alfred": None}) # Holds compiled agent
290
 
291
  # Wire up events
292
+ # Token submit initializes the runtime (sets env var, builds chat + graph)
293
+ token1.submit(
294
+ setup_runtime,
295
+ inputs=[token1, chatbot, agent_state, runtime_state],
296
+ outputs=[token1, chatbot, agent_state, runtime_state],
297
+ )
298
+
299
+ txt.submit(
300
+ submit_user_message,
301
+ [txt, chatbot, agent_state, runtime_state],
302
+ [txt, chatbot, agent_state, runtime_state],
303
+ )
304
+ send_btn.click(
305
+ submit_user_message,
306
+ [txt, chatbot, agent_state, runtime_state],
307
+ [txt, chatbot, agent_state, runtime_state],
308
+ )
309
+ clear_btn.click(
310
+ clear_chat,
311
+ inputs=[runtime_state],
312
+ outputs=[chatbot, agent_state, runtime_state],
313
+ )
314
 
315
  # Entry point
316
  if __name__ == "__main__":
317
  # You can tweak server_name/port as needed
318
+ demo.launch()
requirement.txt CHANGED
@@ -1,6 +1,8 @@
1
  gradio
2
  langchain
3
- langchain-huggingface
 
 
4
  datasets
5
  faiss-cpu
6
  ddgs
 
1
  gradio
2
  langchain
3
+ langgraph
4
+ langchain-community
5
+ langchain_huggingface
6
  datasets
7
  faiss-cpu
8
  ddgs