Skip to content

Commit bf45364

Browse files
committed
Benchmark update and PR suggestions
1 parent 06d6a9a commit bf45364

10 files changed

Lines changed: 409 additions & 1506 deletions

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,53 @@ result = execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
328328

329329
## Semantic Search
330330

331-
Semantic search allows tool discovery using natural language instead of exact keyword matching. It understands intent and synonyms, so queries like "fire someone" or "check my to-do list" resolve to the right actions. Enable it via `toolset.search_tools(query)` or by passing a `semantic_client` to utility tools.
331+
Semantic search enables tool discovery using natural language instead of exact keyword matching. It understands intent and synonyms, so queries like "fire someone" or "check my to-do list" resolve to the right StackOne actions.
332+
333+
**How it works:** Your query is matched against all StackOne actions using semantic vector search. Results are automatically filtered to only the connectors available in your linked accounts, so you only get tools you can actually use.
334+
335+
### `search_tools()` — Recommended
336+
337+
High-level method that returns a `Tools` collection ready for any framework:
338+
339+
```python
340+
from stackone_ai import StackOneToolSet
341+
342+
toolset = StackOneToolSet()
343+
344+
# Natural language search — no need to know exact tool names
345+
tools = toolset.search_tools("manage employee records", top_k=5)
346+
347+
# Use with any framework
348+
openai_tools = tools.to_openai()
349+
langchain_tools = tools.to_langchain()
350+
351+
# Filter by connector
352+
tools = toolset.search_tools("create time off request", connector="bamboohr", top_k=3)
353+
```
354+
355+
### `search_action_names()` — Lightweight
356+
357+
Returns action names and similarity scores without fetching full tool definitions. Useful for inspecting results before committing:
358+
359+
```python
360+
results = toolset.search_action_names("time off requests", top_k=5)
361+
for r in results:
362+
print(f"{r.action_name} ({r.connector_key}): {r.similarity_score:.2f}")
363+
```
364+
365+
### Utility Tools with Semantic Search
366+
367+
For agent loops using `tool_search` / `tool_execute`, pass `semantic_client` to upgrade from local keyword matching to semantic search:
368+
369+
```python
370+
tools = toolset.fetch_tools()
371+
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
372+
373+
search_tool = utility.get_tool("tool_search")
374+
results = search_tool.call(query="onboard a new team member", limit=5)
375+
```
376+
377+
See [Semantic Search Example](examples/semantic_search_example.py) for complete patterns including OpenAI and LangChain integration.
332378

333379
## Examples
334380

@@ -340,6 +386,7 @@ For more examples, check out the [examples/](examples/) directory:
340386
- [LangChain Integration](examples/langchain_integration.py)
341387
- [CrewAI Integration](examples/crewai_integration.py)
342388
- [Utility Tools](examples/utility_tools_example.py)
389+
- [Semantic Search](examples/semantic_search_example.py)
343390

344391
## Development
345392

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example demonstrating semantic search for AI-powered tool discovery.
4+
5+
Semantic search understands natural language intent and synonyms, so queries like
6+
"fire someone" or "check my to-do list" resolve to the right StackOne actions —
7+
unlike keyword matching which requires exact tool names.
8+
9+
This example is runnable with the following command:
10+
```bash
11+
uv run examples/semantic_search_example.py
12+
```
13+
14+
Prerequisites:
15+
- STACKONE_API_KEY environment variable set
16+
- At least one linked account in StackOne
17+
"""
18+
19+
import os
20+
21+
from dotenv import load_dotenv
22+
23+
from stackone_ai import StackOneToolSet
24+
25+
load_dotenv()
26+
27+
28+
def example_search_tools():
29+
"""High-level semantic search returning a Tools collection.
30+
31+
search_tools() is the recommended way to use semantic search. It:
32+
1. Fetches all available tools from your linked accounts
33+
2. Queries the semantic search API with your natural language query
34+
3. Filters results to only connectors available in your accounts
35+
4. Returns a Tools collection ready for any framework (.to_openai(), .to_langchain(), etc.)
36+
"""
37+
print("Example 1: search_tools() — high-level semantic search\n")
38+
39+
toolset = StackOneToolSet()
40+
41+
# Search using natural language — no need to know exact tool names
42+
tools = toolset.search_tools(
43+
"manage employee records",
44+
top_k=5,
45+
min_score=0.3,
46+
)
47+
48+
print(f"Found {len(tools)} matching tools:")
49+
for tool in tools:
50+
print(f" - {tool.name}: {tool.description[:80]}...")
51+
52+
# The result is a standard Tools collection — convert to any framework format
53+
openai_tools = tools.to_openai()
54+
print(f"\nConverted to {len(openai_tools)} OpenAI function definitions")
55+
56+
print()
57+
58+
59+
def example_search_tools_with_connector():
60+
"""Semantic search filtered by connector.
61+
62+
Use the connector parameter to scope results to a specific provider,
63+
for example when you know the user works with BambooHR.
64+
"""
65+
print("Example 2: search_tools() with connector filter\n")
66+
67+
toolset = StackOneToolSet()
68+
69+
# Search within a specific connector
70+
tools = toolset.search_tools(
71+
"create time off request",
72+
connector="bamboohr",
73+
top_k=3,
74+
min_score=0.3,
75+
)
76+
77+
print(f"Found {len(tools)} BambooHR tools for 'create time off request':")
78+
for tool in tools:
79+
print(f" - {tool.name}")
80+
81+
print()
82+
83+
84+
def example_search_action_names():
85+
"""Lightweight search returning action names and scores without fetching tools.
86+
87+
search_action_names() is useful when you want to inspect search results
88+
before committing to fetching full tool definitions — for example, to
89+
show the user a list of options.
90+
"""
91+
print("Example 3: search_action_names() — lightweight inspection\n")
92+
93+
toolset = StackOneToolSet()
94+
95+
results = toolset.search_action_names(
96+
"time off requests",
97+
top_k=5,
98+
min_score=0.3,
99+
)
100+
101+
print("Search results (action names + scores):")
102+
for r in results:
103+
print(f" {r.action_name} ({r.connector_key}) — score: {r.similarity_score:.2f}")
104+
print(f" {r.description[:80]}...")
105+
106+
print()
107+
108+
109+
def example_utility_tools_semantic():
110+
"""Using utility tools with semantic search for agent loops.
111+
112+
When building agent loops (search → select → execute), pass
113+
semantic_client to utility_tools() to upgrade tool_search from
114+
local BM25+TF-IDF to cloud-based semantic search.
115+
"""
116+
print("Example 4: Utility tools with semantic search\n")
117+
118+
toolset = StackOneToolSet()
119+
120+
# Fetch tools for your accounts
121+
tools = toolset.fetch_tools()
122+
123+
# Pass semantic_client to switch tool_search to semantic mode
124+
utility = tools.utility_tools(semantic_client=toolset.semantic_client)
125+
126+
# tool_search now uses semantic search under the hood
127+
search_tool = utility.get_tool("tool_search")
128+
if search_tool:
129+
result = search_tool.call(query="onboard a new team member", limit=5)
130+
print("Semantic tool_search results:")
131+
for tool_info in result.get("tools", []):
132+
print(f" - {tool_info['name']} (score: {tool_info['score']:.2f})")
133+
print(f" {tool_info['description'][:80]}...")
134+
135+
print()
136+
137+
138+
def example_openai_agent_loop():
139+
"""Complete agent loop: semantic search → OpenAI → execute.
140+
141+
This demonstrates the full pattern for building an AI agent that
142+
discovers tools via semantic search and executes them via OpenAI.
143+
"""
144+
print("Example 5: OpenAI agent loop with semantic search\n")
145+
146+
try:
147+
from openai import OpenAI
148+
except ImportError:
149+
print("OpenAI library not installed. Install with: pip install openai")
150+
print()
151+
return
152+
153+
if not os.getenv("OPENAI_API_KEY"):
154+
print("Set OPENAI_API_KEY to run this example")
155+
print()
156+
return
157+
158+
client = OpenAI()
159+
toolset = StackOneToolSet()
160+
161+
# Step 1: Discover relevant tools using semantic search
162+
tools = toolset.search_tools("list employees and their details", top_k=3)
163+
print(f"Discovered {len(tools)} tools via semantic search")
164+
for tool in tools:
165+
print(f" - {tool.name}")
166+
167+
# Step 2: Convert to OpenAI format and call the LLM
168+
openai_tools = tools.to_openai()
169+
170+
messages = [
171+
{"role": "system", "content": "You are a helpful HR assistant."},
172+
{"role": "user", "content": "Can you list the first 5 employees?"},
173+
]
174+
175+
response = client.chat.completions.create(
176+
model="gpt-4o-mini",
177+
messages=messages,
178+
tools=openai_tools,
179+
tool_choice="auto",
180+
)
181+
182+
# Step 3: Execute the tool calls
183+
if response.choices[0].message.tool_calls:
184+
print("\nLLM chose to call:")
185+
for tool_call in response.choices[0].message.tool_calls:
186+
print(f" - {tool_call.function.name}({tool_call.function.arguments})")
187+
188+
tool = tools.get_tool(tool_call.function.name)
189+
if tool:
190+
result = tool.execute(tool_call.function.arguments)
191+
print(f" Result keys: {list(result.keys()) if isinstance(result, dict) else type(result)}")
192+
else:
193+
print(f"\nLLM response: {response.choices[0].message.content}")
194+
195+
print()
196+
197+
198+
def example_langchain_semantic():
199+
"""Semantic search with LangChain tools.
200+
201+
search_tools() returns a Tools collection that converts directly
202+
to LangChain format — no extra steps needed.
203+
"""
204+
print("Example 6: Semantic search with LangChain\n")
205+
206+
try:
207+
from langchain_core.tools import BaseTool # noqa: F401
208+
except ImportError:
209+
print("LangChain not installed. Install with: pip install langchain-core")
210+
print()
211+
return
212+
213+
toolset = StackOneToolSet()
214+
215+
# Semantic search → LangChain tools in two lines
216+
tools = toolset.search_tools("employee management", top_k=5)
217+
langchain_tools = tools.to_langchain()
218+
219+
print(f"Created {len(langchain_tools)} LangChain tools from semantic search:")
220+
for tool in langchain_tools:
221+
print(f" - {tool.name}: {tool.description[:80]}...")
222+
223+
print()
224+
225+
226+
def main():
227+
"""Run all semantic search examples."""
228+
print("=" * 60)
229+
print("StackOne AI SDK — Semantic Search Examples")
230+
print("=" * 60)
231+
print()
232+
233+
# Core patterns (require STACKONE_API_KEY)
234+
if not os.getenv("STACKONE_API_KEY"):
235+
print("Set STACKONE_API_KEY to run these examples")
236+
return
237+
238+
example_search_tools()
239+
example_search_tools_with_connector()
240+
example_search_action_names()
241+
example_utility_tools_semantic()
242+
243+
# Framework integration patterns
244+
example_openai_agent_loop()
245+
example_langchain_semantic()
246+
247+
print("=" * 60)
248+
print("Examples completed!")
249+
print("=" * 60)
250+
251+
252+
if __name__ == "__main__":
253+
main()

examples/test_examples.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def get_example_files() -> list[str]:
3131
"file_uploads.py": ["mcp"],
3232
"stackone_account_ids.py": ["mcp"],
3333
"utility_tools_example.py": ["mcp"],
34+
"semantic_search_example.py": ["mcp"],
3435
"mcp_server.py": ["mcp"],
3536
}
3637

examples/utility_tools_example.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,39 @@ def example_utility_tools_with_execution():
8080
print()
8181

8282

83+
def example_utility_tools_semantic():
84+
"""Semantic search variant of utility tools.
85+
86+
By passing semantic_client to utility_tools(), tool_search switches from
87+
local BM25+TF-IDF to cloud-based semantic search for better natural language
88+
understanding. See examples/semantic_search_example.py for more patterns.
89+
"""
90+
print("Example 3: Utility tools with semantic search\n")
91+
92+
toolset = StackOneToolSet()
93+
94+
# Fetch tools — these define the available tool catalog
95+
all_tools = toolset.fetch_tools(actions=["bamboohr_*"])
96+
print(f"Total BambooHR tools available: {len(all_tools)}")
97+
98+
# Pass semantic_client to switch from local BM25 to cloud semantic search
99+
utility_tools = all_tools.utility_tools(semantic_client=toolset.semantic_client)
100+
101+
filter_tool = utility_tools.get_tool("tool_search")
102+
if filter_tool:
103+
# Semantic search understands intent — "fire someone" finds termination tools
104+
result = filter_tool.call(query="onboard a new team member", limit=5, minScore=0.0)
105+
106+
print("Found relevant tools (semantic search):")
107+
for tool in result.get("tools", []):
108+
print(f" - {tool['name']} (score: {tool['score']:.2f}): {tool['description']}")
109+
110+
print()
111+
112+
83113
def example_with_openai():
84114
"""Example of using utility tools with OpenAI"""
85-
print("Example 3: Using utility tools with OpenAI\n")
115+
print("Example 4: Using utility tools with OpenAI\n")
86116

87117
try:
88118
from openai import OpenAI
@@ -131,7 +161,7 @@ def example_with_openai():
131161

132162
def example_with_langchain():
133163
"""Example of using tools with LangChain"""
134-
print("Example 4: Using tools with LangChain\n")
164+
print("Example 5: Using tools with LangChain\n")
135165

136166
try:
137167
from langchain.agents import AgentExecutor, create_tool_calling_agent
@@ -197,6 +227,7 @@ def main():
197227
# Basic examples that work without external APIs
198228
example_utility_tools_basic()
199229
example_utility_tools_with_execution()
230+
example_utility_tools_semantic()
200231

201232
# Examples that require OpenAI API
202233
if os.getenv("OPENAI_API_KEY"):

0 commit comments

Comments
 (0)