Skip to content

Commit 45745e8

Browse files
Adopt changes from the new API
1 parent 97f59f4 commit 45745e8

11 files changed

Lines changed: 159 additions & 193 deletions

examples/agent_tool_search.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
keeping token usage constant regardless of catalog size.
1111
1212
This example demonstrates approach 2 with two patterns:
13-
- Raw client (Gemini): manual agent loop with ``toolset.execute()``
13+
- Raw client (OpenAI): manual agent loop with ``toolset.execute()``
1414
- LangChain: framework handles tool execution automatically
1515
1616
Prerequisites:
1717
- STACKONE_API_KEY environment variable
1818
- STACKONE_ACCOUNT_ID environment variable
19-
- GOOGLE_API_KEY environment variable (for Gemini/LangChain)
19+
- OPENAI_API_KEY environment variable
2020
2121
Run with:
2222
uv run python examples/agent_tool_search.py
@@ -37,13 +37,13 @@
3737
from stackone_ai import StackOneToolSet
3838

3939

40-
def example_gemini() -> None:
41-
"""Raw client: Gemini via OpenAI-compatible API.
40+
def example_openai() -> None:
41+
"""Raw client: OpenAI.
4242
4343
Shows: init toolset -> get OpenAI tools -> manual agent loop with toolset.execute().
4444
"""
4545
print("=" * 60)
46-
print("Example 1: Raw client (Gemini) — manual execution")
46+
print("Example 1: Raw client (OpenAI) — manual execution")
4747
print("=" * 60)
4848
print()
4949

@@ -54,9 +54,8 @@ def example_gemini() -> None:
5454
print()
5555
return
5656

57-
google_key = os.getenv("GOOGLE_API_KEY")
58-
if not google_key:
59-
print("Skipped: Set GOOGLE_API_KEY to run this example.")
57+
if not os.getenv("OPENAI_API_KEY"):
58+
print("Skipped: Set OPENAI_API_KEY to run this example.")
6059
print()
6160
return
6261

@@ -71,18 +70,25 @@ def example_gemini() -> None:
7170
# 2. Get tools in OpenAI format
7271
openai_tools = toolset.openai(mode="search_and_execute")
7372

74-
# 3. Create Gemini client (OpenAI-compatible) and run agent loop
75-
client = OpenAI(
76-
api_key=google_key,
77-
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
78-
)
73+
# 3. Create OpenAI client and run agent loop
74+
client = OpenAI()
7975
messages: list[dict] = [
76+
{
77+
"role": "system",
78+
"content": (
79+
"You are a helpful scheduling assistant. Use tool_search to find relevant tools, "
80+
"then tool_execute to run them. Always read the parameter schemas from tool_search "
81+
"results carefully. If a tool needs a user URI, first search for and call a "
82+
'"get current user" tool to obtain it. If a tool execution fails, try different '
83+
"parameters or a different tool."
84+
),
85+
},
8086
{"role": "user", "content": "List my upcoming Calendly events for the next week."},
8187
]
8288

8389
for _step in range(10):
8490
response = client.chat.completions.create(
85-
model="gemini-3-pro-preview",
91+
model="gpt-5.4",
8692
messages=messages,
8793
tools=openai_tools,
8894
tool_choice="auto",
@@ -123,15 +129,15 @@ def example_langchain() -> None:
123129
print()
124130

125131
try:
126-
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
127-
from langchain_google_genai import ChatGoogleGenerativeAI
132+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
133+
from langchain_openai import ChatOpenAI
128134
except ImportError:
129-
print("Skipped: pip install langchain-google-genai")
135+
print("Skipped: pip install langchain-openai")
130136
print()
131137
return
132138

133-
if not os.getenv("GOOGLE_API_KEY"):
134-
print("Skipped: Set GOOGLE_API_KEY to run this example.")
139+
if not os.getenv("OPENAI_API_KEY"):
140+
print("Skipped: Set OPENAI_API_KEY to run this example.")
135141
print()
136142
return
137143

@@ -146,10 +152,21 @@ def example_langchain() -> None:
146152
# 2. Get tools in LangChain format and bind to model
147153
langchain_tools = toolset.langchain(mode="search_and_execute")
148154
tools_by_name = {tool.name: tool for tool in langchain_tools}
149-
model = ChatGoogleGenerativeAI(model="gemini-3-pro-preview").bind_tools(langchain_tools)
155+
model = ChatOpenAI(model="gpt-5.4").bind_tools(langchain_tools)
150156

151157
# 3. Run agent loop
152-
messages = [HumanMessage(content="List my upcoming Calendly events for the next week.")]
158+
messages = [
159+
SystemMessage(
160+
content=(
161+
"You are a helpful scheduling assistant. Use tool_search to find relevant tools, "
162+
"then tool_execute to run them. Always read the parameter schemas from tool_search "
163+
"results carefully. If a tool needs a user URI, first search for and call a "
164+
'"get current user" tool to obtain it. If a tool execution fails, try different '
165+
"parameters or a different tool."
166+
),
167+
),
168+
HumanMessage(content="List my upcoming Calendly events for the next week."),
169+
]
153170

154171
for _step in range(10):
155172
response: AIMessage = model.invoke(messages)
@@ -177,7 +194,7 @@ def main() -> None:
177194
print("Set STACKONE_API_KEY to run these examples.")
178195
return
179196

180-
example_gemini()
197+
example_openai()
181198
example_langchain()
182199

183200

examples/crewai_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def crewai_integration():
3434
goal=f"What is the employee with the id {employee_id}?",
3535
backstory="With over 10 years of experience in HR and employee management, "
3636
"you excel at finding patterns in complex datasets.",
37-
llm="gpt-4o-mini",
37+
llm="gpt-5.4",
3838
tools=langchain_tools,
3939
max_iter=2,
4040
)

examples/langchain_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def langchain_integration() -> None:
3333
assert hasattr(tool, "args_schema"), "Expected tool to have args_schema"
3434

3535
# Create model with tools
36-
model = ChatOpenAI(model="gpt-4o-mini")
36+
model = ChatOpenAI(model="gpt-5.4")
3737
model_with_tools = model.bind_tools(langchain_tools)
3838

3939
result = model_with_tools.invoke(f"Can you get me information about employee with ID: {employee_id}?")

examples/openai_integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def openai_integration() -> None:
5353
]
5454

5555
response = client.chat.completions.create(
56-
model="gpt-4o-mini",
56+
model="gpt-5.4",
5757
messages=messages,
5858
tools=openai_tools,
5959
tool_choice="auto",
@@ -81,7 +81,7 @@ def openai_integration() -> None:
8181

8282
# Verify the final response
8383
final_response = client.chat.completions.create(
84-
model="gpt-4o-mini",
84+
model="gpt-5.4",
8585
messages=messages,
8686
tools=openai_tools,
8787
tool_choice="auto",

examples/search_tool_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def example_with_openai():
198198

199199
# Create a chat completion with discovered tools
200200
response = client.chat.completions.create(
201-
model="gpt-4",
201+
model="gpt-5.4",
202202
messages=[
203203
{
204204
"role": "system",
@@ -246,7 +246,7 @@ def example_with_langchain():
246246
print(f" - {tool.name}: {tool.description}")
247247

248248
# Create LangChain agent
249-
llm = ChatOpenAI(model="gpt-4", temperature=0)
249+
llm = ChatOpenAI(model="gpt-5.4", temperature=0)
250250

251251
prompt = ChatPromptTemplate.from_messages(
252252
[

examples/semantic_search_example.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def example_search_action_names():
132132
# Show the limited results
133133
print(f"Top {len(results_limited)} matches from the full catalog:")
134134
for r in results_limited:
135-
print(f" [{r.similarity_score:.2f}] {r.action_name} ({r.connector_key})")
135+
print(f" [{r.similarity_score:.2f}] {r.id}")
136136
print(f" {r.description}")
137137
print()
138138

@@ -143,7 +143,7 @@ def example_search_action_names():
143143
filtered = toolset.search_action_names(query, account_ids=_account_ids, top_k=5)
144144
print(f" Filtered to {len(filtered)} matches (only your connectors):")
145145
for r in filtered:
146-
print(f" [{r.similarity_score:.2f}] {r.action_name} ({r.connector_key})")
146+
print(f" [{r.similarity_score:.2f}] {r.id}")
147147
else:
148148
print("Tip: Set STACKONE_ACCOUNT_ID to see results filtered to your linked connectors.")
149149

@@ -197,7 +197,7 @@ def example_search_tools_with_connector():
197197
print("=" * 60)
198198
print()
199199

200-
toolset = StackOneToolSet()
200+
toolset = StackOneToolSet(search={})
201201

202202
query = "book a meeting"
203203
connector = "calendly"
@@ -230,7 +230,7 @@ def example_search_tool_agent_loop():
230230
print("=" * 60)
231231
print()
232232

233-
toolset = StackOneToolSet()
233+
toolset = StackOneToolSet(search={})
234234

235235
print("Step 1: Fetching tools from your linked accounts via MCP...")
236236
all_tools = toolset.fetch_tools(account_ids=_account_ids)
@@ -281,7 +281,7 @@ def example_openai_agent_loop():
281281

282282
if openai_key:
283283
client = OpenAI()
284-
model = "gpt-4o-mini"
284+
model = "gpt-5.4"
285285
provider = "OpenAI"
286286
elif google_key:
287287
client = OpenAI(
@@ -298,7 +298,7 @@ def example_openai_agent_loop():
298298
print(f"Using {provider} ({model})")
299299
print()
300300

301-
toolset = StackOneToolSet()
301+
toolset = StackOneToolSet(search={})
302302

303303
query = "list upcoming events"
304304
print(f'Step 1: Discovering tools for "{query}" via semantic search...')
@@ -358,7 +358,7 @@ def example_langchain_semantic():
358358
print()
359359
return
360360

361-
toolset = StackOneToolSet()
361+
toolset = StackOneToolSet(search={})
362362

363363
query = "remove a user from the team"
364364
print(f'Step 1: Searching for "{query}" via semantic search...')

stackone_ai/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,10 @@ def to_langchain(self) -> BaseTool:
414414

415415
for name, details in self.parameters.properties.items():
416416
python_type: type = str # Default to str
417+
is_nullable = False
417418
if isinstance(details, dict):
418419
type_str = details.get("type", "string")
420+
is_nullable = details.get("nullable", False)
419421
if type_str == "number":
420422
python_type = float
421423
elif type_str == "integer":
@@ -427,12 +429,18 @@ def to_langchain(self) -> BaseTool:
427429
elif type_str == "array":
428430
python_type = list
429431

430-
field = Field(description=details.get("description", ""))
432+
if is_nullable:
433+
field = Field(default=None, description=details.get("description", ""))
434+
else:
435+
field = Field(description=details.get("description", ""))
431436
else:
432437
field = Field(description="")
433438

434439
schema_props[name] = field
435-
annotations[name] = python_type
440+
if is_nullable:
441+
annotations[name] = python_type | None
442+
else:
443+
annotations[name] = python_type
436444

437445
# Create the schema class with proper annotations
438446
schema_class = type(

stackone_ai/semantic_search.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,16 @@
1212
This is the primary method used when integrating with OpenAI, LangChain, or CrewAI.
1313
The internal flow is:
1414
15-
1. Fetch ALL tools from linked accounts via MCP (uses account_ids to scope the request)
16-
2. Extract available connectors from the fetched tools (e.g. {bamboohr, hibob})
17-
3. Search EACH connector in parallel via the semantic search API (/actions/search)
18-
4. Collect results, sort by relevance score, apply top_k if specified
19-
5. Match semantic results back to the fetched tool definitions
15+
1. Fetch tools from linked accounts via MCP to discover available connectors
16+
2. Search EACH connector in parallel via the semantic search API (/actions/search)
17+
3. The search API returns results with full ``input_schema`` for each action
18+
4. Build executable tools directly from search results (no match-back needed)
19+
5. Deduplicate by action_id, sort by relevance score, apply top_k
2020
6. Return Tools sorted by relevance score
2121
2222
Key point: only the user's own connectors are searched — no wasted results
23-
from connectors the user doesn't have. Tools are fetched first, semantic
24-
search runs second, and only tools that exist in the user's linked
25-
accounts AND match the semantic query are returned. This prevents
26-
suggesting tools the user cannot execute.
23+
from connectors the user doesn't have. The search API returns ``input_schema``
24+
with each result, so tools can be built directly without a separate fetch.
2725
2826
If the semantic API is unavailable, the SDK falls back to a local
2927
BM25 + TF-IDF hybrid search over the fetched tools (unless
@@ -33,10 +31,10 @@
3331
2. ``search_action_names(query)`` — Lightweight discovery
3432
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3533
36-
Queries the semantic API directly and returns action name metadata
37-
(name, connector, score, description) **without** fetching full tool
38-
definitions. This is useful for previewing results before committing
39-
to a full fetch.
34+
Queries the semantic API directly and returns action metadata
35+
(action_id, connector, score, description, input_schema) **without**
36+
building full tool objects. Useful for previewing results before
37+
committing to a full fetch.
4038
4139
When ``account_ids`` are provided, each connector is searched in
4240
parallel (same as ``search_tools``). Without ``account_ids``, results
@@ -71,12 +69,8 @@ class SemanticSearchError(Exception):
7169
class SemanticSearchResult(BaseModel):
7270
"""Single result from semantic search API."""
7371

74-
action_name: str
75-
connector_key: str
72+
id: str
7673
similarity_score: float
77-
label: str
78-
description: str
79-
project_id: str = "global"
8074

8175

8276
class SemanticSearchResponse(BaseModel):
@@ -99,7 +93,7 @@ class SemanticSearchClient:
9993
client = SemanticSearchClient(api_key="sk-xxx")
10094
response = client.search("create employee", connector="bamboohr", top_k=5)
10195
for result in response.results:
102-
print(f"{result.action_name}: {result.similarity_score:.2f}")
96+
print(f"{result.action_id}: {result.similarity_score:.2f}")
10397
"""
10498

10599
def __init__(
@@ -152,7 +146,7 @@ def search(
152146
Example:
153147
response = client.search("onboard a new team member", top_k=5)
154148
for result in response.results:
155-
print(f"{result.action_name}: {result.similarity_score:.2f}")
149+
print(f"{result.action_id}: {result.similarity_score:.2f}")
156150
"""
157151
url = f"{self.base_url}/actions/search"
158152
headers = {
@@ -210,4 +204,4 @@ def search_action_names(
210204
)
211205
"""
212206
response = self.search(query, connector, top_k, project_id, min_similarity=min_similarity)
213-
return [r.action_name for r in response.results]
207+
return [r.id for r in response.results]

0 commit comments

Comments
 (0)