Skip to content

Commit 5bf1cc6

Browse files
Update the client to use PR suggested client and use min_similarity from the server
1 parent 71457af commit 5bf1cc6

File tree

8 files changed

+218
-119
lines changed

8 files changed

+218
-119
lines changed

examples/semantic_search_example.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,17 @@ def example_utility_tools_semantic():
226226
print(" Pass semantic_client=toolset.semantic_client to enable semantic search.")
227227
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
228228

229-
search_tool = utility.get_tool("tool_search")
230-
if search_tool:
231-
query = "cancel an event or meeting"
232-
print()
233-
print(f'Step 3: Calling tool_search with query="{query}"...')
234-
print(" (Searches are scoped to your linked connectors)")
235-
print()
236-
result = search_tool.call(query=query, limit=5)
237-
tools_data = result.get("tools", [])
238-
print(f"tool_search returned {len(tools_data)} results:")
239-
for tool_info in tools_data:
240-
print(f" [{tool_info['score']:.2f}] {tool_info['name']}")
241-
print(f" {tool_info['description']}")
229+
query = "cancel an event or meeting"
230+
print()
231+
print(f'Step 3: Calling tool_search with query="{query}"...')
232+
print(" (Searches are scoped to your linked connectors)")
233+
print()
234+
result = utility.search_tool.call(query=query, limit=5)
235+
tools_data = result.get("tools", [])
236+
print(f"tool_search returned {len(tools_data)} results:")
237+
for tool_info in tools_data:
238+
print(f" [{tool_info['score']:.2f}] {tool_info['name']}")
239+
print(f" {tool_info['description']}")
242240

243241
print()
244242

examples/utility_tools_example.py

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
55
Utility tools allow AI agents to search for relevant tools based on natural language queries
66
and execute them dynamically without hardcoding tool names.
7+
8+
Prerequisites:
9+
- STACKONE_API_KEY environment variable set
10+
- STACKONE_ACCOUNT_ID environment variable set (comma-separated for multiple)
11+
- At least one linked account in StackOne (this example uses BambooHR)
12+
13+
This example is runnable with the following command:
14+
```bash
15+
uv run examples/utility_tools_example.py
16+
```
717
"""
818

919
import os
@@ -15,6 +25,9 @@
1525
# Load environment variables
1626
load_dotenv()
1727

28+
# Read account IDs from environment — supports comma-separated values
29+
_account_ids = [aid.strip() for aid in os.getenv("STACKONE_ACCOUNT_ID", "").split(",") if aid.strip()]
30+
1831

1932
def example_utility_tools_basic():
2033
"""Basic example of using utility tools for tool discovery"""
@@ -24,21 +37,22 @@ def example_utility_tools_basic():
2437
toolset = StackOneToolSet()
2538

2639
# Get all available tools using MCP-backed fetch_tools()
27-
all_tools = toolset.fetch_tools(actions=["bamboohr_*"])
28-
print(f"Total BambooHR tools available: {len(all_tools)}")
40+
all_tools = toolset.fetch_tools(account_ids=_account_ids)
41+
print(f"Total tools available: {len(all_tools)}")
42+
43+
if not all_tools:
44+
print("No tools found. Check your linked accounts.")
45+
return
2946

3047
# Get utility tools for dynamic discovery
3148
utility_tools = all_tools.utility_tools()
3249

33-
# Get the filter tool to search for relevant tools
34-
filter_tool = utility_tools.get_tool("tool_search")
35-
if filter_tool:
36-
# Search for employee management tools
37-
result = filter_tool.call(query="manage employees create update list", limit=5, minScore=0.0)
50+
# Search for employee management tools
51+
result = utility_tools.search_tool.call(query="manage employees create update list", limit=5)
3852

39-
print("Found relevant tools:")
40-
for tool in result.get("tools", []):
41-
print(f" - {tool['name']} (score: {tool['score']:.2f}): {tool['description']}")
53+
print("Found relevant tools:")
54+
for tool in result.get("tools", []):
55+
print(f" - {tool['name']} (score: {tool['score']:.2f}): {tool['description']}")
4256

4357
print()
4458

@@ -51,31 +65,31 @@ def example_utility_tools_with_execution():
5165
toolset = StackOneToolSet()
5266

5367
# Get all tools using MCP-backed fetch_tools()
54-
all_tools = toolset.fetch_tools()
68+
all_tools = toolset.fetch_tools(account_ids=_account_ids)
69+
70+
if not all_tools:
71+
print("No tools found. Check your linked accounts.")
72+
return
73+
5574
utility_tools = all_tools.utility_tools()
5675

5776
# Step 1: Search for relevant tools
58-
filter_tool = utility_tools.get_tool("tool_search")
59-
execute_tool = utility_tools.get_tool("tool_execute")
60-
61-
if filter_tool and execute_tool:
62-
# Find tools for listing employees
63-
search_result = filter_tool.call(query="list all employees", limit=1)
64-
65-
tools_found = search_result.get("tools", [])
66-
if tools_found:
67-
best_tool = tools_found[0]
68-
print(f"Best matching tool: {best_tool['name']}")
69-
print(f"Description: {best_tool['description']}")
70-
print(f"Relevance score: {best_tool['score']:.2f}")
71-
72-
# Step 2: Execute the found tool
73-
try:
74-
print(f"\nExecuting {best_tool['name']}...")
75-
result = execute_tool.call(toolName=best_tool["name"], params={"limit": 5})
76-
print(f"Execution result: {result}")
77-
except Exception as e:
78-
print(f"Execution failed (expected in example): {e}")
77+
search_result = utility_tools.search_tool.call(query="list all employees", limit=1)
78+
79+
tools_found = search_result.get("tools", [])
80+
if tools_found:
81+
best_tool = tools_found[0]
82+
print(f"Best matching tool: {best_tool['name']}")
83+
print(f"Description: {best_tool['description']}")
84+
print(f"Relevance score: {best_tool['score']:.2f}")
85+
86+
# Step 2: Execute the found tool
87+
try:
88+
print(f"\nExecuting {best_tool['name']}...")
89+
result = utility_tools.execute_tool.call(toolName=best_tool["name"], params={"limit": 5})
90+
print(f"Execution result: {result}")
91+
except Exception as e:
92+
print(f"Execution failed (expected in example): {e}")
7993

8094
print()
8195

@@ -94,7 +108,7 @@ def example_with_openai():
94108
toolset = StackOneToolSet()
95109

96110
# Get BambooHR tools and their utility tools using MCP-backed fetch_tools()
97-
bamboohr_tools = toolset.fetch_tools(actions=["bamboohr_*"])
111+
bamboohr_tools = toolset.fetch_tools(account_ids=_account_ids, actions=["bamboohr_*"])
98112
utility_tools = bamboohr_tools.utility_tools()
99113

100114
# Convert to OpenAI format
@@ -142,7 +156,7 @@ def example_with_langchain():
142156
toolset = StackOneToolSet()
143157

144158
# Get tools and convert to LangChain format using MCP-backed fetch_tools()
145-
tools = toolset.fetch_tools(actions=["bamboohr_list_*"])
159+
tools = toolset.fetch_tools(account_ids=_account_ids, actions=["bamboohr_list_*"])
146160
langchain_tools = tools.to_langchain()
147161

148162
# Get utility tools as well
@@ -194,6 +208,15 @@ def main():
194208
print("=" * 60)
195209
print()
196210

211+
if not os.getenv("STACKONE_API_KEY"):
212+
print("Set STACKONE_API_KEY to run these examples.")
213+
return
214+
215+
if not _account_ids:
216+
print("Set STACKONE_ACCOUNT_ID to run these examples.")
217+
print("(Comma-separated for multiple accounts)")
218+
return
219+
197220
# Basic examples that work without external APIs
198221
example_utility_tools_basic()
199222
example_utility_tools_with_execution()

stackone_ai/__init__.py

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

3-
from stackone_ai.models import StackOneTool, Tools
3+
from stackone_ai.models import StackOneTool, Tools, UtilityTools
44
from stackone_ai.semantic_search import (
55
SemanticSearchClient,
66
SemanticSearchError,
@@ -13,6 +13,7 @@
1313
"StackOneToolSet",
1414
"StackOneTool",
1515
"Tools",
16+
"UtilityTools",
1617
# Semantic search
1718
"SemanticSearchClient",
1819
"SemanticSearchResult",

stackone_ai/models.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def utility_tools(
561561
self,
562562
hybrid_alpha: float | None = None,
563563
semantic_client: SemanticSearchClient | None = None,
564-
) -> Tools:
564+
) -> UtilityTools:
565565
"""Return utility tools for tool discovery and execution
566566
567567
Utility tools enable dynamic tool discovery and execution based on natural language queries.
@@ -576,7 +576,7 @@ def utility_tools(
576576
toolset.semantic_client to enable cloud-based semantic search.
577577
578578
Returns:
579-
Tools collection containing tool_search and tool_execute
579+
UtilityTools collection with search_tool and execute_tool accessors
580580
581581
Note:
582582
This feature is in beta and may change in future versions
@@ -586,9 +586,11 @@ def utility_tools(
586586
toolset = StackOneToolSet()
587587
tools = toolset.fetch_tools()
588588
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
589+
result = utility.search_tool.call(query="onboard new hire")
589590
590591
# Local BM25+TF-IDF search (default, no semantic_client)
591592
utility = tools.utility_tools()
593+
result = utility.search_tool.call(query="onboard new hire")
592594
"""
593595
from stackone_ai.utility_tools import create_tool_execute
594596

@@ -599,7 +601,7 @@ def utility_tools(
599601
semantic_client, available_connectors=self.get_connectors()
600602
)
601603
execute_tool = create_tool_execute(self)
602-
return Tools([search_tool, execute_tool])
604+
return UtilityTools([search_tool, execute_tool])
603605

604606
# Default: local BM25+TF-IDF search
605607
from stackone_ai.utility_tools import ToolIndex, create_tool_search
@@ -608,4 +610,38 @@ def utility_tools(
608610
filter_tool = create_tool_search(index)
609611
execute_tool = create_tool_execute(self)
610612

611-
return Tools([filter_tool, execute_tool])
613+
return UtilityTools([filter_tool, execute_tool])
614+
615+
616+
class UtilityTools(Tools):
617+
"""Utility tools collection with typed accessors for search and execute tools."""
618+
619+
@property
620+
def search_tool(self) -> StackOneTool:
621+
"""Get the tool_search utility tool.
622+
623+
Returns:
624+
The tool_search tool for discovering relevant tools
625+
626+
Raises:
627+
StackOneError: If tool_search is not found in the collection
628+
"""
629+
tool = self.get_tool("tool_search")
630+
if tool is None:
631+
raise StackOneError("tool_search not found in this UtilityTools collection")
632+
return tool
633+
634+
@property
635+
def execute_tool(self) -> StackOneTool:
636+
"""Get the tool_execute utility tool.
637+
638+
Returns:
639+
The tool_execute tool for running discovered tools
640+
641+
Raises:
642+
StackOneError: If tool_execute is not found in the collection
643+
"""
644+
tool = self.get_tool("tool_execute")
645+
if tool is None:
646+
raise StackOneError("tool_execute not found in this UtilityTools collection")
647+
return tool

stackone_ai/semantic_search.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def search(
133133
connector: str | None = None,
134134
top_k: int | None = None,
135135
project_id: str | None = None,
136+
min_similarity: float | None = None,
136137
) -> SemanticSearchResponse:
137138
"""Search for relevant actions using semantic search.
138139
@@ -142,6 +143,8 @@ def search(
142143
top_k: Maximum number of results to return. If not provided, uses the backend default.
143144
project_id: Optional project scope (e.g., "103/dev-56501"). When provided,
144145
results include both global actions and project-specific actions.
146+
min_similarity: Minimum similarity score threshold (0-1). If not provided,
147+
the server uses its default (currently 0.4).
145148
146149
Returns:
147150
SemanticSearchResponse containing matching actions with similarity scores
@@ -166,6 +169,8 @@ def search(
166169
payload["connector"] = connector
167170
if project_id:
168171
payload["project_id"] = project_id
172+
if min_similarity is not None:
173+
payload["min_similarity"] = min_similarity
169174

170175
try:
171176
response = httpx.post(url, json=payload, headers=headers, timeout=self.timeout)
@@ -184,7 +189,7 @@ def search_action_names(
184189
query: str,
185190
connector: str | None = None,
186191
top_k: int | None = None,
187-
min_score: float = 0.0,
192+
min_similarity: float | None = None,
188193
project_id: str | None = None,
189194
) -> list[str]:
190195
"""Convenience method returning just action names.
@@ -193,7 +198,8 @@ def search_action_names(
193198
query: Natural language query
194199
connector: Optional connector/provider filter
195200
top_k: Maximum number of results. If not provided, uses the backend default.
196-
min_score: Minimum similarity score threshold (0-1)
201+
min_similarity: Minimum similarity score threshold (0-1). If not provided,
202+
the server uses its default.
197203
project_id: Optional project scope for multi-tenant filtering
198204
199205
Returns:
@@ -203,8 +209,8 @@ def search_action_names(
203209
action_names = client.search_action_names(
204210
"create employee",
205211
connector="bamboohr",
206-
min_score=0.5
212+
min_similarity=0.5
207213
)
208214
"""
209-
response = self.search(query, connector, top_k, project_id)
210-
return [r.action_name for r in response.results if r.similarity_score >= min_score]
215+
response = self.search(query, connector, top_k, project_id, min_similarity=min_similarity)
216+
return [r.action_name for r in response.results]

0 commit comments

Comments
 (0)