Skip to content

Commit 8216ab6

Browse files
feat(integrations): add native Pydantic AI support (#184)
* Add Native Pydantic AI Integration to StackOne SDK * Keep the implementation minimum for now * Type chcking enhancements for pydantic tools
1 parent 72345c3 commit 8216ab6

7 files changed

Lines changed: 327 additions & 27 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug
2626
- LangChain Tools
2727
- CrewAI Tools
2828
- LangGraph Tool Node
29+
- Pydantic AI Toolset
2930

3031
## Requirements
3132

@@ -195,6 +196,43 @@ for tool_call in response.tool_calls:
195196

196197
</details>
197198

199+
<details>
200+
<summary>Pydantic AI Integration</summary>
201+
202+
StackOne tools convert to Pydantic AI `Tool` instances via `.to_pydantic_ai()`, parallel to `.to_openai()` and `.to_langchain()`:
203+
204+
Prerequisites:
205+
206+
```bash
207+
pip install 'stackone-ai[pydantic-ai]'
208+
```
209+
210+
```python
211+
import os
212+
from pydantic_ai import Agent
213+
from stackone_ai import StackOneToolSet
214+
215+
toolset = StackOneToolSet()
216+
tools = toolset.fetch_tools(
217+
actions=["workday_list_workers", "workday_get_worker"],
218+
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
219+
).to_pydantic_ai()
220+
221+
agent = Agent("openai:gpt-5.4", tools=tools)
222+
result = agent.run_sync("List the first 5 employees")
223+
print(result.output)
224+
```
225+
226+
For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()` method on `StackOneToolSet` — parallel to `.openai()` / `.langchain()`:
227+
228+
```python
229+
toolset = StackOneToolSet()
230+
tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
231+
# or pydantic_ai(mode="search_and_execute") for agent-driven discovery
232+
```
233+
234+
</details>
235+
198236
<details>
199237
<summary>LangGraph Integration</summary>
200238

examples/pydantic_ai_integration.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
```bash
44
uv run examples/pydantic_ai_integration.py
55
```
6+
7+
Install with `pip install 'stackone-ai[pydantic-ai]'` (or
8+
`pip install 'stackone-ai[examples]'` to run this file).
69
"""
710

811
from __future__ import annotations
912

10-
import json
1113
import os
1214

1315
try:
@@ -18,31 +20,15 @@
1820
pass
1921

2022
try:
21-
from pydantic_ai import Agent, Tool
23+
from pydantic_ai import Agent
2224
except ImportError:
23-
print("Install pydantic-ai to run this example: pip install pydantic-ai")
25+
print("Install pydantic-ai to run this example: pip install 'stackone-ai[pydantic-ai]'")
2426
raise SystemExit(1) from None
2527

2628
from stackone_ai import StackOneToolSet
2729

2830

29-
def _to_pydantic_ai_tool(stackone_tool) -> Tool:
30-
"""Convert a StackOneTool to a Pydantic AI Tool with the proper JSON schema."""
31-
params_schema = stackone_tool.to_openai_function()["function"]["parameters"]
32-
33-
def execute(**kwargs: object) -> str:
34-
return json.dumps(stackone_tool.execute(kwargs))
35-
36-
return Tool.from_schema(
37-
execute,
38-
name=stackone_tool.name,
39-
description=stackone_tool.description,
40-
json_schema=params_schema,
41-
)
42-
43-
4431
def pydantic_ai_integration() -> None:
45-
account_id = os.getenv("STACKONE_ACCOUNT_ID")
4632
for var in ["STACKONE_API_KEY", "STACKONE_ACCOUNT_ID", "OPENAI_API_KEY"]:
4733
if not os.getenv(var):
4834
print(f"Set {var} to run this example.")
@@ -51,12 +37,14 @@ def pydantic_ai_integration() -> None:
5137
toolset = StackOneToolSet()
5238
tools = toolset.fetch_tools(
5339
actions=["workday_list_workers", "workday_get_worker", "workday_get_current_user"],
54-
account_ids=[account_id],
55-
)
56-
pydantic_tools = [_to_pydantic_ai_tool(t) for t in tools]
57-
print(f"Loaded {len(pydantic_tools)} tools for Pydantic AI.")
40+
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
41+
).to_pydantic_ai()
5842

59-
agent = Agent("openai:gpt-5.4", system_prompt="You are a helpful HR assistant.", tools=pydantic_tools)
43+
agent = Agent(
44+
"openai:gpt-5.4",
45+
system_prompt="You are a helpful HR assistant.",
46+
tools=tools,
47+
)
6048
result = agent.run_sync("List the first 5 employees")
6149
print(f"Result:\n{result.output}")
6250

pyproject.toml

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

3838
[project.optional-dependencies]
3939
mcp = ["mcp>=1.3.0"]
40+
pydantic-ai = ["pydantic-ai-slim>=1.83.0,<2.0.0"]
4041
examples = [
4142
"crewai>=0.102.0",
4243
"langchain-openai>=1.1.14",

stackone_ai/models.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
from collections.abc import Sequence
77
from datetime import datetime, timezone
88
from enum import Enum
9-
from typing import Annotated, Any, ClassVar, TypeAlias, cast
9+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypeAlias, cast
1010
from urllib.parse import quote
1111

1212
import httpx
1313
from langchain_core.tools import BaseTool
1414
from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr
1515

16+
if TYPE_CHECKING:
17+
from pydantic_ai.tools import Tool as PydanticAITool
18+
1619
# Type aliases for common types
1720
JsonDict: TypeAlias = dict[str, Any]
1821
Headers: TypeAlias = dict[str, str]
@@ -467,6 +470,36 @@ def _run(self, **kwargs: Any) -> Any:
467470

468471
return StackOneLangChainTool()
469472

473+
def to_pydantic_ai_tool(self) -> PydanticAITool:
474+
"""Convert this tool to a Pydantic AI ``Tool``.
475+
476+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
477+
478+
Returns:
479+
A ``pydantic_ai.tools.Tool`` ready to pass to ``Agent(tools=[...])``.
480+
"""
481+
try:
482+
from pydantic_ai.tools import Tool
483+
except ImportError as e:
484+
raise ImportError(
485+
"Install `pydantic-ai-slim` (or `stackone-ai[pydantic-ai]`) "
486+
"to use the Pydantic AI integration."
487+
) from e
488+
489+
openai_function = self.to_openai_function()
490+
json_schema = openai_function["function"]["parameters"]
491+
parent_tool = self
492+
493+
def implementation(**kwargs: Any) -> Any:
494+
return parent_tool.execute(kwargs)
495+
496+
return Tool.from_schema(
497+
function=implementation,
498+
name=self.name,
499+
description=self.description,
500+
json_schema=json_schema,
501+
)
502+
470503
def set_account_id(self, account_id: str | None) -> None:
471504
"""Set the account ID for this tool
472505
@@ -577,3 +610,13 @@ def to_langchain(self) -> Sequence[BaseTool]:
577610
Sequence of tools in LangChain format
578611
"""
579612
return [tool.to_langchain() for tool in self.tools]
613+
614+
def to_pydantic_ai(self) -> list[PydanticAITool]:
615+
"""Convert all tools to Pydantic AI ``Tool`` instances.
616+
617+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
618+
619+
Returns:
620+
List of ``pydantic_ai.tools.Tool`` ready to pass to ``Agent(tools=[...])``.
621+
"""
622+
return [tool.to_pydantic_ai_tool() for tool in self.tools]

stackone_ai/toolset.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from collections.abc import Coroutine, Sequence
1212
from dataclasses import dataclass
1313
from importlib import metadata
14-
from typing import Any, Literal, TypedDict, TypeVar
14+
from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar
1515

1616
from pydantic import BaseModel, Field, PrivateAttr, ValidationError, field_validator
1717

@@ -32,6 +32,9 @@
3232
)
3333
from stackone_ai.utils.normalize import _normalize_action_name
3434

35+
if TYPE_CHECKING:
36+
from pydantic_ai.tools import Tool as PydanticAITool
37+
3538
logger = logging.getLogger("stackone.tools")
3639

3740
SearchMode = Literal["auto", "semantic", "local"]
@@ -752,6 +755,46 @@ def langchain(
752755

753756
return self.fetch_tools(account_ids=effective_account_ids).to_langchain()
754757

758+
def pydantic_ai(
759+
self,
760+
*,
761+
mode: Literal["search_and_execute"] | None = None,
762+
account_ids: list[str] | None = None,
763+
) -> list[PydanticAITool]:
764+
"""Get tools as Pydantic AI ``Tool`` instances.
765+
766+
Args:
767+
mode: Tool mode.
768+
``None`` (default): fetch all tools and convert to Pydantic AI tools.
769+
``"search_and_execute"``: return two meta tools (tool_search + tool_execute)
770+
that let the LLM discover and execute tools on-demand.
771+
account_ids: Account IDs to scope tools. Overrides the ``execute``
772+
config from the constructor.
773+
774+
Returns:
775+
List of Pydantic AI ``Tool`` objects ready to pass to ``Agent(tools=...)``.
776+
777+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
778+
779+
Examples::
780+
781+
# All tools
782+
toolset = StackOneToolSet()
783+
tools = toolset.pydantic_ai()
784+
agent = Agent("openai:gpt-5.4", tools=tools)
785+
786+
# Meta tools for agent-driven discovery
787+
tools = toolset.pydantic_ai(mode="search_and_execute")
788+
"""
789+
effective_account_ids = account_ids or (
790+
self._execute_config.get("account_ids") if self._execute_config else None
791+
)
792+
793+
if mode == "search_and_execute":
794+
return self._build_tools(account_ids=effective_account_ids).to_pydantic_ai()
795+
796+
return self.fetch_tools(account_ids=effective_account_ids).to_pydantic_ai()
797+
755798
def execute(
756799
self,
757800
tool_name: str,

0 commit comments

Comments
 (0)