stackone-ai 2.9.0__tar.gz → 2.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackone_ai-2.10.0/.release-please-manifest.json +3 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/CHANGELOG.md +15 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/PKG-INFO +42 -8
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/README.md +41 -7
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/pyproject.toml +1 -1
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/__init__.py +1 -1
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/models.py +68 -5
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/toolset.py +52 -27
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_fetch_tools.py +18 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_tool_calling.py +212 -10
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/uv.lock +1 -1
- stackone_ai-2.9.0/.release-please-manifest.json +0 -3
- stackone_ai-2.9.0/vendor/stackone-ai-node/.git +0 -1
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/examples-standards.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/nix-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/no-relative-imports.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/package-installation.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/release-please-standards.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.claude/rules/uv-scripts.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/examples-standards.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/no-relative-imports.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/package-installation.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/release-please-standards.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.cursor/rules/uv-scripts.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.env.example +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.envrc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/dependabot.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/nix-flake-update.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitignore +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitleaks.toml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.gitmodules +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.mcp.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/.release-please-config.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/CLAUDE.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/LICENSE +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/auth_management.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/crewai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/langchain_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/langgraph_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/openai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/pydantic_ai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/search_tools.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/examples/test_examples.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/flake.lock +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/flake.nix +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/justfile +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/scripts/benchmark_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/constants.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/feedback/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/feedback/tool.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/integrations/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/integrations/langgraph.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/local_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/py.typed +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/semantic_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/normalize.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/stackone_ai/utils/tfidf_index.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/conftest.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/mocks/serve.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_agent_tools.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_feedback.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_integrations_langgraph.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_integrations_pydantic_ai.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_local_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_models.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_semantic_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_tfidf_index.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/tests/test_toolset.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.envrc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.gitignore +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.mcp.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/AGENTS.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/CLAUDE.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/LICENSE +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/README.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/README.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/package.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/flake.lock +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/flake.nix +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/knip.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/lefthook.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/mocks/node.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/package.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/consts.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/feedback.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/headers.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/index.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/schema.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/tool.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/types.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/tsconfig.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/vitest.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.10.0}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.10.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.9.1...stackone-ai-v2.10.0) (2026-07-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **ENG-733:** support flat_prefixed MCP param style ([#192](https://github.com/StackOneHQ/stackone-ai-python/issues/192)) ([ff8fb9a](https://github.com/StackOneHQ/stackone-ai-python/commit/ff8fb9a41add55e9c31bd84e04447bb3685ab024))
|
|
9
|
+
|
|
10
|
+
## [2.9.1](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.9.0...stackone-ai-v2.9.1) (2026-06-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **models:** handle binary file downloads in tool execution ([#190](https://github.com/StackOneHQ/stackone-ai-python/issues/190)) ([8983c68](https://github.com/StackOneHQ/stackone-ai-python/commit/8983c68dd2fdbdbdd52af531a9a9f1de6d72e1b8))
|
|
16
|
+
* **search:** make ToolsetConfigError actionable and fix misleading docs ([#188](https://github.com/StackOneHQ/stackone-ai-python/issues/188)) ([d332c27](https://github.com/StackOneHQ/stackone-ai-python/commit/d332c27d2f7b827e7ca043f60c37cb0831a92361))
|
|
17
|
+
|
|
3
18
|
## [2.9.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.8.0...stackone-ai-v2.9.0) (2026-04-30)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackone-ai
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
4
4
|
Summary: agents performing actions on your SaaS
|
|
5
5
|
Author-email: StackOne <support@stackone.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -149,6 +149,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
|
|
|
149
149
|
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
|
|
150
150
|
- Provider prefix: `["workday_*"]` matches all Workday tools
|
|
151
151
|
|
|
152
|
+
## File Downloads
|
|
153
|
+
|
|
154
|
+
Actions that return a file — e.g. `googledrive_unified_download_file`, `documents_download_file`, any `*_unified_download_file` — resolve to **raw bytes plus metadata**, not parsed JSON. The SDK decides this from the response `Content-Type`: a JSON content type is parsed as before; anything else is treated as a file download. This applies to both `tool.execute()` and `tool.call()`.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
tools = toolset.fetch_tools(actions=["googledrive_*"], account_ids=[account_id])
|
|
158
|
+
download = tools.get_tool("googledrive_unified_download_file")
|
|
159
|
+
|
|
160
|
+
result = download.execute({"id": "file-id"})
|
|
161
|
+
|
|
162
|
+
# `result` is a dict describing the file — write the bytes straight to disk:
|
|
163
|
+
with open(result["file_name"] or "download.bin", "wb") as f:
|
|
164
|
+
f.write(result["content"])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The returned dict:
|
|
168
|
+
|
|
169
|
+
| Key | Type | Description |
|
|
170
|
+
| -------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
|
171
|
+
| `content` | `bytes` | Raw file bytes. **Not JSON-serializable** — see the caveat below. |
|
|
172
|
+
| `content_type` | `str` | The file's MIME type (e.g. `application/pdf`), or `application/octet-stream` if unspecified. |
|
|
173
|
+
| `status_code` | `int` | HTTP status of the download response. |
|
|
174
|
+
| `headers` | `dict` | Response headers. |
|
|
175
|
+
| `file_name` | `str \| None` | Filename from the `Content-Disposition` header (handles RFC 5987 `filename*`), else `None`. |
|
|
176
|
+
|
|
177
|
+
> **Caveat:** `content` holds raw bytes, which are not JSON-serializable. If you forward tool results to an LLM — or anywhere that re-serializes them to JSON — handle or strip the `content` key (for example, base64-encode it on the LLM-facing path).
|
|
178
|
+
|
|
179
|
+
JSON responses are unchanged: any action returning `application/json` (or a `…+json` type) is parsed and returned as a dict exactly as before.
|
|
180
|
+
|
|
152
181
|
## Implicit Feedback (Beta)
|
|
153
182
|
|
|
154
183
|
The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
|
|
@@ -263,7 +292,10 @@ For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()
|
|
|
263
292
|
```python
|
|
264
293
|
toolset = StackOneToolSet()
|
|
265
294
|
tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
|
|
266
|
-
|
|
295
|
+
|
|
296
|
+
# For agent-driven discovery, enable search on the constructor:
|
|
297
|
+
# toolset = StackOneToolSet(search={"method": "auto"})
|
|
298
|
+
# tools = toolset.pydantic_ai(mode="search_and_execute")
|
|
267
299
|
```
|
|
268
300
|
|
|
269
301
|
</details>
|
|
@@ -394,15 +426,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
|
|
|
394
426
|
|
|
395
427
|
```python
|
|
396
428
|
import os
|
|
429
|
+
from stackone_ai import StackOneToolSet
|
|
397
430
|
|
|
398
|
-
# Get a callable search tool
|
|
399
|
-
toolset = StackOneToolSet()
|
|
431
|
+
# Get a callable search tool — search must be enabled on the toolset
|
|
432
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
400
433
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
401
|
-
all_tools = toolset.fetch_tools(account_ids=[account_id])
|
|
402
434
|
search_tool = toolset.get_search_tool()
|
|
403
435
|
|
|
404
|
-
# Search for relevant tools — returns a Tools collection
|
|
405
|
-
tools = search_tool("manage employees", top_k=5)
|
|
436
|
+
# Search for relevant tools — returns a Tools collection scoped to the account
|
|
437
|
+
tools = search_tool("manage employees", top_k=5, account_ids=[account_id])
|
|
406
438
|
|
|
407
439
|
# Execute a discovered tool directly
|
|
408
440
|
tools[0](limit=10)
|
|
@@ -416,7 +448,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
|
|
|
416
448
|
import os
|
|
417
449
|
from stackone_ai import StackOneToolSet
|
|
418
450
|
|
|
419
|
-
|
|
451
|
+
# Search must be enabled on the constructor — pass `search={}` for defaults,
|
|
452
|
+
# or set a backend / top_k explicitly.
|
|
453
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
420
454
|
|
|
421
455
|
# Search by intent — returns Tools collection ready for any framework
|
|
422
456
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
@@ -114,6 +114,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
|
|
|
114
114
|
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
|
|
115
115
|
- Provider prefix: `["workday_*"]` matches all Workday tools
|
|
116
116
|
|
|
117
|
+
## File Downloads
|
|
118
|
+
|
|
119
|
+
Actions that return a file — e.g. `googledrive_unified_download_file`, `documents_download_file`, any `*_unified_download_file` — resolve to **raw bytes plus metadata**, not parsed JSON. The SDK decides this from the response `Content-Type`: a JSON content type is parsed as before; anything else is treated as a file download. This applies to both `tool.execute()` and `tool.call()`.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
tools = toolset.fetch_tools(actions=["googledrive_*"], account_ids=[account_id])
|
|
123
|
+
download = tools.get_tool("googledrive_unified_download_file")
|
|
124
|
+
|
|
125
|
+
result = download.execute({"id": "file-id"})
|
|
126
|
+
|
|
127
|
+
# `result` is a dict describing the file — write the bytes straight to disk:
|
|
128
|
+
with open(result["file_name"] or "download.bin", "wb") as f:
|
|
129
|
+
f.write(result["content"])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The returned dict:
|
|
133
|
+
|
|
134
|
+
| Key | Type | Description |
|
|
135
|
+
| -------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `content` | `bytes` | Raw file bytes. **Not JSON-serializable** — see the caveat below. |
|
|
137
|
+
| `content_type` | `str` | The file's MIME type (e.g. `application/pdf`), or `application/octet-stream` if unspecified. |
|
|
138
|
+
| `status_code` | `int` | HTTP status of the download response. |
|
|
139
|
+
| `headers` | `dict` | Response headers. |
|
|
140
|
+
| `file_name` | `str \| None` | Filename from the `Content-Disposition` header (handles RFC 5987 `filename*`), else `None`. |
|
|
141
|
+
|
|
142
|
+
> **Caveat:** `content` holds raw bytes, which are not JSON-serializable. If you forward tool results to an LLM — or anywhere that re-serializes them to JSON — handle or strip the `content` key (for example, base64-encode it on the LLM-facing path).
|
|
143
|
+
|
|
144
|
+
JSON responses are unchanged: any action returning `application/json` (or a `…+json` type) is parsed and returned as a dict exactly as before.
|
|
145
|
+
|
|
117
146
|
## Implicit Feedback (Beta)
|
|
118
147
|
|
|
119
148
|
The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
|
|
@@ -228,7 +257,10 @@ For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()
|
|
|
228
257
|
```python
|
|
229
258
|
toolset = StackOneToolSet()
|
|
230
259
|
tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
|
|
231
|
-
|
|
260
|
+
|
|
261
|
+
# For agent-driven discovery, enable search on the constructor:
|
|
262
|
+
# toolset = StackOneToolSet(search={"method": "auto"})
|
|
263
|
+
# tools = toolset.pydantic_ai(mode="search_and_execute")
|
|
232
264
|
```
|
|
233
265
|
|
|
234
266
|
</details>
|
|
@@ -359,15 +391,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
|
|
|
359
391
|
|
|
360
392
|
```python
|
|
361
393
|
import os
|
|
394
|
+
from stackone_ai import StackOneToolSet
|
|
362
395
|
|
|
363
|
-
# Get a callable search tool
|
|
364
|
-
toolset = StackOneToolSet()
|
|
396
|
+
# Get a callable search tool — search must be enabled on the toolset
|
|
397
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
365
398
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
366
|
-
all_tools = toolset.fetch_tools(account_ids=[account_id])
|
|
367
399
|
search_tool = toolset.get_search_tool()
|
|
368
400
|
|
|
369
|
-
# Search for relevant tools — returns a Tools collection
|
|
370
|
-
tools = search_tool("manage employees", top_k=5)
|
|
401
|
+
# Search for relevant tools — returns a Tools collection scoped to the account
|
|
402
|
+
tools = search_tool("manage employees", top_k=5, account_ids=[account_id])
|
|
371
403
|
|
|
372
404
|
# Execute a discovered tool directly
|
|
373
405
|
tools[0](limit=10)
|
|
@@ -381,7 +413,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
|
|
|
381
413
|
import os
|
|
382
414
|
from stackone_ai import StackOneToolSet
|
|
383
415
|
|
|
384
|
-
|
|
416
|
+
# Search must be enabled on the constructor — pass `search={}` for defaults,
|
|
417
|
+
# or set a backend / top_k explicitly.
|
|
418
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
385
419
|
|
|
386
420
|
# Search by intent — returns Tools collection ready for any framework
|
|
387
421
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
import base64
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
+
import re
|
|
6
7
|
from collections.abc import Sequence
|
|
7
8
|
from datetime import datetime, timezone
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypeAlias, cast
|
|
10
|
-
from urllib.parse import quote
|
|
11
|
+
from urllib.parse import quote, unquote
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
14
|
from langchain_core.tools import BaseTool
|
|
@@ -57,6 +58,47 @@ def validate_method(v: str) -> str:
|
|
|
57
58
|
return method
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def _is_json_content_type(content_type: str) -> bool:
|
|
62
|
+
"""Whether a response body should be parsed as JSON based on its Content-Type.
|
|
63
|
+
|
|
64
|
+
Only genuine JSON media types are parsed (``application/json`` and structured
|
|
65
|
+
suffixes such as ``application/problem+json``). Anything else - including a
|
|
66
|
+
missing Content-Type - is treated as opaque content (a file download), so the
|
|
67
|
+
raw bytes are returned instead of being force-decoded as UTF-8/JSON. This mirrors
|
|
68
|
+
how the StackOne generated SDKs default unknown bodies to ``application/octet-stream``.
|
|
69
|
+
"""
|
|
70
|
+
media_type = content_type.split(";", 1)[0].strip().lower()
|
|
71
|
+
return media_type == "application/json" or media_type.endswith("+json")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _filename_from_content_disposition(value: str | None) -> str | None:
|
|
75
|
+
"""Extract the filename from a Content-Disposition header value, if present.
|
|
76
|
+
|
|
77
|
+
Handles both the plain ``filename="example.pdf"`` form and the RFC 5987 extended
|
|
78
|
+
``filename*=UTF-8''example%20file.pdf`` form (which takes precedence when present).
|
|
79
|
+
The extended form is percent-decoded using its declared charset (RFC 5987 permits
|
|
80
|
+
both ``UTF-8`` and ``ISO-8859-1``); an unknown or empty charset falls back to UTF-8.
|
|
81
|
+
"""
|
|
82
|
+
if not value:
|
|
83
|
+
return None
|
|
84
|
+
extended = re.search(r"filename\*\s*=\s*([^']*)'[^']*'([^;]+)", value, re.IGNORECASE)
|
|
85
|
+
if extended:
|
|
86
|
+
charset = extended.group(1).strip() or "utf-8"
|
|
87
|
+
encoded = extended.group(2).strip().strip('"')
|
|
88
|
+
try:
|
|
89
|
+
return unquote(encoded, encoding=charset, errors="replace") or None
|
|
90
|
+
except LookupError:
|
|
91
|
+
# Unrecognised charset label - decode as UTF-8 rather than failing.
|
|
92
|
+
return unquote(encoded, encoding="utf-8", errors="replace") or None
|
|
93
|
+
quoted = re.search(r'filename\s*=\s*"([^"]*)"', value, re.IGNORECASE)
|
|
94
|
+
if quoted:
|
|
95
|
+
return quoted.group(1).strip() or None
|
|
96
|
+
bare = re.search(r"filename\s*=\s*([^;]+)", value, re.IGNORECASE)
|
|
97
|
+
if bare:
|
|
98
|
+
return bare.group(1).strip().strip('"') or None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
60
102
|
class ExecuteConfig(BaseModel):
|
|
61
103
|
"""Configuration for executing a tool against an API endpoint"""
|
|
62
104
|
|
|
@@ -206,7 +248,14 @@ class StackOneTool(BaseModel):
|
|
|
206
248
|
options: Execution options (e.g. feedback metadata)
|
|
207
249
|
|
|
208
250
|
Returns:
|
|
209
|
-
API response as dict
|
|
251
|
+
For JSON responses, the parsed API response as a dict.
|
|
252
|
+
|
|
253
|
+
For file downloads (any non-JSON Content-Type, e.g. a
|
|
254
|
+
``documents_download_file`` action), a dict describing the file:
|
|
255
|
+
``{"content": <bytes>, "content_type": str, "status_code": int,
|
|
256
|
+
"headers": dict, "file_name": str | None}``. Note ``content`` holds
|
|
257
|
+
the raw bytes and is therefore not JSON-serializable - callers that
|
|
258
|
+
re-serialize tool results (e.g. for an LLM) should handle this key.
|
|
210
259
|
|
|
211
260
|
Raises:
|
|
212
261
|
StackOneAPIError: If the API request fails
|
|
@@ -257,9 +306,23 @@ class StackOneTool(BaseModel):
|
|
|
257
306
|
response_status = response.status_code
|
|
258
307
|
response.raise_for_status()
|
|
259
308
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
309
|
+
content_type = response.headers.get("content-type", "")
|
|
310
|
+
if _is_json_content_type(content_type):
|
|
311
|
+
result = response.json()
|
|
312
|
+
result_payload = cast(JsonDict, result) if isinstance(result, dict) else {"result": result}
|
|
313
|
+
return result_payload
|
|
314
|
+
|
|
315
|
+
# Non-JSON bodies are file downloads (e.g. documents_download_file), which the
|
|
316
|
+
# API serves as raw binary with the file's own MIME type and a Content-Disposition
|
|
317
|
+
# header. Return the bytes plus metadata rather than forcing a JSON/UTF-8 decode.
|
|
318
|
+
# The shape mirrors the StackOne generated SDKs' download response.
|
|
319
|
+
return {
|
|
320
|
+
"content": response.content,
|
|
321
|
+
"content_type": content_type or "application/octet-stream",
|
|
322
|
+
"status_code": response.status_code,
|
|
323
|
+
"headers": dict(response.headers),
|
|
324
|
+
"file_name": _filename_from_content_disposition(response.headers.get("content-disposition")),
|
|
325
|
+
}
|
|
263
326
|
|
|
264
327
|
except json.JSONDecodeError as exc:
|
|
265
328
|
status = "error"
|
|
@@ -7,6 +7,7 @@ import fnmatch
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import threading
|
|
11
12
|
from collections.abc import Coroutine, Sequence
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -92,6 +93,14 @@ _RPC_PARAMETER_LOCATIONS = {
|
|
|
92
93
|
}
|
|
93
94
|
_USER_AGENT = f"stackone-ai-python/{_SDK_VERSION}"
|
|
94
95
|
|
|
96
|
+
# Param-style pinned on the /mcp tool-listing URL. The MCP schema and the RPC-execution unwrap
|
|
97
|
+
# (_split_envelope_params) must agree on this, so it is pinned rather than following the server
|
|
98
|
+
# default — the server default is free to change without breaking the SDK.
|
|
99
|
+
_MCP_PARAM_STYLE = "flat_prefixed"
|
|
100
|
+
|
|
101
|
+
# Matches a flat_prefixed envelope key: `<location>_<field>` (e.g. `path_id`, `query_limit`).
|
|
102
|
+
_FLAT_ENVELOPE_KEY_PATTERN = re.compile(r"^(path|query|body|headers)_(.+)$")
|
|
103
|
+
|
|
95
104
|
|
|
96
105
|
# --- Internal tool_search + tool_execute ---
|
|
97
106
|
|
|
@@ -444,24 +453,17 @@ class _StackOneRpcTool(StackOneTool):
|
|
|
444
453
|
) -> dict[str, Any]:
|
|
445
454
|
parsed_arguments = self._parse_arguments(arguments)
|
|
446
455
|
|
|
447
|
-
|
|
448
|
-
headers_payload = self._extract_record(parsed_arguments.pop("headers", None))
|
|
449
|
-
path_payload = self._extract_record(parsed_arguments.pop("path", None))
|
|
450
|
-
query_payload = self._extract_record(parsed_arguments.pop("query", None))
|
|
451
|
-
|
|
452
|
-
rpc_body: dict[str, Any] = dict(body_payload or {})
|
|
453
|
-
for key, value in parsed_arguments.items():
|
|
454
|
-
rpc_body[key] = value
|
|
456
|
+
envelope = self._split_envelope_params(parsed_arguments)
|
|
455
457
|
|
|
456
458
|
payload: dict[str, Any] = {
|
|
457
459
|
"action": self.name,
|
|
458
|
-
"body":
|
|
459
|
-
"headers": self._build_action_headers(
|
|
460
|
+
"body": envelope["body"],
|
|
461
|
+
"headers": self._build_action_headers(envelope["headers"] or None),
|
|
460
462
|
}
|
|
461
|
-
if
|
|
462
|
-
payload["path"] =
|
|
463
|
-
if
|
|
464
|
-
payload["query"] =
|
|
463
|
+
if envelope["path"]:
|
|
464
|
+
payload["path"] = envelope["path"]
|
|
465
|
+
if envelope["query"]:
|
|
466
|
+
payload["query"] = envelope["query"]
|
|
465
467
|
|
|
466
468
|
return super().execute(payload, options=options)
|
|
467
469
|
|
|
@@ -477,10 +479,28 @@ class _StackOneRpcTool(StackOneTool):
|
|
|
477
479
|
return dict(parsed)
|
|
478
480
|
|
|
479
481
|
@staticmethod
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
def _split_envelope_params(params: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
483
|
+
"""Split LLM-supplied tool arguments into the RPC envelope (path/query/headers/body).
|
|
484
|
+
|
|
485
|
+
Tools are listed with ``?param-style=flat_prefixed``, so keys arrive as
|
|
486
|
+
``<location>_<field>`` (for example ``path_id``, ``query_limit``). The prefix carries
|
|
487
|
+
the parameter location, so the split needs no per-action schema. A bare dict-valued
|
|
488
|
+
``path``/``query``/``headers``/``body`` key is still bucketed for clients holding a
|
|
489
|
+
cached nested schema, and any other key falls through to the body.
|
|
490
|
+
"""
|
|
491
|
+
buckets: dict[str, dict[str, Any]] = {"path": {}, "query": {}, "headers": {}, "body": {}}
|
|
492
|
+
for key, value in params.items():
|
|
493
|
+
match = _FLAT_ENVELOPE_KEY_PATTERN.match(key)
|
|
494
|
+
if match:
|
|
495
|
+
location, field = match.group(1), match.group(2)
|
|
496
|
+
buckets[location].setdefault(field, value)
|
|
497
|
+
continue
|
|
498
|
+
if key in ("path", "query", "headers", "body") and isinstance(value, dict):
|
|
499
|
+
for field, field_value in value.items():
|
|
500
|
+
buckets[key].setdefault(field, field_value)
|
|
501
|
+
continue
|
|
502
|
+
buckets["body"][key] = value
|
|
503
|
+
return buckets
|
|
484
504
|
|
|
485
505
|
def _build_action_headers(self, additional_headers: dict[str, Any] | None) -> dict[str, str]:
|
|
486
506
|
headers: dict[str, str] = {}
|
|
@@ -645,13 +665,14 @@ class StackOneToolSet:
|
|
|
645
665
|
|
|
646
666
|
Example::
|
|
647
667
|
|
|
648
|
-
toolset = StackOneToolSet()
|
|
668
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
649
669
|
search_tool = toolset.get_search_tool()
|
|
650
670
|
tools = search_tool("manage employee records", account_ids=["acc-123"])
|
|
651
671
|
"""
|
|
652
672
|
if self._search_config is None:
|
|
653
673
|
raise ToolsetConfigError(
|
|
654
|
-
"Search is disabled.
|
|
674
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
675
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
655
676
|
)
|
|
656
677
|
|
|
657
678
|
config: SearchConfig = {**self._search_config}
|
|
@@ -664,7 +685,8 @@ class StackOneToolSet:
|
|
|
664
685
|
"""Build tool_search + tool_execute tools scoped to this toolset."""
|
|
665
686
|
if self._search_config is None:
|
|
666
687
|
raise ToolsetConfigError(
|
|
667
|
-
"Search is disabled.
|
|
688
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
689
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
668
690
|
)
|
|
669
691
|
|
|
670
692
|
if account_ids:
|
|
@@ -713,8 +735,8 @@ class StackOneToolSet:
|
|
|
713
735
|
toolset = StackOneToolSet()
|
|
714
736
|
tools = toolset.openai()
|
|
715
737
|
|
|
716
|
-
# Meta tools for agent-driven discovery
|
|
717
|
-
toolset = StackOneToolSet()
|
|
738
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
739
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
718
740
|
tools = toolset.openai(mode="search_and_execute")
|
|
719
741
|
"""
|
|
720
742
|
effective_account_ids = account_ids or (
|
|
@@ -783,7 +805,8 @@ class StackOneToolSet:
|
|
|
783
805
|
tools = toolset.pydantic_ai()
|
|
784
806
|
agent = Agent("openai:gpt-5.4", tools=tools)
|
|
785
807
|
|
|
786
|
-
# Meta tools for agent-driven discovery
|
|
808
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
809
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
787
810
|
tools = toolset.pydantic_ai(mode="search_and_execute")
|
|
788
811
|
"""
|
|
789
812
|
effective_account_ids = account_ids or (
|
|
@@ -929,7 +952,8 @@ class StackOneToolSet:
|
|
|
929
952
|
"""
|
|
930
953
|
if self._search_config is None:
|
|
931
954
|
raise ToolsetConfigError(
|
|
932
|
-
"Search is disabled.
|
|
955
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
956
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
933
957
|
)
|
|
934
958
|
|
|
935
959
|
# Merge constructor defaults with per-call overrides
|
|
@@ -1082,7 +1106,8 @@ class StackOneToolSet:
|
|
|
1082
1106
|
"""
|
|
1083
1107
|
if self._search_config is None:
|
|
1084
1108
|
raise ToolsetConfigError(
|
|
1085
|
-
"Search is disabled.
|
|
1109
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
1110
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
1086
1111
|
)
|
|
1087
1112
|
|
|
1088
1113
|
# Merge constructor defaults with per-call overrides
|
|
@@ -1235,7 +1260,7 @@ class StackOneToolSet:
|
|
|
1235
1260
|
if cached is not None:
|
|
1236
1261
|
return cached
|
|
1237
1262
|
|
|
1238
|
-
endpoint = f"{self.base_url.rstrip('/')}/mcp"
|
|
1263
|
+
endpoint = f"{self.base_url.rstrip('/')}/mcp?param-style={_MCP_PARAM_STYLE}"
|
|
1239
1264
|
|
|
1240
1265
|
def _fetch_for_account(account: str | None) -> list[StackOneTool]:
|
|
1241
1266
|
headers = self._build_mcp_headers(account)
|
|
@@ -662,3 +662,21 @@ class TestToolIndexCache:
|
|
|
662
662
|
toolset.search_tools("bar")
|
|
663
663
|
|
|
664
664
|
assert build_count["count"] == 2
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class TestMcpParamStylePinning:
|
|
668
|
+
"""The /mcp listing URL must pin param-style so the schema matches the RPC unwrap."""
|
|
669
|
+
|
|
670
|
+
def test_fetch_tools_pins_flat_prefixed_param_style(self, monkeypatch):
|
|
671
|
+
captured: dict[str, str] = {}
|
|
672
|
+
|
|
673
|
+
def fake_fetch(endpoint: str, headers: dict[str, str]) -> list[_McpToolDefinition]:
|
|
674
|
+
captured["endpoint"] = endpoint
|
|
675
|
+
return []
|
|
676
|
+
|
|
677
|
+
monkeypatch.setattr("stackone_ai.toolset._fetch_mcp_tools", fake_fetch)
|
|
678
|
+
|
|
679
|
+
toolset = StackOneToolSet(api_key="test-key", base_url="https://api.example.com")
|
|
680
|
+
toolset.fetch_tools(account_ids=["acc1"])
|
|
681
|
+
|
|
682
|
+
assert captured["endpoint"] == "https://api.example.com/mcp?param-style=flat_prefixed"
|