Milkfish033 commited on
Commit
e2815a0
·
1 Parent(s): d6a77ec

get 30% score

Browse files
Files changed (2) hide show
  1. app.py +191 -34
  2. tool.py +147 -115
app.py CHANGED
@@ -10,46 +10,200 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
 
11
  # --- Basic Agent Definition ---
12
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
- from smolagents import CodeAgent, OpenAIModel
14
- from tool import web_search_tool, web_fetch_tool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- #model used for CodeAgent, need to be multi-modal
17
- llm= OpenAIModel(
18
- model="gpt-4o", # or gpt-4.1, gpt-4-turbo
19
- api_key="YOUR_API_KEY",
20
  )
21
 
22
- class BasicAgent:
23
- def __init__(self):
24
-
25
- print("BasicAgent initialized.")
26
- self.smart_agent = CodeAgent(
27
- tools=[web_search_tool, web_fetch_tool],
28
- model=llm,
29
- max_steps = 5,
30
- )
31
 
32
- def __call__(self, question: str) -> str:
33
- print(f"Agent received question (first 50 chars): {question[:50]}...")
34
-
35
- help_prompt = (
36
- "You are a general AI assistant. "
37
- "Answer the following question and respond ONLY using this template:\n"
38
- "FINAL ANSWER: [ANSWER]\n\n"
39
- "Rules:\n"
40
- "- The answer should be a number, a few words, or a comma-separated list.\n"
41
- "- Do NOT include explanations or reasoning.\n"
42
- "- Do NOT use units unless explicitly requested.\n"
43
- "- Do NOT use articles (a, an, the).\n"
44
- "- Write digits in plain text unless specified otherwise.\n\n"
45
- "Question:\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  )
47
- fixed_answer = self.smart_agent.run(help_prompt + question)
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- print(f"Agent returning fixed answer: {fixed_answer}")
51
-
52
- return fixed_answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
 
@@ -75,7 +229,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
75
 
76
  # 1. Instantiate Agent ( modify this part to create your agent)
77
  try:
78
- agent = BasicAgent()
79
  except Exception as e:
80
  print(f"Error instantiating agent: {e}")
81
  return f"Error initializing agent: {e}", None
@@ -208,6 +362,9 @@ with gr.Blocks() as demo:
208
 
209
  if __name__ == "__main__":
210
  print("\n" + "-"*30 + " App Starting " + "-"*30)
 
 
 
211
  # Check for SPACE_HOST and SPACE_ID at startup for information
212
  space_host_startup = os.getenv("SPACE_HOST")
213
  space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
 
10
 
11
  # --- Basic Agent Definition ---
12
  # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
+ import os
14
+ from typing import TypedDict, List, Optional, Literal
15
+
16
+ from langchain_openai import ChatOpenAI
17
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, BaseMessage
18
+ from langchain_core.output_parsers import StrOutputParser
19
+ from langchain_core.tools import Tool
20
+ from langgraph.graph import StateGraph, END
21
+ from langgraph.prebuilt import ToolNode
22
+ from typing_extensions import Annotated
23
+
24
+ from langchain_core.messages import BaseMessage
25
+ from langgraph.graph.message import add_messages
26
+
27
+ from tool import web_search, web_fetch, _extract_video_id, youtube_transcript # wrappers from step 2
28
+
29
+
30
+ # -----------------------------
31
+ # State
32
+ # -----------------------------
33
+ class AgentState(TypedDict):
34
+ question: str
35
+ messages: Annotated[list[BaseMessage], add_messages]
36
+ final: Optional[str]
37
+ steps: int
38
+ last_error: Optional[str]
39
+
40
 
41
+ MAX_STEPS = 10
42
+
43
+ HELP_PROMPT = (
44
+ "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]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. 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. 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. 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."
45
  )
46
 
 
 
 
 
 
 
 
 
 
47
 
48
+ # -----------------------------
49
+ # LLM + Tools
50
+ # -----------------------------
51
+ llm = ChatOpenAI(
52
+ model="gpt-4o",
53
+ api_key=os.environ["OPENAI_API_KEY"],
54
+ temperature=0,
55
+ )
56
+
57
+ tools = [web_search, web_fetch, youtube_transcript]
58
+ tool_node = ToolNode(tools)
59
+
60
+
61
+ # -----------------------------
62
+ # Helper: check final format
63
+ # -----------------------------
64
+ import re
65
+
66
+ def extract_final(text: str) -> Optional[str]:
67
+ """
68
+ Robustly extracts the final answer, handling case sensitivity and bold formatting.
69
+ """
70
+ # Use regex to find "FINAL ANSWER:" case-insensitive, potentially with ** or ##
71
+ match = re.search(r"(?i)(\*\*|##)?\s*FINAL ANSWER\s*(\*\*|##)?\s*:\s*(.*)", text, re.DOTALL)
72
+
73
+ if match:
74
+ # Return the captured content (group 3)
75
+ return match.group(3).strip()
76
+ return None
77
+
78
+
79
+ # -----------------------------
80
+ # Nodes
81
+ # -----------------------------
82
+ def start(state: AgentState) -> AgentState:
83
+ state["messages"] = [
84
+ SystemMessage(content=HELP_PROMPT),
85
+ HumanMessage(content=state["question"]),
86
+ ]
87
+ state["steps"] = 0
88
+ state["final"] = None
89
+ state["last_error"] = None
90
+ return state
91
+
92
+
93
+ def call_model(state: AgentState) -> AgentState:
94
+ state["steps"] += 1
95
+ resp = llm.bind_tools(tools).invoke(state["messages"])
96
+ state["messages"].append(resp)
97
+ return state
98
+
99
+
100
+ def maybe_finalize(state: AgentState) -> AgentState:
101
+ """If the model produced FINAL ANSWER, store it. Otherwise keep going."""
102
+ last = state["messages"][-1]
103
+ if isinstance(last, AIMessage):
104
+ final_line = extract_final(last.content if isinstance(last.content, str) else str(last.content))
105
+ if final_line:
106
+ state["final"] = final_line
107
+ return state
108
+
109
+
110
+ def format_guard(state: AgentState) -> AgentState:
111
+ """If we hit step limit and still no FINAL ANSWER, force one."""
112
+ if state["final"] is None:
113
+ # Ask model to rewrite into the required format only
114
+ state["messages"].append(
115
+ HumanMessage(
116
+ content="Rewrite your response to follow the required format exactly. "
117
+ "Return only one line: FINAL ANSWER: ...")
118
  )
119
+ return state
120
 
121
+
122
+ # -----------------------------
123
+ # Router: decide next step
124
+ # -----------------------------
125
+ def route(state: AgentState) -> Literal["tools", "finalize", "guard", "end"]:
126
+ # 1. First, check if the model wants to call tools.
127
+ # We MUST execute tools if requested, otherwise we break the conversation chain.
128
+ last = state["messages"][-1]
129
+ if isinstance(last, AIMessage) and getattr(last, "tool_calls", None):
130
+ return "tools"
131
+
132
+ # 2. If no tools, check if we are done.
133
+ if state["final"] is not None:
134
+ return "end"
135
+
136
+ # 3. TIME LIMIT CHECK
137
+ if state["steps"] >= MAX_STEPS:
138
+ # CHECK FOR DEATH LOOP:
139
+ # Look at the message before the last one. Was it our "Rewrite" prompt?
140
+ # If yes, we already tried to guard and it failed. Don't try again.
141
+ messages = state["messages"]
142
+ if len(messages) >= 2:
143
+ second_to_last = messages[-2]
144
+ if isinstance(second_to_last, HumanMessage) and "Rewrite your response" in str(second_to_last.content):
145
+ # We tried, we failed. Just give up to save the recursion limit.
146
+ return "end"
147
 
148
+ # Otherwise, try the guard rail once.
149
+ return "guard"
150
+
151
+ # 4. Default loop
152
+ return "finalize"
153
+
154
+
155
+ # -----------------------------
156
+ # Build graph
157
+ # -----------------------------
158
+ graph = StateGraph(AgentState)
159
+
160
+ graph.add_node("start", start)
161
+ graph.add_node("model", call_model)
162
+ graph.add_node("tools", tool_node)
163
+ graph.add_node("finalize", maybe_finalize)
164
+ graph.add_node("guard", format_guard)
165
+
166
+ graph.set_entry_point("start")
167
+ graph.add_edge("start", "model")
168
+ graph.add_edge("model", "finalize")
169
+
170
+ graph.add_conditional_edges(
171
+ "finalize",
172
+ route,
173
+ {
174
+ "tools": "tools",
175
+ "finalize": "model",
176
+ "guard": "guard",
177
+ "end": END,
178
+ },
179
+ )
180
+
181
+ graph.add_edge("tools", "model")
182
+ graph.add_edge("guard", "model")
183
+
184
+ app = graph.compile()
185
+
186
+
187
+
188
+ # -----------------------------
189
+ # Public callable (like your BasicAgent)
190
+ # -----------------------------
191
+ class BasicAgentLangGraph:
192
+ def __init__(self):
193
+ print("BasicAgentLangGraph initialized.")
194
+
195
+ def __call__(self, question: str) -> str:
196
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
197
+ state: AgentState = {
198
+ "question": question,
199
+ "messages": [],
200
+ "final": None,
201
+ "steps": 0,
202
+ "last_error": None,
203
+ }
204
+ out = app.invoke(state)
205
+ # If still none, fallback
206
+ return out["final"] or "FINAL ANSWER: not available"
207
 
208
 
209
 
 
229
 
230
  # 1. Instantiate Agent ( modify this part to create your agent)
231
  try:
232
+ agent = BasicAgentLangGraph()
233
  except Exception as e:
234
  print(f"Error instantiating agent: {e}")
235
  return f"Error initializing agent: {e}", None
 
362
 
363
  if __name__ == "__main__":
364
  print("\n" + "-"*30 + " App Starting " + "-"*30)
365
+
366
+
367
+
368
  # Check for SPACE_HOST and SPACE_ID at startup for information
369
  space_host_startup = os.getenv("SPACE_HOST")
370
  space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
tool.py CHANGED
@@ -1,123 +1,155 @@
1
- from smolagents import DuckDuckGoSearchTool
2
 
3
- # Initialize the DuckDuckGo search tool
4
- web_search_tool = DuckDuckGoSearchTool()
5
 
6
- from smolagents import Tool
7
- import json
8
  import os
9
-
10
- # PDF support
11
- import pdfplumber
12
-
13
- # CSV support
14
  import csv
 
15
 
16
-
17
- class FileReader(Tool):
18
- name = "file_reader"
19
- description = (
20
- "Read local files and return extracted text. "
21
- "Supports PDF, JSON, TXT, and CSV."
22
- )
23
-
24
- inputs = {
25
- "path": {
26
- "type": "string",
27
- "description": "Path to the file on disk"
28
- }
29
- }
30
-
31
- output_type = "string"
32
-
33
- def forward(self, path: str) -> str:
34
- if not os.path.exists(path):
35
- return f"Error: file not found at {path}"
36
-
37
- ext = os.path.splitext(path)[1].lower()
38
-
39
- try:
40
- if ext == ".pdf":
41
- return self._read_pdf(path)
42
-
43
- elif ext == ".json":
44
- return self._read_json(path)
45
-
46
- elif ext == ".txt":
47
- return self._read_txt(path)
48
-
49
- elif ext == ".csv":
50
- return self._read_csv(path)
51
-
52
- else:
53
- return f"Unsupported file type: {ext}"
54
-
55
- except Exception as e:
56
- return f"Error reading file: {str(e)}"
57
-
58
- def _read_pdf(self, path: str) -> str:
59
- text = []
60
- with pdfplumber.open(path) as pdf:
61
- for page in pdf.pages:
62
- page_text = page.extract_text()
63
- if page_text:
64
- text.append(page_text)
65
- return "\n".join(text)
66
-
67
- def _read_json(self, path: str) -> str:
68
- with open(path, "r", encoding="utf-8") as f:
69
- data = json.load(f)
70
- return json.dumps(data, indent=2)
71
-
72
- def _read_txt(self, path: str) -> str:
73
- with open(path, "r", encoding="utf-8") as f:
74
- return f.read()
75
-
76
- def _read_csv(self, path: str) -> str:
77
- rows = []
78
- with open(path, newline="", encoding="utf-8") as f:
79
- reader = csv.reader(f)
80
- for row in reader:
81
- rows.append(", ".join(row))
82
- return "\n".join(rows)
83
-
84
-
85
- file_reader_tool = FileReader()
86
-
87
-
88
- from smolagents import Tool
89
  import httpx
90
  from bs4 import BeautifulSoup
91
 
92
- class WebFetch(Tool):
93
- name = "web_fetch"
94
- description = "Fetch and read webpage content from a URL."
95
- inputs = {
96
- "url": {
97
- "type": "string",
98
- "description": "URL of the webpage to read"
99
- }
100
- }
101
- output_type = "string"
102
-
103
- def forward(self, url: str) -> str:
104
- try:
105
- with httpx.Client(follow_redirects=True, timeout=20) as client:
106
- r = client.get(url)
107
- r.raise_for_status()
108
-
109
- soup = BeautifulSoup(r.text, "html.parser")
110
-
111
- # Remove scripts, styles, nav, footer
112
- for tag in soup(["script", "style", "noscript", "nav", "footer", "header", "aside"]):
113
- tag.decompose()
114
-
115
- text = soup.get_text(separator="\n")
116
- lines = [line.strip() for line in text.splitlines() if line.strip()]
117
-
118
- return "\n".join(lines[:5000]) # cap length for LLM
119
-
120
- except Exception as e:
121
- return f"Error fetching page: {str(e)}"
122
-
123
- web_fetch_tool = WebFetch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ from __future__ import annotations
 
3
 
 
 
4
  import os
5
+ import json
 
 
 
 
6
  import csv
7
+ from typing import Optional
8
 
9
+ import pdfplumber
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  import httpx
11
  from bs4 import BeautifulSoup
12
 
13
+ from langchain_core.tools import tool
14
+ from langchain_community.tools import DuckDuckGoSearchRun
15
+
16
+
17
+ # -------------------------
18
+ # 1) DuckDuckGo search tool
19
+ # -------------------------
20
+ _ddg = DuckDuckGoSearchRun()
21
+
22
+ @tool("web_search")
23
+ def web_search(query: str) -> str:
24
+ """Search the web (DuckDuckGo) and return text results."""
25
+ # DuckDuckGoSearchRun returns a string summary of results
26
+ return _ddg.run(query)
27
+
28
+
29
+ # # -------------------------
30
+ # # 2) Local file reader tool
31
+ # # -------------------------
32
+ # def _read_pdf(path: str) -> str:
33
+ # text = []
34
+ # with pdfplumber.open(path) as pdf:
35
+ # for page in pdf.pages:
36
+ # page_text = page.extract_text()
37
+ # if page_text:
38
+ # text.append(page_text)
39
+ # return "\n".join(text)
40
+
41
+ # def _read_json(path: str) -> str:
42
+ # with open(path, "r", encoding="utf-8") as f:
43
+ # data = json.load(f)
44
+ # return json.dumps(data, indent=2, ensure_ascii=False)
45
+
46
+ # def _read_txt(path: str) -> str:
47
+ # with open(path, "r", encoding="utf-8") as f:
48
+ # return f.read()
49
+
50
+ # def _read_csv(path: str) -> str:
51
+ # rows = []
52
+ # with open(path, newline="", encoding="utf-8") as f:
53
+ # reader = csv.reader(f)
54
+ # for row in reader:
55
+ # rows.append(", ".join(row))
56
+ # return "\n".join(rows)
57
+
58
+ # @tool("file_reader")
59
+ # def file_reader(path: str) -> str:
60
+ # """
61
+ # Read local files and return extracted text.
62
+ # Supports PDF, JSON, TXT, and CSV.
63
+ # """
64
+ # if not os.path.exists(path):
65
+ # return f"Error: file not found at {path}"
66
+
67
+ # ext = os.path.splitext(path)[1].lower()
68
+
69
+ # try:
70
+ # if ext == ".pdf":
71
+ # return _read_pdf(path)
72
+ # if ext == ".json":
73
+ # return _read_json(path)
74
+ # if ext == ".txt":
75
+ # return _read_txt(path)
76
+ # if ext == ".csv":
77
+ # return _read_csv(path)
78
+ # return f"Unsupported file type: {ext}"
79
+ # except Exception as e:
80
+ # return f"Error reading file: {e}"
81
+
82
+
83
+ # -------------------------
84
+ # 3) Web fetch tool
85
+ # -------------------------
86
+ def _clean_html_to_text(html: str, max_lines: int = 5000) -> str:
87
+ soup = BeautifulSoup(html, "html.parser")
88
+
89
+ # Remove noisy tags
90
+ for tag in soup(["script", "style", "noscript", "nav", "footer", "header", "aside"]):
91
+ tag.decompose()
92
+
93
+ text = soup.get_text(separator="\n")
94
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
95
+ return "\n".join(lines[:max_lines])
96
+
97
+ @tool("web_fetch")
98
+ def web_fetch(url: str) -> str:
99
+ """
100
+ Retrieves and reads the text content of a specific URL.
101
+
102
+ Use this to read articles, documentation, or static webpages.
103
+
104
+ Do NOT use this tool for YouTube URLs (use 'youtube_transcript' instead).
105
+ Limitations:
106
+ - Returns cleaned plain text, not raw HTML.
107
+ - Cannot execute JavaScript (may fail on heavy SPAs or dynamic sites).
108
+ - Content is truncated at 5000 lines.
109
+ """
110
+ try:
111
+ with httpx.Client(follow_redirects=True, timeout=20) as client:
112
+ r = client.get(
113
+ url,
114
+ headers={
115
+ # Some sites block empty UA; this helps
116
+ "User-Agent": "Mozilla/5.0 (compatible; LangChainTool/1.0)"
117
+ },
118
+ )
119
+ r.raise_for_status()
120
+
121
+ return _clean_html_to_text(r.text, max_lines=5000)
122
+ except Exception as e:
123
+ return f"Error fetching page: {e}"
124
+
125
+ from langchain_core.tools import tool
126
+ from youtube_transcript_api import YouTubeTranscriptApi
127
+
128
+ def _extract_video_id(url: str) -> str:
129
+ # handles https://www.youtube.com/watch?v=VIDEOID
130
+ import urllib.parse as up
131
+ q = up.urlparse(url)
132
+ if q.hostname in ("www.youtube.com", "youtube.com"):
133
+ return up.parse_qs(q.query).get("v", [""])[0]
134
+ if q.hostname == "youtu.be":
135
+ return q.path.lstrip("/")
136
+ return ""
137
+
138
+ @tool("youtube_transcript")
139
+ def youtube_transcript(url: str) -> str:
140
+ """
141
+ Retrieves the full English transcript text from a YouTube video URL.
142
+
143
+ Use this tool when a user asks questions about a video's content, wants a summary,
144
+ or needs specific quotes.
145
+
146
+ Note: This tool only supports videos with English captions/subtitles.
147
+ """
148
+ vid = _extract_video_id(url)
149
+ if not vid:
150
+ return "Error: could not parse video id"
151
+ try:
152
+ chunks = YouTubeTranscriptApi.get_transcript(vid, languages=["en"])
153
+ return "\n".join([c["text"] for c in chunks])
154
+ except Exception as e:
155
+ return f"Error fetching transcript: {e}"