-
Notifications
You must be signed in to change notification settings - Fork 0
feat(search): Semantic Tool Search #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 48 commits
c7ad71f
0210c1f
b1105fa
d49f52b
680fa8e
1ee842b
d6fba69
3eb0641
fd37d93
f5ef955
b7b522f
e9c6b86
34e1ca6
82082cb
8a74517
85b0395
79c762a
6ee1adf
7a65367
64a0a60
4083642
b926db1
d2dd2f5
719b391
b360b00
7b77f33
bab931b
d62943d
5eaa3c5
173121d
1e4cc9a
f1db9f2
a87fa00
c9c0358
71457af
5bf1cc6
9fe1e40
010a275
8137538
90d8aa3
3d6000f
a0dd833
86c9c64
ce3443c
53828e5
b8b331a
f6920c8
4785d87
34c8bc9
0bc6ee0
6017b44
bf508b0
ea74fe2
1f51bf0
d930406
741fdf6
4109108
4eddc74
ff01d34
8e8e2e3
532ac23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,7 +19,8 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug | |||||
| - Glob pattern filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"` | ||||||
| - Provider and action filtering | ||||||
| - Multi-account support | ||||||
| - **Utility Tools** (Beta): Dynamic tool discovery and execution based on natural language queries | ||||||
| - **Semantic Search**: AI-powered tool discovery using natural language queries | ||||||
| - **Search Tool**: Callable tool discovery for agent loops via `get_search_tool()` | ||||||
| - Integration with popular AI frameworks: | ||||||
| - OpenAI Functions | ||||||
| - LangChain Tools | ||||||
|
|
@@ -305,26 +306,44 @@ result = feedback_tool.call( | |||||
| - "Are you ok with sending feedback to StackOne? The LLM will take care of sending it." | ||||||
| - Only call the tool after the user explicitly agrees. | ||||||
|
|
||||||
| ## Utility Tools (Beta) | ||||||
| ## Search Tool | ||||||
|
|
||||||
| Utility tools enable dynamic tool discovery and execution without hardcoding tool names. | ||||||
| Search for tools using natural language queries. Works with both semantic (cloud) and local BM25+TF-IDF search. | ||||||
|
|
||||||
| ### Basic Usage | ||||||
|
|
||||||
| ```python | ||||||
| # Get utility tools for dynamic discovery | ||||||
| tools = toolset.fetch_tools(actions=["hris_*"]) | ||||||
| utility_tools = tools.utility_tools() | ||||||
| # Get a callable search tool | ||||||
| toolset = StackOneToolSet() | ||||||
| all_tools = toolset.fetch_tools(account_ids=["your-account-id"]) | ||||||
| search_tool = toolset.get_search_tool() | ||||||
|
|
||||||
| # Search for relevant tools — returns a Tools collection | ||||||
| tools = search_tool("manage employees", top_k=5) | ||||||
|
|
||||||
| # Execute a discovered tool directly | ||||||
| tools[0](limit=10) | ||||||
| ``` | ||||||
|
|
||||||
| ## Semantic Search | ||||||
|
|
||||||
| Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `hris_create_employee`. | ||||||
|
||||||
| Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `hris_create_employee`. | |
| Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the actual tool is named `bamboohr_create_employee` (rather than a generic `hris_create_employee`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shashi-stackone that looks like a unified action, the copilot comment is accurate hire
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| #!/usr/bin/env python | ||
| """ | ||
| Example demonstrating dynamic tool discovery using search_tool. | ||
|
|
||
| The search tool allows AI agents to discover relevant tools based on natural language | ||
| queries without hardcoding tool names. | ||
|
|
||
| Prerequisites: | ||
| - STACKONE_API_KEY environment variable set | ||
| - STACKONE_ACCOUNT_ID environment variable set (comma-separated for multiple) | ||
| - At least one linked account in StackOne (this example uses BambooHR) | ||
|
|
||
| This example is runnable with the following command: | ||
| ```bash | ||
| uv run examples/search_tool_example.py | ||
| ``` | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| from stackone_ai import StackOneToolSet | ||
|
|
||
| try: | ||
| from dotenv import load_dotenv | ||
|
|
||
| load_dotenv() | ||
| except ModuleNotFoundError: | ||
| pass | ||
|
|
||
| # Read account IDs from environment — supports comma-separated values | ||
| _account_ids = [aid.strip() for aid in os.getenv("STACKONE_ACCOUNT_ID", "").split(",") if aid.strip()] | ||
|
|
||
|
|
||
| def example_search_tool_basic(): | ||
| """Basic example of using the search tool for tool discovery""" | ||
| print("Example 1: Dynamic tool discovery\n") | ||
|
|
||
| # Initialize StackOne toolset | ||
| toolset = StackOneToolSet() | ||
|
|
||
| # Get all available tools using MCP-backed fetch_tools() | ||
| all_tools = toolset.fetch_tools(account_ids=_account_ids) | ||
| print(f"Total tools available: {len(all_tools)}") | ||
|
|
||
| if not all_tools: | ||
| print("No tools found. Check your linked accounts.") | ||
| return | ||
|
|
||
| # Get a search tool for dynamic discovery | ||
| search_tool = toolset.get_search_tool() | ||
|
|
||
| # Search for employee management tools — returns a Tools collection | ||
| tools = search_tool("manage employees create update list", top_k=5, account_ids=_account_ids) | ||
|
|
||
| print(f"Found {len(tools)} relevant tools:") | ||
| for tool in tools: | ||
| print(f" - {tool.name}: {tool.description}") | ||
|
|
||
| print() | ||
|
|
||
|
|
||
| def example_search_modes(): | ||
| """Comparing semantic vs local search modes. | ||
|
|
||
| The search parameter controls which backend search_tools() uses: | ||
| - "semantic": cloud-based semantic vector search (higher accuracy for natural language) | ||
| - "local": local BM25+TF-IDF hybrid search (no network call to semantic API) | ||
| - "auto" (default): tries semantic first, falls back to local on failure | ||
| """ | ||
| print("Example 2: Semantic vs local search modes\n") | ||
|
|
||
| toolset = StackOneToolSet() | ||
| query = "manage employee time off" | ||
|
|
||
| # Semantic search — uses StackOne's semantic search API | ||
| print('search="semantic": cloud-based semantic vector search') | ||
| try: | ||
| tools_semantic = toolset.search_tools(query, account_ids=_account_ids, top_k=5, search="semantic") | ||
| print(f" Found {len(tools_semantic)} tools:") | ||
| for tool in tools_semantic: | ||
| print(f" - {tool.name}") | ||
| except Exception as e: | ||
| print(f" Semantic search unavailable: {e}") | ||
| print() | ||
|
|
||
| # Local search — BM25+TF-IDF, no semantic API call | ||
| print('search="local": local BM25+TF-IDF hybrid search') | ||
| tools_local = toolset.search_tools(query, account_ids=_account_ids, top_k=5, search="local") | ||
| print(f" Found {len(tools_local)} tools:") | ||
| for tool in tools_local: | ||
| print(f" - {tool.name}") | ||
| print() | ||
|
|
||
| # Auto (default) — tries semantic, falls back to local | ||
| print('search="auto" (default): semantic with local fallback') | ||
| tools_auto = toolset.search_tools(query, account_ids=_account_ids, top_k=5, search="auto") | ||
| print(f" Found {len(tools_auto)} tools:") | ||
| for tool in tools_auto: | ||
| print(f" - {tool.name}") | ||
| print() | ||
|
|
||
|
|
||
| def example_search_tool_with_execution(): | ||
| """Example of discovering and executing tools dynamically""" | ||
| print("Example 2: Dynamic tool execution\n") | ||
|
shashi-stackone marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Initialize toolset | ||
| toolset = StackOneToolSet() | ||
|
|
||
| # Get all tools using MCP-backed fetch_tools() | ||
| all_tools = toolset.fetch_tools(account_ids=_account_ids) | ||
|
|
||
| if not all_tools: | ||
| print("No tools found. Check your linked accounts.") | ||
| return | ||
|
|
||
| search_tool = toolset.get_search_tool() | ||
|
|
||
| # Step 1: Search for relevant tools | ||
| tools = search_tool("list all employees", top_k=1, account_ids=_account_ids) | ||
|
|
||
| if tools: | ||
| best_tool = tools[0] | ||
| print(f"Best matching tool: {best_tool.name}") | ||
| print(f"Description: {best_tool.description}") | ||
|
|
||
| # Step 2: Execute the found tool directly | ||
| try: | ||
| print(f"\nExecuting {best_tool.name}...") | ||
| result = best_tool(limit=5) | ||
| print(f"Execution result: {result}") | ||
| except Exception as e: | ||
| print(f"Execution failed (expected in example): {e}") | ||
|
|
||
| print() | ||
|
|
||
|
|
||
| def example_with_openai(): | ||
| """Example of using search tool with OpenAI""" | ||
| print("Example 3: Using search tool with OpenAI\n") | ||
|
|
||
| try: | ||
| from openai import OpenAI | ||
|
|
||
| # Initialize OpenAI client | ||
| client = OpenAI() | ||
|
|
||
| # Initialize StackOne toolset | ||
| toolset = StackOneToolSet() | ||
|
|
||
| # Search for BambooHR employee tools | ||
| tools = toolset.search_tools("manage employees", account_ids=_account_ids, top_k=5) | ||
|
|
||
| # Convert to OpenAI format | ||
| openai_tools = tools.to_openai() | ||
|
|
||
| # Create a chat completion with discovered tools | ||
| response = client.chat.completions.create( | ||
| model="gpt-4", | ||
| messages=[ | ||
| { | ||
| "role": "system", | ||
| "content": "You are an HR assistant with access to employee management tools.", | ||
| }, | ||
| {"role": "user", "content": "Can you help me find tools for managing employee records?"}, | ||
| ], | ||
| tools=openai_tools, | ||
| tool_choice="auto", | ||
| ) | ||
|
|
||
| print("OpenAI Response:", response.choices[0].message.content) | ||
|
|
||
| if response.choices[0].message.tool_calls: | ||
| print("\nTool calls made:") | ||
| for tool_call in response.choices[0].message.tool_calls: | ||
| print(f" - {tool_call.function.name}") | ||
|
|
||
| except ImportError: | ||
| print("OpenAI library not installed. Install with: pip install openai") | ||
| except Exception as e: | ||
| print(f"OpenAI example failed: {e}") | ||
|
|
||
| print() | ||
|
|
||
|
|
||
| def example_with_langchain(): | ||
| """Example of using tools with LangChain""" | ||
| print("Example 4: Using tools with LangChain\n") | ||
|
|
||
| try: | ||
| from langchain.agents import AgentExecutor, create_tool_calling_agent | ||
| from langchain_core.prompts import ChatPromptTemplate | ||
| from langchain_openai import ChatOpenAI | ||
|
|
||
| # Initialize StackOne toolset | ||
| toolset = StackOneToolSet() | ||
|
|
||
| # Get tools and convert to LangChain format using MCP-backed fetch_tools() | ||
| tools = toolset.search_tools("list employees", account_ids=_account_ids, top_k=5) | ||
| langchain_tools = list(tools.to_langchain()) | ||
|
|
||
| print(f"Available tools for LangChain: {len(langchain_tools)}") | ||
| for tool in langchain_tools: | ||
| print(f" - {tool.name}: {tool.description}") | ||
|
|
||
| # Create LangChain agent | ||
| llm = ChatOpenAI(model="gpt-4", temperature=0) | ||
|
|
||
| prompt = ChatPromptTemplate.from_messages( | ||
| [ | ||
| ( | ||
| "system", | ||
| "You are an HR assistant. Use the available tools to help the user.", | ||
| ), | ||
| ("human", "{input}"), | ||
| ("placeholder", "{agent_scratchpad}"), | ||
| ] | ||
| ) | ||
|
|
||
| agent = create_tool_calling_agent(llm, langchain_tools, prompt) | ||
| agent_executor = AgentExecutor(agent=agent, tools=langchain_tools, verbose=True) | ||
|
|
||
| # Run the agent | ||
| result = agent_executor.invoke({"input": "Find tools that can list employee data"}) | ||
|
|
||
| print(f"\nAgent result: {result['output']}") | ||
|
|
||
| except ImportError as e: | ||
| print(f"LangChain dependencies not installed: {e}") | ||
| print("Install with: pip install langchain-openai") | ||
| except Exception as e: | ||
| print(f"LangChain example failed: {e}") | ||
|
|
||
| print() | ||
|
|
||
|
|
||
| def main(): | ||
| """Run all examples""" | ||
| print("=" * 60) | ||
| print("StackOne AI SDK - Search Tool Examples") | ||
| print("=" * 60) | ||
| print() | ||
|
|
||
| if not os.getenv("STACKONE_API_KEY"): | ||
| print("Set STACKONE_API_KEY to run these examples.") | ||
| return | ||
|
|
||
| if not _account_ids: | ||
| print("Set STACKONE_ACCOUNT_ID to run these examples.") | ||
| print("(Comma-separated for multiple accounts)") | ||
| return | ||
|
|
||
| # Basic examples that work without external APIs | ||
| example_search_tool_basic() | ||
| example_search_modes() | ||
| example_search_tool_with_execution() | ||
|
|
||
| # Examples that require OpenAI API | ||
| if os.getenv("OPENAI_API_KEY"): | ||
| example_with_openai() | ||
| example_with_langchain() | ||
| else: | ||
| print("Set OPENAI_API_KEY to run OpenAI and LangChain examples\n") | ||
|
|
||
| print("=" * 60) | ||
| print("Examples completed!") | ||
| print("=" * 60) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
|
shashi-stackone marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.