Skip to content
Open
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
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,47 @@ result = execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
- Integration with popular AI frameworks:
- OpenAI Functions
- LangChain Tools
- Pydantic AI Tools
- CrewAI Tools
- LangGraph Tool Node

## Documentation
## AI Framework Integration

For more examples and documentation, visit:
StackOne tools integrate seamlessly with popular AI frameworks. Convert your tools to the appropriate format:

- [Error Handling](docs/error-handling.md)
- [StackOne Account IDs](docs/stackone-account-ids.md)
- [Available Tools](docs/available-tools.md)
- [File Uploads](docs/file-uploads.md)
### Pydantic AI

## AI Framework Integration
```python
# Convert to Pydantic AI format
pydantic_ai_tools = tools.to_pydantic_ai()

# Use with Pydantic AI agent
from pydantic_ai import Agent
agent = Agent(model="openai:gpt-4", tools=pydantic_ai_tools)
```

### LangChain

```python
# Convert to LangChain format
langchain_tools = tools.to_langchain()

# Use with LangChain
from langchain_openai import ChatOpenAI
model = ChatOpenAI().bind_tools(langchain_tools)
```

### OpenAI Functions

- [OpenAI Integration](docs/openai-integration.md)
- [LangChain Integration](docs/langchain-integration.md)
- [CrewAI Integration](docs/crewai-integration.md)
- [LangGraph Tool Node](docs/langgraph-tool-node.md)
```python
# Convert to OpenAI function format
openai_tools = tools.to_openai()

# Use with OpenAI client
import openai
client = openai.OpenAI()
client.chat.completions.create(tools=openai_tools, ...)
```

## License

Expand Down
52 changes: 52 additions & 0 deletions examples/pydantic_ai_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
This example demonstrates how to use StackOne tools with Pydantic AI.

```bash
uv run examples/pydantic_ai_integration.py
```
"""

import asyncio

from dotenv import load_dotenv
from pydantic_ai import Agent

from stackone_ai import StackOneToolSet

load_dotenv()

account_id = "45072196112816593343"
employee_id = "c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA"


async def pydantic_ai_integration() -> None:
"""Example of using StackOne tools with Pydantic AI"""
# Initialize StackOne toolset
toolset = StackOneToolSet()
tools = toolset.get_tools("hris_*", account_id=account_id)

# Convert to Pydantic AI format
pydantic_ai_tools = tools.to_pydantic_ai()
assert len(pydantic_ai_tools) > 0, "Expected at least one Pydantic AI tool"

# Create a Pydantic AI agent with StackOne tools
agent = Agent(
model="openai:gpt-4o-mini",
tools=pydantic_ai_tools,
)

# Test the integration
result = await agent.run(
f"Can you get me information about employee with ID: {employee_id}? Use the HRIS tools."
)

print("Agent response:", result.data)

# Verify tool calls were made
assert result.all_messages(), "Expected messages from the agent"
tool_calls_made = any(msg.kind == "tool_call" for msg in result.all_messages())
print(f"Tool calls were made: {tool_calls_made}")


if __name__ == "__main__":
asyncio.run(pydantic_ai_integration())
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies = [
"pydantic>=2.10.6",
"requests>=2.32.3",
"langchain-core>=0.1.0",
"pydantic-ai>=0.1.0",
"mcp[cli]>=1.3.0",
"bm25s>=0.2.2",
"numpy>=1.24.0",
Expand Down
27 changes: 27 additions & 0 deletions stackone_ai/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import requests
from langchain_core.tools import BaseTool
from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr
from pydantic_ai.tools import Tool as PydanticAITool
from requests.exceptions import RequestException

# Type aliases for common types
Expand Down Expand Up @@ -397,6 +398,24 @@ async def _arun(self, **kwargs: Any) -> Any:

return StackOneLangChainTool()

def to_pydantic_ai(self) -> PydanticAITool:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of adding a specific to_framework() method for each new AI framework integration directly to the core StackOneTool class violates the Open/Closed Principle (OCP). This design choice tightly couples the core tool definition to external, third-party frameworks. As the number of supported integrations grows, this approach will require continuous modification of the core StackOneTool class, increasing maintenance burden and the risk of regressions. A more scalable and architecturally sound approach would involve an Adapter or Plugin pattern, where new framework converters can be added without modifying the core tool's source code, adhering to OCP and reducing coupling.

Prompt for AI agents
Address the following comment on stackone_ai/models.py at line 401:

<comment>The pattern of adding a specific `to_framework()` method for each new AI framework integration directly to the core `StackOneTool` class violates the Open/Closed Principle (OCP). This design choice tightly couples the core tool definition to external, third-party frameworks. As the number of supported integrations grows, this approach will require continuous modification of the core `StackOneTool` class, increasing maintenance burden and the risk of regressions. A more scalable and architecturally sound approach would involve an Adapter or Plugin pattern, where new framework converters can be added without modifying the core tool&#39;s source code, adhering to OCP and reducing coupling.</comment>

<file context>
@@ -397,6 +398,24 @@ async def _arun(self, **kwargs: Any) -&gt; Any:
 
         return StackOneLangChainTool()
 
+    def to_pydantic_ai(self) -&gt; PydanticAITool:
+        &quot;&quot;&quot;Convert this tool to Pydantic AI format
+
</file context>

"""Convert this tool to Pydantic AI format

Returns:
Tool in Pydantic AI format
"""
parent_tool = self

async def pydantic_ai_wrapper(**kwargs: Any) -> JsonDict:
"""Async wrapper for the tool execution compatible with Pydantic AI"""
return await parent_tool.acall(kwargs)

# Create the Pydantic AI tool with proper schema
return PydanticAITool(
pydantic_ai_wrapper,
description=self.description,
)

def set_account_id(self, account_id: str | None) -> None:
"""Set the account ID for this tool

Expand Down Expand Up @@ -480,6 +499,14 @@ def to_langchain(self) -> Sequence[BaseTool]:
"""
return [tool.to_langchain() for tool in self.tools]

def to_pydantic_ai(self) -> list[PydanticAITool]:
"""Convert all tools to Pydantic AI format

Returns:
List of tools in Pydantic AI format
"""
return [tool.to_pydantic_ai() for tool in self.tools]

def meta_tools(self) -> "Tools":
"""Return meta tools for tool discovery and execution

Expand Down
79 changes: 79 additions & 0 deletions tests/test_pydantic_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Tests for Pydantic AI integration"""

import pytest
from pydantic_ai.tools import Tool as PydanticAITool

from stackone_ai import StackOneToolSet


@pytest.fixture
def toolset() -> StackOneToolSet:
"""Create a toolset for testing"""
return StackOneToolSet(api_key="test-key")


def test_single_tool_pydantic_ai_conversion(toolset: StackOneToolSet) -> None:
"""Test converting a single tool to Pydantic AI format"""
tools = toolset.get_tools("hris_get_employee")

if not tools.tools:
pytest.skip("No tools found for testing")

tool = tools.tools[0]
pydantic_ai_tool = tool.to_pydantic_ai()

# Verify it's a Pydantic AI tool
assert isinstance(pydantic_ai_tool, PydanticAITool)
assert pydantic_ai_tool.description == tool.description


def test_tools_pydantic_ai_conversion(toolset: StackOneToolSet) -> None:
"""Test converting all tools to Pydantic AI format"""
tools = toolset.get_tools("hris_*")

if not tools.tools:
pytest.skip("No tools found for testing")

pydantic_ai_tools = tools.to_pydantic_ai()

# Verify conversion
assert len(pydantic_ai_tools) == len(tools.tools)
assert all(isinstance(tool, PydanticAITool) for tool in pydantic_ai_tools)

# Verify tool properties are preserved
for i, pydantic_ai_tool in enumerate(pydantic_ai_tools):
original_tool = tools.tools[i]
assert pydantic_ai_tool.description == original_tool.description


@pytest.mark.asyncio
async def test_pydantic_ai_tool_execution(toolset: StackOneToolSet) -> None:
"""Test that Pydantic AI tools can be executed"""
tools = toolset.get_tools("hris_get_employee")

if not tools.tools:
pytest.skip("No tools found for testing")

tool = tools.tools[0]
pydantic_ai_tool = tool.to_pydantic_ai()

# Test that the tool function exists and is callable
assert callable(pydantic_ai_tool.function)
assert callable(pydantic_ai_tool.function)


def test_pydantic_ai_tool_schema_generation(toolset: StackOneToolSet) -> None:
"""Test that Pydantic AI tools generate proper schemas"""
tools = toolset.get_tools("hris_get_employee")

if not tools.tools:
pytest.skip("No tools found for testing")

tool = tools.tools[0]
pydantic_ai_tool = tool.to_pydantic_ai()

# Verify the tool has required attributes
assert hasattr(pydantic_ai_tool, "function")
assert hasattr(pydantic_ai_tool, "description")
assert pydantic_ai_tool.description is not None
assert len(pydantic_ai_tool.description.strip()) > 0
Loading