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 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."""