|
1 | | -#!/usr/bin/env python |
2 | | -""" |
3 | | -Example demonstrating dynamic tool discovery using search_tool. |
| 1 | +"""Search tool patterns: callable wrapper and config overrides. |
4 | 2 |
|
5 | | -The search tool allows AI agents to discover relevant tools based on natural language |
6 | | -queries without hardcoding tool names. |
| 3 | +For semantic search basics, see semantic_search_example.py. |
| 4 | +For full agent execution, see agent_tool_search.py. |
7 | 5 |
|
8 | 6 | 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) |
| 7 | + - STACKONE_API_KEY environment variable |
| 8 | + - STACKONE_ACCOUNT_ID environment variable |
12 | 9 |
|
13 | | -This example is runnable with the following command: |
14 | | -```bash |
15 | | -uv run examples/search_tool_example.py |
16 | | -``` |
| 10 | +Run with: |
| 11 | + uv run python examples/search_tool_example.py |
17 | 12 | """ |
18 | 13 |
|
19 | | -import os |
| 14 | +from __future__ import annotations |
20 | 15 |
|
21 | | -from stackone_ai import StackOneToolSet |
| 16 | +import os |
22 | 17 |
|
23 | 18 | try: |
24 | 19 | from dotenv import load_dotenv |
|
27 | 22 | except ModuleNotFoundError: |
28 | 23 | pass |
29 | 24 |
|
30 | | -# Read account IDs from environment — supports comma-separated values |
31 | | -_account_ids = [aid.strip() for aid in os.getenv("STACKONE_ACCOUNT_ID", "").split(",") if aid.strip()] |
32 | | - |
33 | | - |
34 | | -def example_search_tool_basic(): |
35 | | - """Basic example of using the search tool for tool discovery""" |
36 | | - print("Example 1: Dynamic tool discovery\n") |
| 25 | +from stackone_ai import StackOneToolSet |
37 | 26 |
|
38 | | - # Initialize StackOne toolset |
39 | | - toolset = StackOneToolSet() |
40 | 27 |
|
41 | | - # Get all available tools using MCP-backed fetch_tools() |
42 | | - all_tools = toolset.fetch_tools(account_ids=_account_ids) |
43 | | - print(f"Total tools available: {len(all_tools)}") |
| 28 | +def main() -> None: |
| 29 | + api_key = os.getenv("STACKONE_API_KEY") |
| 30 | + account_id = os.getenv("STACKONE_ACCOUNT_ID") |
44 | 31 |
|
45 | | - if not all_tools: |
46 | | - print("No tools found. Check your linked accounts.") |
| 32 | + if not api_key: |
| 33 | + print("Set STACKONE_API_KEY to run this example.") |
47 | 34 | return |
48 | | - |
49 | | - # Get a search tool for dynamic discovery |
50 | | - search_tool = toolset.get_search_tool() |
51 | | - |
52 | | - # Search for employee management tools — returns a Tools collection |
53 | | - tools = search_tool("manage employees create update list", top_k=5, account_ids=_account_ids) |
54 | | - |
55 | | - print(f"Found {len(tools)} relevant tools:") |
56 | | - for tool in tools: |
57 | | - print(f" - {tool.name}: {tool.description}") |
58 | | - |
59 | | - print() |
60 | | - |
61 | | - |
62 | | -def example_search_modes(): |
63 | | - """Comparing semantic vs local search modes. |
64 | | -
|
65 | | - Search config can be set at the constructor level or overridden per call: |
66 | | - - Constructor: StackOneToolSet(search={"method": "semantic"}) |
67 | | - - Per-call: toolset.search_tools(query, search="local") |
68 | | -
|
69 | | - The search method controls which backend search_tools() uses: |
70 | | - - "semantic": cloud-based semantic vector search (higher accuracy for natural language) |
71 | | - - "local": local BM25+TF-IDF hybrid search (no network call to semantic API) |
72 | | - - "auto" (default): tries semantic first, falls back to local on failure |
73 | | - """ |
74 | | - print("Example 2: Semantic vs local search modes\n") |
75 | | - |
76 | | - query = "manage employee time off" |
77 | | - |
78 | | - # Constructor-level config — semantic search as the default for this toolset |
79 | | - print('Constructor config: StackOneToolSet(search={"method": "semantic"})') |
80 | | - toolset_semantic = StackOneToolSet(search={"method": "semantic"}) |
81 | | - try: |
82 | | - tools_semantic = toolset_semantic.search_tools(query, account_ids=_account_ids, top_k=5) |
83 | | - print(f" Found {len(tools_semantic)} tools:") |
84 | | - for tool in tools_semantic: |
85 | | - print(f" - {tool.name}") |
86 | | - except Exception as e: |
87 | | - print(f" Semantic search unavailable: {e}") |
88 | | - print() |
89 | | - |
90 | | - # Constructor-level config — local search (no network call to semantic API) |
91 | | - print('Constructor config: StackOneToolSet(search={"method": "local"})') |
92 | | - toolset_local = StackOneToolSet(search={"method": "local"}) |
93 | | - tools_local = toolset_local.search_tools(query, account_ids=_account_ids, top_k=5) |
94 | | - print(f" Found {len(tools_local)} tools:") |
95 | | - for tool in tools_local: |
96 | | - print(f" - {tool.name}") |
97 | | - print() |
98 | | - |
99 | | - # Per-call override — constructor defaults can be overridden on each call |
100 | | - print("Per-call override: constructor uses semantic, but this call uses local") |
101 | | - tools_override = toolset_semantic.search_tools(query, account_ids=_account_ids, top_k=5, search="local") |
102 | | - print(f" Found {len(tools_override)} tools:") |
103 | | - for tool in tools_override: |
104 | | - print(f" - {tool.name}") |
105 | | - print() |
106 | | - |
107 | | - # Auto (default) — tries semantic, falls back to local |
108 | | - print('Default: StackOneToolSet() uses search="auto" (semantic with local fallback)') |
109 | | - toolset_auto = StackOneToolSet() |
110 | | - tools_auto = toolset_auto.search_tools(query, account_ids=_account_ids, top_k=5) |
111 | | - print(f" Found {len(tools_auto)} tools:") |
112 | | - for tool in tools_auto: |
113 | | - print(f" - {tool.name}") |
114 | | - print() |
115 | | - |
116 | | - |
117 | | -def example_top_k_config(): |
118 | | - """Configuring top_k at the constructor level vs per-call. |
119 | | -
|
120 | | - Constructor-level top_k applies to all search_tools() and search_action_names() |
121 | | - calls. Per-call top_k overrides the constructor default for that single call. |
122 | | - """ |
123 | | - print("Example 3: top_k at constructor vs per-call\n") |
124 | | - |
125 | | - # Constructor-level top_k — all calls default to returning 3 results |
126 | | - toolset = StackOneToolSet(search={"top_k": 3}) |
127 | | - |
128 | | - query = "manage employee records" |
129 | | - print(f'Constructor top_k=3: searching for "{query}"') |
130 | | - tools_default = toolset.search_tools(query, account_ids=_account_ids) |
131 | | - print(f" Got {len(tools_default)} tools (constructor default)") |
132 | | - for tool in tools_default: |
133 | | - print(f" - {tool.name}") |
134 | | - print() |
135 | | - |
136 | | - # Per-call override — this single call returns up to 10 results |
137 | | - print("Per-call top_k=10: overriding constructor default") |
138 | | - tools_override = toolset.search_tools(query, account_ids=_account_ids, top_k=10) |
139 | | - print(f" Got {len(tools_override)} tools (per-call override)") |
140 | | - for tool in tools_override: |
141 | | - print(f" - {tool.name}") |
142 | | - print() |
143 | | - |
144 | | - |
145 | | -def example_search_tool_with_execution(): |
146 | | - """Example of discovering and executing tools dynamically""" |
147 | | - print("Example 4: Dynamic tool execution\n") |
148 | | - |
149 | | - # Initialize toolset |
150 | | - toolset = StackOneToolSet() |
151 | | - |
152 | | - # Get all tools using MCP-backed fetch_tools() |
153 | | - all_tools = toolset.fetch_tools(account_ids=_account_ids) |
154 | | - |
155 | | - if not all_tools: |
156 | | - print("No tools found. Check your linked accounts.") |
| 35 | + if not account_id: |
| 36 | + print("Set STACKONE_ACCOUNT_ID to run this example.") |
157 | 37 | return |
158 | 38 |
|
159 | | - search_tool = toolset.get_search_tool() |
160 | | - |
161 | | - # Step 1: Search for relevant tools |
162 | | - tools = search_tool("list all employees", top_k=1, account_ids=_account_ids) |
163 | | - |
164 | | - if tools: |
165 | | - best_tool = tools[0] |
166 | | - print(f"Best matching tool: {best_tool.name}") |
167 | | - print(f"Description: {best_tool.description}") |
168 | | - |
169 | | - # Step 2: Execute the found tool directly |
170 | | - try: |
171 | | - print(f"\nExecuting {best_tool.name}...") |
172 | | - result = best_tool(limit=5) |
173 | | - print(f"Execution result: {result}") |
174 | | - except Exception as e: |
175 | | - print(f"Execution failed (expected in example): {e}") |
176 | | - |
177 | | - print() |
178 | | - |
179 | | - |
180 | | -def example_with_openai(): |
181 | | - """Example of using search tool with OpenAI""" |
182 | | - print("Example 5: Using search tool with OpenAI\n") |
183 | | - |
184 | | - try: |
185 | | - from openai import OpenAI |
186 | | - |
187 | | - # Initialize OpenAI client |
188 | | - client = OpenAI() |
189 | | - |
190 | | - # Initialize StackOne toolset |
191 | | - toolset = StackOneToolSet() |
192 | | - |
193 | | - # Search for BambooHR employee tools |
194 | | - tools = toolset.search_tools("manage employees", account_ids=_account_ids, top_k=5) |
195 | | - |
196 | | - # Convert to OpenAI format |
197 | | - openai_tools = tools.to_openai() |
198 | | - |
199 | | - # Create a chat completion with discovered tools |
200 | | - response = client.chat.completions.create( |
201 | | - model="gpt-5.4", |
202 | | - messages=[ |
203 | | - { |
204 | | - "role": "system", |
205 | | - "content": "You are an HR assistant with access to employee management tools.", |
206 | | - }, |
207 | | - {"role": "user", "content": "Can you help me find tools for managing employee records?"}, |
208 | | - ], |
209 | | - tools=openai_tools, |
210 | | - tool_choice="auto", |
211 | | - ) |
212 | | - |
213 | | - print("OpenAI Response:", response.choices[0].message.content) |
214 | | - |
215 | | - if response.choices[0].message.tool_calls: |
216 | | - print("\nTool calls made:") |
217 | | - for tool_call in response.choices[0].message.tool_calls: |
218 | | - print(f" - {tool_call.function.name}") |
219 | | - |
220 | | - except ImportError: |
221 | | - print("OpenAI library not installed. Install with: pip install openai") |
222 | | - except Exception as e: |
223 | | - print(f"OpenAI example failed: {e}") |
224 | | - |
225 | | - print() |
226 | | - |
227 | | - |
228 | | -def example_with_langchain(): |
229 | | - """Example of using tools with LangChain""" |
230 | | - print("Example 6: Using tools with LangChain\n") |
231 | | - |
232 | | - try: |
233 | | - from langchain.agents import AgentExecutor, create_tool_calling_agent |
234 | | - from langchain_core.prompts import ChatPromptTemplate |
235 | | - from langchain_openai import ChatOpenAI |
236 | | - |
237 | | - # Initialize StackOne toolset |
238 | | - toolset = StackOneToolSet() |
239 | | - |
240 | | - # Get tools and convert to LangChain format using MCP-backed fetch_tools() |
241 | | - tools = toolset.search_tools("list employees", account_ids=_account_ids, top_k=5) |
242 | | - langchain_tools = list(tools.to_langchain()) |
243 | | - |
244 | | - print(f"Available tools for LangChain: {len(langchain_tools)}") |
245 | | - for tool in langchain_tools: |
246 | | - print(f" - {tool.name}: {tool.description}") |
247 | | - |
248 | | - # Create LangChain agent |
249 | | - llm = ChatOpenAI(model="gpt-5.4", temperature=0) |
250 | | - |
251 | | - prompt = ChatPromptTemplate.from_messages( |
252 | | - [ |
253 | | - ( |
254 | | - "system", |
255 | | - "You are an HR assistant. Use the available tools to help the user.", |
256 | | - ), |
257 | | - ("human", "{input}"), |
258 | | - ("placeholder", "{agent_scratchpad}"), |
259 | | - ] |
260 | | - ) |
261 | | - |
262 | | - agent = create_tool_calling_agent(llm, langchain_tools, prompt) |
263 | | - agent_executor = AgentExecutor(agent=agent, tools=langchain_tools, verbose=True) |
264 | | - |
265 | | - # Run the agent |
266 | | - result = agent_executor.invoke({"input": "Find tools that can list employee data"}) |
267 | | - |
268 | | - print(f"\nAgent result: {result['output']}") |
269 | | - |
270 | | - except ImportError as e: |
271 | | - print(f"LangChain dependencies not installed: {e}") |
272 | | - print("Install with: pip install langchain-openai") |
273 | | - except Exception as e: |
274 | | - print(f"LangChain example failed: {e}") |
275 | | - |
276 | | - print() |
| 39 | + # --- Example 1: get_search_tool() callable --- |
| 40 | + print("=== get_search_tool() callable ===\n") |
277 | 41 |
|
| 42 | + toolset = StackOneToolSet(api_key=api_key, account_id=account_id, search={}) |
| 43 | + search_tool = toolset.get_search_tool() |
278 | 44 |
|
279 | | -def main(): |
280 | | - """Run all examples""" |
281 | | - print("=" * 60) |
282 | | - print("StackOne AI SDK - Search Tool Examples") |
283 | | - print("=" * 60) |
284 | | - print() |
| 45 | + queries = ["cancel an event", "list employees", "send a message"] |
| 46 | + for query in queries: |
| 47 | + tools = search_tool(query, top_k=3) |
| 48 | + names = [t.name for t in tools] |
| 49 | + print(f' "{query}" -> {", ".join(names) or "(none)"}') |
285 | 50 |
|
286 | | - if not os.getenv("STACKONE_API_KEY"): |
287 | | - print("Set STACKONE_API_KEY to run these examples.") |
288 | | - return |
| 51 | + # --- Example 2: Constructor top_k vs per-call override --- |
| 52 | + print("\n=== Constructor top_k vs per-call override ===\n") |
289 | 53 |
|
290 | | - if not _account_ids: |
291 | | - print("Set STACKONE_ACCOUNT_ID to run these examples.") |
292 | | - print("(Comma-separated for multiple accounts)") |
293 | | - return |
| 54 | + toolset_3 = StackOneToolSet(api_key=api_key, account_id=account_id, search={"top_k": 3}) |
294 | 55 |
|
295 | | - # Basic examples that work without external APIs |
296 | | - example_search_tool_basic() |
297 | | - example_search_modes() |
298 | | - example_top_k_config() |
299 | | - example_search_tool_with_execution() |
| 56 | + query = "manage employee records" |
300 | 57 |
|
301 | | - # Examples that require OpenAI API |
302 | | - if os.getenv("OPENAI_API_KEY"): |
303 | | - example_with_openai() |
304 | | - example_with_langchain() |
305 | | - else: |
306 | | - print("Set OPENAI_API_KEY to run OpenAI and LangChain examples\n") |
| 58 | + tools_3 = toolset_3.search_tools(query) |
| 59 | + print(f"Constructor top_k=3: got {len(tools_3)} tools") |
307 | 60 |
|
308 | | - print("=" * 60) |
309 | | - print("Examples completed!") |
310 | | - print("=" * 60) |
| 61 | + tools_override = toolset_3.search_tools(query, top_k=10) |
| 62 | + print(f"Per-call top_k=10 (overrides constructor 3): got {len(tools_override)} tools") |
311 | 63 |
|
312 | 64 |
|
313 | 65 | if __name__ == "__main__": |
|
0 commit comments