stackone-ai 2.9.0__tar.gz → 2.9.1__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.9.1/.release-please-manifest.json +3 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/CHANGELOG.md +8 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/PKG-INFO +42 -8
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/README.md +41 -7
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/pyproject.toml +1 -1
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/__init__.py +1 -1
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/models.py +68 -5
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/toolset.py +13 -8
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_tool_calling.py +186 -1
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/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.9.1}/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/examples-standards.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/nix-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/no-relative-imports.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/package-installation.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/release-please-standards.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/uv-scripts.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/examples-standards.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/no-relative-imports.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/package-installation.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/release-please-standards.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/uv-scripts.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.env.example +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.envrc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/dependabot.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake-update.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitignore +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitleaks.toml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitmodules +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.mcp.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.release-please-config.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/CLAUDE.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/LICENSE +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/auth_management.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/crewai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/langchain_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/langgraph_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/openai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/pydantic_ai_integration.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/search_tools.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/test_examples.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/flake.lock +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/flake.nix +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/justfile +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/scripts/benchmark_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/constants.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/feedback/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/feedback/tool.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/integrations/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/integrations/langgraph.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/local_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/py.typed +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/semantic_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/__init__.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/normalize.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/tfidf_index.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/conftest.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/mocks/serve.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_agent_tools.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_feedback.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_fetch_tools.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_integrations_langgraph.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_integrations_pydantic_ai.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_local_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_models.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_semantic_search.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_tfidf_index.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_toolset.py +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.envrc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitignore +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.mcp.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/AGENTS.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CLAUDE.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/LICENSE +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/README.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/README.md +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/package.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.lock +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.nix +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/knip.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/lefthook.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/node.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/package.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/consts.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/index.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/schema.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/types.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsconfig.json +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.config.ts +0 -0
- {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.9.1](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.9.0...stackone-ai-v2.9.1) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **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))
|
|
9
|
+
* **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))
|
|
10
|
+
|
|
3
11
|
## [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
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackone-ai
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.1
|
|
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"
|
|
@@ -645,13 +645,14 @@ class StackOneToolSet:
|
|
|
645
645
|
|
|
646
646
|
Example::
|
|
647
647
|
|
|
648
|
-
toolset = StackOneToolSet()
|
|
648
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
649
649
|
search_tool = toolset.get_search_tool()
|
|
650
650
|
tools = search_tool("manage employee records", account_ids=["acc-123"])
|
|
651
651
|
"""
|
|
652
652
|
if self._search_config is None:
|
|
653
653
|
raise ToolsetConfigError(
|
|
654
|
-
"Search is disabled.
|
|
654
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
655
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
655
656
|
)
|
|
656
657
|
|
|
657
658
|
config: SearchConfig = {**self._search_config}
|
|
@@ -664,7 +665,8 @@ class StackOneToolSet:
|
|
|
664
665
|
"""Build tool_search + tool_execute tools scoped to this toolset."""
|
|
665
666
|
if self._search_config is None:
|
|
666
667
|
raise ToolsetConfigError(
|
|
667
|
-
"Search is disabled.
|
|
668
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
669
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
668
670
|
)
|
|
669
671
|
|
|
670
672
|
if account_ids:
|
|
@@ -713,8 +715,8 @@ class StackOneToolSet:
|
|
|
713
715
|
toolset = StackOneToolSet()
|
|
714
716
|
tools = toolset.openai()
|
|
715
717
|
|
|
716
|
-
# Meta tools for agent-driven discovery
|
|
717
|
-
toolset = StackOneToolSet()
|
|
718
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
719
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
718
720
|
tools = toolset.openai(mode="search_and_execute")
|
|
719
721
|
"""
|
|
720
722
|
effective_account_ids = account_ids or (
|
|
@@ -783,7 +785,8 @@ class StackOneToolSet:
|
|
|
783
785
|
tools = toolset.pydantic_ai()
|
|
784
786
|
agent = Agent("openai:gpt-5.4", tools=tools)
|
|
785
787
|
|
|
786
|
-
# Meta tools for agent-driven discovery
|
|
788
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
789
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
787
790
|
tools = toolset.pydantic_ai(mode="search_and_execute")
|
|
788
791
|
"""
|
|
789
792
|
effective_account_ids = account_ids or (
|
|
@@ -929,7 +932,8 @@ class StackOneToolSet:
|
|
|
929
932
|
"""
|
|
930
933
|
if self._search_config is None:
|
|
931
934
|
raise ToolsetConfigError(
|
|
932
|
-
"Search is disabled.
|
|
935
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
936
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
933
937
|
)
|
|
934
938
|
|
|
935
939
|
# Merge constructor defaults with per-call overrides
|
|
@@ -1082,7 +1086,8 @@ class StackOneToolSet:
|
|
|
1082
1086
|
"""
|
|
1083
1087
|
if self._search_config is None:
|
|
1084
1088
|
raise ToolsetConfigError(
|
|
1085
|
-
"Search is disabled.
|
|
1089
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
1090
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
1086
1091
|
)
|
|
1087
1092
|
|
|
1088
1093
|
# Merge constructor defaults with per-call overrides
|
|
@@ -7,7 +7,12 @@ import pytest
|
|
|
7
7
|
import respx
|
|
8
8
|
|
|
9
9
|
from stackone_ai import StackOneTool
|
|
10
|
-
from stackone_ai.models import
|
|
10
|
+
from stackone_ai.models import (
|
|
11
|
+
ExecuteConfig,
|
|
12
|
+
ToolParameters,
|
|
13
|
+
_filename_from_content_disposition,
|
|
14
|
+
_is_json_content_type,
|
|
15
|
+
)
|
|
11
16
|
from stackone_ai.toolset import _StackOneRpcTool
|
|
12
17
|
from tests.conftest import TEST_BASE_URL
|
|
13
18
|
|
|
@@ -332,3 +337,183 @@ class TestStackOneRpcTool:
|
|
|
332
337
|
assert rpc_tool._extract_record("string") is None
|
|
333
338
|
assert rpc_tool._extract_record(123) is None
|
|
334
339
|
assert rpc_tool._extract_record(None) is None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TestBinaryDownloadResponse:
|
|
343
|
+
"""File-download actions return raw bytes + metadata instead of failing on JSON parsing.
|
|
344
|
+
|
|
345
|
+
The StackOne API serves file downloads as raw binary with the file's own MIME type
|
|
346
|
+
(e.g. application/pdf) and a Content-Disposition header - never JSON. The returned
|
|
347
|
+
shape mirrors the StackOne generated SDKs' download response (content + content_type +
|
|
348
|
+
status_code + headers), with content as raw bytes (the Python analog of the Java
|
|
349
|
+
client's byte[] body / the TypeScript client's response stream).
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
@respx.mock
|
|
353
|
+
def test_binary_response_returns_content_dict(self, mock_tool):
|
|
354
|
+
"""A non-JSON (binary) body is returned as bytes + metadata, not JSON-parsed."""
|
|
355
|
+
# Leading bytes of a real PDF; the 0xc4 byte is invalid UTF-8 and is exactly
|
|
356
|
+
# what makes the unconditional response.json() raise UnicodeDecodeError.
|
|
357
|
+
pdf_bytes = b"%PDF-1.4\n%\xc4\xe5\xf2\xe5\xeb\xa7\xf3\xa0\xd0\xc4\xc6\n1 0 obj\n"
|
|
358
|
+
respx.post("https://api.example.com/test").mock(
|
|
359
|
+
return_value=httpx.Response(
|
|
360
|
+
200,
|
|
361
|
+
headers={
|
|
362
|
+
"content-type": "application/pdf",
|
|
363
|
+
"content-disposition": 'attachment; filename="download.pdf"',
|
|
364
|
+
},
|
|
365
|
+
content=pdf_bytes,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
result = mock_tool.execute({"name": "report", "value": 1})
|
|
370
|
+
|
|
371
|
+
assert result["content"] == pdf_bytes
|
|
372
|
+
assert result["content_type"] == "application/pdf"
|
|
373
|
+
assert result["status_code"] == 200
|
|
374
|
+
assert result["file_name"] == "download.pdf"
|
|
375
|
+
assert result["headers"]["content-type"] == "application/pdf"
|
|
376
|
+
|
|
377
|
+
@respx.mock
|
|
378
|
+
def test_rpc_download_action_returns_content_dict(self):
|
|
379
|
+
"""The RPC download path (e.g. googledrive_unified_download_file) returns bytes.
|
|
380
|
+
|
|
381
|
+
Reproduces the reported failure: a download action invoked through /actions/rpc
|
|
382
|
+
previously raised UnicodeDecodeError because the binary body was JSON-parsed.
|
|
383
|
+
"""
|
|
384
|
+
parameters = ToolParameters(
|
|
385
|
+
type="object",
|
|
386
|
+
properties={"id": {"type": "string", "description": "File ID"}},
|
|
387
|
+
)
|
|
388
|
+
tool = _StackOneRpcTool(
|
|
389
|
+
name="googledrive_unified_download_file",
|
|
390
|
+
description="Download a file",
|
|
391
|
+
parameters=parameters,
|
|
392
|
+
api_key="test_api_key",
|
|
393
|
+
base_url=TEST_BASE_URL,
|
|
394
|
+
account_id="test_account",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
rtf_bytes = b"{\\rtf1\\ansi\\ansicpg1252\\\xc4\xe5 hello}"
|
|
398
|
+
respx.post(f"{TEST_BASE_URL}/actions/rpc").mock(
|
|
399
|
+
return_value=httpx.Response(
|
|
400
|
+
200,
|
|
401
|
+
headers={
|
|
402
|
+
"content-type": "application/rtf",
|
|
403
|
+
"content-disposition": 'attachment; filename="download.rtf"',
|
|
404
|
+
},
|
|
405
|
+
content=rtf_bytes,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
result = tool.execute({"path": {"id": "file-123"}})
|
|
410
|
+
|
|
411
|
+
assert result["content"] == rtf_bytes
|
|
412
|
+
assert result["content_type"] == "application/rtf"
|
|
413
|
+
assert result["file_name"] == "download.rtf"
|
|
414
|
+
|
|
415
|
+
@respx.mock
|
|
416
|
+
def test_octet_stream_without_filename(self, mock_tool):
|
|
417
|
+
"""A binary body with no Content-Disposition still returns content with file_name=None."""
|
|
418
|
+
blob = b"\x00\x01\x02\xc4\xff\xfe"
|
|
419
|
+
respx.post("https://api.example.com/test").mock(
|
|
420
|
+
return_value=httpx.Response(
|
|
421
|
+
200,
|
|
422
|
+
headers={"content-type": "application/octet-stream"},
|
|
423
|
+
content=blob,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
result = mock_tool.execute({})
|
|
428
|
+
|
|
429
|
+
assert result["content"] == blob
|
|
430
|
+
assert result["content_type"] == "application/octet-stream"
|
|
431
|
+
assert result["file_name"] is None
|
|
432
|
+
|
|
433
|
+
@respx.mock
|
|
434
|
+
def test_json_response_still_parsed(self, mock_tool):
|
|
435
|
+
"""Regression guard: JSON responses are unchanged - parsed to a dict, not wrapped."""
|
|
436
|
+
respx.post("https://api.example.com/test").mock(
|
|
437
|
+
return_value=httpx.Response(200, json={"id": "123", "ok": True})
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
result = mock_tool.execute({"name": "x", "value": 1})
|
|
441
|
+
|
|
442
|
+
assert result == {"id": "123", "ok": True}
|
|
443
|
+
assert "content" not in result
|
|
444
|
+
|
|
445
|
+
@respx.mock
|
|
446
|
+
def test_json_with_charset_param_still_parsed(self, mock_tool):
|
|
447
|
+
"""A JSON Content-Type with parameters (charset) is still parsed as JSON."""
|
|
448
|
+
respx.post("https://api.example.com/test").mock(
|
|
449
|
+
return_value=httpx.Response(
|
|
450
|
+
200,
|
|
451
|
+
headers={"content-type": "application/json; charset=utf-8"},
|
|
452
|
+
content=b'{"ok": true}',
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
result = mock_tool.execute({})
|
|
457
|
+
|
|
458
|
+
assert result == {"ok": True}
|
|
459
|
+
|
|
460
|
+
@respx.mock
|
|
461
|
+
def test_missing_content_type_returns_bytes(self, mock_tool):
|
|
462
|
+
"""A body with no Content-Type is treated as opaque content (bytes), not JSON.
|
|
463
|
+
|
|
464
|
+
Pins the deliberate contract: the SDK trusts Content-Type to decide JSON vs
|
|
465
|
+
file, so an absent Content-Type is returned as raw bytes rather than risking
|
|
466
|
+
a UTF-8/JSON decode of binary. (StackOne always labels JSON as application/json.)
|
|
467
|
+
"""
|
|
468
|
+
blob = b"\xff\xd8\xff\xe0\x00\x10JFIF" # JPEG magic bytes, no content-type
|
|
469
|
+
respx.post("https://api.example.com/test").mock(return_value=httpx.Response(200, content=blob))
|
|
470
|
+
|
|
471
|
+
result = mock_tool.execute({})
|
|
472
|
+
|
|
473
|
+
assert result["content"] == blob
|
|
474
|
+
assert result["content_type"] == "application/octet-stream"
|
|
475
|
+
assert result["file_name"] is None
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class TestResponseHelpers:
|
|
479
|
+
"""Unit tests for the Content-Type and Content-Disposition helpers."""
|
|
480
|
+
|
|
481
|
+
@pytest.mark.parametrize(
|
|
482
|
+
("content_type", "expected"),
|
|
483
|
+
[
|
|
484
|
+
("application/json", True),
|
|
485
|
+
("application/json; charset=utf-8", True),
|
|
486
|
+
("APPLICATION/JSON", True),
|
|
487
|
+
("application/problem+json", True),
|
|
488
|
+
("application/vnd.api+json", True),
|
|
489
|
+
("", False),
|
|
490
|
+
("application/pdf", False),
|
|
491
|
+
("application/octet-stream", False),
|
|
492
|
+
("text/plain", False),
|
|
493
|
+
("text/json-but-not-really", False),
|
|
494
|
+
],
|
|
495
|
+
)
|
|
496
|
+
def test_is_json_content_type(self, content_type, expected):
|
|
497
|
+
assert _is_json_content_type(content_type) is expected
|
|
498
|
+
|
|
499
|
+
@pytest.mark.parametrize(
|
|
500
|
+
("header", "expected"),
|
|
501
|
+
[
|
|
502
|
+
('attachment; filename="download.pdf"', "download.pdf"),
|
|
503
|
+
("attachment; filename=download.pdf", "download.pdf"),
|
|
504
|
+
('inline; filename="my report.docx"', "my report.docx"),
|
|
505
|
+
# RFC 5987 extended form is percent-decoded and takes precedence.
|
|
506
|
+
("attachment; filename=\"fallback.txt\"; filename*=UTF-8''na%C3%AFve.txt", "naïve.txt"),
|
|
507
|
+
# Non-UTF-8 charset is honoured: 0xA3 is "£" in ISO-8859-1, not UTF-8.
|
|
508
|
+
("attachment; filename*=ISO-8859-1'en'%A3%20rates.txt", "£ rates.txt"),
|
|
509
|
+
# Unknown charset label falls back to UTF-8 instead of raising.
|
|
510
|
+
("attachment; filename*=bogus-charset''%C2%A3.txt", "£.txt"),
|
|
511
|
+
# Non-conformant quoted extended value: surrounding quotes are stripped.
|
|
512
|
+
("attachment; filename*=\"UTF-8''na%C3%AFve.txt\"", "naïve.txt"),
|
|
513
|
+
("attachment", None),
|
|
514
|
+
(None, None),
|
|
515
|
+
("", None),
|
|
516
|
+
],
|
|
517
|
+
)
|
|
518
|
+
def test_filename_from_content_disposition(self, header, expected):
|
|
519
|
+
assert _filename_from_content_disposition(header) == expected
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
gitdir: ../../.git/modules/vendor/stackone-ai-node
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|