Skip to content

Commit 9e5618c

Browse files
author
Matt Carey
committed
fix: tools
1 parent 95fe4f6 commit 9e5618c

10 files changed

Lines changed: 411 additions & 364 deletions

File tree

examples/available_tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99

10+
# TODO: Add examples
1011
def get_available_tools():
1112
print("Getting available tools")
1213

examples/crewai_integration.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,41 @@
88
```
99
"""
1010

11+
from crewai import Agent, Crew, Task
12+
from stackone_ai import StackOneToolSet
13+
14+
account_id = "45072196112816593343"
15+
employee_id = "c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA"
16+
1117

1218
def crewai_integration():
13-
print("CrewAI integration")
19+
toolset = StackOneToolSet()
20+
tools = toolset.get_tools(
21+
vertical="hris",
22+
account_id=account_id,
23+
)
24+
25+
# CrewAI uses LangChain tools natively
26+
langchain_tools = tools.to_langchain()
27+
28+
agent = Agent(
29+
role="HR Manager",
30+
goal=f"What is the employee with the id {employee_id}?",
31+
backstory="With over 10 years of experience in HR and employee management, "
32+
"you excel at finding patterns in complex datasets.",
33+
llm="gpt-4o-mini",
34+
tools=langchain_tools,
35+
max_iter=2,
36+
)
37+
38+
task = Task(
39+
description="What is the employee with the id c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA?",
40+
agent=agent,
41+
expected_output="A JSON object containing the employee's information",
42+
)
43+
44+
crew = Crew(agents=[agent], tasks=[task])
45+
print(crew.kickoff())
1446

1547

1648
if __name__ == "__main__":

examples/index.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,11 @@ def quickstart():
6262
## Next Steps
6363
6464
Check out some examples:
65+
- [Error Handling](error-handling.md)
66+
- [StackOne Account IDs](stackone_account_ids.md)
67+
- [Available Tools](available_tools.md)
6568
- [OpenAI Integration](openai-integration.md)
69+
- [LangChain Integration](langchain-integration.md)
70+
- [CrewAI Integration](crewai-integration.md)
71+
- [LangGraph Tool Node](langgraph-tool-node.md)
6672
"""

examples/langgraph_tool_node.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99

10+
# TODO: Add examples
1011
def langgraph_tool_node() -> None:
1112
print("LangGraph tool node")
1213

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""StackOne AI SDK"""
22

3-
from .tools import StackOneToolSet
3+
from .toolset import StackOneToolSet
44

55
__all__ = ["StackOneToolSet"]
Lines changed: 25 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import base64
2-
import json
3-
from collections.abc import Callable, Sequence
4-
from typing import Annotated, Any, ClassVar
1+
from collections.abc import Sequence
2+
from typing import Any
53

6-
import requests
7-
from langchain_core.tools import BaseTool as LangChainBaseTool
8-
from pydantic import BaseModel, Field, PrivateAttr
4+
from langchain_core.tools import BaseTool
5+
from pydantic import BaseModel, Field
96

107

118
class ExecuteConfig(BaseModel):
@@ -30,117 +27,23 @@ class ToolDefinition(BaseModel):
3027
execute: ExecuteConfig
3128

3229

33-
class BaseTool(BaseModel):
34-
"""Base Tool model with Pydantic validation"""
30+
class Tool(BaseModel):
31+
"""Base Tool model"""
3532

33+
name: str
3634
description: str
3735
parameters: ToolParameters
3836

39-
# Private attributes in Pydantic v2
40-
_execute_config: ExecuteConfig = PrivateAttr()
41-
_api_key: str = PrivateAttr()
42-
_account_id: str | None = PrivateAttr(default=None)
43-
44-
def __init__(self, **data: Any) -> None:
45-
super().__init__(**data)
46-
execute_config = data.get("_execute_config")
47-
api_key = data.get("_api_key")
48-
49-
if not isinstance(execute_config, ExecuteConfig):
50-
raise ValueError("_execute_config must be an ExecuteConfig instance")
51-
if not isinstance(api_key, str):
52-
raise ValueError("_api_key must be a string")
53-
54-
self._execute_config = execute_config
55-
self._api_key = api_key
56-
self._account_id = data.get("_account_id")
57-
5837
def execute(self, arguments: str | dict | None = None) -> dict[str, Any]:
59-
"""
60-
Execute the tool with the given parameters
61-
62-
Args:
63-
arguments: Either a JSON string or dict of arguments
64-
"""
65-
# Handle both string and dict arguments
66-
if isinstance(arguments, str):
67-
kwargs = json.loads(arguments)
68-
else:
69-
kwargs = arguments or {}
70-
71-
url = self._execute_config.url
72-
body_params = {}
73-
query_params = {}
74-
header_params = {}
75-
76-
# Separate parameters based on their OpenAPI location
77-
if kwargs:
78-
for key, value in kwargs.items():
79-
param_location = self._execute_config.parameter_locations.get(key)
80-
81-
if param_location == "header":
82-
header_params[key] = str(value)
83-
elif param_location == "query":
84-
query_params[key] = value
85-
elif param_location == "path":
86-
url = url.replace(f"{{{key}}}", str(value))
87-
elif param_location == "body":
88-
body_params[key] = value
89-
else:
90-
# Default behavior for backward compatibility
91-
if f"{{{key}}}" in url:
92-
url = url.replace(f"{{{key}}}", str(value))
93-
elif self._execute_config.method.upper() in ["GET", "DELETE"]:
94-
query_params[key] = value
95-
else:
96-
body_params[key] = value
97-
98-
# Create basic auth header with API key as username
99-
auth_string = base64.b64encode(f"{self._api_key}:".encode()).decode()
100-
101-
headers = {
102-
"Authorization": f"Basic {auth_string}",
103-
"User-Agent": "stackone-python/1.0.0",
104-
}
105-
106-
if self._account_id:
107-
headers["x-account-id"] = self._account_id
108-
109-
# Add custom header parameters
110-
headers.update(header_params)
111-
112-
# Add predefined headers last to ensure they take precedence
113-
headers.update(self._execute_config.headers)
114-
115-
request_kwargs: dict[str, Any] = {
116-
"method": self._execute_config.method,
117-
"url": url,
118-
"headers": headers,
119-
}
120-
121-
# Handle request body if we have body parameters
122-
if body_params:
123-
body_type = self._execute_config.body_type or "json"
124-
if body_type == "json":
125-
request_kwargs["json"] = body_params
126-
elif body_type == "form":
127-
request_kwargs["data"] = body_params
128-
129-
if query_params:
130-
request_kwargs["params"] = query_params
131-
132-
response = requests.request(**request_kwargs)
133-
response.raise_for_status()
134-
135-
result: dict[str, Any] = response.json()
136-
return result
38+
"""Execute the tool with the given parameters"""
39+
raise NotImplementedError
13740

13841
def to_openai_function(self) -> dict:
13942
"""Convert this tool to OpenAI's function format"""
14043
return {
14144
"type": "function",
14245
"function": {
143-
"name": self._execute_config.name,
46+
"name": self.name,
14447
"description": self.description,
14548
"parameters": {
14649
"type": self.parameters.type,
@@ -152,46 +55,24 @@ def to_openai_function(self) -> dict:
15255
},
15356
}
15457

155-
@property
156-
def name(self) -> str:
157-
"""Get the tool's name"""
158-
return self._execute_config.name
159-
160-
def set_account_id(self, account_id: str | None) -> None:
161-
"""Set the account ID for this tool.
162-
163-
Args:
164-
account_id: The account ID to use, or None to clear it
165-
"""
166-
self._account_id = account_id
167-
168-
def get_account_id(self) -> str | None:
169-
"""Get the current account ID for this tool."""
170-
return self._account_id
171-
17258

17359
class Tools:
17460
"""Container for Tool instances"""
17561

176-
def __init__(self, tools: list[BaseTool]):
62+
def __init__(self, tools: list[Tool]):
17763
self.tools = tools
178-
# Create a name -> tool mapping for faster lookups
17964
self._tool_map = {tool.name: tool for tool in tools}
18065

181-
def __getitem__(self, index: int) -> BaseTool:
66+
def __getitem__(self, index: int) -> Tool:
18267
return self.tools[index]
18368

18469
def __len__(self) -> int:
18570
return len(self.tools)
18671

187-
def get_tool(self, name: str) -> BaseTool | None:
72+
def get_tool(self, name: str) -> Tool | None:
18873
"""Get a tool by its name"""
18974
return self._tool_map.get(name)
19075

191-
def to_openai(self) -> list[dict]:
192-
"""Convert all tools to OpenAI function format"""
193-
return [tool.to_openai_function() for tool in self.tools]
194-
19576
def set_account_id(self, account_id: str | None) -> None:
19677
"""Set the account ID for all tools in this collection.
19778
@@ -201,81 +82,18 @@ def set_account_id(self, account_id: str | None) -> None:
20182
for tool in self.tools:
20283
tool.set_account_id(account_id)
20384

204-
def to_langchain(self) -> Sequence[LangChainBaseTool]:
205-
"""Convert all tools to LangChain tool format"""
206-
langchain_tools = []
207-
85+
def get_account_id(self) -> str | None:
86+
"""Get the current account ID for this tool."""
20887
for tool in self.tools:
209-
# Create properly annotated schema for the tool
210-
schema_props: dict[str, Field] = {}
211-
annotations: dict[str, Any] = {}
212-
213-
for name, details in tool.parameters.properties.items():
214-
# Convert OpenAPI types to Python types
215-
python_type: type = str # Default to str
216-
if isinstance(details, dict): # Check if details is a dict
217-
type_str = details.get("type", "string")
218-
if type_str == "number":
219-
python_type = float
220-
elif type_str == "integer":
221-
python_type = int
222-
elif type_str == "boolean":
223-
python_type = bool
88+
account_id = tool.get_account_id()
89+
if isinstance(account_id, str): # Type guard to ensure we return str | None
90+
return account_id
91+
return None
22492

225-
# Create Field with description if available
226-
field = Field(description=details.get("description", ""))
227-
else:
228-
# Handle case where details is a string
229-
field = Field(description="")
230-
231-
schema_props[name] = field
232-
annotations[name] = Annotated[python_type, field]
233-
234-
# Create the schema class with proper annotations
235-
schema_class = type(
236-
f"{tool.name.title()}Args",
237-
(BaseModel,),
238-
{
239-
"__annotations__": annotations,
240-
"__module__": __name__,
241-
**schema_props,
242-
},
243-
)
244-
245-
# Create the LangChain tool with proper type annotations
246-
tool_annotations = {
247-
"name": ClassVar[str],
248-
"description": ClassVar[str],
249-
"args_schema": ClassVar[type],
250-
}
251-
252-
def create_run_method(t: BaseTool) -> Callable[..., Any]:
253-
def _run(self: Any, **kwargs: Any) -> Any:
254-
# Convert kwargs to dict for execution
255-
return t.execute(kwargs)
256-
257-
return _run
258-
259-
def create_arun_method(t: BaseTool) -> Callable[..., Any]:
260-
async def _arun(self: Any, **kwargs: Any) -> Any:
261-
# Convert kwargs to dict for execution
262-
return t.execute(kwargs)
263-
264-
return _arun
265-
266-
langchain_tool = type(
267-
f"StackOne{tool.name.title()}Tool",
268-
(LangChainBaseTool,),
269-
{
270-
"__annotations__": tool_annotations,
271-
"__module__": __name__,
272-
"name": tool.name,
273-
"description": tool.description,
274-
"args_schema": schema_class,
275-
"_run": create_run_method(tool),
276-
"_arun": create_arun_method(tool),
277-
},
278-
)
279-
langchain_tools.append(langchain_tool())
93+
def to_openai(self) -> list[dict]:
94+
"""Convert all tools to OpenAI function format"""
95+
return [tool.to_openai_function() for tool in self.tools]
28096

281-
return langchain_tools
97+
def to_langchain(self) -> Sequence[BaseTool]:
98+
"""Convert all tools to LangChain format"""
99+
return [tool.to_langchain() for tool in self.tools]

0 commit comments

Comments
 (0)