CTPC commited on
Commit
39ef7a2
·
verified ·
1 Parent(s): b614540

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +372 -0
app.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import getpass
2
+ import os
3
+ import gradio as gr
4
+ import requests
5
+ import inspect
6
+ import pandas as pd
7
+ from langgraph.graph import START, StateGraph, END
8
+ from langgraph.prebuilt import ToolNode
9
+ from typing_extensions import TypedDict
10
+ from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
11
+ from langchain.schema import AIMessage
12
+ from typing import Annotated, List, Any, Optional
13
+ from langgraph.graph.message import add_messages
14
+ from tools import extract_text, describe_image, transcribe_audio, web_search, read_file
15
+ from langchain_openai import AzureChatOpenAI
16
+ from azure.identity import EnvironmentCredential
17
+ from langchain_google_genai import ChatGoogleGenerativeAI
18
+
19
+ #DEFINE AGENT STATE
20
+ class AgentState(TypedDict):
21
+ # The input document
22
+ messages: Annotated[List[AnyMessage], add_messages]
23
+ task_id: Optional[str] # The task ID for the agent
24
+ file_name: Optional[str] # Contains file name if the task is file-based
25
+ local_file_name: Optional[str] # Contains local file name if file has been downloaded
26
+ final_response: Optional[str] # The final response from the agent
27
+
28
+ #CREATE STRUCTURED OUTPUT
29
+ class FinalAnswer(TypedDict):
30
+ """Always use this tool to structure your response to the user."""
31
+ final_answer: Annotated[str,...,"The final answer provided by the agent, formatted as per the instructions."]
32
+
33
+ # (Keep Constants as is)
34
+ # --- Constants ---
35
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
36
+
37
+ # --- Basic Agent Definition ---
38
+ # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
39
+ class BasicAgent:
40
+ def __init__(self):
41
+ print("BasicAgent initialized.")
42
+ self.tools=[
43
+ extract_text,
44
+ web_search,
45
+ describe_image,
46
+ read_file,
47
+ transcribe_audio
48
+ ]
49
+ self.llm = AzureChatOpenAI(
50
+ model_name="gpt-4o",
51
+ api_key=self.get_access_token(),
52
+ azure_endpoint="https://cog-sandbox-dev-eastus2-001.openai.azure.com/",
53
+ api_version="2024-08-01-preview"
54
+ )
55
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
56
+ self.google_llm = ChatGoogleGenerativeAI(model='gemini-2.0-flash-lite')
57
+ self.graph = self.create_graph()
58
+
59
+ def __call__(self, task_id: str, question: str, file_name: str = None, local_file_name: str = None) -> str:
60
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
61
+ # fixed_answer = "This is a default answer."
62
+ # print(f"Agent returning fixed answer: {fixed_answer}")
63
+ # return fixed_answer
64
+ state = {
65
+ "messages": [
66
+ HumanMessage(content=question)
67
+ ],
68
+ "task_id": task_id,
69
+ "file_name": file_name,
70
+ "local_file_name": local_file_name
71
+ }
72
+ print(f"Initial state: {state}")
73
+ response = self.graph.invoke(state)
74
+ return response['final_response']['final_answer']
75
+
76
+ def get_access_token(self):
77
+ credential = EnvironmentCredential()
78
+ access_token = credential.get_token("https://cognitiveservices.azure.com/.default")
79
+ return access_token.token
80
+
81
+ #CREATE ASSISTANT FUNCTION
82
+ def assistant(self, state: AgentState):
83
+ local_file_name=state.get("local_file_name","NOT AVAILABLE")
84
+
85
+ sys_msg = SystemMessage(content=f"""
86
+ You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
87
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
88
+ If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
89
+ If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
90
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
91
+
92
+ You may be asked to review an image or file, if available, the file location is: {local_file_name}.
93
+ """)
94
+ new_state = state.copy()
95
+ new_state["messages"] = [self.llm_with_tools.invoke([sys_msg] + state["messages"])]
96
+
97
+ return new_state
98
+
99
+ #CREATE FILE DOWNLOADER
100
+ def download_file(self, state: AgentState) -> None:
101
+ """
102
+ Downloads a file from the given URL and saves it to the specified path.
103
+
104
+ Args:
105
+ state: The AgentState.
106
+ """
107
+ print(f'download_file called with state:{state}')
108
+
109
+ new_state = state.copy()
110
+
111
+ if "file_name" in state and state.get("file_name") is not None and state.get("file_name", '') != '':
112
+ file_name = state["file_name"]
113
+ task_id = state.get("task_id", "unknown_task")
114
+ url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
115
+ save_path = f"./attachments/{file_name}"
116
+ response = requests.get(url, stream=True)
117
+ response.raise_for_status()
118
+ with open(save_path, 'wb') as f:
119
+ for chunk in response.iter_content(chunk_size=8192):
120
+ if chunk:
121
+ f.write(chunk)
122
+ print(f"File downloaded and saved to {save_path}")
123
+ new_state["local_file_name"] = save_path
124
+ else:
125
+ print("No file name provided in state, skipping download.")
126
+ return new_state
127
+
128
+ # STRUCTURED OUTPUT AGENT
129
+ def structured_output(self, state: AgentState):
130
+ """
131
+ Create a structured output from the agent's response.
132
+
133
+ Args:
134
+ state: The AgentState containing the agent's messages.
135
+
136
+ Returns:
137
+ A string representing the structured output.
138
+ """
139
+ response = self.google_llm.with_structured_output(FinalAnswer).invoke(state['messages'][-1].content)
140
+ return {'final_response': response}
141
+
142
+ # ROUTING FUNCTION
143
+ def routing_function(self, state: AgentState) -> str:
144
+ """
145
+ Routing function to determine the next step based on the assistant's last message.
146
+
147
+ Args:
148
+ state: The AgentState containing the agent's messages.
149
+
150
+ Returns:
151
+ The next node to route to.
152
+ """
153
+ if not state["messages"]:
154
+ return "END"
155
+
156
+ last_message = state["messages"][-1]
157
+
158
+ if isinstance(last_message, AIMessage) and last_message.tool_calls:
159
+ return "tools"
160
+
161
+ return "structured_output"
162
+
163
+ #BUILD GRAPH
164
+ def create_graph(self):
165
+ builder = StateGraph(AgentState)
166
+
167
+ # Define nodes: these do the work
168
+ builder.add_node("downloader", self.download_file)
169
+ builder.add_node("assistant", self.assistant)
170
+ builder.add_node("tools", ToolNode(self.tools))
171
+ builder.add_node("structured_output", self.structured_output)
172
+
173
+ # Define edges: these determine how the control flow moves
174
+ builder.add_edge(START, "downloader")
175
+ builder.add_edge("downloader", "assistant")
176
+ builder.add_conditional_edges(
177
+ "assistant",
178
+ # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
179
+ # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
180
+ self.routing_function,
181
+ ["tools", "structured_output"]
182
+ )
183
+ builder.add_edge("structured_output", END)
184
+ builder.add_edge("tools", "assistant")
185
+ react_graph = builder.compile()
186
+ return react_graph
187
+
188
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
189
+ """
190
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
191
+ and displays the results.
192
+ """
193
+ # --- Determine HF Space Runtime URL and Repo URL ---
194
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
195
+
196
+ if profile:
197
+ username= f"{profile.username}"
198
+ print(f"User logged in: {username}")
199
+ else:
200
+ print("User not logged in.")
201
+ return "Please Login to Hugging Face with the button.", None
202
+
203
+ api_url = DEFAULT_API_URL
204
+ questions_url = f"{api_url}/questions"
205
+ submit_url = f"{api_url}/submit"
206
+
207
+ # 1. Instantiate Agent ( modify this part to create your agent)
208
+ try:
209
+ agent = BasicAgent()
210
+ except Exception as e:
211
+ print(f"Error instantiating agent: {e}")
212
+ return f"Error initializing agent: {e}", None
213
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
214
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
215
+ print(agent_code)
216
+
217
+ # 2. Fetch Questions
218
+ print(f"Fetching questions from: {questions_url}")
219
+ try:
220
+ response = requests.get(questions_url, timeout=15)
221
+ response.raise_for_status()
222
+ questions_data = response.json()
223
+ if not questions_data:
224
+ print("Fetched questions list is empty.")
225
+ return "Fetched questions list is empty or invalid format.", None
226
+ print(f"Fetched {len(questions_data)} questions.")
227
+ except requests.exceptions.RequestException as e:
228
+ print(f"Error fetching questions: {e}")
229
+ return f"Error fetching questions: {e}", None
230
+ except requests.exceptions.JSONDecodeError as e:
231
+ print(f"Error decoding JSON response from questions endpoint: {e}")
232
+ print(f"Response text: {response.text[:500]}")
233
+ return f"Error decoding server response for questions: {e}", None
234
+ except Exception as e:
235
+ print(f"An unexpected error occurred fetching questions: {e}")
236
+ return f"An unexpected error occurred fetching questions: {e}", None
237
+
238
+ # 3. Run your Agent
239
+ results_log = []
240
+ answers_payload = []
241
+ print(f"Running agent on {len(questions_data)} questions...")
242
+ for item in questions_data:
243
+ task_id = item.get("task_id")
244
+ question_text = item.get("question")
245
+ file_name = item.get("file_name")
246
+ if not task_id or question_text is None:
247
+ print(f"Skipping item with missing task_id or question: {item}")
248
+ continue
249
+ try:
250
+ submitted_answer = agent(
251
+ question=question_text,
252
+ task_id=task_id,
253
+ file_name=file_name)
254
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
255
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
256
+ except Exception as e:
257
+ print(f"Error running agent on task {task_id}: {e}")
258
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
259
+
260
+ if not answers_payload:
261
+ print("Agent did not produce any answers to submit.")
262
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
263
+
264
+ # 4. Prepare Submission
265
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
266
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
267
+ print(status_update)
268
+
269
+ # 5. Submit
270
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
271
+ try:
272
+ response = requests.post(submit_url, json=submission_data, timeout=60)
273
+ response.raise_for_status()
274
+ result_data = response.json()
275
+ final_status = (
276
+ f"Submission Successful!\n"
277
+ f"User: {result_data.get('username')}\n"
278
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
279
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
280
+ f"Message: {result_data.get('message', 'No message received.')}"
281
+ )
282
+ print("Submission successful.")
283
+ results_df = pd.DataFrame(results_log)
284
+ return final_status, results_df
285
+ except requests.exceptions.HTTPError as e:
286
+ error_detail = f"Server responded with status {e.response.status_code}."
287
+ try:
288
+ error_json = e.response.json()
289
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
290
+ except requests.exceptions.JSONDecodeError:
291
+ error_detail += f" Response: {e.response.text[:500]}"
292
+ status_message = f"Submission Failed: {error_detail}"
293
+ print(status_message)
294
+ results_df = pd.DataFrame(results_log)
295
+ return status_message, results_df
296
+ except requests.exceptions.Timeout:
297
+ status_message = "Submission Failed: The request timed out."
298
+ print(status_message)
299
+ results_df = pd.DataFrame(results_log)
300
+ return status_message, results_df
301
+ except requests.exceptions.RequestException as e:
302
+ status_message = f"Submission Failed: Network error - {e}"
303
+ print(status_message)
304
+ results_df = pd.DataFrame(results_log)
305
+ return status_message, results_df
306
+ except Exception as e:
307
+ status_message = f"An unexpected error occurred during submission: {e}"
308
+ print(status_message)
309
+ results_df = pd.DataFrame(results_log)
310
+ return status_message, results_df
311
+
312
+
313
+ # --- Build Gradio Interface using Blocks ---
314
+ with gr.Blocks() as demo:
315
+ gr.Markdown("# Basic Agent Evaluation Runner")
316
+ gr.Markdown(
317
+ """
318
+ **Instructions:**
319
+
320
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
321
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
322
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
323
+
324
+ ---
325
+ **Disclaimers:**
326
+ Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
327
+ This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
328
+ """
329
+ )
330
+
331
+ gr.LoginButton()
332
+
333
+ os.environ["AZURE_TENANT_ID"] = gr.Textbox(label="AZURE_TENANT_ID")
334
+ os.environ["AZURE_CLIENT_ID"] = gr.Textbox(label="AZURE_CLIENT_ID")
335
+ os.environ["AZURE_CLIENT_SECRET"] = gr.Textbox(label="AZURE_CLIENT_SECRET")
336
+ os.environ["TAVILY_API_KEY"] = gr.Textbox(label="TAVILY_API_KEY")
337
+ os.environ["TAVILY_API_URL"] = gr.Textbox(label="TAVILY_API_URL")
338
+ os.environ["GOOGLE_API_KEY"] = gr.Textbox(label="GOOGLE_API_KEY")
339
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
340
+
341
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
342
+ # Removed max_rows=10 from DataFrame constructor
343
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
344
+
345
+ run_button.click(
346
+ fn=run_and_submit_all,
347
+ outputs=[status_output, results_table]
348
+ )
349
+
350
+ if __name__ == "__main__":
351
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
352
+ # Check for SPACE_HOST and SPACE_ID at startup for information
353
+ space_host_startup = os.getenv("SPACE_HOST")
354
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
355
+
356
+ if space_host_startup:
357
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
358
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
359
+ else:
360
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
361
+
362
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
363
+ print(f"✅ SPACE_ID found: {space_id_startup}")
364
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
365
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
366
+ else:
367
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
368
+
369
+ print("-"*(60 + len(" App Starting ")) + "\n")
370
+
371
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
372
+ demo.launch(debug=True, share=False)