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
23 changes: 23 additions & 0 deletions examples/stack_overflow_agent/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
from rich import print
from rich.panel import Panel
from dotenv import load_dotenv

from vantage import run_yaml_agent, save_trace_png
from .tools import StackOverflowToolAgent

def main() -> None:
load_dotenv()
base_dir = os.path.dirname(os.path.abspath(__file__))
cfg_path = os.path.join(base_dir, "stack_overflow_agent.yaml")

prompt = "How do I reverse a list in Python?"
resp = run_yaml_agent(cfg_path, "stack_overflow_agent", prompt, tools=[StackOverflowToolAgent()])
print(Panel(resp.content, title="[bold][green]Final Response[/green][/bold]", border_style="green"))

# Save a visual trace
save_trace_png(resp.trace, "examples/stack_overflow_agent/trace.png")
print("\n[bold][green]Visual trace saved to examples/stack_overflow_agent/trace.png[/green][/bold]")

if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions examples/stack_overflow_agent/stack_overflow_agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
agents:
stack_overflow_agent:
model: groq/llama-3.3-70b-versatile
system_prompt: >
You are a programming assistant.
When the user asks a programming question, call the stack_overflow_tool.
tools: [stack_overflow_tool]
response_schema:
answer: string
message: string
7 changes: 7 additions & 0 deletions examples/stack_overflow_agent/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import annotations


from vantage.tools.stack_overflow_tool import StackOverflowTool

class StackOverflowToolAgent(StackOverflowTool):
pass
Binary file added examples/stack_overflow_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.
3 changes: 2 additions & 1 deletion src/vantage/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .calculator import Calculator
from .weather_tool import WeatherTool
from .stack_overflow_tool import StackOverflowTool

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

65 changes: 65 additions & 0 deletions src/vantage/tools/stack_overflow_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 StackOverflowTool(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 "stack_overflow_tool"

@property
def description(self) -> str:
return "Search Stack Overflow for programming questions and return the top answer snippet."

def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Programming question to search on Stack Overflow.",
},
},
"required": ["query"],
"additionalProperties": False,
}

def execute(self, **kwargs: Any) -> str:
query = str(kwargs.get("query", "")).strip()
if not query:
raise ValueError("query is required")
url = f"https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q={quote(query)}&site=stackoverflow&filter=!9_bDDxJY5"
with httpx.Client(timeout=self._timeout) as client:
resp = client.get(url)
resp.raise_for_status()
items = resp.json().get("items", [])
if not items:
return "No results found."
question_id = items[0]["question_id"]
answer_url = f"https://api.stackexchange.com/2.3/questions/{question_id}/answers?order=desc&sort=votes&site=stackoverflow&filter=withbody"
answer_resp = client.get(answer_url)
answer_resp.raise_for_status()
answers = answer_resp.json().get("items", [])
if not answers:
return f"No answers found for: {items[0]['title']}"
# Return a snippet of the top answer
print(answers)
body = answers[0].get("body", "")
return body



60 changes: 60 additions & 0 deletions tests/tools/test_stack_overflow_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Tests for StackOverflowTool."""
from __future__ import annotations

import httpx
import pytest
import respx

from vantage.tools.stack_overflow_tool import StackOverflowTool

@respx.mock
def test_execute_returns_answer_snippet() -> None:
tool = StackOverflowTool()
# Mock search response
respx.get("https://api.stackexchange.com/2.3/search/advanced").mock(
return_value=httpx.Response(200, json={
"items": [{"question_id": 123, "title": "How to reverse a list in Python?"}]
})
)
# Mock answer response
respx.get("https://api.stackexchange.com/2.3/questions/123/answers").mock(
return_value=httpx.Response(200, json={
"items": [{"body": "<p>You can use <code>list[::-1]</code> to reverse a list.</p>"}]
})
)
result = tool.execute(query="How to reverse a list in Python?")
assert "reverse a list" in result
assert "list[::-1]" in result

@respx.mock
def test_execute_no_results() -> None:
tool = StackOverflowTool()
respx.get("https://api.stackexchange.com/2.3/search/advanced").mock(
return_value=httpx.Response(200, json={"items": []})
)
result = tool.execute(query="some totally random query")
assert result == "No results found."

@respx.mock
def test_execute_no_answers() -> None:
tool = StackOverflowTool()
respx.get("https://api.stackexchange.com/2.3/search/advanced").mock(
return_value=httpx.Response(200, json={
"items": [{"question_id": 456, "title": "Unanswered question"}]
})
)
respx.get("https://api.stackexchange.com/2.3/questions/456/answers").mock(
return_value=httpx.Response(200, json={"items": []})
)
result = tool.execute(query="Unanswered question")
assert result.startswith("No answers found for:")

def test_execute_raises_on_empty_query() -> None:
tool = StackOverflowTool()
with pytest.raises(ValueError, match="query is required"):
tool.execute(query="")

def test_execute_raises_on_blank_query() -> None:
tool = StackOverflowTool()
with pytest.raises(ValueError, match="query is required"):
tool.execute(query=" ")
Loading