Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
26 changes: 26 additions & 0 deletions examples/weather_agent/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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
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__":
main()

Binary file added examples/weather_agent/trace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/weather_agent/weather_agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
agents:
weather_agent:
description: An agent that provides weather information using the `weather_tool` tool.
model: groq/llama-3.3-70b-versatile
tools:
- weather_tool
Comment thread
saqlain2204 marked this conversation as resolved.
system_prompt: |
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
location: string
2 changes: 2 additions & 0 deletions src/vantage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -14,6 +15,7 @@
"run_yaml_agent",
"async_run_yaml_agent",
"Calculator",
"WeatherTool",
"save_trace_png",
]

Expand Down
3 changes: 2 additions & 1 deletion src/vantage/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .calculator import Calculator
from .weather_tool import WeatherTool

__all__ = ["Calculator"]
__all__ = ["Calculator", "WeatherTool"]

61 changes: 61 additions & 0 deletions src/vantage/tools/weather_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

from typing import Any, Dict
from urllib.parse import quote

import httpx

from ..core.bases import ToolBase

_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"

@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")
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


71 changes: 71 additions & 0 deletions tests/tools/test_weather_tool.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading