Skip to content
Merged
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
2 changes: 1 addition & 1 deletion examples/crewai_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

def crewai_integration():
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["hris_*"], account_ids=[account_id])
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=[account_id])

# CrewAI uses LangChain tools natively
langchain_tools = tools.to_langchain()
Expand Down
10 changes: 5 additions & 5 deletions examples/file_uploads.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Example demonstrating file upload functionality using StackOne.
Shows how to upload an employee document using an HRIS integration.
Shows how to upload an employee document using a BambooHR integration.

This example is runnable with the following command:
```bash
Expand All @@ -24,7 +24,7 @@
"""
# Resume content

This is a sample resume content that will be uploaded using the `hris_upload_employee_document` tool.
This is a sample resume content that will be uploaded using the `bamboohr_upload_employee_document` tool.
"""

resume_content = """
Expand All @@ -46,7 +46,7 @@
"""
# Upload employee document

This function uploads a resume using the `hris_upload_employee_document` tool.
This function uploads a resume using the `bamboohr_upload_employee_document` tool.

"""

Expand All @@ -57,9 +57,9 @@ def upload_employee_document() -> None:
resume_file.write_text(resume_content)

toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["hris_*"], account_ids=[account_id])
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=[account_id])

upload_tool = tools.get_tool("hris_upload_employee_document")
upload_tool = tools.get_tool("bamboohr_upload_employee_document")
assert upload_tool is not None

with open(resume_file, "rb") as f:
Expand Down
8 changes: 4 additions & 4 deletions examples/index.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
StackOne AI SDK provides an AI-friendly interface for accessing various SaaS tools through the StackOne Unified API.
StackOne AI SDK provides an AI-friendly interface for accessing various SaaS tools through the StackOne API.

This SDK is available on [PyPI](https://pypi.org/project/stackone-ai/) for python projects. There is a node version in the works.

Expand Down Expand Up @@ -72,11 +72,11 @@
def quickstart():
toolset = StackOneToolSet()

# Get all HRIS-related tools using MCP-backed fetch_tools()
tools = toolset.fetch_tools(actions=["hris_*"], account_ids=[account_id])
# Get all BambooHR-related tools using MCP-backed fetch_tools()
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=[account_id])

# Use a specific tool
employee_tool = tools.get_tool("hris_list_employees")
employee_tool = tools.get_tool("bamboohr_list_employees")
assert employee_tool is not None

employees = employee_tool.execute()
Expand Down
2 changes: 1 addition & 1 deletion examples/langchain_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

def langchain_integration() -> None:
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["hris_*"], account_ids=[account_id])
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=[account_id])

# Convert to LangChain format and verify
langchain_tools = tools.to_langchain()
Expand Down
12 changes: 6 additions & 6 deletions examples/meta_tools_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def example_meta_tools_basic():
toolset = StackOneToolSet()

# Get all available tools using MCP-backed fetch_tools()
all_tools = toolset.fetch_tools(actions=["hris_*"])
print(f"Total HRIS tools available: {len(all_tools)}")
all_tools = toolset.fetch_tools(actions=["bamboohr_*"])
print(f"Total BambooHR tools available: {len(all_tools)}")

# Get meta tools for dynamic discovery
meta_tools = all_tools.meta_tools()
Expand Down Expand Up @@ -93,9 +93,9 @@ def example_with_openai():
# Initialize StackOne toolset
toolset = StackOneToolSet()

# Get HRIS tools and their meta tools using MCP-backed fetch_tools()
hris_tools = toolset.fetch_tools(actions=["hris_*"])
meta_tools = hris_tools.meta_tools()
# Get BambooHR tools and their meta tools using MCP-backed fetch_tools()
bamboohr_tools = toolset.fetch_tools(actions=["bamboohr_*"])
meta_tools = bamboohr_tools.meta_tools()

# Convert to OpenAI format
openai_tools = meta_tools.to_openai()
Expand Down Expand Up @@ -142,7 +142,7 @@ def example_with_langchain():
toolset = StackOneToolSet()

# Get tools and convert to LangChain format using MCP-backed fetch_tools()
tools = toolset.fetch_tools(actions=["hris_list_*"])
tools = toolset.fetch_tools(actions=["bamboohr_list_*"])
langchain_tools = tools.to_langchain()

# Get meta tools as well
Expand Down
6 changes: 3 additions & 3 deletions examples/openai_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def openai_integration() -> None:
# Filter tools to only the ones we need to avoid context window limits
tools = toolset.fetch_tools(
actions=[
"hris_get_employee",
"hris_list_employee_employments",
"hris_get_employee_employment",
"bamboohr_get_employee",
"bamboohr_list_employee_employments",
"bamboohr_get_employee_employment",
],
account_ids=[account_id],
)
Expand Down
4 changes: 2 additions & 2 deletions examples/stackone_account_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ def stackone_account_ids():
"""
Set the account ID whilst getting tools using fetch_tools().
"""
tools = toolset.fetch_tools(actions=["hris_*"], account_ids=["test_id"])
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=["test_id"])

"""
You can override the account ID on fetched tools.
"""
tools.set_account_id("a_different_id")

employee_tool = tools.get_tool("hris_get_employee")
employee_tool = tools.get_tool("bamboohr_get_employee")
assert employee_tool is not None

"""
Expand Down
2 changes: 1 addition & 1 deletion stackone_ai/integrations/langgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from stackone_ai.integrations.langgraph import to_tool_node

toolset = StackOneToolSet()
tools = toolset.get_tools("hris_*", account_id="...")
tools = toolset.fetch_tools(actions=["bamboohr_*"], account_ids=["..."])
node = to_tool_node(tools) # langgraph.prebuilt.ToolNode
"""

Expand Down
2 changes: 1 addition & 1 deletion tests/test_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def test_live_feedback_submission() -> None:
{
"feedback": f"CI live test feedback {feedback_token}",
"account_id": f"acc-ci-{feedback_token}",
"tool_names": ["hris_list_employees"],
"tool_names": ["hibob_list_employees"],
}
)

Expand Down
30 changes: 15 additions & 15 deletions tests/test_meta_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ def sample_tools():
"""Create sample tools for testing"""
tools = []

# Create HRIS tools
# Create HiBob tools
for action in ["create", "list", "update", "delete"]:
for entity in ["employee", "department", "timeoff"]:
tool_name = f"hris_{action}_{entity}"
tool_name = f"hibob_{action}_{entity}"
execute_config = ExecuteConfig(
name=tool_name,
method="POST" if action in ["create", "update"] else "GET",
url=f"https://api.example.com/hris/{entity}",
url=f"https://api.example.com/hibob/{entity}",
headers={},
)

Expand All @@ -37,21 +37,21 @@ def sample_tools():
)

tool = StackOneTool(
description=f"{action.capitalize()} {entity} in HRIS system",
description=f"{action.capitalize()} {entity} in HiBob system",
parameters=parameters,
_execute_config=execute_config,
_api_key="test_key",
)
tools.append(tool)

# Create ATS tools
# Create BambooHR tools
for action in ["create", "list", "search"]:
for entity in ["candidate", "job", "application"]:
tool_name = f"ats_{action}_{entity}"
tool_name = f"bamboohr_{action}_{entity}"
execute_config = ExecuteConfig(
name=tool_name,
method="POST" if action == "create" else "GET",
url=f"https://api.example.com/ats/{entity}",
url=f"https://api.example.com/bamboohr/{entity}",
headers={},
)

Expand All @@ -64,7 +64,7 @@ def sample_tools():
)

tool = StackOneTool(
description=f"{action.capitalize()} {entity} in ATS system",
description=f"{action.capitalize()} {entity} in BambooHR system",
parameters=parameters,
_execute_config=execute_config,
_api_key="test_key",
Expand Down Expand Up @@ -217,13 +217,13 @@ def test_execute_tool_call(self, tools_collection):
with responses.RequestsMock() as rsps:
rsps.add(
responses.GET,
"https://api.example.com/hris/employee",
"https://api.example.com/hibob/employee",
json={"success": True, "employees": []},
status=200,
)

# Call the meta execute tool
result = execute_tool.call(toolName="hris_list_employee", params={"limit": 10})
result = execute_tool.call(toolName="hibob_list_employee", params={"limit": 10})

assert result is not None
assert "success" in result or "employees" in result
Expand Down Expand Up @@ -288,14 +288,14 @@ def test_hybrid_search_returns_results(self, sample_tools):
"""Test that hybrid search returns meaningful results"""
index = ToolIndex(sample_tools, hybrid_alpha=0.2)
# Use more specific query to ensure we get employee tools
results = index.search("employee hris", limit=10)
results = index.search("employee hibob", limit=10)

assert len(results) > 0
# Should find HRIS employee tools - check in broader result set
# Should find HiBob employee tools - check in broader result set
result_names = [r.name for r in results]
# At least one result should contain "employee" or "hris"
assert any("employee" in name or "hris" in name for name in result_names), (
f"Expected 'employee' or 'hris' in results: {result_names}"
# At least one result should contain "employee" or "hibob"
assert any("employee" in name or "hibob" in name for name in result_names), (
f"Expected 'employee' or 'hibob' in results: {result_names}"
)

def test_hybrid_search_with_different_alphas(self, sample_tools):
Expand Down
18 changes: 9 additions & 9 deletions tests/test_tfidf_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def test_stopword_filtering(self):

def test_underscore_preservation(self):
"""Test that underscores are preserved"""
text = "hris_list_employees"
text = "hibob_list_employees"
tokens = tokenize(text)
assert "hris_list_employees" in tokens
assert "hibob_list_employees" in tokens

def test_empty_string(self):
"""Test tokenization of empty string"""
Expand Down Expand Up @@ -279,17 +279,17 @@ def test_tool_name_matching(self):
"""Test matching tool names"""
index = TfidfIndex()
docs = [
TfidfDocument(id="hris_create_employee", text="create employee hris system"),
TfidfDocument(id="hris_list_employees", text="list employees hris system"),
TfidfDocument(id="ats_create_candidate", text="create candidate ats system"),
TfidfDocument(id="crm_list_contacts", text="list contacts crm system"),
TfidfDocument(id="hibob_create_employee", text="create employee hibob system"),
TfidfDocument(id="hibob_list_employees", text="list employees hibob system"),
TfidfDocument(id="bamboohr_create_candidate", text="create candidate bamboohr system"),
TfidfDocument(id="workday_list_contacts", text="list contacts workday system"),
]
index.build(docs)

# Search for HRIS tools
results = index.search("employee hris", k=5)
# Search for HiBob tools
results = index.search("employee hibob", k=5)
top_ids = [r.id for r in results[:2]]
assert "hris_create_employee" in top_ids or "hris_list_employees" in top_ids
assert "hibob_create_employee" in top_ids or "hibob_list_employees" in top_ids

# Search for create operations
results = index.search("create", k=5)
Expand Down
44 changes: 22 additions & 22 deletions tests/test_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,58 +18,58 @@ def test_filter_by_provider():
toolset = StackOneToolSet(api_key="test_key")

# Test matching providers
assert toolset._filter_by_provider("hris_list_employees", ["hris", "ats"])
assert toolset._filter_by_provider("ats_create_job", ["hris", "ats"])
assert toolset._filter_by_provider("hibob_list_employees", ["hibob", "bamboohr"])
assert toolset._filter_by_provider("bamboohr_create_job", ["hibob", "bamboohr"])

# Test non-matching providers
assert not toolset._filter_by_provider("crm_list_contacts", ["hris", "ats"])
assert not toolset._filter_by_provider("workday_list_contacts", ["hibob", "bamboohr"])

# Test case-insensitive matching
assert toolset._filter_by_provider("HRIS_list_employees", ["hris"])
assert toolset._filter_by_provider("hris_list_employees", ["HRIS"])
assert toolset._filter_by_provider("HIBOB_list_employees", ["hibob"])
assert toolset._filter_by_provider("hibob_list_employees", ["HIBOB"])


def test_filter_by_action():
"""Test action filtering with glob patterns"""
toolset = StackOneToolSet(api_key="test_key")

# Test exact match
assert toolset._filter_by_action("hris_list_employees", ["hris_list_employees"])
assert toolset._filter_by_action("hibob_list_employees", ["hibob_list_employees"])

# Test glob pattern
assert toolset._filter_by_action("hris_list_employees", ["*_list_employees"])
assert toolset._filter_by_action("ats_list_employees", ["*_list_employees"])
assert toolset._filter_by_action("hris_list_employees", ["hris_*"])
assert toolset._filter_by_action("hris_create_employee", ["hris_*"])
assert toolset._filter_by_action("hibob_list_employees", ["*_list_employees"])
assert toolset._filter_by_action("bamboohr_list_employees", ["*_list_employees"])
assert toolset._filter_by_action("hibob_list_employees", ["hibob_*"])
assert toolset._filter_by_action("hibob_create_employee", ["hibob_*"])

# Test non-matching patterns
assert not toolset._filter_by_action("crm_list_contacts", ["*_list_employees"])
assert not toolset._filter_by_action("ats_create_job", ["hris_*"])
assert not toolset._filter_by_action("workday_list_contacts", ["*_list_employees"])
assert not toolset._filter_by_action("bamboohr_create_job", ["hibob_*"])


def test_matches_filter_positive_patterns():
"""Test _matches_filter with positive patterns"""
toolset = StackOneToolSet(api_key="test_key")

# Single pattern
assert toolset._matches_filter("hris_list_employees", "hris_*")
assert toolset._matches_filter("ats_create_job", "ats_*")
assert not toolset._matches_filter("crm_contacts", "hris_*")
assert toolset._matches_filter("hibob_list_employees", "hibob_*")
assert toolset._matches_filter("bamboohr_create_job", "bamboohr_*")
assert not toolset._matches_filter("workday_contacts", "hibob_*")

# Multiple patterns (OR logic)
assert toolset._matches_filter("hris_list_employees", ["hris_*", "ats_*"])
assert toolset._matches_filter("ats_create_job", ["hris_*", "ats_*"])
assert not toolset._matches_filter("crm_contacts", ["hris_*", "ats_*"])
assert toolset._matches_filter("hibob_list_employees", ["hibob_*", "bamboohr_*"])
assert toolset._matches_filter("bamboohr_create_job", ["hibob_*", "bamboohr_*"])
assert not toolset._matches_filter("workday_contacts", ["hibob_*", "bamboohr_*"])


def test_matches_filter_negative_patterns():
"""Test _matches_filter with negative patterns (exclusion)"""
toolset = StackOneToolSet(api_key="test_key")

# Negative pattern
assert not toolset._matches_filter("hris_delete_employee", ["hris_*", "!hris_delete_*"])
assert toolset._matches_filter("hris_list_employees", ["hris_*", "!hris_delete_*"])
assert not toolset._matches_filter("hibob_delete_employee", ["hibob_*", "!hibob_delete_*"])
assert toolset._matches_filter("hibob_list_employees", ["hibob_*", "!hibob_delete_*"])

# Only negative patterns (should match everything not excluded)
assert not toolset._matches_filter("hris_delete_employee", "!hris_delete_*")
assert toolset._matches_filter("hris_list_employees", "!hris_delete_*")
assert not toolset._matches_filter("hibob_delete_employee", "!hibob_delete_*")
assert toolset._matches_filter("hibob_list_employees", "!hibob_delete_*")