kenqia commited on
Commit
d36b5fa
·
verified ·
1 Parent(s): eba8bcf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -26
app.py CHANGED
@@ -3,6 +3,7 @@ import gradio as gr
3
  import requests
4
  import inspect
5
  import pandas as pd
 
6
 
7
  from typing import List, TypedDict, Annotated, Optional
8
  from langchain_openai import ChatOpenAI
@@ -26,8 +27,11 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
26
  # --- Basic Agent Definition ---
27
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
28
 
29
- class AgentState(TypedDict):
30
  messages: Annotated[list[AnyMessage], add_messages]
 
 
 
31
 
32
  class BasicAgent:
33
  def __init__(self):
@@ -69,20 +73,36 @@ class BasicAgent:
69
  # Define nodes: these do the work
70
  builder.add_node("assistant", self.assistant)
71
  builder.add_node("tools", ToolNode(self.tools))
 
 
72
  builder.add_node("final_process", self.clean_answer)
73
 
74
  # Define edges: these determine how the control flow moves
75
  builder.add_edge(START, "assistant")
 
76
  builder.add_conditional_edges(
77
  "assistant",
78
  self.route_after_assistant,
79
  {
80
  "tools": "tools",
81
- "final_process": "final_process",
82
  },
83
  )
 
84
  builder.add_edge("tools", "assistant")
 
 
 
 
 
 
 
 
 
 
 
85
  builder.add_edge("final_process", END)
 
86
  self.react_graph = builder.compile()
87
 
88
  def __call__(self, question: str, task_id: str | None = None) -> str:
@@ -124,6 +144,141 @@ class BasicAgent:
124
  return {
125
  "messages": [self.chat_with_tools.invoke([sys_msg] + state["messages"])],
126
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  def clean_answer(self, state: AgentState):
129
  messages = state["messages"]
@@ -132,42 +287,77 @@ class BasicAgent:
132
  raw_answer = messages[-1].content
133
 
134
  response = self.model.invoke([
135
- SystemMessage(content=(
136
- "You are an answer formatter. "
137
- "Extract only the final answer required by the question. "
138
- "No explanation. No markdown. No extra words."
139
- )),
140
- HumanMessage(content=f"Question:\n{question}\n\nRaw answer:\n{raw_answer}")
141
- ])
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  final_answer = response.content.strip()
144
 
145
  return {
146
  "messages": [AIMessage(content=final_answer)]
147
  }
 
148
 
149
- def route_after_assistant(self, state: AgentState):
150
- last_message = state["messages"][-1]
151
 
152
- if getattr(last_message, "tool_calls", None):
153
- return "tools"
 
 
 
 
154
 
155
- return "final_process"
156
-
157
- def answer_question(self, question: str, task_id: str | None = None) -> str:
158
- user_content = question
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  if task_id:
161
- user_content += (
162
- f"\n\nTask ID: {task_id}. "
163
- "If the question mentions an attached file, spreadsheet, code file, audio, or image, "
164
- "use the available tools with this task_id."
165
- )
166
-
167
- messages = [HumanMessage(content=user_content)]
168
- result = self.react_graph.invoke({"messages": messages})
169
- return result["messages"][-1].content
170
 
 
 
 
171
 
172
 
173
  def run_and_submit_all( profile: gr.OAuthProfile | None):
 
3
  import requests
4
  import inspect
5
  import pandas as pd
6
+ import json
7
 
8
  from typing import List, TypedDict, Annotated, Optional
9
  from langchain_openai import ChatOpenAI
 
27
  # --- Basic Agent Definition ---
28
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
29
 
30
+ class AgentState(TypedDict, total=False):
31
  messages: Annotated[list[AnyMessage], add_messages]
32
+ verification_status: str
33
+ verification_notes: str
34
+ verify_retries: int
35
 
36
  class BasicAgent:
37
  def __init__(self):
 
73
  # Define nodes: these do the work
74
  builder.add_node("assistant", self.assistant)
75
  builder.add_node("tools", ToolNode(self.tools))
76
+ builder.add_node("verify_answer", self.verify_answer)
77
+ builder.add_node("retry_with_feedback", self.retry_with_feedback)
78
  builder.add_node("final_process", self.clean_answer)
79
 
80
  # Define edges: these determine how the control flow moves
81
  builder.add_edge(START, "assistant")
82
+
83
  builder.add_conditional_edges(
84
  "assistant",
85
  self.route_after_assistant,
86
  {
87
  "tools": "tools",
88
+ "verify_answer": "verify_answer",
89
  },
90
  )
91
+
92
  builder.add_edge("tools", "assistant")
93
+
94
+ builder.add_conditional_edges(
95
+ "verify_answer",
96
+ self.route_after_verify,
97
+ {
98
+ "retry": "retry_with_feedback",
99
+ "final_process": "final_process",
100
+ },
101
+ )
102
+
103
+ builder.add_edge("retry_with_feedback", "assistant")
104
  builder.add_edge("final_process", END)
105
+
106
  self.react_graph = builder.compile()
107
 
108
  def __call__(self, question: str, task_id: str | None = None) -> str:
 
144
  return {
145
  "messages": [self.chat_with_tools.invoke([sys_msg] + state["messages"])],
146
  }
147
+ def _safe_parse_json(self, text: str) -> dict:
148
+ """
149
+ Parse JSON from model output safely.
150
+ The model may sometimes wrap JSON with extra text.
151
+ """
152
+ text = text.strip()
153
+
154
+ try:
155
+ return json.loads(text)
156
+ except Exception:
157
+ pass
158
+
159
+ start = text.find("{")
160
+ end = text.rfind("}")
161
+
162
+ if start != -1 and end != -1 and end > start:
163
+ try:
164
+ return json.loads(text[start:end + 1])
165
+ except Exception:
166
+ pass
167
+
168
+ return {
169
+ "verdict": "PASS",
170
+ "revised_answer": text,
171
+ "issue": "JSON parse failed, using raw verifier output.",
172
+ }
173
+
174
+
175
+ def verify_answer(self, state: AgentState):
176
+ messages = state["messages"]
177
+
178
+ question = messages[0].content
179
+ candidate_answer = messages[-1].content
180
+ retry_count = state.get("verify_retries", 0)
181
+
182
+ # 只拿最近一部分上下文,避免 prompt 过长
183
+ recent_context = []
184
+ for m in messages[-8:]:
185
+ role = m.__class__.__name__
186
+ content = getattr(m, "content", "")
187
+ recent_context.append(f"{role}:\n{content}")
188
+
189
+ context_text = "\n\n---\n\n".join(recent_context)
190
+
191
+ response = self.model.invoke([
192
+ SystemMessage(content="""
193
+ You are a strict answer verifier for an exact-match QA benchmark.
194
+
195
+ Your job:
196
+ 1. Check whether the candidate answer satisfies the question.
197
+ 2. Check whether the answer format is exactly what the question asks.
198
+ 3. Use the available conversation/tool evidence only.
199
+ 4. Do not introduce unsupported new facts.
200
+ 5. If the answer is probably correct but badly formatted, revise the format.
201
+ 6. If the answer is unsupported, clearly wrong, says file/audio/image is unavailable, or ignores a required source/date constraint, request a retry.
202
+
203
+ Return only valid JSON with this schema:
204
+ {
205
+ "verdict": "PASS" or "RETRY",
206
+ "revised_answer": "final answer if PASS, otherwise empty string",
207
+ "issue": "short reason"
208
+ }
209
+
210
+ Important exact-match formatting rules:
211
+ - Remove explanations, markdown, citations, and extra punctuation.
212
+ - If the question asks for a number, return only the number.
213
+ - If the question asks for a name part, return only that part.
214
+ - If the question asks comma-separated values, use comma + space.
215
+ - If the question says without abbreviations, expand abbreviations.
216
+ - If the candidate says no file/image/audio is available but the question has a task_id or attachment, use RETRY.
217
+ - If the candidate ignores a date/source constraint, use RETRY.
218
+ """),
219
+ HumanMessage(content=f"""
220
+ Original question:
221
+ {question}
222
+
223
+ Candidate answer:
224
+ {candidate_answer}
225
+
226
+ Recent conversation and tool context:
227
+ {context_text}
228
+ """)
229
+ ])
230
+
231
+ data = self._safe_parse_json(response.content)
232
+
233
+ verdict = str(data.get("verdict", "PASS")).upper().strip()
234
+ revised_answer = str(data.get("revised_answer", candidate_answer)).strip()
235
+ issue = str(data.get("issue", "")).strip()
236
+
237
+ max_verify_retries = 1
238
+
239
+ if verdict == "RETRY" and retry_count < max_verify_retries:
240
+ return {
241
+ "verification_status": "retry",
242
+ "verification_notes": issue,
243
+ "verify_retries": retry_count + 1,
244
+ }
245
+
246
+ # 如果 verifier 通过,或者已经重试过一次还不行,就进入最终清洗
247
+ final_candidate = revised_answer if revised_answer else candidate_answer
248
+
249
+ return {
250
+ "messages": [AIMessage(content=final_candidate)],
251
+ "verification_status": "pass",
252
+ "verification_notes": issue,
253
+ "verify_retries": retry_count,
254
+ }
255
+
256
+
257
+ def route_after_verify(self, state: AgentState):
258
+ if state.get("verification_status") == "retry":
259
+ return "retry"
260
+
261
+ return "final_process"
262
+
263
+
264
+ def retry_with_feedback(self, state: AgentState):
265
+ notes = state.get("verification_notes", "")
266
+
267
+ return {
268
+ "messages": [
269
+ HumanMessage(content=f"""
270
+ Your previous answer failed verification.
271
+
272
+ Verifier notes:
273
+ {notes}
274
+
275
+ Please answer the original question again.
276
+ Use tools if needed.
277
+ Pay attention to source/date constraints and exact output format.
278
+ Return only the final answer.
279
+ """.strip())
280
+ ]
281
+ }
282
 
283
  def clean_answer(self, state: AgentState):
284
  messages = state["messages"]
 
287
  raw_answer = messages[-1].content
288
 
289
  response = self.model.invoke([
290
+ SystemMessage(content="""
291
+ You are an exact-match answer formatter.
292
+
293
+ Rules:
294
+ - Return only the final answer.
295
+ - No explanation.
296
+ - No markdown.
297
+ - No citations.
298
+ - No extra punctuation.
299
+ - If the question asks for a number, return only the number.
300
+ - If the question asks for USD with two decimals, return only the number with two decimals unless $ is explicitly requested.
301
+ - If the question asks comma-separated values, use ", " between items.
302
+ - If the question asks for a first name, surname, city, country code, or algebraic notation, return only that.
303
+ - If the question says "without abbreviations", expand abbreviations.
304
+ - Preserve required capitalization when obvious.
305
+ """),
306
+ HumanMessage(content=f"Question:\n{question}\n\nRaw answer:\n{raw_answer}")
307
+ ])
308
 
309
  final_answer = response.content.strip()
310
 
311
  return {
312
  "messages": [AIMessage(content=final_answer)]
313
  }
314
+
315
 
316
+ def answer_question(self, question: str, task_id: str | None = None) -> str:
317
+ file_info = None
318
 
319
+ if task_id:
320
+ info_str = download_task_file.invoke({"task_id": task_id})
321
+ try:
322
+ file_info = json.loads(info_str)
323
+ except Exception:
324
+ file_info = None
325
 
326
+ if file_info and "file_path" in file_info:
327
+ suffix = file_info.get("suffix", "").lower()
328
+ file_path = file_info["file_path"]
 
329
 
330
+ if suffix in [".png", ".jpg", ".jpeg", ".webp"]:
331
+ return answer_image_question.invoke({
332
+ "file_path": file_path,
333
+ "question": question
334
+ })
335
+
336
+ if suffix in [".mp3", ".wav", ".m4a"]:
337
+ return answer_audio_question.invoke({
338
+ "file_path": file_path,
339
+ "question": question
340
+ })
341
+
342
+ if suffix == ".py":
343
+ return answer_python_question.invoke({
344
+ "file_path": file_path
345
+ })
346
+
347
+ if suffix in [".xlsx", ".xls", ".csv"]:
348
+ raw = answer_excel_question.invoke({
349
+ "file_path": file_path,
350
+ "question": question
351
+ })
352
+ return self.format_final_answer(question, raw)
353
+
354
+ user_content = question
355
  if task_id:
356
+ user_content += f"\n\nTask ID: {task_id}."
 
 
 
 
 
 
 
 
357
 
358
+ result = self.react_graph.invoke({"messages": [HumanMessage(content=user_content)]})
359
+ return result["messages"][-1].content
360
+
361
 
362
 
363
  def run_and_submit_all( profile: gr.OAuthProfile | None):