Skip to content

Commit 649b139

Browse files
Address copilot review about the timeout and trim th search tool example
1 parent 8454734 commit 649b139

2 files changed

Lines changed: 36 additions & 320 deletions

File tree

examples/search_tool_example.py

Lines changed: 31 additions & 316 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
#!/usr/bin/env python
2-
"""
3-
Example demonstrating dynamic tool discovery using search_tool.
4-
5-
The search tool allows AI agents to discover relevant tools based on natural language
6-
queries without hardcoding tool names.
1+
"""Search tool patterns: callable wrapper and config overrides.
72
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)
3+
For semantic search basics, see semantic_search_example.py.
4+
For full agent execution, see agent_tool_search.py.
125
13-
This example is runnable with the following command:
14-
```bash
15-
uv run examples/search_tool_example.py
16-
```
6+
Run with:
7+
uv run python examples/search_tool_example.py
178
"""
189

19-
import json
20-
import os
10+
from __future__ import annotations
2111

22-
from stackone_ai import StackOneToolSet
12+
import os
2313

2414
try:
2515
from dotenv import load_dotenv
@@ -28,314 +18,39 @@
2818
except ModuleNotFoundError:
2919
pass
3020

31-
# Read account IDs from environment — supports comma-separated values
32-
_account_ids = [aid.strip() for aid in os.getenv("STACKONE_ACCOUNT_ID", "").split(",") if aid.strip()]
33-
34-
35-
def example_search_tool_basic():
36-
"""Basic example of using the search tool for tool discovery"""
37-
print("Example 1: Dynamic tool discovery\n")
38-
39-
# Initialize StackOne toolset
40-
toolset = StackOneToolSet(search={})
41-
42-
# Get all available tools using MCP-backed fetch_tools()
43-
all_tools = toolset.fetch_tools(account_ids=_account_ids)
44-
print(f"Total tools available: {len(all_tools)}")
45-
46-
if not all_tools:
47-
print("No tools found. Check your linked accounts.")
48-
return
49-
50-
# Get a search tool for dynamic discovery
51-
search_tool = toolset.get_search_tool()
52-
53-
# Search for employee management tools — returns a Tools collection
54-
tools = search_tool("manage employees create update list", top_k=5, account_ids=_account_ids)
55-
56-
print(f"Found {len(tools)} relevant tools:")
57-
for tool in tools:
58-
print(f" - {tool.name}: {tool.description}")
59-
60-
print()
61-
62-
63-
def example_search_modes():
64-
"""Comparing semantic vs local search modes.
65-
66-
Search config can be set at the constructor level or overridden per call:
67-
- Constructor: StackOneToolSet(search={"method": "semantic"})
68-
- Per-call: toolset.search_tools(query, search="local")
69-
70-
The search method controls which backend search_tools() uses:
71-
- "semantic": cloud-based semantic vector search (higher accuracy for natural language)
72-
- "local": local BM25+TF-IDF hybrid search (no network call to semantic API)
73-
- "auto" (default): tries semantic first, falls back to local on failure
74-
"""
75-
print("Example 2: Semantic vs local search modes\n")
76-
77-
query = "manage employee time off"
78-
79-
# Constructor-level config — semantic search as the default for this toolset
80-
print('Constructor config: StackOneToolSet(search={"method": "semantic"})')
81-
toolset_semantic = StackOneToolSet(search={"method": "semantic"})
82-
try:
83-
tools_semantic = toolset_semantic.search_tools(query, account_ids=_account_ids, top_k=5)
84-
print(f" Found {len(tools_semantic)} tools:")
85-
for tool in tools_semantic:
86-
print(f" - {tool.name}")
87-
except Exception as e:
88-
print(f" Semantic search unavailable: {e}")
89-
print()
90-
91-
# Constructor-level config — local search (no network call to semantic API)
92-
print('Constructor config: StackOneToolSet(search={"method": "local"})')
93-
toolset_local = StackOneToolSet(search={"method": "local"})
94-
tools_local = toolset_local.search_tools(query, account_ids=_account_ids, top_k=5)
95-
print(f" Found {len(tools_local)} tools:")
96-
for tool in tools_local:
97-
print(f" - {tool.name}")
98-
print()
99-
100-
# Per-call override — constructor defaults can be overridden on each call
101-
print("Per-call override: constructor uses semantic, but this call uses local")
102-
tools_override = toolset_semantic.search_tools(query, account_ids=_account_ids, top_k=5, search="local")
103-
print(f" Found {len(tools_override)} tools:")
104-
for tool in tools_override:
105-
print(f" - {tool.name}")
106-
print()
107-
108-
# Auto (default) — tries semantic, falls back to local
109-
print('Default: StackOneToolSet() uses search="auto" (semantic with local fallback)')
110-
toolset_auto = StackOneToolSet(search={})
111-
tools_auto = toolset_auto.search_tools(query, account_ids=_account_ids, top_k=5)
112-
print(f" Found {len(tools_auto)} tools:")
113-
for tool in tools_auto:
114-
print(f" - {tool.name}")
115-
print()
116-
117-
118-
def example_top_k_config():
119-
"""Configuring top_k at the constructor level vs per-call.
120-
121-
Constructor-level top_k applies to all search_tools() and search_action_names()
122-
calls. Per-call top_k overrides the constructor default for that single call.
123-
"""
124-
print("Example 3: top_k at constructor vs per-call\n")
125-
126-
# Constructor-level top_k — all calls default to returning 3 results
127-
toolset = StackOneToolSet(search={"top_k": 3})
128-
129-
query = "manage employee records"
130-
print(f'Constructor top_k=3: searching for "{query}"')
131-
tools_default = toolset.search_tools(query, account_ids=_account_ids)
132-
print(f" Got {len(tools_default)} tools (constructor default)")
133-
for tool in tools_default:
134-
print(f" - {tool.name}")
135-
print()
136-
137-
# Per-call override — this single call returns up to 10 results
138-
print("Per-call top_k=10: overriding constructor default")
139-
tools_override = toolset.search_tools(query, account_ids=_account_ids, top_k=10)
140-
print(f" Got {len(tools_override)} tools (per-call override)")
141-
for tool in tools_override:
142-
print(f" - {tool.name}")
143-
print()
144-
145-
146-
def example_search_tool_with_execution():
147-
"""Example of discovering and executing tools dynamically"""
148-
print("Example 4: Dynamic tool execution\n")
149-
150-
try:
151-
from openai import OpenAI
152-
except ImportError:
153-
print("OpenAI not installed: pip install openai")
154-
print()
155-
return
156-
157-
if not os.getenv("OPENAI_API_KEY"):
158-
print("Skipped: Set OPENAI_API_KEY to run this example.")
159-
print()
160-
return
161-
162-
toolset = StackOneToolSet(search={})
163-
164-
# Step 1: Search for relevant tools
165-
search_tool = toolset.get_search_tool()
166-
tools = search_tool("list all employees", top_k=3, account_ids=_account_ids)
167-
168-
if not tools:
169-
print("No matching tools found.")
170-
print()
171-
return
172-
173-
print(f"Found {len(tools)} tools:")
174-
for t in tools:
175-
print(f" - {t.name}")
176-
177-
# Step 2: Let the LLM pick the right tool and params
178-
openai_tools = tools.to_openai()
179-
client = OpenAI()
180-
messages: list[dict] = [
181-
{"role": "user", "content": "List all employees. Use the available tools."},
182-
]
183-
184-
for _step in range(5):
185-
response = client.chat.completions.create(model="gpt-5.4", messages=messages, tools=openai_tools)
186-
choice = response.choices[0]
187-
188-
if not choice.message.tool_calls:
189-
print(f"\nAnswer: {choice.message.content}")
190-
break
191-
192-
messages.append(choice.message.model_dump(exclude_none=True))
193-
for tc in choice.message.tool_calls:
194-
print(f" -> {tc.function.name}({tc.function.arguments[:80]})")
195-
tool = tools.get_tool(tc.function.name)
196-
if tool:
197-
try:
198-
result = tool.execute(json.loads(tc.function.arguments))
199-
except Exception as e:
200-
result = {"error": str(e)}
201-
messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)})
202-
203-
print()
204-
205-
206-
def example_with_openai():
207-
"""Example of using search tool with OpenAI"""
208-
print("Example 5: Using search tool with OpenAI\n")
209-
210-
try:
211-
from openai import OpenAI
212-
213-
# Initialize OpenAI client
214-
client = OpenAI()
215-
216-
# Initialize StackOne toolset
217-
toolset = StackOneToolSet(search={})
218-
219-
# Search for BambooHR employee tools
220-
tools = toolset.search_tools("manage employees", account_ids=_account_ids, top_k=5)
221-
222-
# Convert to OpenAI format
223-
openai_tools = tools.to_openai()
224-
225-
# Create a chat completion with discovered tools
226-
response = client.chat.completions.create(
227-
model="gpt-5.4",
228-
messages=[
229-
{
230-
"role": "system",
231-
"content": "You are an HR assistant with access to employee management tools.",
232-
},
233-
{"role": "user", "content": "Can you help me find tools for managing employee records?"},
234-
],
235-
tools=openai_tools,
236-
tool_choice="auto",
237-
)
238-
239-
print("OpenAI Response:", response.choices[0].message.content)
240-
241-
if response.choices[0].message.tool_calls:
242-
print("\nTool calls made:")
243-
for tool_call in response.choices[0].message.tool_calls:
244-
print(f" - {tool_call.function.name}")
245-
246-
except ImportError:
247-
print("OpenAI library not installed. Install with: pip install openai")
248-
except Exception as e:
249-
print(f"OpenAI example failed: {e}")
250-
251-
print()
252-
253-
254-
def example_with_langchain():
255-
"""Example of using tools with LangChain"""
256-
print("Example 6: Using tools with LangChain\n")
257-
258-
try:
259-
from langchain_core.messages import HumanMessage, ToolMessage
260-
from langchain_openai import ChatOpenAI
261-
except ImportError as e:
262-
print(f"LangChain dependencies not installed: {e}")
263-
print("Install with: pip install langchain-openai")
264-
print()
265-
return
266-
267-
if not os.getenv("OPENAI_API_KEY"):
268-
print("Skipped: Set OPENAI_API_KEY to run this example.")
269-
print()
270-
return
271-
272-
toolset = StackOneToolSet(search={})
273-
274-
# Search for tools and convert to LangChain format
275-
tools = toolset.search_tools("list employees", account_ids=_account_ids, top_k=5)
276-
langchain_tools = list(tools.to_langchain())
277-
278-
print(f"Available tools: {len(langchain_tools)}")
279-
for tool in langchain_tools:
280-
print(f" - {tool.name}")
281-
282-
# Bind tools to model and run
283-
model = ChatOpenAI(model="gpt-5.4").bind_tools(langchain_tools)
284-
tools_by_name = {t.name: t for t in langchain_tools}
285-
messages = [HumanMessage(content="What employee tools do I have access to?")]
21+
from stackone_ai import StackOneToolSet
28622

287-
for _ in range(5):
288-
response = model.invoke(messages)
289-
if not response.tool_calls:
290-
print(f"\nAnswer: {response.content}")
291-
break
23+
account_id = os.getenv("STACKONE_ACCOUNT_ID", "")
24+
_account_ids = [a.strip() for a in account_id.split(",") if a.strip()] if account_id else []
29225

293-
messages.append(response)
294-
for tc in response.tool_calls:
295-
print(f" -> {tc['name']}({json.dumps(tc['args'])[:80]})")
296-
tool = tools_by_name[tc["name"]]
297-
try:
298-
result = tool.invoke(tc["args"])
299-
except Exception as e:
300-
result = {"error": str(e)}
301-
messages.append(ToolMessage(content=json.dumps(result), tool_call_id=tc["id"]))
30226

303-
print()
27+
# --- Example 1: get_search_tool() callable ---
28+
print("=== get_search_tool() callable ===\n")
30429

30+
toolset = StackOneToolSet(search={})
31+
search_tool = toolset.get_search_tool()
30532

306-
def main():
307-
"""Run all examples"""
308-
print("=" * 60)
309-
print("StackOne AI SDK - Search Tool Examples")
310-
print("=" * 60)
311-
print()
33+
queries = ["cancel an event", "list employees", "send a message"]
34+
for query in queries:
35+
tools = search_tool(query, top_k=3, account_ids=_account_ids)
36+
names = [t.name for t in tools]
37+
print(f' "{query}" -> {", ".join(names) or "(none)"}')
31238

313-
if not os.getenv("STACKONE_API_KEY"):
314-
print("Set STACKONE_API_KEY to run these examples.")
315-
return
31639

317-
if not _account_ids:
318-
print("Set STACKONE_ACCOUNT_ID to run these examples.")
319-
print("(Comma-separated for multiple accounts)")
320-
return
40+
# --- Example 2: Constructor top_k vs per-call override ---
41+
print("\n=== Constructor top_k vs per-call override ===\n")
32142

322-
# Basic examples that work without external APIs
323-
example_search_tool_basic()
324-
example_search_modes()
325-
example_top_k_config()
326-
example_search_tool_with_execution()
43+
toolset_3 = StackOneToolSet(search={"top_k": 3})
44+
toolset_10 = StackOneToolSet(search={"top_k": 10})
32745

328-
# Examples that require OpenAI API
329-
if os.getenv("OPENAI_API_KEY"):
330-
example_with_openai()
331-
example_with_langchain()
332-
else:
333-
print("Set OPENAI_API_KEY to run OpenAI and LangChain examples\n")
46+
query = "manage employee records"
33447

335-
print("=" * 60)
336-
print("Examples completed!")
337-
print("=" * 60)
48+
tools_3 = toolset_3.search_tools(query, account_ids=_account_ids)
49+
print(f"Constructor top_k=3: got {len(tools_3)} tools")
33850

51+
tools_10 = toolset_10.search_tools(query, account_ids=_account_ids)
52+
print(f"Constructor top_k=10: got {len(tools_10)} tools")
33953

340-
if __name__ == "__main__":
341-
main()
54+
# Per-call override: constructor says 3 but this call says 10
55+
tools_override = toolset_3.search_tools(query, top_k=10, account_ids=_account_ids)
56+
print(f"Per-call top_k=10 (overrides constructor 3): got {len(tools_override)} tools")

0 commit comments

Comments
 (0)