Skip to content

Commit 010a275

Browse files
Implement PR sugggestions and use the search and execute tools as standard way
1 parent 9fe1e40 commit 010a275

File tree

7 files changed

+97
-70
lines changed

7 files changed

+97
-70
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,10 @@ tools = toolset.fetch_tools(actions=["hris_*"])
318318
utility_tools = tools.utility_tools()
319319

320320
# Search for relevant tools using natural language
321-
filter_tool = utility_tools.get_tool("tool_search")
322-
results = filter_tool.call(query="manage employees", limit=5)
321+
results = utility_tools.search_tool.call(query="manage employees", limit=5)
323322

324323
# Execute discovered tools dynamically
325-
execute_tool = utility_tools.get_tool("tool_execute")
326-
result = execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
324+
result = utility_tools.execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
327325
```
328326

329327
## Semantic Search

examples/semantic_search_example.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
f) Match results back to the fetched tool definitions
3131
g) Return a Tools collection sorted by relevance score
3232
33+
Tip: when you know which provider the user works with, pass
34+
``connector="calendly"`` to scope the search to a single connector —
35+
this is faster and returns more relevant results.
36+
3337
Key point: only the user's own connectors are searched — no wasted results
3438
from connectors the user doesn't have. When top_k is not specified, the
3539
backend decides how many results to return per connector. If the semantic
@@ -46,11 +50,11 @@
4650
3. utility_tools() — Agent-loop pattern
4751
4852
Creates tool_search and tool_execute utility tools that agents can call
49-
inside an agentic loop. Pass semantic_client=toolset.semantic_client to
50-
enable cloud-based semantic search; without it, local BM25+TF-IDF is
51-
used. When created via utility_tools(), tool_search is automatically
52-
scoped to the user's linked connectors. The agent searches, inspects,
53-
and executes tools dynamically.
53+
inside an agentic loop. Pass search_method="semantic" to enable
54+
cloud-based semantic search; without it, local BM25+TF-IDF is used.
55+
When created via utility_tools(), tool_search is automatically scoped
56+
to the user's linked connectors. The agent searches, inspects, and
57+
executes tools dynamically.
5458
5559
5660
This example is runnable with the following command:
@@ -69,11 +73,14 @@
6973
import logging
7074
import os
7175

72-
from dotenv import load_dotenv
73-
7476
from stackone_ai import StackOneToolSet
7577

76-
load_dotenv()
78+
try:
79+
from dotenv import load_dotenv
80+
81+
load_dotenv()
82+
except ModuleNotFoundError:
83+
pass
7784

7885
# Show SDK warnings (e.g., semantic search fallback to local search)
7986
logging.basicConfig(level=logging.WARNING)
@@ -203,9 +210,9 @@ def example_search_tools_with_connector():
203210
def example_utility_tools_semantic():
204211
"""Using utility tools with semantic search for agent loops.
205212
206-
Pass semantic_client=toolset.semantic_client to utility_tools() to enable
207-
cloud-based semantic search. Without it, utility_tools() uses local
208-
BM25+TF-IDF search instead.
213+
Pass search_method="semantic" to utility_tools() to enable cloud-based
214+
semantic search. Without it, utility_tools() uses local BM25+TF-IDF
215+
search instead.
209216
210217
When created via utility_tools(), tool_search is automatically scoped to
211218
the connectors available in your fetched tools collection.
@@ -223,8 +230,8 @@ def example_utility_tools_semantic():
223230
print()
224231

225232
print("Step 2: Creating utility tools with semantic search enabled...")
226-
print(" Pass semantic_client=toolset.semantic_client to enable semantic search.")
227-
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
233+
print(' Pass search_method="semantic" to enable cloud-based semantic search.')
234+
utility = tools.utility_tools(search_method="semantic")
228235

229236
query = "cancel an event or meeting"
230237
print()

examples/utility_tools_example.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
import os
2020

21-
from dotenv import load_dotenv
22-
2321
from stackone_ai import StackOneToolSet
2422

25-
# Load environment variables
26-
load_dotenv()
23+
try:
24+
from dotenv import load_dotenv
25+
26+
load_dotenv()
27+
except ModuleNotFoundError:
28+
pass
2729

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

stackone_ai/models.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -469,14 +469,19 @@ def get_account_id(self) -> str | None:
469469
class Tools:
470470
"""Container for Tool instances with lookup capabilities"""
471471

472-
def __init__(self, tools: list[StackOneTool]) -> None:
472+
def __init__(
473+
self,
474+
tools: list[StackOneTool],
475+
_semantic_client: SemanticSearchClient | None = None,
476+
) -> None:
473477
"""Initialize Tools container
474478
475479
Args:
476480
tools: List of Tool instances to manage
477481
"""
478482
self.tools = tools
479483
self._tool_map = {tool.name: tool for tool in tools}
484+
self._semantic_client = _semantic_client
480485

481486
def __getitem__(self, index: int) -> StackOneTool:
482487
return self.tools[index]
@@ -559,58 +564,69 @@ def to_langchain(self) -> Sequence[BaseTool]:
559564

560565
def utility_tools(
561566
self,
567+
search_method: str = "bm25",
562568
hybrid_alpha: float | None = None,
563-
semantic_client: SemanticSearchClient | None = None,
564569
) -> UtilityTools:
565570
"""Return utility tools for tool discovery and execution
566571
567572
Utility tools enable dynamic tool discovery and execution based on natural language queries.
568-
By default, uses local hybrid BM25 + TF-IDF search. When a semantic_client is provided,
569-
uses cloud-based semantic search for higher accuracy on natural language queries.
573+
Choose the search method via ``search_method``:
574+
575+
- ``"bm25"`` (default) — local hybrid BM25 + TF-IDF search, no network calls.
576+
- ``"semantic"`` — cloud-based semantic vector search for higher accuracy on
577+
natural language queries. Requires tools fetched via ``StackOneToolSet``.
570578
571579
Args:
580+
search_method: Search backend to use. ``"bm25"`` for local search,
581+
``"semantic"`` for cloud-based semantic search.
572582
hybrid_alpha: Weight for BM25 in hybrid search (0-1). Only used when
573-
semantic_client is not provided. If not provided, uses DEFAULT_HYBRID_ALPHA (0.2),
574-
which gives more weight to BM25 scoring.
575-
semantic_client: Optional SemanticSearchClient instance. Pass
576-
toolset.semantic_client to enable cloud-based semantic search.
583+
search_method is ``"bm25"``. If not provided, uses DEFAULT_HYBRID_ALPHA (0.2).
577584
578585
Returns:
579586
UtilityTools collection with search_tool and execute_tool accessors
580587
588+
Raises:
589+
StackOneError: If ``search_method="semantic"`` but tools were not created
590+
via ``StackOneToolSet`` (no semantic client available).
591+
ValueError: If ``search_method`` is not ``"bm25"`` or ``"semantic"``.
592+
581593
Note:
582594
This feature is in beta and may change in future versions
583595
584596
Example:
585-
# Semantic search (pass semantic_client explicitly)
597+
# Semantic search
586598
toolset = StackOneToolSet()
587599
tools = toolset.fetch_tools()
588-
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
600+
utility = tools.utility_tools(search_method="semantic")
589601
result = utility.search_tool.call(query="onboard new hire")
590602
591-
# Local BM25+TF-IDF search (default, no semantic_client)
603+
# Local BM25+TF-IDF search (default)
592604
utility = tools.utility_tools()
593605
result = utility.search_tool.call(query="onboard new hire")
594606
"""
595607
from stackone_ai.utility_tools import create_tool_execute
596608

597-
if semantic_client is not None:
609+
if search_method == "semantic":
610+
if self._semantic_client is None:
611+
raise StackOneError(
612+
"Semantic search requires tools fetched via StackOneToolSet. "
613+
"Use toolset.fetch_tools() or toolset.search_tools() first."
614+
)
598615
from stackone_ai.utility_tools import create_semantic_tool_search
599616

600617
search_tool = create_semantic_tool_search(
601-
semantic_client, available_connectors=self.get_connectors()
618+
self._semantic_client, available_connectors=self.get_connectors()
602619
)
603-
execute_tool = create_tool_execute(self)
604-
return UtilityTools([search_tool, execute_tool])
620+
elif search_method == "bm25":
621+
from stackone_ai.utility_tools import ToolIndex, create_tool_search
605622

606-
# Default: local BM25+TF-IDF search
607-
from stackone_ai.utility_tools import ToolIndex, create_tool_search
623+
index = ToolIndex(self.tools, hybrid_alpha=hybrid_alpha)
624+
search_tool = create_tool_search(index)
625+
else:
626+
raise ValueError(f"Unknown search_method: {search_method!r}. Use 'bm25' or 'semantic'.")
608627

609-
index = ToolIndex(self.tools, hybrid_alpha=hybrid_alpha)
610-
filter_tool = create_tool_search(index)
611628
execute_tool = create_tool_execute(self)
612-
613-
return UtilityTools([filter_tool, execute_tool])
629+
return UtilityTools([search_tool, execute_tool])
614630

615631

616632
class UtilityTools(Tools):

stackone_ai/toolset.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,14 @@ def search_tools(
344344
available_connectors = all_tools.get_connectors()
345345

346346
if not available_connectors:
347-
return Tools([])
347+
return Tools([], _semantic_client=self._semantic_client)
348348

349349
try:
350350
# Step 2: Determine which connectors to search
351351
if connector:
352352
connectors_to_search = {connector.lower()} & available_connectors
353353
if not connectors_to_search:
354-
return Tools([])
354+
return Tools([], _semantic_client=self._semantic_client)
355355
else:
356356
connectors_to_search = available_connectors
357357

@@ -383,7 +383,7 @@ def _search_one(c: str) -> list[SemanticSearchResult]:
383383
all_results = all_results[:top_k]
384384

385385
if not all_results:
386-
return Tools([])
386+
return Tools([], _semantic_client=self._semantic_client)
387387

388388
# Step 5: Match back to fetched tool definitions
389389
action_names = {_normalize_action_name(r.action_name) for r in all_results}
@@ -393,7 +393,7 @@ def _search_one(c: str) -> list[SemanticSearchResult]:
393393
action_order = {_normalize_action_name(r.action_name): i for i, r in enumerate(all_results)}
394394
matched_tools.sort(key=lambda t: action_order.get(t.name, float("inf")))
395395

396-
return Tools(matched_tools)
396+
return Tools(matched_tools, _semantic_client=self._semantic_client)
397397

398398
except SemanticSearchError as e:
399399
if not fallback_to_local:
@@ -420,7 +420,10 @@ def _search_one(c: str) -> list[SemanticSearchResult]:
420420
for name in matched_names
421421
if name in tool_map and name.split("_")[0].lower() in filter_connectors
422422
]
423-
return Tools(matched_tools[:top_k] if top_k is not None else matched_tools)
423+
return Tools(
424+
matched_tools[:top_k] if top_k is not None else matched_tools,
425+
_semantic_client=self._semantic_client,
426+
)
424427

425428
return all_tools
426429

@@ -626,7 +629,7 @@ def fetch_tools(
626629
if actions:
627630
all_tools = [tool for tool in all_tools if self._filter_by_action(tool.name, actions)]
628631

629-
return Tools(all_tools)
632+
return Tools(all_tools, _semantic_client=self.semantic_client)
630633

631634
except ToolsetError:
632635
raise

tests/test_semantic_search.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,9 @@ def test_utility_tools_semantic_search(self) -> None:
497497
utility = tools.utility_tools()
498498
assert len(utility) == 2 # tool_search + tool_execute
499499

500-
# With semantic search - presence of semantic_client enables it
500+
# With semantic search - set _semantic_client and use search_method="semantic"
501501
mock_client = MagicMock(spec=SemanticSearchClient)
502+
tools._semantic_client = mock_client
502503
with (
503504
patch("stackone_ai.utility_tools.create_semantic_tool_search") as mock_create,
504505
patch("stackone_ai.utility_tools.create_tool_execute") as mock_create_execute,
@@ -509,7 +510,7 @@ def test_utility_tools_semantic_search(self) -> None:
509510
mock_execute_tool.name = "tool_execute"
510511
mock_create.return_value = mock_search_tool
511512
mock_create_execute.return_value = mock_execute_tool
512-
utility = tools.utility_tools(semantic_client=mock_client)
513+
utility = tools.utility_tools(search_method="semantic")
513514
assert len(utility) == 2
514515
# Should pass available connectors from the tools collection
515516
mock_create.assert_called_once_with(mock_client, available_connectors={"test"})

tests/test_utility_tools.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -239,36 +239,36 @@ def test_search_limit_pbt(self, limit: int):
239239
class TestToolSearch:
240240
"""Test the tool_search functionality"""
241241

242-
def test_filter_tool_creation(self, sample_tools):
242+
def test_search_tool_creation(self, sample_tools):
243243
"""Test creating the filter tool"""
244244
index = ToolIndex(sample_tools)
245-
filter_tool = create_tool_search(index)
245+
search_tool = create_tool_search(index)
246246

247-
assert filter_tool.name == "tool_search"
248-
assert "natural language query" in filter_tool.description.lower()
247+
assert search_tool.name == "tool_search"
248+
assert "natural language query" in search_tool.description.lower()
249249

250-
def test_filter_tool_execute_with_json_string(self, sample_tools):
250+
def test_search_tool_execute_with_json_string(self, sample_tools):
251251
"""Test executing the filter tool with JSON string input."""
252252
import json
253253

254254
index = ToolIndex(sample_tools)
255-
filter_tool = create_tool_search(index)
255+
search_tool = create_tool_search(index)
256256

257257
# Execute with JSON string
258258
json_input = json.dumps({"query": "employee", "limit": 2, "minScore": 0.0})
259-
result = filter_tool.execute(json_input)
259+
result = search_tool.execute(json_input)
260260

261261
assert "tools" in result
262262
assert isinstance(result["tools"], list)
263263
assert len(result["tools"]) <= 2
264264

265-
def test_filter_tool_execute(self, sample_tools):
265+
def test_search_tool_execute(self, sample_tools):
266266
"""Test executing the filter tool"""
267267
index = ToolIndex(sample_tools)
268-
filter_tool = create_tool_search(index)
268+
search_tool = create_tool_search(index)
269269

270270
# Execute with a query
271-
result = filter_tool.execute(
271+
result = search_tool.execute(
272272
{
273273
"query": "manage employees",
274274
"limit": 3,
@@ -287,13 +287,13 @@ def test_filter_tool_execute(self, sample_tools):
287287
assert "description" in tool
288288
assert "score" in tool
289289

290-
def test_filter_tool_call(self, sample_tools):
290+
def test_search_tool_call(self, sample_tools):
291291
"""Test calling the filter tool with call method"""
292292
index = ToolIndex(sample_tools)
293-
filter_tool = create_tool_search(index)
293+
search_tool = create_tool_search(index)
294294

295295
# Call with kwargs
296-
result = filter_tool.call(query="candidate", limit=2)
296+
result = search_tool.call(query="candidate", limit=2)
297297

298298
assert "tools" in result
299299
assert len(result["tools"]) <= 2
@@ -377,11 +377,11 @@ def test_utility_tools_functionality(self, tools_collection):
377377
utility_tools = tools_collection.utility_tools()
378378

379379
# Get the filter tool
380-
filter_tool = utility_tools.get_tool("tool_search")
381-
assert filter_tool is not None
380+
search_tool = utility_tools.get_tool("tool_search")
381+
assert search_tool is not None
382382

383383
# Search for tools
384-
result = filter_tool.execute(
384+
result = search_tool.execute(
385385
{
386386
"query": "create employee",
387387
"limit": 1,
@@ -482,13 +482,13 @@ def test_utility_tools_with_custom_alpha(self, sample_tools):
482482
# Create utility tools with custom alpha
483483
utility_tools = tools_collection.utility_tools(hybrid_alpha=0.3)
484484

485-
filter_tool = utility_tools.get_tool("tool_search")
486-
assert filter_tool is not None
485+
search_tool = utility_tools.get_tool("tool_search")
486+
assert search_tool is not None
487487

488488
# Check that description mentions the alpha value
489-
assert "alpha=0.3" in filter_tool.description
489+
assert "alpha=0.3" in search_tool.description
490490

491491
# Test it works
492-
result = filter_tool.execute({"query": "list employees", "limit": 3})
492+
result = search_tool.execute({"query": "list employees", "limit": 3})
493493
assert "tools" in result
494494
assert len(result["tools"]) > 0

0 commit comments

Comments
 (0)