From 52b6ba547bdf0a4f93c2b21711915610bee0ae0c Mon Sep 17 00:00:00 2001 From: Mohammed Saqlain Date: Tue, 31 Mar 2026 20:14:03 +0530 Subject: [PATCH 1/5] feat: implement WeatherTool for fetching weather information by location --- src/vantage/tools/weather_tool.py | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/vantage/tools/weather_tool.py diff --git a/src/vantage/tools/weather_tool.py b/src/vantage/tools/weather_tool.py new file mode 100644 index 0000000..b5a05cf --- /dev/null +++ b/src/vantage/tools/weather_tool.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import ast +import operator +from typing import Any, Dict + +from ..core.bases import ToolBase +from typing import Callable +import requests + + +class WeatherTool(ToolBase): + @property + def name(self) -> str: + return "weather_tool" + + @property + def description(self) -> str: + return "Fetch weather information for a given location." + + def input_schema(self) -> Dict[str, Any]: + return { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "Location for which to fetch weather information, e.g. 'New York, NY'", + }, + }, + "required": ["location"], + "additionalProperties": False, + } + + def execute(self, **kwargs: Any) -> str: + location = str(kwargs.get("location", "")) + if not location.strip(): + raise ValueError("location is required") + url = f"https://wttr.in/{location}?format=3" + response = requests.get(url) + if response.status_code != 200: + raise RuntimeError(f"Failed to fetch weather data for {location}") + return response.text + + From 43fb8905bcf3ee59f9c6b6962c82d859edde8b80 Mon Sep 17 00:00:00 2001 From: Mohammed Saqlain Date: Tue, 31 Mar 2026 20:14:53 +0530 Subject: [PATCH 2/5] feat: add WeatherTool and example agent for weather information retrieval --- examples/weather_agent/__init__.py | 0 examples/weather_agent/run.py | 25 ++++++++++++++++++++++ examples/weather_agent/trace.png | Bin 0 -> 29672 bytes examples/weather_agent/weather_agent.yaml | 12 +++++++++++ src/vantage/__init__.py | 2 ++ src/vantage/tools/__init__.py | 3 ++- src/vantage/tools/weather_tool.py | 3 --- tests/tools/test_weather_tool.py | 0 8 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 examples/weather_agent/__init__.py create mode 100644 examples/weather_agent/run.py create mode 100644 examples/weather_agent/trace.png create mode 100644 examples/weather_agent/weather_agent.yaml create mode 100644 tests/tools/test_weather_tool.py diff --git a/examples/weather_agent/__init__.py b/examples/weather_agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/weather_agent/run.py b/examples/weather_agent/run.py new file mode 100644 index 0000000..02b77c6 --- /dev/null +++ b/examples/weather_agent/run.py @@ -0,0 +1,25 @@ +import os +from rich import print +from rich.panel import Panel +from dotenv import load_dotenv + +from vantage import run_yaml_agent, WeatherTool, save_trace_png + + +def main() -> None: + load_dotenv() + base_dir = os.path.dirname(os.path.abspath(__file__)) + cfg_path = os.path.join(base_dir, "weather_agent.yaml") + + prompt = "What is the weather like in New York?" + resp = run_yaml_agent(cfg_path, "weather_agent", prompt, tools=[WeatherTool()]) + print(Panel(resp.content, title="[bold][green]Final Response[/green][/bold]", border_style="green")) + + # Save a visual trace of the execution + save_trace_png(resp.trace, "examples/weather_agent/trace.png") + print("\n[bold][green]Visual trace saved to examples/weather_agent/trace.png[/green][/bold]") + + +if __name__ == "__main__": + main() + diff --git a/examples/weather_agent/trace.png b/examples/weather_agent/trace.png new file mode 100644 index 0000000000000000000000000000000000000000..97917cc51517a67736a102662228c44165b0c15b GIT binary patch literal 29672 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 MByNi5h~2vXUzX?Op#T5? literal 0 HcmV?d00001 diff --git a/examples/weather_agent/weather_agent.yaml b/examples/weather_agent/weather_agent.yaml new file mode 100644 index 0000000..22dc2ed --- /dev/null +++ b/examples/weather_agent/weather_agent.yaml @@ -0,0 +1,12 @@ +agents: + weather_agent: + description: An agent that provides weather information using WeatherTool. + model: groq/llama-3.3-70b-versatile + tools: + - weather_tool + system_prompt: | + You are a helpful weather assistant. Use the WeatherTool to answer weather-related questions accurately. + response_schema: + weather_summary: string + temperature_celsius: number + location: string \ No newline at end of file diff --git a/src/vantage/__init__.py b/src/vantage/__init__.py index 84a5eae..ea95f9f 100644 --- a/src/vantage/__init__.py +++ b/src/vantage/__init__.py @@ -2,6 +2,7 @@ from .core.models import Message, Role, AgentResponse, ToolResult from .runtime import run_yaml_agent, async_run_yaml_agent from .tools import Calculator +from .tools import WeatherTool from .utils.viz import save_trace_png __all__ = [ @@ -14,6 +15,7 @@ "run_yaml_agent", "async_run_yaml_agent", "Calculator", + "WeatherTool", "save_trace_png", ] diff --git a/src/vantage/tools/__init__.py b/src/vantage/tools/__init__.py index aeb3775..2f968d0 100644 --- a/src/vantage/tools/__init__.py +++ b/src/vantage/tools/__init__.py @@ -1,4 +1,5 @@ from .calculator import Calculator +from .weather_tool import WeatherTool -__all__ = ["Calculator"] +__all__ = ["Calculator", "WeatherTool"] diff --git a/src/vantage/tools/weather_tool.py b/src/vantage/tools/weather_tool.py index b5a05cf..166d200 100644 --- a/src/vantage/tools/weather_tool.py +++ b/src/vantage/tools/weather_tool.py @@ -1,11 +1,8 @@ from __future__ import annotations -import ast -import operator from typing import Any, Dict from ..core.bases import ToolBase -from typing import Callable import requests diff --git a/tests/tools/test_weather_tool.py b/tests/tools/test_weather_tool.py new file mode 100644 index 0000000..e69de29 From 6359cb719e03cf0b0bb0bbba3628063e04dd4c13 Mon Sep 17 00:00:00 2001 From: Mohammed Saqlain Date: Tue, 31 Mar 2026 20:21:17 +0530 Subject: [PATCH 3/5] feat: add types-requests to dev dependencies for type checking support --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 561f2d3..12e9121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dev = [ "ruff>=0.6.0", "mypy>=1.10.0", "types-PyYAML>=0.1.0", # For mypy type checking support + "types-requests" ] [project.urls] From 8cd80ef08f8c07c218cd8685b9ca92a382d97aba Mon Sep 17 00:00:00 2001 From: Mohammed Saqlain Date: Tue, 31 Mar 2026 20:22:35 +0530 Subject: [PATCH 4/5] feat: add requests to dependencies and types-requests to optional dev dependencies --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 12e9121..6215799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "python-dotenv>=1.0.1", "rich>=13.0.0", "Pillow>=10.0.0", + "requests" ] [project.optional-dependencies] @@ -39,7 +40,7 @@ dev = [ "ruff>=0.6.0", "mypy>=1.10.0", "types-PyYAML>=0.1.0", # For mypy type checking support - "types-requests" + "types-requests", ] [project.urls] From d62224918d11d331ea26649d1552bcf391c4e871 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:09:01 +0000 Subject: [PATCH 5/5] refactor: replace requests with httpx in WeatherTool, add URL encoding, timeout, error handling, and tests Agent-Logs-Url: https://github.com/saqlain2204/Vantage/sessions/8580fb44-d014-4bfd-9531-deafbb2007d1 Co-authored-by: saqlain2204 <118016760+saqlain2204@users.noreply.github.com> --- examples/weather_agent/run.py | 5 +- examples/weather_agent/weather_agent.yaml | 4 +- pyproject.toml | 2 - src/vantage/tools/weather_tool.py | 30 ++++++++-- tests/tools/test_weather_tool.py | 71 +++++++++++++++++++++++ 5 files changed, 101 insertions(+), 11 deletions(-) diff --git a/examples/weather_agent/run.py b/examples/weather_agent/run.py index 02b77c6..0b1a4a1 100644 --- a/examples/weather_agent/run.py +++ b/examples/weather_agent/run.py @@ -16,8 +16,9 @@ def main() -> None: print(Panel(resp.content, title="[bold][green]Final Response[/green][/bold]", border_style="green")) # Save a visual trace of the execution - save_trace_png(resp.trace, "examples/weather_agent/trace.png") - print("\n[bold][green]Visual trace saved to examples/weather_agent/trace.png[/green][/bold]") + trace_path = os.path.join(base_dir, "trace.png") + save_trace_png(resp.trace, trace_path) + print(f"\n[bold][green]Visual trace saved to {trace_path}[/green][/bold]") if __name__ == "__main__": diff --git a/examples/weather_agent/weather_agent.yaml b/examples/weather_agent/weather_agent.yaml index 22dc2ed..c19763b 100644 --- a/examples/weather_agent/weather_agent.yaml +++ b/examples/weather_agent/weather_agent.yaml @@ -1,11 +1,11 @@ agents: weather_agent: - description: An agent that provides weather information using WeatherTool. + description: An agent that provides weather information using the `weather_tool` tool. model: groq/llama-3.3-70b-versatile tools: - weather_tool system_prompt: | - You are a helpful weather assistant. Use the WeatherTool to answer weather-related questions accurately. + You are a helpful weather assistant. Use the `weather_tool` tool to answer weather-related questions accurately. response_schema: weather_summary: string temperature_celsius: number diff --git a/pyproject.toml b/pyproject.toml index 6215799..561f2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dependencies = [ "python-dotenv>=1.0.1", "rich>=13.0.0", "Pillow>=10.0.0", - "requests" ] [project.optional-dependencies] @@ -40,7 +39,6 @@ dev = [ "ruff>=0.6.0", "mypy>=1.10.0", "types-PyYAML>=0.1.0", # For mypy type checking support - "types-requests", ] [project.urls] diff --git a/src/vantage/tools/weather_tool.py b/src/vantage/tools/weather_tool.py index 166d200..a6530ab 100644 --- a/src/vantage/tools/weather_tool.py +++ b/src/vantage/tools/weather_tool.py @@ -1,12 +1,22 @@ from __future__ import annotations from typing import Any, Dict +from urllib.parse import quote + +import httpx from ..core.bases import ToolBase -import requests + +_DEFAULT_TIMEOUT = 10.0 +_RESPONSE_SNIPPET_LENGTH = 200 class WeatherTool(ToolBase): + def __init__(self, timeout: float = _DEFAULT_TIMEOUT) -> None: + if timeout <= 0: + raise ValueError("timeout must be positive") + self._timeout = timeout + @property def name(self) -> str: return "weather_tool" @@ -32,10 +42,20 @@ def execute(self, **kwargs: Any) -> str: location = str(kwargs.get("location", "")) if not location.strip(): raise ValueError("location is required") - url = f"https://wttr.in/{location}?format=3" - response = requests.get(url) - if response.status_code != 200: - raise RuntimeError(f"Failed to fetch weather data for {location}") + encoded_location = quote(location) + url = f"https://wttr.in/{encoded_location}" + try: + response = httpx.get(url, params={"format": "3"}, timeout=self._timeout) + response.raise_for_status() + except httpx.HTTPStatusError as exc: + body = exc.response.text + snippet = body[:_RESPONSE_SNIPPET_LENGTH] + ("..." if len(body) > _RESPONSE_SNIPPET_LENGTH else "") + message = f"Failed to fetch weather data for {location} (status {exc.response.status_code})." + if snippet: + message += f" Response snippet: {snippet}" + raise RuntimeError(message) from exc + except httpx.RequestError as exc: + raise RuntimeError(f"Failed to fetch weather data for {location}: {exc}") from exc return response.text diff --git a/tests/tools/test_weather_tool.py b/tests/tools/test_weather_tool.py index e69de29..f755339 100644 --- a/tests/tools/test_weather_tool.py +++ b/tests/tools/test_weather_tool.py @@ -0,0 +1,71 @@ +"""Tests for WeatherTool.""" +from __future__ import annotations + +import httpx +import pytest +import respx + +from vantage.tools.weather_tool import WeatherTool + + +@respx.mock +def test_execute_returns_weather_text() -> None: + tool = WeatherTool() + respx.get("https://wttr.in/London").mock( + return_value=httpx.Response(200, text="London: ⛅ +15°C") + ) + result = tool.execute(location="London") + assert result == "London: ⛅ +15°C" + + +@respx.mock +def test_execute_url_encodes_location() -> None: + tool = WeatherTool() + respx.get("https://wttr.in/New%20York").mock( + return_value=httpx.Response(200, text="New York: ☀ +22°C") + ) + result = tool.execute(location="New York") + assert "New York" in result + + +def test_execute_raises_on_empty_location() -> None: + tool = WeatherTool() + with pytest.raises(ValueError, match="location is required"): + tool.execute(location="") + + +def test_execute_raises_on_blank_location() -> None: + tool = WeatherTool() + with pytest.raises(ValueError, match="location is required"): + tool.execute(location=" ") + + +@respx.mock +def test_execute_raises_runtime_error_on_http_error() -> None: + tool = WeatherTool() + respx.get("https://wttr.in/BadLocation").mock( + return_value=httpx.Response(404, text="Not Found") + ) + with pytest.raises(RuntimeError, match="status 404"): + tool.execute(location="BadLocation") + + +@respx.mock +def test_execute_raises_runtime_error_on_network_error() -> None: + tool = WeatherTool() + respx.get("https://wttr.in/London").mock(side_effect=httpx.ConnectError("DNS failure")) + with pytest.raises(RuntimeError, match="Failed to fetch weather data for London"): + tool.execute(location="London") + + +def test_tool_name() -> None: + assert WeatherTool().name == "weather_tool" + + +def test_tool_description() -> None: + assert "weather" in WeatherTool().description.lower() + + +def test_input_schema_requires_location() -> None: + schema = WeatherTool().input_schema() + assert "location" in schema["required"]