Skip to content

Commit 5eff322

Browse files
committed
feat: add Agno framework integration
- Add Agno as optional dependency - Implement to_agno() method for StackOneTool and Tools classes - Create agno_integration.py example with sync and async usage - Add comprehensive tests for Agno integration - Update documentation and README with Agno support - Support both individual tool and batch tool conversion to Agno format
1 parent a46bf39 commit 5eff322

File tree

9 files changed

+386
-2
lines changed

9 files changed

+386
-2
lines changed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1212

1313
## Project Overview
1414

15-
StackOne AI SDK is a Python library that provides a unified interface for accessing various SaaS tools through AI-friendly APIs. It acts as a bridge between AI applications and multiple SaaS platforms (HRIS, CRM, ATS, LMS, Marketing, etc.) with support for OpenAI, LangChain, CrewAI, and Model Context Protocol (MCP).
15+
StackOne AI SDK is a Python library that provides a unified interface for accessing various SaaS tools through AI-friendly APIs. It acts as a bridge between AI applications and multiple SaaS platforms (HRIS, CRM, ATS, LMS, Marketing, etc.) with support for Agno, OpenAI, LangChain, CrewAI, and Model Context Protocol (MCP).
1616

1717
## Essential Development Commands
1818

@@ -50,7 +50,7 @@ make mcp-inspector # Run MCP server inspector for debugging
5050
2. **Models** (`stackone_ai/models.py`): Data structures
5151
- `StackOneTool`: Base class with execution logic
5252
- `Tools`: Container for managing multiple tools
53-
- Format converters for different AI frameworks
53+
- Format converters for different AI frameworks (Agno, OpenAI, LangChain)
5454

5555
3. **OpenAPI Parser** (`stackone_ai/specs/parser.py`): Spec conversion
5656
- Converts OpenAPI specs to tool definitions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ result = execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
5656
- **Glob Pattern Filtering**: Advanced tool filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
5757
- **Meta Tools** (Beta): Dynamic tool discovery and execution based on natural language queries
5858
- Integration with popular AI frameworks:
59+
- Agno Agents
5960
- OpenAI Functions
6061
- LangChain Tools
6162
- CrewAI Tools
@@ -72,6 +73,7 @@ For more examples and documentation, visit:
7273

7374
## AI Framework Integration
7475

76+
- [Agno Integration](docs/agno-integration.md)
7577
- [OpenAI Integration](docs/openai-integration.md)
7678
- [LangChain Integration](docs/langchain-integration.md)
7779
- [CrewAI Integration](docs/crewai-integration.md)

examples/agno_integration.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
This example demonstrates how to use StackOne tools with Agno agents.
3+
4+
This example is runnable with the following command:
5+
```bash
6+
uv run examples/agno_integration.py
7+
```
8+
9+
You can find out more about Agno framework at https://docs.agno.com
10+
"""
11+
12+
from agno import Agent
13+
from agno.models.openai import OpenAIChat
14+
from dotenv import load_dotenv
15+
16+
from stackone_ai import StackOneToolSet
17+
18+
load_dotenv()
19+
20+
account_id = "45072196112816593343"
21+
employee_id = "c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA"
22+
23+
24+
def agno_integration() -> None:
25+
"""Demonstrate StackOne tools with Agno agents"""
26+
toolset = StackOneToolSet()
27+
28+
# Filter tools to only the ones we need to avoid context window limits
29+
tools = toolset.get_tools(
30+
[
31+
"hris_get_employee",
32+
"hris_list_employee_employments",
33+
"hris_get_employee_employment",
34+
],
35+
account_id=account_id,
36+
)
37+
38+
# Convert to Agno format
39+
agno_tools = tools.to_agno()
40+
41+
# Create an Agno agent with the tools
42+
agent = Agent(
43+
name="HR Assistant Agent",
44+
role="Helpful HR assistant that can access employee data",
45+
model=OpenAIChat(id="gpt-4o-mini"),
46+
tools=agno_tools,
47+
instructions=[
48+
"You are a helpful HR assistant.",
49+
"Use the provided tools to access employee information.",
50+
"Always be professional and respectful when handling employee data.",
51+
],
52+
show_tool_calls=True,
53+
markdown=True,
54+
)
55+
56+
# Test the agent with a query
57+
query = f"Can you get me information about employee with ID: {employee_id}?"
58+
59+
print(f"Query: {query}")
60+
print("Agent response:")
61+
62+
response = agent.run(query)
63+
print(response.content)
64+
65+
# Verify we got a meaningful response
66+
assert response.content is not None, "Expected response content"
67+
assert len(response.content) > 0, "Expected non-empty response"
68+
69+
70+
def agno_async_integration() -> None:
71+
"""Demonstrate async StackOne tools with Agno agents"""
72+
import asyncio
73+
74+
async def run_async_agent() -> None:
75+
toolset = StackOneToolSet()
76+
77+
# Filter tools to only the ones we need
78+
tools = toolset.get_tools(
79+
["hris_get_employee"],
80+
account_id=account_id,
81+
)
82+
83+
# Convert to Agno format
84+
agno_tools = tools.to_agno()
85+
86+
# Create an Agno agent
87+
agent = Agent(
88+
name="Async HR Agent",
89+
role="Async HR assistant",
90+
model=OpenAIChat(id="gpt-4o-mini"),
91+
tools=agno_tools,
92+
instructions=["You are an async HR assistant."],
93+
)
94+
95+
# Run the agent asynchronously
96+
query = f"Get employee information for ID: {employee_id}"
97+
response = await agent.arun(query)
98+
99+
print(f"Async query: {query}")
100+
print("Async agent response:")
101+
print(response.content)
102+
103+
# Verify response
104+
assert response.content is not None, "Expected async response content"
105+
106+
# Run the async example
107+
asyncio.run(run_async_agent())
108+
109+
110+
if __name__ == "__main__":
111+
print("=== StackOne + Agno Integration Demo ===\n")
112+
113+
print("1. Basic Agno Integration:")
114+
agno_integration()
115+
116+
print("\n2. Async Agno Integration:")
117+
agno_async_integration()
118+
119+
print("\n=== Demo completed successfully! ===")

examples/test_examples.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def get_example_files() -> list[str]:
2424

2525
# Map of example files to required optional packages
2626
OPTIONAL_DEPENDENCIES = {
27+
"agno_integration.py": ["agno"],
2728
"openai_integration.py": ["openai"],
2829
"langchain_integration.py": ["langchain_openai"],
2930
"crewai_integration.py": ["crewai"],

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nav:
4646
- Available Tools: available-tools.md
4747
- File Uploads: file-uploads.md
4848
- Integrations:
49+
- Agno: agno-integration.md
4950
- OpenAI: openai-integration.md
5051
- CrewAI: crewai-integration.md
5152
- LangChain: langchain-integration.md

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ packages = ["stackone_ai"]
4242

4343
[project.optional-dependencies]
4444
examples = [
45+
"agno>=1.7.0",
4546
"crewai>=0.102.0",
4647
"langchain-openai>=0.3.6",
4748
"openai>=1.63.2",

stackone_ai/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,35 @@ async def _arun(self, **kwargs: Any) -> Any:
397397

398398
return StackOneLangChainTool()
399399

400+
def to_agno(self) -> Any:
401+
"""Convert this tool to Agno format
402+
403+
Returns:
404+
Tool in Agno format
405+
"""
406+
try:
407+
from agno.tools import Tool as AgnoBaseTool # type: ignore[import-not-found]
408+
except ImportError as e:
409+
raise ImportError(
410+
"Agno is not installed. Please install it with 'pip install agno>=1.7.0' "
411+
"or add 'agno>=1.7.0' to your requirements."
412+
) from e
413+
414+
parent_tool = self
415+
416+
class StackOneAgnoTool(AgnoBaseTool):
417+
def __init__(self) -> None:
418+
super().__init__(
419+
name=parent_tool.name,
420+
description=parent_tool.description,
421+
)
422+
423+
def run(self, **kwargs: Any) -> JsonDict:
424+
"""Run the tool with the provided arguments"""
425+
return parent_tool.execute(kwargs)
426+
427+
return StackOneAgnoTool()
428+
400429
def set_account_id(self, account_id: str | None) -> None:
401430
"""Set the account ID for this tool
402431
@@ -480,6 +509,14 @@ def to_langchain(self) -> Sequence[BaseTool]:
480509
"""
481510
return [tool.to_langchain() for tool in self.tools]
482511

512+
def to_agno(self) -> list[Any]:
513+
"""Convert all tools to Agno format
514+
515+
Returns:
516+
List of tools in Agno format
517+
"""
518+
return [tool.to_agno() for tool in self.tools]
519+
483520
def meta_tools(self) -> "Tools":
484521
"""Return meta tools for tool discovery and execution
485522

tests/test_agno_integration.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
5+
from stackone_ai.models import (
6+
ExecuteConfig,
7+
StackOneTool,
8+
ToolParameters,
9+
Tools,
10+
)
11+
12+
13+
@pytest.fixture
14+
def mock_tool() -> StackOneTool:
15+
"""Create a mock tool for testing"""
16+
return StackOneTool(
17+
description="Test HRIS tool for getting employee data",
18+
parameters=ToolParameters(
19+
type="object",
20+
properties={
21+
"id": {"type": "string", "description": "Employee ID"},
22+
"include_personal": {"type": "boolean", "description": "Include personal information"},
23+
},
24+
),
25+
_execute_config=ExecuteConfig(
26+
headers={},
27+
method="GET",
28+
url="https://api.stackone.com/unified/hris/employees/{id}",
29+
name="hris_get_employee",
30+
parameter_locations={"id": "path"},
31+
),
32+
_api_key="test_key",
33+
_account_id="test_account",
34+
)
35+
36+
37+
@pytest.fixture
38+
def tools_collection(mock_tool: StackOneTool) -> Tools:
39+
"""Create a Tools collection with mock tools"""
40+
return Tools([mock_tool])
41+
42+
43+
class TestAgnoIntegration:
44+
"""Test Agno integration functionality"""
45+
46+
def test_to_agno_without_agno_installed(self, mock_tool: StackOneTool) -> None:
47+
"""Test that proper error is raised when Agno is not installed"""
48+
with patch.dict('sys.modules', {'agno': None, 'agno.tools': None}):
49+
with pytest.raises(ImportError) as exc_info:
50+
mock_tool.to_agno()
51+
52+
assert "Agno is not installed" in str(exc_info.value)
53+
assert "pip install agno>=1.7.0" in str(exc_info.value)
54+
55+
def test_to_agno_with_mocked_agno(self, mock_tool: StackOneTool) -> None:
56+
"""Test Agno conversion with mocked Agno classes"""
57+
# Mock the Agno Tool class
58+
mock_agno_base_tool = MagicMock()
59+
mock_agno_module = MagicMock()
60+
mock_agno_module.Tool = mock_agno_base_tool
61+
62+
with patch.dict('sys.modules', {'agno.tools': mock_agno_module}):
63+
agno_tool = mock_tool.to_agno()
64+
65+
# Verify an Agno tool instance was created
66+
assert agno_tool is not None
67+
68+
def test_to_agno_tool_execution(self, mock_tool: StackOneTool) -> None:
69+
"""Test that the Agno tool can execute the underlying StackOne tool"""
70+
mock_agno_base_tool = MagicMock()
71+
mock_agno_module = MagicMock()
72+
mock_agno_module.Tool = mock_agno_base_tool
73+
74+
with patch.dict('sys.modules', {'agno.tools': mock_agno_module}):
75+
agno_tool = mock_tool.to_agno()
76+
77+
# Verify the tool was created (basic functionality test)
78+
assert agno_tool is not None
79+
assert hasattr(agno_tool, 'run')
80+
81+
def test_tools_to_agno(self, tools_collection: Tools) -> None:
82+
"""Test converting Tools collection to Agno format"""
83+
mock_agno_base_tool = MagicMock()
84+
mock_agno_module = MagicMock()
85+
mock_agno_module.Tool = mock_agno_base_tool
86+
87+
with patch.dict('sys.modules', {'agno.tools': mock_agno_module}):
88+
agno_tools = tools_collection.to_agno()
89+
90+
# Verify we got the expected number of tools
91+
assert len(agno_tools) == 1
92+
assert agno_tools[0] is not None
93+
94+
def test_tools_to_agno_multiple_tools(self) -> None:
95+
"""Test converting multiple tools to Agno format"""
96+
# Create multiple mock tools
97+
tool1 = StackOneTool(
98+
description="Test tool 1",
99+
parameters=ToolParameters(type="object", properties={"id": {"type": "string"}}),
100+
_execute_config=ExecuteConfig(
101+
headers={}, method="GET", url="https://api.example.com/test1/{id}", name="test_tool_1"
102+
),
103+
_api_key="test_key",
104+
)
105+
tool2 = StackOneTool(
106+
description="Test tool 2",
107+
parameters=ToolParameters(type="object", properties={"name": {"type": "string"}}),
108+
_execute_config=ExecuteConfig(
109+
headers={}, method="POST", url="https://api.example.com/test2", name="test_tool_2"
110+
),
111+
_api_key="test_key",
112+
)
113+
114+
tools = Tools([tool1, tool2])
115+
116+
mock_agno_base_tool = MagicMock()
117+
mock_agno_module = MagicMock()
118+
mock_agno_module.Tool = mock_agno_base_tool
119+
120+
with patch.dict('sys.modules', {'agno.tools': mock_agno_module}):
121+
agno_tools = tools.to_agno()
122+
123+
assert len(agno_tools) == 2
124+
assert all(tool is not None for tool in agno_tools)
125+
126+
def test_agno_tool_preserves_metadata(self, mock_tool: StackOneTool) -> None:
127+
"""Test that Agno tool conversion preserves important metadata"""
128+
mock_agno_base_tool = MagicMock()
129+
mock_agno_module = MagicMock()
130+
mock_agno_module.Tool = mock_agno_base_tool
131+
132+
with patch.dict('sys.modules', {'agno.tools': mock_agno_module}):
133+
agno_tool = mock_tool.to_agno()
134+
135+
# Verify the tool was created with expected attributes
136+
assert agno_tool is not None
137+
# For real integration, name and description would be set by the Agno base class
138+
assert hasattr(agno_tool, 'name')
139+
assert hasattr(agno_tool, 'description')
140+
141+
142+
class TestAgnoIntegrationErrors:
143+
"""Test error handling in Agno integration"""
144+
145+
def test_agno_import_error_message(self, mock_tool: StackOneTool) -> None:
146+
"""Test that ImportError contains helpful installation instructions"""
147+
with patch.dict('sys.modules', {'agno': None, 'agno.tools': None}):
148+
with pytest.raises(ImportError) as exc_info:
149+
mock_tool.to_agno()
150+
151+
error_msg = str(exc_info.value)
152+
assert "Agno is not installed" in error_msg
153+
assert "pip install agno>=1.7.0" in error_msg
154+
assert "requirements" in error_msg
155+
156+
def test_tools_to_agno_with_failed_conversion(self) -> None:
157+
"""Test Tools.to_agno() when individual tool conversion fails"""
158+
mock_tool = MagicMock()
159+
mock_tool.to_agno.side_effect = ImportError("Agno not available")
160+
161+
tools = Tools([mock_tool])
162+
163+
with pytest.raises(ImportError):
164+
tools.to_agno()

0 commit comments

Comments
 (0)