From f67e46169c7b5be09edee00db1e0c91eb44cd175 Mon Sep 17 00:00:00 2001 From: Will Leeney Date: Fri, 11 Sep 2026 14:00:31 +0100 Subject: [PATCH 1/2] fix(toolset): honour STACKONE_BASE_URL and remove documented fiction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STACKONE_BASE_URL was ignored entirely, so a consumer pointing at staging silently talked to production. Now read as a fallback, matching the STACKONE_API_KEY handling directly above it and node's behaviour. Precedence is explicit arg > env var > default; the docstring says so rather than leaving the env var undocumented. Also removes three documented-but-untrue claims (consumer contract §3): - `configure_implicit_feedback` and the whole "Implicit Feedback (Beta)" section. None of it exists in the source; models.py carries a `# Implicit feedback removed` marker where it used to live. - Action-filter "exclusions" like `"!*_delete_*"`. `_filter_by_action` is a bare `any(fnmatch(...))` with no negation, so the pattern matches nothing and excludes nothing. - "This tool is automatically included in the toolset." The feedback tool is never injected client-side, so the worked example returned None and the next line raised AttributeError. Replaced with the MCP-served `stackone_submit_feedback` and the `create_feedback_tool` path that actually works — verified by running it. --- CLAUDE.md | 2 +- README.md | 59 +++++------------------------------------- stackone_ai/toolset.py | 5 ++-- 3 files changed, 10 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e38f285..4896ec0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ StackOne AI SDK is a Python library that provides a unified interface for access ```python # Use glob patterns for tool selection -tools = StackOneToolSet(include_tools=["bamboohr_*", "!bamboohr_create_*"]) +tools = toolset.fetch_tools(actions=["bamboohr_*"]) ``` ### Authentication diff --git a/README.md b/README.md index 39ebe0b..02d77bf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug - **Tool Calling**: Direct method calling with `tool.call()` for intuitive usage - **MCP-backed Dynamic Discovery**: Fetch tools at runtime via `fetch_tools()` with provider, action, and account filtering - **Advanced Tool Filtering**: - - Glob pattern filtering with patterns like `"salesforce_*"` and exclusions `"!*_delete_*"` + - Glob pattern filtering with patterns like `"salesforce_*"` - Provider and action filtering - Multi-account support - **Semantic Search**: AI-powered tool discovery using natural language queries @@ -143,51 +143,6 @@ The returned dict: JSON responses are unchanged: any action returning `application/json` (or a `…+json` type) is parsed and returned as a dict exactly as before. -## Implicit Feedback (Beta) - -The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs. - -### Automatic configuration - -Set `LANGSMITH_API_KEY` in your environment and the SDK will initialize the implicit feedback manager on first tool execution. You can optionally fine-tune behavior with: - -- `STACKONE_IMPLICIT_FEEDBACK_ENABLED` (`true`/`false`, defaults to `true` when an API key is present) -- `STACKONE_IMPLICIT_FEEDBACK_PROJECT` to pin a LangSmith project name -- `STACKONE_IMPLICIT_FEEDBACK_TAGS` with a comma-separated list of tags applied to every run - -### Manual configuration - -If you want custom session or user resolvers, call `configure_implicit_feedback` during start-up: - -```python -from stackone_ai import configure_implicit_feedback - -configure_implicit_feedback( - api_key="/path/to/langsmith.key", - project_name="stackone-agents", - default_tags=["python-sdk"], -) -``` - -Providing your own `session_resolver`/`user_resolver` callbacks lets you derive identifiers from the request context before events are sent to LangSmith. - -### Attaching session context to tool calls - -Both `tool.execute` and `tool.call` accept an `options` keyword that is excluded from the API request but forwarded to the feedback manager: - -```python -tool.execute( - {"id": "employee-id"}, - options={ - "feedback_session_id": "chat-42", - "feedback_user_id": "user-123", - "feedback_metadata": {"conversation_id": "abc"}, - }, -) -``` - -When two calls for the same session happen within a few seconds, the SDK emits a `refinement_needed` event, and you can inspect suitability scores directly in LangSmith. - ## Integration Examples
@@ -359,16 +314,14 @@ result = crew.kickoff() ## Feedback Collection -The SDK includes a feedback collection tool (`tool_feedback`) that allows users to submit feedback about their experience with StackOne tools. This tool is automatically included in the toolset and is designed to be invoked by AI agents after user permission. +StackOne serves a feedback tool (`stackone_submit_feedback`) from the MCP endpoint, so it arrives in `fetch_tools()` alongside your action tools when it is enabled for your project. It is designed to be invoked by AI agents after user permission. -```python -from stackone_ai import StackOneToolSet +A client-side equivalent is also available for constructing the tool yourself: -toolset = StackOneToolSet() +```python +from stackone_ai.feedback.tool import create_feedback_tool -# Get the feedback tool (included with "tool_*" pattern or all tools) -tools = toolset.fetch_tools(actions=["tool_*"]) -feedback_tool = tools.get_tool("tool_feedback") +feedback_tool = create_feedback_tool(api_key="your-api-key", account_id="acc_123456") # Submit feedback (typically invoked by AI after user consent) result = feedback_tool.call( diff --git a/stackone_ai/toolset.py b/stackone_ai/toolset.py index d6006d5..09e40f4 100644 --- a/stackone_ai/toolset.py +++ b/stackone_ai/toolset.py @@ -589,7 +589,8 @@ def __init__( Args: api_key: Optional API key. If not provided, will try to get from STACKONE_API_KEY env var account_id: Optional account ID - base_url: Optional base URL override for API requests + base_url: Optional base URL override. If not provided, will try to get from + STACKONE_BASE_URL env var, then fall back to the production default search: Search configuration. Controls default search behavior. Pass ``None`` (default) to disable search — ``toolset.openai()`` will return all regular tools. @@ -614,7 +615,7 @@ def __init__( ) self.api_key: str = api_key_value self.account_id = account_id - self.base_url = base_url or DEFAULT_BASE_URL + self.base_url = base_url or os.getenv("STACKONE_BASE_URL") or DEFAULT_BASE_URL self._account_ids: list[str] = execute.get("account_ids", []) if execute else [] self._semantic_client: SemanticSearchClient | None = None self._search_config: SearchConfig | None = search From e402437cab0e0bff16a336901b822903f81163e1 Mon Sep 17 00:00:00 2001 From: Will Leeney Date: Mon, 14 Sep 2026 08:51:04 +0100 Subject: [PATCH 2/2] test(toolset): cover STACKONE_BASE_URL precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The env-var read had no regression cover: reverting it passed the suite clean, so the headline behaviour change of this PR was unprotected. Three tests pin the contract — explicit argument > env var > default. test_init_with_api_key also asserted the default base_url without clearing the environment, so this change made it fail for anyone with STACKONE_BASE_URL exported. It now clears the env for that assertion. --- tests/test_toolset.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_toolset.py b/tests/test_toolset.py index 8ccd26a..63d5dd1 100644 --- a/tests/test_toolset.py +++ b/tests/test_toolset.py @@ -171,7 +171,10 @@ class TestStackOneToolSetInit: def test_init_with_api_key(self): """Test initialization with explicit API key.""" - toolset = StackOneToolSet(api_key="test_key") + # STACKONE_BASE_URL is cleared: the base_url assertion below is about the + # default, and a developer with that var exported would otherwise fail it. + with patch.dict(os.environ, {}, clear=True): + toolset = StackOneToolSet(api_key="test_key") assert toolset.api_key == "test_key" assert toolset.account_id is None assert toolset.base_url == DEFAULT_BASE_URL @@ -200,6 +203,24 @@ def test_init_with_custom_base_url(self): toolset = StackOneToolSet(api_key="test_key", base_url="https://custom.api.com") assert toolset.base_url == "https://custom.api.com" + def test_base_url_from_env(self): + """STACKONE_BASE_URL is honoured when no base_url argument is given.""" + with patch.dict(os.environ, {"STACKONE_BASE_URL": "https://staging.api.com"}): + toolset = StackOneToolSet(api_key="test_key") + assert toolset.base_url == "https://staging.api.com" + + def test_explicit_base_url_beats_env(self): + """An explicit argument wins over the environment.""" + with patch.dict(os.environ, {"STACKONE_BASE_URL": "https://staging.api.com"}): + toolset = StackOneToolSet(api_key="test_key", base_url="https://explicit.api.com") + assert toolset.base_url == "https://explicit.api.com" + + def test_base_url_falls_back_to_default(self): + """With neither argument nor env var, the production default applies.""" + with patch.dict(os.environ, {}, clear=True): + toolset = StackOneToolSet(api_key="test_key") + assert toolset.base_url == DEFAULT_BASE_URL + class TestStackOneToolSetNormalizeSchemaProperties: """Test _normalize_schema_properties method."""