stackone-ai 2.8.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.8.0 → stackone_ai-2.9.1}/CHANGELOG.md +15 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/PKG-INFO +81 -7
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/README.md +78 -6
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/pydantic_ai_integration.py +12 -24
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/pyproject.toml +2 -1
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/__init__.py +1 -1
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/models.py +112 -6
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/toolset.py +56 -8
- stackone_ai-2.9.1/tests/test_integrations_pydantic_ai.py +183 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_tool_calling.py +186 -1
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/uv.lock +6 -2
- stackone_ai-2.8.0/.release-please-manifest.json +0 -3
- stackone_ai-2.8.0/vendor/stackone-ai-node/.git +0 -1
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/examples-standards.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/nix-workflow.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/no-relative-imports.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/package-installation.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/release-please-standards.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.claude/rules/uv-scripts.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/examples-standards.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/no-relative-imports.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/package-installation.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/release-please-standards.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.cursor/rules/uv-scripts.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.env.example +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.envrc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/dependabot.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake-update.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.gitignore +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.gitleaks.toml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.gitmodules +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.mcp.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/.release-please-config.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/CLAUDE.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/LICENSE +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/auth_management.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/crewai_integration.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/langchain_integration.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/langgraph_integration.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/openai_integration.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/search_tools.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/examples/test_examples.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/flake.lock +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/flake.nix +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/justfile +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/scripts/benchmark_search.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/constants.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/feedback/__init__.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/feedback/tool.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/integrations/__init__.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/integrations/langgraph.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/local_search.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/py.typed +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/semantic_search.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/utils/__init__.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/utils/normalize.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/stackone_ai/utils/tfidf_index.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/conftest.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/mocks/serve.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_agent_tools.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_feedback.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_fetch_tools.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_integrations_langgraph.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_local_search.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_models.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_semantic_search.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_tfidf_index.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/tests/test_toolset.py +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.envrc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitignore +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.mcp.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/AGENTS.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CLAUDE.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/LICENSE +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/README.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/README.md +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/package.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.lock +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.nix +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/knip.config.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/lefthook.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/node.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/package.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/consts.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/index.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/schema.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/types.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsconfig.json +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.config.ts +0 -0
- {stackone_ai-2.8.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
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)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **integrations:** add native Pydantic AI support ([#184](https://github.com/StackOneHQ/stackone-ai-python/issues/184)) ([8216ab6](https://github.com/StackOneHQ/stackone-ai-python/commit/8216ab6c0776b0753cf3e68461bf5e8a9b044b06))
|
|
17
|
+
|
|
3
18
|
## [2.8.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.7.0...stackone-ai-v2.8.0) (2026-04-20)
|
|
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.9.1
|
|
4
4
|
Summary: agents performing actions on your SaaS
|
|
5
5
|
Author-email: StackOne <support@stackone.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -29,6 +29,8 @@ Requires-Dist: pydantic-ai>=1.83.0; extra == 'examples'
|
|
|
29
29
|
Requires-Dist: python-dotenv>=1.0.1; extra == 'examples'
|
|
30
30
|
Provides-Extra: mcp
|
|
31
31
|
Requires-Dist: mcp>=1.3.0; extra == 'mcp'
|
|
32
|
+
Provides-Extra: pydantic-ai
|
|
33
|
+
Requires-Dist: pydantic-ai-slim<2.0.0,>=1.83.0; extra == 'pydantic-ai'
|
|
32
34
|
Description-Content-Type: text/markdown
|
|
33
35
|
|
|
34
36
|
# StackOne AI SDK
|
|
@@ -59,6 +61,7 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug
|
|
|
59
61
|
- LangChain Tools
|
|
60
62
|
- CrewAI Tools
|
|
61
63
|
- LangGraph Tool Node
|
|
64
|
+
- Pydantic AI Toolset
|
|
62
65
|
|
|
63
66
|
## Requirements
|
|
64
67
|
|
|
@@ -146,6 +149,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
|
|
|
146
149
|
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
|
|
147
150
|
- Provider prefix: `["workday_*"]` matches all Workday tools
|
|
148
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
|
+
|
|
149
181
|
## Implicit Feedback (Beta)
|
|
150
182
|
|
|
151
183
|
The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
|
|
@@ -228,6 +260,46 @@ for tool_call in response.tool_calls:
|
|
|
228
260
|
|
|
229
261
|
</details>
|
|
230
262
|
|
|
263
|
+
<details>
|
|
264
|
+
<summary>Pydantic AI Integration</summary>
|
|
265
|
+
|
|
266
|
+
StackOne tools convert to Pydantic AI `Tool` instances via `.to_pydantic_ai()`, parallel to `.to_openai()` and `.to_langchain()`:
|
|
267
|
+
|
|
268
|
+
Prerequisites:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
pip install 'stackone-ai[pydantic-ai]'
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
import os
|
|
276
|
+
from pydantic_ai import Agent
|
|
277
|
+
from stackone_ai import StackOneToolSet
|
|
278
|
+
|
|
279
|
+
toolset = StackOneToolSet()
|
|
280
|
+
tools = toolset.fetch_tools(
|
|
281
|
+
actions=["workday_list_workers", "workday_get_worker"],
|
|
282
|
+
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
|
|
283
|
+
).to_pydantic_ai()
|
|
284
|
+
|
|
285
|
+
agent = Agent("openai:gpt-5.4", tools=tools)
|
|
286
|
+
result = agent.run_sync("List the first 5 employees")
|
|
287
|
+
print(result.output)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()` method on `StackOneToolSet` — parallel to `.openai()` / `.langchain()`:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
toolset = StackOneToolSet()
|
|
294
|
+
tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
|
|
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")
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
</details>
|
|
302
|
+
|
|
231
303
|
<details>
|
|
232
304
|
<summary>LangGraph Integration</summary>
|
|
233
305
|
|
|
@@ -354,15 +426,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
|
|
|
354
426
|
|
|
355
427
|
```python
|
|
356
428
|
import os
|
|
429
|
+
from stackone_ai import StackOneToolSet
|
|
357
430
|
|
|
358
|
-
# Get a callable search tool
|
|
359
|
-
toolset = StackOneToolSet()
|
|
431
|
+
# Get a callable search tool — search must be enabled on the toolset
|
|
432
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
360
433
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
361
|
-
all_tools = toolset.fetch_tools(account_ids=[account_id])
|
|
362
434
|
search_tool = toolset.get_search_tool()
|
|
363
435
|
|
|
364
|
-
# Search for relevant tools — returns a Tools collection
|
|
365
|
-
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])
|
|
366
438
|
|
|
367
439
|
# Execute a discovered tool directly
|
|
368
440
|
tools[0](limit=10)
|
|
@@ -376,7 +448,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
|
|
|
376
448
|
import os
|
|
377
449
|
from stackone_ai import StackOneToolSet
|
|
378
450
|
|
|
379
|
-
|
|
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"})
|
|
380
454
|
|
|
381
455
|
# Search by intent — returns Tools collection ready for any framework
|
|
382
456
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
@@ -26,6 +26,7 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug
|
|
|
26
26
|
- LangChain Tools
|
|
27
27
|
- CrewAI Tools
|
|
28
28
|
- LangGraph Tool Node
|
|
29
|
+
- Pydantic AI Toolset
|
|
29
30
|
|
|
30
31
|
## Requirements
|
|
31
32
|
|
|
@@ -113,6 +114,35 @@ tools = toolset.fetch_tools(providers=["hibob"])
|
|
|
113
114
|
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
|
|
114
115
|
- Provider prefix: `["workday_*"]` matches all Workday tools
|
|
115
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
|
+
|
|
116
146
|
## Implicit Feedback (Beta)
|
|
117
147
|
|
|
118
148
|
The Python SDK can emit implicit behavioral feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
|
|
@@ -195,6 +225,46 @@ for tool_call in response.tool_calls:
|
|
|
195
225
|
|
|
196
226
|
</details>
|
|
197
227
|
|
|
228
|
+
<details>
|
|
229
|
+
<summary>Pydantic AI Integration</summary>
|
|
230
|
+
|
|
231
|
+
StackOne tools convert to Pydantic AI `Tool` instances via `.to_pydantic_ai()`, parallel to `.to_openai()` and `.to_langchain()`:
|
|
232
|
+
|
|
233
|
+
Prerequisites:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pip install 'stackone-ai[pydantic-ai]'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import os
|
|
241
|
+
from pydantic_ai import Agent
|
|
242
|
+
from stackone_ai import StackOneToolSet
|
|
243
|
+
|
|
244
|
+
toolset = StackOneToolSet()
|
|
245
|
+
tools = toolset.fetch_tools(
|
|
246
|
+
actions=["workday_list_workers", "workday_get_worker"],
|
|
247
|
+
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
|
|
248
|
+
).to_pydantic_ai()
|
|
249
|
+
|
|
250
|
+
agent = Agent("openai:gpt-5.4", tools=tools)
|
|
251
|
+
result = agent.run_sync("List the first 5 employees")
|
|
252
|
+
print(result.output)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
For the full catalog (or the meta search/execute tools), use the `.pydantic_ai()` method on `StackOneToolSet` — parallel to `.openai()` / `.langchain()`:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
toolset = StackOneToolSet()
|
|
259
|
+
tools = toolset.pydantic_ai(account_ids=[os.environ["STACKONE_ACCOUNT_ID"]])
|
|
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")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
</details>
|
|
267
|
+
|
|
198
268
|
<details>
|
|
199
269
|
<summary>LangGraph Integration</summary>
|
|
200
270
|
|
|
@@ -321,15 +391,15 @@ Search for tools using natural language queries. Works with both semantic (cloud
|
|
|
321
391
|
|
|
322
392
|
```python
|
|
323
393
|
import os
|
|
394
|
+
from stackone_ai import StackOneToolSet
|
|
324
395
|
|
|
325
|
-
# Get a callable search tool
|
|
326
|
-
toolset = StackOneToolSet()
|
|
396
|
+
# Get a callable search tool — search must be enabled on the toolset
|
|
397
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
327
398
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
328
|
-
all_tools = toolset.fetch_tools(account_ids=[account_id])
|
|
329
399
|
search_tool = toolset.get_search_tool()
|
|
330
400
|
|
|
331
|
-
# Search for relevant tools — returns a Tools collection
|
|
332
|
-
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])
|
|
333
403
|
|
|
334
404
|
# Execute a discovered tool directly
|
|
335
405
|
tools[0](limit=10)
|
|
@@ -343,7 +413,9 @@ Discover tools using natural language instead of exact names. Queries like "onbo
|
|
|
343
413
|
import os
|
|
344
414
|
from stackone_ai import StackOneToolSet
|
|
345
415
|
|
|
346
|
-
|
|
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"})
|
|
347
419
|
|
|
348
420
|
# Search by intent — returns Tools collection ready for any framework
|
|
349
421
|
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
```bash
|
|
4
4
|
uv run examples/pydantic_ai_integration.py
|
|
5
5
|
```
|
|
6
|
+
|
|
7
|
+
Install with `pip install 'stackone-ai[pydantic-ai]'` (or
|
|
8
|
+
`pip install 'stackone-ai[examples]'` to run this file).
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
9
12
|
|
|
10
|
-
import json
|
|
11
13
|
import os
|
|
12
14
|
|
|
13
15
|
try:
|
|
@@ -18,31 +20,15 @@ except ModuleNotFoundError:
|
|
|
18
20
|
pass
|
|
19
21
|
|
|
20
22
|
try:
|
|
21
|
-
from pydantic_ai import Agent
|
|
23
|
+
from pydantic_ai import Agent
|
|
22
24
|
except ImportError:
|
|
23
|
-
print("Install pydantic-ai to run this example: pip install pydantic-ai")
|
|
25
|
+
print("Install pydantic-ai to run this example: pip install 'stackone-ai[pydantic-ai]'")
|
|
24
26
|
raise SystemExit(1) from None
|
|
25
27
|
|
|
26
28
|
from stackone_ai import StackOneToolSet
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def _to_pydantic_ai_tool(stackone_tool) -> Tool:
|
|
30
|
-
"""Convert a StackOneTool to a Pydantic AI Tool with the proper JSON schema."""
|
|
31
|
-
params_schema = stackone_tool.to_openai_function()["function"]["parameters"]
|
|
32
|
-
|
|
33
|
-
def execute(**kwargs: object) -> str:
|
|
34
|
-
return json.dumps(stackone_tool.execute(kwargs))
|
|
35
|
-
|
|
36
|
-
return Tool.from_schema(
|
|
37
|
-
execute,
|
|
38
|
-
name=stackone_tool.name,
|
|
39
|
-
description=stackone_tool.description,
|
|
40
|
-
json_schema=params_schema,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
31
|
def pydantic_ai_integration() -> None:
|
|
45
|
-
account_id = os.getenv("STACKONE_ACCOUNT_ID")
|
|
46
32
|
for var in ["STACKONE_API_KEY", "STACKONE_ACCOUNT_ID", "OPENAI_API_KEY"]:
|
|
47
33
|
if not os.getenv(var):
|
|
48
34
|
print(f"Set {var} to run this example.")
|
|
@@ -51,12 +37,14 @@ def pydantic_ai_integration() -> None:
|
|
|
51
37
|
toolset = StackOneToolSet()
|
|
52
38
|
tools = toolset.fetch_tools(
|
|
53
39
|
actions=["workday_list_workers", "workday_get_worker", "workday_get_current_user"],
|
|
54
|
-
account_ids=[
|
|
55
|
-
)
|
|
56
|
-
pydantic_tools = [_to_pydantic_ai_tool(t) for t in tools]
|
|
57
|
-
print(f"Loaded {len(pydantic_tools)} tools for Pydantic AI.")
|
|
40
|
+
account_ids=[os.environ["STACKONE_ACCOUNT_ID"]],
|
|
41
|
+
).to_pydantic_ai()
|
|
58
42
|
|
|
59
|
-
agent = Agent(
|
|
43
|
+
agent = Agent(
|
|
44
|
+
"openai:gpt-5.4",
|
|
45
|
+
system_prompt="You are a helpful HR assistant.",
|
|
46
|
+
tools=tools,
|
|
47
|
+
)
|
|
60
48
|
result = agent.run_sync("List the first 5 employees")
|
|
61
49
|
print(f"Result:\n{result.output}")
|
|
62
50
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stackone-ai"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.9.1"
|
|
4
4
|
description = "agents performing actions on your SaaS"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -37,6 +37,7 @@ packages = ["stackone_ai"]
|
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
mcp = ["mcp>=1.3.0"]
|
|
40
|
+
pydantic-ai = ["pydantic-ai-slim>=1.83.0,<2.0.0"]
|
|
40
41
|
examples = [
|
|
41
42
|
"crewai>=0.102.0",
|
|
42
43
|
"langchain-openai>=1.1.14",
|
|
@@ -3,16 +3,20 @@ 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
|
-
from typing import Annotated, Any, ClassVar, TypeAlias, cast
|
|
10
|
-
from urllib.parse import quote
|
|
10
|
+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypeAlias, cast
|
|
11
|
+
from urllib.parse import quote, unquote
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
14
|
from langchain_core.tools import BaseTool
|
|
14
15
|
from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr
|
|
15
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pydantic_ai.tools import Tool as PydanticAITool
|
|
19
|
+
|
|
16
20
|
# Type aliases for common types
|
|
17
21
|
JsonDict: TypeAlias = dict[str, Any]
|
|
18
22
|
Headers: TypeAlias = dict[str, str]
|
|
@@ -54,6 +58,47 @@ def validate_method(v: str) -> str:
|
|
|
54
58
|
return method
|
|
55
59
|
|
|
56
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
|
+
|
|
57
102
|
class ExecuteConfig(BaseModel):
|
|
58
103
|
"""Configuration for executing a tool against an API endpoint"""
|
|
59
104
|
|
|
@@ -203,7 +248,14 @@ class StackOneTool(BaseModel):
|
|
|
203
248
|
options: Execution options (e.g. feedback metadata)
|
|
204
249
|
|
|
205
250
|
Returns:
|
|
206
|
-
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.
|
|
207
259
|
|
|
208
260
|
Raises:
|
|
209
261
|
StackOneAPIError: If the API request fails
|
|
@@ -254,9 +306,23 @@ class StackOneTool(BaseModel):
|
|
|
254
306
|
response_status = response.status_code
|
|
255
307
|
response.raise_for_status()
|
|
256
308
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
326
|
|
|
261
327
|
except json.JSONDecodeError as exc:
|
|
262
328
|
status = "error"
|
|
@@ -467,6 +533,36 @@ class StackOneTool(BaseModel):
|
|
|
467
533
|
|
|
468
534
|
return StackOneLangChainTool()
|
|
469
535
|
|
|
536
|
+
def to_pydantic_ai_tool(self) -> PydanticAITool:
|
|
537
|
+
"""Convert this tool to a Pydantic AI ``Tool``.
|
|
538
|
+
|
|
539
|
+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
A ``pydantic_ai.tools.Tool`` ready to pass to ``Agent(tools=[...])``.
|
|
543
|
+
"""
|
|
544
|
+
try:
|
|
545
|
+
from pydantic_ai.tools import Tool
|
|
546
|
+
except ImportError as e:
|
|
547
|
+
raise ImportError(
|
|
548
|
+
"Install `pydantic-ai-slim` (or `stackone-ai[pydantic-ai]`) "
|
|
549
|
+
"to use the Pydantic AI integration."
|
|
550
|
+
) from e
|
|
551
|
+
|
|
552
|
+
openai_function = self.to_openai_function()
|
|
553
|
+
json_schema = openai_function["function"]["parameters"]
|
|
554
|
+
parent_tool = self
|
|
555
|
+
|
|
556
|
+
def implementation(**kwargs: Any) -> Any:
|
|
557
|
+
return parent_tool.execute(kwargs)
|
|
558
|
+
|
|
559
|
+
return Tool.from_schema(
|
|
560
|
+
function=implementation,
|
|
561
|
+
name=self.name,
|
|
562
|
+
description=self.description,
|
|
563
|
+
json_schema=json_schema,
|
|
564
|
+
)
|
|
565
|
+
|
|
470
566
|
def set_account_id(self, account_id: str | None) -> None:
|
|
471
567
|
"""Set the account ID for this tool
|
|
472
568
|
|
|
@@ -577,3 +673,13 @@ class Tools:
|
|
|
577
673
|
Sequence of tools in LangChain format
|
|
578
674
|
"""
|
|
579
675
|
return [tool.to_langchain() for tool in self.tools]
|
|
676
|
+
|
|
677
|
+
def to_pydantic_ai(self) -> list[PydanticAITool]:
|
|
678
|
+
"""Convert all tools to Pydantic AI ``Tool`` instances.
|
|
679
|
+
|
|
680
|
+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
List of ``pydantic_ai.tools.Tool`` ready to pass to ``Agent(tools=[...])``.
|
|
684
|
+
"""
|
|
685
|
+
return [tool.to_pydantic_ai_tool() for tool in self.tools]
|
|
@@ -11,7 +11,7 @@ import threading
|
|
|
11
11
|
from collections.abc import Coroutine, Sequence
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from importlib import metadata
|
|
14
|
-
from typing import Any, Literal, TypedDict, TypeVar
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar
|
|
15
15
|
|
|
16
16
|
from pydantic import BaseModel, Field, PrivateAttr, ValidationError, field_validator
|
|
17
17
|
|
|
@@ -32,6 +32,9 @@ from stackone_ai.semantic_search import (
|
|
|
32
32
|
)
|
|
33
33
|
from stackone_ai.utils.normalize import _normalize_action_name
|
|
34
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from pydantic_ai.tools import Tool as PydanticAITool
|
|
37
|
+
|
|
35
38
|
logger = logging.getLogger("stackone.tools")
|
|
36
39
|
|
|
37
40
|
SearchMode = Literal["auto", "semantic", "local"]
|
|
@@ -642,13 +645,14 @@ class StackOneToolSet:
|
|
|
642
645
|
|
|
643
646
|
Example::
|
|
644
647
|
|
|
645
|
-
toolset = StackOneToolSet()
|
|
648
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
646
649
|
search_tool = toolset.get_search_tool()
|
|
647
650
|
tools = search_tool("manage employee records", account_ids=["acc-123"])
|
|
648
651
|
"""
|
|
649
652
|
if self._search_config is None:
|
|
650
653
|
raise ToolsetConfigError(
|
|
651
|
-
"Search is disabled.
|
|
654
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
655
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
652
656
|
)
|
|
653
657
|
|
|
654
658
|
config: SearchConfig = {**self._search_config}
|
|
@@ -661,7 +665,8 @@ class StackOneToolSet:
|
|
|
661
665
|
"""Build tool_search + tool_execute tools scoped to this toolset."""
|
|
662
666
|
if self._search_config is None:
|
|
663
667
|
raise ToolsetConfigError(
|
|
664
|
-
"Search is disabled.
|
|
668
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
669
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
665
670
|
)
|
|
666
671
|
|
|
667
672
|
if account_ids:
|
|
@@ -710,8 +715,8 @@ class StackOneToolSet:
|
|
|
710
715
|
toolset = StackOneToolSet()
|
|
711
716
|
tools = toolset.openai()
|
|
712
717
|
|
|
713
|
-
# Meta tools for agent-driven discovery
|
|
714
|
-
toolset = StackOneToolSet()
|
|
718
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
719
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
715
720
|
tools = toolset.openai(mode="search_and_execute")
|
|
716
721
|
"""
|
|
717
722
|
effective_account_ids = account_ids or (
|
|
@@ -752,6 +757,47 @@ class StackOneToolSet:
|
|
|
752
757
|
|
|
753
758
|
return self.fetch_tools(account_ids=effective_account_ids).to_langchain()
|
|
754
759
|
|
|
760
|
+
def pydantic_ai(
|
|
761
|
+
self,
|
|
762
|
+
*,
|
|
763
|
+
mode: Literal["search_and_execute"] | None = None,
|
|
764
|
+
account_ids: list[str] | None = None,
|
|
765
|
+
) -> list[PydanticAITool]:
|
|
766
|
+
"""Get tools as Pydantic AI ``Tool`` instances.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
mode: Tool mode.
|
|
770
|
+
``None`` (default): fetch all tools and convert to Pydantic AI tools.
|
|
771
|
+
``"search_and_execute"``: return two meta tools (tool_search + tool_execute)
|
|
772
|
+
that let the LLM discover and execute tools on-demand.
|
|
773
|
+
account_ids: Account IDs to scope tools. Overrides the ``execute``
|
|
774
|
+
config from the constructor.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
List of Pydantic AI ``Tool`` objects ready to pass to ``Agent(tools=...)``.
|
|
778
|
+
|
|
779
|
+
Requires ``stackone-ai[pydantic-ai]`` (installs ``pydantic-ai-slim``).
|
|
780
|
+
|
|
781
|
+
Examples::
|
|
782
|
+
|
|
783
|
+
# All tools
|
|
784
|
+
toolset = StackOneToolSet()
|
|
785
|
+
tools = toolset.pydantic_ai()
|
|
786
|
+
agent = Agent("openai:gpt-5.4", tools=tools)
|
|
787
|
+
|
|
788
|
+
# Meta tools for agent-driven discovery — search must be enabled
|
|
789
|
+
toolset = StackOneToolSet(search={"method": "auto"})
|
|
790
|
+
tools = toolset.pydantic_ai(mode="search_and_execute")
|
|
791
|
+
"""
|
|
792
|
+
effective_account_ids = account_ids or (
|
|
793
|
+
self._execute_config.get("account_ids") if self._execute_config else None
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if mode == "search_and_execute":
|
|
797
|
+
return self._build_tools(account_ids=effective_account_ids).to_pydantic_ai()
|
|
798
|
+
|
|
799
|
+
return self.fetch_tools(account_ids=effective_account_ids).to_pydantic_ai()
|
|
800
|
+
|
|
755
801
|
def execute(
|
|
756
802
|
self,
|
|
757
803
|
tool_name: str,
|
|
@@ -886,7 +932,8 @@ class StackOneToolSet:
|
|
|
886
932
|
"""
|
|
887
933
|
if self._search_config is None:
|
|
888
934
|
raise ToolsetConfigError(
|
|
889
|
-
"Search is disabled.
|
|
935
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
936
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
890
937
|
)
|
|
891
938
|
|
|
892
939
|
# Merge constructor defaults with per-call overrides
|
|
@@ -1039,7 +1086,8 @@ class StackOneToolSet:
|
|
|
1039
1086
|
"""
|
|
1040
1087
|
if self._search_config is None:
|
|
1041
1088
|
raise ToolsetConfigError(
|
|
1042
|
-
"Search is disabled.
|
|
1089
|
+
"Search is disabled. Pass search={} (or search={'method': 'auto'}) to "
|
|
1090
|
+
"StackOneToolSet(...) to enable. See README 'Search Tool' for options."
|
|
1043
1091
|
)
|
|
1044
1092
|
|
|
1045
1093
|
# Merge constructor defaults with per-call overrides
|