-
Notifications
You must be signed in to change notification settings - Fork 0
feat(stackone): add StackOne AI integration #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
888b2d4
e369d07
a5e02c3
1cb34c3
24929da
8927e2c
f8288e4
68653ea
92c89dd
3739381
fd1225b
e95b7e3
c6700e7
a41f476
da2358e
6453f38
3585e04
f5f6526
47f75a6
27511c2
403c5d6
dca5b53
a8d4d23
580cea4
df805ee
ae117f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,5 @@ | |
| ::: pydantic_ai.ext.langchain | ||
|
|
||
| ::: pydantic_ai.ext.aci | ||
|
|
||
| ::: pydantic_ai.ext.stackone | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,10 +100,64 @@ toolset = ACIToolset( | |
| agent = Agent('openai:gpt-5', toolsets=[toolset]) | ||
| ``` | ||
|
|
||
| ## StackOne Tools {#stackone-tools} | ||
|
|
||
| If you'd like to use a tool from [StackOne](https://www.stackone.com/) with Pydantic AI, you can use the [`tool_from_stackone`][pydantic_ai.ext.stackone.tool_from_stackone] convenience method. StackOne provides integration infrastructure for AI agents, enabling them to execute actions across 200+ enterprise applications. | ||
|
|
||
| You will need to install the `stackone-ai` package (requires Python 3.10+), set your StackOne API key in the `STACKONE_API_KEY` environment variable, and provide your StackOne account ID via the `STACKONE_ACCOUNT_ID` environment variable or pass it directly to the function. | ||
|
|
||
| Here is how you can use a StackOne tool: | ||
|
|
||
| ```python {test="skip"} | ||
| import os | ||
|
|
||
| from pydantic_ai import Agent | ||
| from pydantic_ai.ext.stackone import tool_from_stackone | ||
|
|
||
| employee_tool = tool_from_stackone( | ||
| 'bamboohr_list_employees', | ||
| account_id=os.getenv('STACKONE_ACCOUNT_ID'), | ||
| api_key=os.getenv('STACKONE_API_KEY'), | ||
| ) | ||
|
|
||
| agent = Agent( | ||
| 'google-gla:gemini-3-pro-preview', | ||
| tools=[employee_tool], | ||
| ) | ||
|
|
||
| result = agent.run_sync('List all employees in the HR system') | ||
| print(result.output) | ||
| ``` | ||
|
|
||
| If you'd like to use multiple StackOne tools, you can use the [`StackOneToolset`][pydantic_ai.ext.stackone.StackOneToolset] [toolset](toolsets.md#stackone-tools). It supports pattern matching via `filter_pattern`, or natural-language tool search via `search_query` to discover relevant tools across your linked accounts. | ||
|
|
||
| Tool search uses the StackOne SDK's semantic search to match tools by natural-language description instead of exact names. This is useful when you don't know the exact tool names available for your connected accounts. You can control the search behavior with three modes: `auto` (default) tries semantic search first and falls back to local search, `semantic` uses only the semantic search API, and `local` uses only local BM25+TF-IDF ranking. Use `top_k` to limit the number of results and `min_similarity` (0-1) to set a minimum relevance threshold. Requires `stackone-ai >= 2.4.0`. | ||
|
|
||
| ```python {test="skip"} | ||
| import os | ||
|
|
||
| from pydantic_ai import Agent | ||
| from pydantic_ai.ext.stackone import StackOneToolset | ||
|
|
||
| toolset = StackOneToolset( | ||
| search_query='list and manage employees', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it make sense to do the search ahead of the agent call? Shouldn't it be more that the agent has search tool in the stackone toolset? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Pydantic AI toolsets define their tools at construction time and not sure it support dynamically adding tools during an agent run.. I will double check the source but pretty sure not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not talking about adding tools during an agent run, i'm talking about having search & execute in the agent run. |
||
| account_id=os.getenv('STACKONE_ACCOUNT_ID'), | ||
| api_key=os.getenv('STACKONE_API_KEY'), | ||
| top_k=5, | ||
| search='auto', | ||
| ) | ||
|
|
||
| agent = Agent('google-gla:gemini-3-pro-preview', toolsets=[toolset]) | ||
|
|
||
| result = agent.run_sync('List all employees in the HR system') | ||
| print(result.output) | ||
| ``` | ||
|
|
||
| ## See Also | ||
|
|
||
| - [Function Tools](tools.md) - Basic tool concepts and registration | ||
| - [Toolsets](toolsets.md) - Managing collections of tools | ||
| - [MCP Client](mcp/client.md) - Using MCP servers with Pydantic AI | ||
| - [LangChain Toolsets](toolsets.md#langchain-tools) - Using LangChain toolsets | ||
| - [ACI.dev Toolsets](toolsets.md#aci-tools) - Using ACI.dev toolsets | ||
| - [StackOne Toolsets](toolsets.md#stackone-tools) - Using StackOne toolsets | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Sequence | ||
| from typing import Any, Literal | ||
|
|
||
| from pydantic.json_schema import JsonSchemaValue | ||
|
|
||
| from pydantic_ai.tools import Tool | ||
| from pydantic_ai.toolsets.function import FunctionToolset | ||
|
|
||
| try: | ||
| from stackone_ai import StackOneToolSet | ||
| except ImportError as _import_error: | ||
| raise ImportError('Please install `stackone-ai` to use StackOne tools.') from _import_error | ||
|
|
||
| __all__ = ('tool_from_stackone', 'StackOneToolset') | ||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def _tool_from_stackone_tool(stackone_tool: Any) -> Tool: | ||
| openai_function = stackone_tool.to_openai_function() | ||
| json_schema: JsonSchemaValue = openai_function['function']['parameters'] | ||
|
|
||
| def implementation(**kwargs: Any) -> Any: | ||
| return stackone_tool.execute(kwargs) | ||
|
|
||
| return Tool.from_schema( | ||
| function=implementation, | ||
| name=stackone_tool.name, | ||
| description=stackone_tool.description or '', | ||
| json_schema=json_schema, | ||
| ) | ||
|
|
||
|
|
||
| def tool_from_stackone( | ||
| tool_name: str, | ||
| *, | ||
| account_id: str | None = None, | ||
| api_key: str | None = None, | ||
| base_url: str | None = None, | ||
| ) -> Tool: | ||
| """Creates a Pydantic AI tool proxy from a StackOne tool. | ||
|
|
||
| Args: | ||
| tool_name: The name of the StackOne tool to wrap (e.g., `"bamboohr_list_employees"`). | ||
| account_id: The StackOne account ID. If not provided, uses `STACKONE_ACCOUNT_ID` env var. | ||
| api_key: The StackOne API key. If not provided, uses `STACKONE_API_KEY` env var. | ||
| base_url: Optional custom base URL for the StackOne API. | ||
|
|
||
| Returns: | ||
| A Pydantic AI tool that corresponds to the StackOne tool. | ||
| """ | ||
| stackone_toolset = StackOneToolSet(api_key=api_key, account_id=account_id, base_url=base_url) | ||
| tools = stackone_toolset.fetch_tools(actions=[tool_name]) | ||
| stackone_tool = tools.get_tool(tool_name) | ||
| if stackone_tool is None: | ||
| raise ValueError(f"Tool '{tool_name}' not found in StackOne") | ||
| return _tool_from_stackone_tool(stackone_tool) | ||
|
|
||
|
|
||
| class StackOneToolset(FunctionToolset): | ||
| """A toolset that wraps StackOne tools.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| tools: Sequence[str] | None = None, | ||
| *, | ||
| account_id: str | None = None, | ||
| api_key: str | None = None, | ||
| base_url: str | None = None, | ||
| filter_pattern: str | list[str] | None = None, | ||
| search_query: str | None = None, | ||
| search: Literal['auto', 'semantic', 'local'] | None = None, | ||
| top_k: int | None = None, | ||
| min_similarity: float | None = None, | ||
| id: str | None = None, | ||
| ): | ||
| if search_query is not None and tools is not None: | ||
| raise ValueError("Cannot specify both 'tools' and 'search_query'") | ||
|
|
||
| stackone_toolset = StackOneToolSet(api_key=api_key, account_id=account_id, base_url=base_url) | ||
|
|
||
| if search_query is not None: | ||
| if not hasattr(stackone_toolset, 'search_tools'): | ||
| raise ImportError( | ||
| 'search_query requires stackone-ai >= 2.4.0. Install with `pip install stackone-ai>=2.4.0`' | ||
| ) | ||
| search_kwargs: dict[str, Any] = {} | ||
| if top_k is not None: | ||
| search_kwargs['top_k'] = top_k | ||
| if min_similarity is not None: | ||
| search_kwargs['min_similarity'] = min_similarity | ||
| if search is not None: | ||
| search_kwargs['search'] = search | ||
| fetched_tools = stackone_toolset.search_tools(search_query, **search_kwargs) | ||
| else: | ||
| if tools is not None: | ||
| actions = list(tools) | ||
| else: | ||
| actions = [filter_pattern] if isinstance(filter_pattern, str) else filter_pattern | ||
|
|
||
| fetched_tools = stackone_toolset.fetch_tools(actions=actions) | ||
|
|
||
| pydantic_tools: list[Tool] = [] | ||
| for stackone_tool in fetched_tools: | ||
| pydantic_tools.append(_tool_from_stackone_tool(stackone_tool)) | ||
|
|
||
| super().__init__(pydantic_tools, id=id) | ||
Uh oh!
There was an error while loading. Please reload this page.