|
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. |
7 | 2 |
|
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. |
12 | 5 |
|
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 |
17 | 8 | """ |
18 | 9 |
|
19 | | -import json |
20 | | -import os |
| 10 | +from __future__ import annotations |
21 | 11 |
|
22 | | -from stackone_ai import StackOneToolSet |
| 12 | +import os |
23 | 13 |
|
24 | 14 | try: |
25 | 15 | from dotenv import load_dotenv |
|
28 | 18 | except ModuleNotFoundError: |
29 | 19 | pass |
30 | 20 |
|
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 |
286 | 22 |
|
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 [] |
292 | 25 |
|
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"])) |
302 | 26 |
|
303 | | - print() |
| 27 | +# --- Example 1: get_search_tool() callable --- |
| 28 | +print("=== get_search_tool() callable ===\n") |
304 | 29 |
|
| 30 | +toolset = StackOneToolSet(search={}) |
| 31 | +search_tool = toolset.get_search_tool() |
305 | 32 |
|
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)"}') |
312 | 38 |
|
313 | | - if not os.getenv("STACKONE_API_KEY"): |
314 | | - print("Set STACKONE_API_KEY to run these examples.") |
315 | | - return |
316 | 39 |
|
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") |
321 | 42 |
|
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}) |
327 | 45 |
|
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" |
334 | 47 |
|
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") |
338 | 50 |
|
| 51 | +tools_10 = toolset_10.search_tools(query, account_ids=_account_ids) |
| 52 | +print(f"Constructor top_k=10: got {len(tools_10)} tools") |
339 | 53 |
|
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