File size: 3,267 Bytes
3f46b6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""

agent.py – central coordinator for smolagents-powered agent.



This file exposes a single helper function `my_agent()` that returns an

object which is **callable** (i.e. implements `__call__(question:str) -> str`)

so that `app.py` can stay unchanged apart from a single import.



* Adding new tools

------------------

1. Drop the tool file inside the ``/tools`` package.

2. Import the tool class in `my_agent` and append it to the ``tools`` list.

   The rest of the application will automatically pick it up.

"""

from typing import List, Sequence

try:
    from smolagents import Agent, Tool  # type: ignore
except ImportError as exc:  # pragma: no cover
    raise ImportError(
        "smolagents must be in requirements.txt. "
        "Add `smolagents` to your dependencies."
    ) from exc

# Available tools
from tools.web_search import DuckDuckGoSearchTool  # noqa: E402


class SmolAgentWrapper:
    """

    Thin wrapper that makes a smolagents.Agent *callable*.



    The evaluation harness in app.py expects an object that can be called

    directly with a single question and that returns a string.  The underlying

    smolagents agent is session-aware and can handle multi-turn conversations

    but we keep the public interface single-turn for now.

    """

    def __init__(self, tools: Sequence["Tool"] | None = None) -> None:  # type: ignore[name-defined]
        if tools is None:
            tools = [DuckDuckGoSearchTool()]
        self._agent = Agent(tools=list(tools))

    # Allow the object itself to be called like a function
    def __call__(self, question: str) -> str:  # noqa: D401  (simple summary ok)
        """

        Ask the underlying smolagents Agent a **single** question and return the answer.



        Any exception is caught and surfaced as a readable string in order not

        to crash the evaluation loop.

        """
        try:
            response = self._agent.run(question)
            # smolagents may return dicts or ToolOutput objects; normalise to str
            if isinstance(response, str):
                return response
            return str(response)
        except Exception as err:  # pragma: no cover
            return f"ERROR: {type(err).__name__}: {err}"


# --------------------------------------------------------------------------- #
# Helper – this is what app.py will import
# --------------------------------------------------------------------------- #

def my_agent(extra_tools: Sequence["Tool"] | None = None) -> SmolAgentWrapper:  # type: ignore[name-defined]
    """

    Factory that returns a ready-to-go agent.



    Parameters

    ----------

    extra_tools:

        Optional sequence of additional smolagents Tool objects to extend the

        agent's capabilities.  They are appended **after** the default search

        tool so they can override it if they expose the same name.



    Returns

    -------

    SmolAgentWrapper

        A callable object compatible with the original BasicAgent.

    """
    tools: List["Tool"] = [DuckDuckGoSearchTool()]
    if extra_tools:
        tools.extend(extra_tools)
    return SmolAgentWrapper(tools=tools)


__all__ = ["my_agent", "SmolAgentWrapper"]