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.
Files changed (179) hide show
  1. stackone_ai-2.9.1/.release-please-manifest.json +3 -0
  2. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/CHANGELOG.md +8 -0
  3. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/PKG-INFO +42 -8
  4. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/README.md +41 -7
  5. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/pyproject.toml +1 -1
  6. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/__init__.py +1 -1
  7. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/models.py +68 -5
  8. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/toolset.py +13 -8
  9. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_tool_calling.py +186 -1
  10. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/uv.lock +1 -1
  11. stackone_ai-2.9.0/.release-please-manifest.json +0 -3
  12. stackone_ai-2.9.0/vendor/stackone-ai-node/.git +0 -1
  13. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/development-workflow.md +0 -0
  14. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/examples-standards.md +0 -0
  15. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/git-workflow.md +0 -0
  16. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/nix-workflow.md +0 -0
  17. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/no-relative-imports.md +0 -0
  18. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/package-installation.md +0 -0
  19. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/release-please-standards.md +0 -0
  20. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.claude/rules/uv-scripts.md +0 -0
  21. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/development-workflow.mdc +0 -0
  22. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/examples-standards.mdc +0 -0
  23. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/git-workflow.mdc +0 -0
  24. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/no-relative-imports.mdc +0 -0
  25. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/package-installation.mdc +0 -0
  26. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/release-please-standards.mdc +0 -0
  27. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.cursor/rules/uv-scripts.mdc +0 -0
  28. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.env.example +0 -0
  29. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.envrc +0 -0
  30. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/actions/setup-nix/action.yaml +0 -0
  31. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/dependabot.yaml +0 -0
  32. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/ci.yaml +0 -0
  33. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake-update.yaml +0 -0
  34. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/nix-flake.yaml +0 -0
  35. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.github/workflows/release.yaml +0 -0
  36. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitignore +0 -0
  37. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitleaks.toml +0 -0
  38. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.gitmodules +0 -0
  39. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.mcp.json +0 -0
  40. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/.release-please-config.json +0 -0
  41. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/CLAUDE.md +0 -0
  42. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/LICENSE +0 -0
  43. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/auth_management.py +0 -0
  44. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/crewai_integration.py +0 -0
  45. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/langchain_integration.py +0 -0
  46. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/langgraph_integration.py +0 -0
  47. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/openai_integration.py +0 -0
  48. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/pydantic_ai_integration.py +0 -0
  49. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/search_tools.py +0 -0
  50. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/examples/test_examples.py +0 -0
  51. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/flake.lock +0 -0
  52. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/flake.nix +0 -0
  53. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/justfile +0 -0
  54. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/scripts/benchmark_search.py +0 -0
  55. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/constants.py +0 -0
  56. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/feedback/__init__.py +0 -0
  57. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/feedback/tool.py +0 -0
  58. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/integrations/__init__.py +0 -0
  59. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/integrations/langgraph.py +0 -0
  60. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/local_search.py +0 -0
  61. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/py.typed +0 -0
  62. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/semantic_search.py +0 -0
  63. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/__init__.py +0 -0
  64. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/normalize.py +0 -0
  65. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/stackone_ai/utils/tfidf_index.py +0 -0
  66. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/conftest.py +0 -0
  67. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/mocks/serve.ts +0 -0
  68. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_agent_tools.py +0 -0
  69. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_feedback.py +0 -0
  70. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_fetch_tools.py +0 -0
  71. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_integrations_langgraph.py +0 -0
  72. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_integrations_pydantic_ai.py +0 -0
  73. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_local_search.py +0 -0
  74. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_models.py +0 -0
  75. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_semantic_search.py +0 -0
  76. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_tfidf_index.py +0 -0
  77. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/tests/test_toolset.py +0 -0
  78. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
  79. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
  80. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
  81. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
  82. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
  83. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
  84. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
  85. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
  86. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
  87. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
  88. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
  89. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
  90. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
  91. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
  92. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.envrc +0 -0
  93. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
  94. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
  95. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
  96. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
  97. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
  98. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
  99. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
  100. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
  101. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitignore +0 -0
  102. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
  103. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.mcp.json +0 -0
  104. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
  105. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
  106. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/AGENTS.md +0 -0
  107. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
  108. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/CLAUDE.md +0 -0
  109. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/LICENSE +0 -0
  110. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/README.md +0 -0
  111. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/README.md +0 -0
  112. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
  113. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
  114. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
  115. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
  116. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
  117. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
  118. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
  119. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
  120. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
  121. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
  122. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
  123. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
  124. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
  125. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/package.json +0 -0
  126. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
  127. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
  128. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
  129. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.lock +0 -0
  130. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/flake.nix +0 -0
  131. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/knip.config.ts +0 -0
  132. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/lefthook.yaml +0 -0
  133. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
  134. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
  135. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
  136. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
  137. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
  138. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
  139. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
  140. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
  141. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/mocks/node.ts +0 -0
  142. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/package.json +0 -0
  143. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
  144. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
  145. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/consts.ts +0 -0
  146. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
  147. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/feedback.ts +0 -0
  148. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
  149. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/headers.ts +0 -0
  150. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/index.ts +0 -0
  151. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
  152. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
  153. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
  154. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
  155. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
  156. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
  157. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/schema.ts +0 -0
  158. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
  159. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
  160. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/tool.ts +0 -0
  161. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
  162. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
  163. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
  164. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/types.ts +0 -0
  165. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
  166. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
  167. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
  168. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
  169. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
  170. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
  171. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
  172. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
  173. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
  174. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
  175. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
  176. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsconfig.json +0 -0
  177. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
  178. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.config.ts +0 -0
  179. {stackone_ai-2.9.0 → stackone_ai-2.9.1}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.9.1"
3
+ }
@@ -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.0
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
- # or pydantic_ai(mode="search_and_execute") for agent-driven discovery
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
- toolset = StackOneToolSet()
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
- # or pydantic_ai(mode="search_and_execute") for agent-driven discovery
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
- toolset = StackOneToolSet()
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")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackone-ai"
3
- version = "2.9.0"
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"
@@ -23,4 +23,4 @@ __all__ = [
23
23
  "SemanticSearchResponse",
24
24
  "SemanticSearchError",
25
25
  ]
26
- __version__ = "2.9.0"
26
+ __version__ = "2.9.1"
@@ -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
- result = response.json()
261
- result_payload = cast(JsonDict, result) if isinstance(result, dict) else {"result": result}
262
- return result_payload
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. Initialize StackOneToolSet with a search config to enable."
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. Initialize StackOneToolSet with a search config to enable."
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. Initialize StackOneToolSet with a search config to enable."
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. Initialize StackOneToolSet with search config to enable."
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 ExecuteConfig, ToolParameters
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
@@ -4762,7 +4762,7 @@ wheels = [
4762
4762
 
4763
4763
  [[package]]
4764
4764
  name = "stackone-ai"
4765
- version = "2.9.0"
4765
+ version = "2.9.1"
4766
4766
  source = { editable = "." }
4767
4767
  dependencies = [
4768
4768
  { name = "bm25s" },
@@ -1,3 +0,0 @@
1
- {
2
- ".": "2.9.0"
3
- }
@@ -1 +0,0 @@
1
- gitdir: ../../.git/modules/vendor/stackone-ai-node
File without changes
File without changes
File without changes
File without changes