Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_*"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Make this example standalone by importing and initializing StackOneToolSet before calling fetch_tools; as written, copying the block raises NameError for toolset.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLAUDE.md, line 60:

<comment>Make this example standalone by importing and initializing `StackOneToolSet` before calling `fetch_tools`; as written, copying the block raises `NameError` for `toolset`.</comment>

<file context>
@@ -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_*"])

</file context>


</details>

```suggestion
from stackone_ai import StackOneToolSet

toolset = StackOneToolSet(api_key="your-api-key")
tools = toolset.fetch_tools(actions=["bamboohr_*"])

```

### Authentication
Expand Down
59 changes: 6 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

<details>
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions stackone_ai/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
23 changes: 22 additions & 1 deletion tests/test_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down