Tesvia commited on
Commit
49f4445
·
1 Parent(s): 4c23cee

added agent, config, exception, web_search

Browse files
Files changed (6) hide show
  1. agent.py +88 -0
  2. app.py +10 -16
  3. config.py +7 -0
  4. exceptions.py +11 -0
  5. requirements.txt +4 -1
  6. tools/web_search.py +71 -0
agent.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ agent.py – central coordinator for smolagents-powered agent.
3
+
4
+ This file exposes a single helper function `my_agent()` that returns an
5
+ object which is **callable** (i.e. implements `__call__(question:str) -> str`)
6
+ so that `app.py` can stay unchanged apart from a single import.
7
+
8
+ * Adding new tools
9
+ ------------------
10
+ 1. Drop the tool file inside the ``/tools`` package.
11
+ 2. Import the tool class in `my_agent` and append it to the ``tools`` list.
12
+ The rest of the application will automatically pick it up.
13
+ """
14
+
15
+ from typing import List, Sequence
16
+
17
+ try:
18
+ from smolagents import Agent, Tool # type: ignore
19
+ except ImportError as exc: # pragma: no cover
20
+ raise ImportError(
21
+ "smolagents must be in requirements.txt. "
22
+ "Add `smolagents` to your dependencies."
23
+ ) from exc
24
+
25
+ # Available tools
26
+ from tools.web_search import DuckDuckGoSearchTool # noqa: E402
27
+
28
+
29
+ class SmolAgentWrapper:
30
+ """
31
+ Thin wrapper that makes a smolagents.Agent *callable*.
32
+
33
+ The evaluation harness in app.py expects an object that can be called
34
+ directly with a single question and that returns a string. The underlying
35
+ smolagents agent is session-aware and can handle multi-turn conversations
36
+ but we keep the public interface single-turn for now.
37
+ """
38
+
39
+ def __init__(self, tools: Sequence["Tool"] | None = None) -> None: # type: ignore[name-defined]
40
+ if tools is None:
41
+ tools = [DuckDuckGoSearchTool()]
42
+ self._agent = Agent(tools=list(tools))
43
+
44
+ # Allow the object itself to be called like a function
45
+ def __call__(self, question: str) -> str: # noqa: D401 (simple summary ok)
46
+ """
47
+ Ask the underlying smolagents Agent a **single** question and return the answer.
48
+
49
+ Any exception is caught and surfaced as a readable string in order not
50
+ to crash the evaluation loop.
51
+ """
52
+ try:
53
+ response = self._agent.run(question)
54
+ # smolagents may return dicts or ToolOutput objects; normalise to str
55
+ if isinstance(response, str):
56
+ return response
57
+ return str(response)
58
+ except Exception as err: # pragma: no cover
59
+ return f"ERROR: {type(err).__name__}: {err}"
60
+
61
+
62
+ # --------------------------------------------------------------------------- #
63
+ # Helper – this is what app.py will import
64
+ # --------------------------------------------------------------------------- #
65
+
66
+ def my_agent(extra_tools: Sequence["Tool"] | None = None) -> SmolAgentWrapper: # type: ignore[name-defined]
67
+ """
68
+ Factory that returns a ready-to-go agent.
69
+
70
+ Parameters
71
+ ----------
72
+ extra_tools:
73
+ Optional sequence of additional smolagents Tool objects to extend the
74
+ agent's capabilities. They are appended **after** the default search
75
+ tool so they can override it if they expose the same name.
76
+
77
+ Returns
78
+ -------
79
+ SmolAgentWrapper
80
+ A callable object compatible with the original BasicAgent.
81
+ """
82
+ tools: List["Tool"] = [DuckDuckGoSearchTool()]
83
+ if extra_tools:
84
+ tools.extend(extra_tools)
85
+ return SmolAgentWrapper(tools=tools)
86
+
87
+
88
+ __all__ = ["my_agent", "SmolAgentWrapper"]
app.py CHANGED
@@ -4,20 +4,13 @@ import requests
4
  import inspect
5
  import pandas as pd
6
 
 
 
 
7
  # (Keep Constants as is)
8
  # --- Constants ---
9
  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
- class BasicAgent:
14
- def __init__(self):
15
- print("BasicAgent initialized.")
16
- def __call__(self, question: str) -> str:
17
- print(f"Agent received question (first 50 chars): {question[:50]}...")
18
- fixed_answer = "This is a default answer."
19
- print(f"Agent returning fixed answer: {fixed_answer}")
20
- return fixed_answer
21
 
22
  def run_and_submit_all( profile: gr.OAuthProfile | None):
23
  """
@@ -38,9 +31,10 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
38
  questions_url = f"{api_url}/questions"
39
  submit_url = f"{api_url}/submit"
40
 
41
- # 1. Instantiate Agent ( modify this part to create your agent)
42
  try:
43
- agent = BasicAgent()
 
44
  except Exception as e:
45
  print(f"Error instantiating agent: {e}")
46
  return f"Error initializing agent: {e}", None
@@ -69,7 +63,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
69
  print(f"An unexpected error occurred fetching questions: {e}")
70
  return f"An unexpected error occurred fetching questions: {e}", None
71
 
72
- # 3. Run your Agent
73
  results_log = []
74
  answers_payload = []
75
  print(f"Running agent on {len(questions_data)} questions...")
@@ -142,7 +136,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
142
 
143
  # --- Build Gradio Interface using Blocks ---
144
  with gr.Blocks() as demo:
145
- gr.Markdown("# Basic Agent Evaluation Runner")
146
  gr.Markdown(
147
  """
148
  **Instructions:**
@@ -192,5 +186,5 @@ if __name__ == "__main__":
192
 
193
  print("-"*(60 + len(" App Starting ")) + "\n")
194
 
195
- print("Launching Gradio Interface for Basic Agent Evaluation...")
196
- demo.launch(debug=True, share=False)
 
4
  import inspect
5
  import pandas as pd
6
 
7
+ # --- Our Agent ---
8
+ from agent import my_agent
9
+
10
  # (Keep Constants as is)
11
  # --- Constants ---
12
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
13
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  def run_and_submit_all( profile: gr.OAuthProfile | None):
16
  """
 
31
  questions_url = f"{api_url}/questions"
32
  submit_url = f"{api_url}/submit"
33
 
34
+ # 1. Instantiate Agent (now using smolagents)
35
  try:
36
+ agent = my_agent()
37
+ print("SmolAgent instantiated successfully.")
38
  except Exception as e:
39
  print(f"Error instantiating agent: {e}")
40
  return f"Error initializing agent: {e}", None
 
63
  print(f"An unexpected error occurred fetching questions: {e}")
64
  return f"An unexpected error occurred fetching questions: {e}", None
65
 
66
+ # 3. Run the Agent
67
  results_log = []
68
  answers_payload = []
69
  print(f"Running agent on {len(questions_data)} questions...")
 
136
 
137
  # --- Build Gradio Interface using Blocks ---
138
  with gr.Blocks() as demo:
139
+ gr.Markdown("# Agent Evaluation Runner")
140
  gr.Markdown(
141
  """
142
  **Instructions:**
 
186
 
187
  print("-"*(60 + len(" App Starting ")) + "\n")
188
 
189
+ print("Launching Gradio Interface for Agent Evaluation")
190
+ demo.launch(debug=True, share=False)
config.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # DuckDuckGo-specific settings
2
+ DUCKDUCKGO_MAX_RESULTS = 10
3
+ DUCKDUCKGO_TIMEOUT = 15 # seconds
4
+ SEARCH_DEFAULT_ENGINE = 'duckduckgo' # Make it easy to add others
5
+
6
+ # Logging
7
+ LOG_LEVEL = "INFO"
exceptions.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class SearchError(Exception):
2
+ """Base exception for search errors."""
3
+ pass
4
+
5
+ class NoResultsFound(SearchError):
6
+ """Raised when no search results are returned."""
7
+ pass
8
+
9
+ class SearchEngineUnavailable(SearchError):
10
+ """Raised when the search engine is down or unreachable."""
11
+ pass
requirements.txt CHANGED
@@ -1,2 +1,5 @@
1
  gradio
2
- requests
 
 
 
 
1
  gradio
2
+ requests
3
+ pandas
4
+ smolagents
5
+ duckduckgo-search
tools/web_search.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # search_tool.py
2
+
3
+ from typing import List, Dict
4
+ import logging
5
+
6
+ from config import DUCKDUCKGO_MAX_RESULTS, DUCKDUCKGO_TIMEOUT, SEARCH_DEFAULT_ENGINE, LOG_LEVEL
7
+ from exceptions import NoResultsFound, SearchEngineUnavailable
8
+
9
+ try:
10
+ from duckduckgo_search import DDGS
11
+ except ImportError as e:
12
+ raise ImportError("Missing dependency: install with `pip install duckduckgo-search`") from e
13
+
14
+ logging.basicConfig(level=LOG_LEVEL)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class DuckDuckGoSearchTool:
19
+ """
20
+ Production-grade DuckDuckGo search tool.
21
+ Returns structured results and handles errors robustly.
22
+ """
23
+
24
+ def __init__(self, max_results: int = None, timeout: int = None):
25
+ self.max_results = max_results or DUCKDUCKGO_MAX_RESULTS
26
+ self.timeout = timeout or DUCKDUCKGO_TIMEOUT
27
+ try:
28
+ self.ddgs = DDGS(timeout=self.timeout)
29
+ except Exception as ex:
30
+ logger.critical("Failed to initialize DDGS: %s", ex)
31
+ raise SearchEngineUnavailable("Failed to initialize DuckDuckGo search engine.") from ex
32
+
33
+ def search(self, query: str) -> List[Dict]:
34
+ if not isinstance(query, str) or not query.strip():
35
+ logger.warning("Invalid search query provided: '%s'", query)
36
+ raise ValueError("Query must be a non-empty string.")
37
+ try:
38
+ results = self.ddgs.text(query, max_results=self.max_results)
39
+ except Exception as ex:
40
+ logger.error("Search failed: %s", ex)
41
+ raise SearchEngineUnavailable("DuckDuckGo search failed.") from ex
42
+
43
+ if not results:
44
+ logger.info("No results found for query: '%s'", query)
45
+ raise NoResultsFound(f"No results found for query: '{query}'")
46
+
47
+ safe_results = [self._sanitize_result(res) for res in results]
48
+ return safe_results
49
+
50
+ @staticmethod
51
+ def _sanitize_result(result: Dict) -> Dict:
52
+ """Sanitize user-facing fields to prevent markdown injection, etc."""
53
+ def escape_md(text: str) -> str:
54
+ # Very simple; improve as needed (real production code may need a markdown library)
55
+ return text.replace('[', '').replace(']', '').replace('(', '').replace(')', '')
56
+
57
+ return {
58
+ "title": escape_md(result.get("title", "")),
59
+ "link": result.get("href", ""),
60
+ "snippet": escape_md(result.get("body", "")),
61
+ }
62
+
63
+ @staticmethod
64
+ def format_markdown(results: List[Dict]) -> str:
65
+ """Format results as markdown. Keep presentation separate from core logic."""
66
+ if not results:
67
+ return "No results found."
68
+ lines = []
69
+ for res in results:
70
+ lines.append(f"- [{res['title']}]({res['link']})\n {res['snippet']}")
71
+ return "## Search Results\n\n" + "\n\n".join(lines)