stackone-ai 2.5.1__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. stackone_ai-2.7.0/.release-please-manifest.json +3 -0
  2. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/CHANGELOG.md +14 -0
  3. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/PKG-INFO +1 -1
  4. stackone_ai-2.7.0/examples/benchmark_search.py +134 -0
  5. stackone_ai-2.7.0/examples/search_tool_example.py +66 -0
  6. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/semantic_search_example.py +0 -1
  7. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/test_examples.py +2 -0
  8. stackone_ai-2.7.0/examples/workday_integration.py +91 -0
  9. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/pyproject.toml +1 -1
  10. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/__init__.py +1 -1
  11. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/models.py +2 -1
  12. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/toolset.py +76 -21
  13. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_agent_tools.py +7 -2
  14. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_fetch_tools.py +222 -0
  15. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/uv.lock +1 -1
  16. stackone_ai-2.5.1/.release-please-manifest.json +0 -3
  17. stackone_ai-2.5.1/examples/search_tool_example.py +0 -314
  18. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/development-workflow.md +0 -0
  19. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/examples-standards.md +0 -0
  20. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/git-workflow.md +0 -0
  21. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/nix-workflow.md +0 -0
  22. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/no-relative-imports.md +0 -0
  23. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/package-installation.md +0 -0
  24. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/release-please-standards.md +0 -0
  25. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/uv-scripts.md +0 -0
  26. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/development-workflow.mdc +0 -0
  27. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/examples-standards.mdc +0 -0
  28. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/git-workflow.mdc +0 -0
  29. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/no-relative-imports.mdc +0 -0
  30. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/package-installation.mdc +0 -0
  31. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/release-please-standards.mdc +0 -0
  32. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/uv-scripts.mdc +0 -0
  33. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.envrc +0 -0
  34. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/actions/setup-nix/action.yaml +0 -0
  35. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/dependabot.yaml +0 -0
  36. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/ci.yaml +0 -0
  37. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/nix-flake-update.yaml +0 -0
  38. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/nix-flake.yaml +0 -0
  39. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/release.yaml +0 -0
  40. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitignore +0 -0
  41. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitleaks.toml +0 -0
  42. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitmodules +0 -0
  43. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.mcp.json +0 -0
  44. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.release-please-config.json +0 -0
  45. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/CLAUDE.md +0 -0
  46. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/LICENSE +0 -0
  47. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/README.md +0 -0
  48. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/agent_tool_search.py +0 -0
  49. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/crewai_integration.py +0 -0
  50. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/file_uploads.py +0 -0
  51. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/index.py +0 -0
  52. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/langchain_integration.py +0 -0
  53. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/openai_integration.py +0 -0
  54. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/stackone_account_ids.py +0 -0
  55. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/flake.lock +0 -0
  56. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/flake.nix +0 -0
  57. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/justfile +0 -0
  58. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/constants.py +0 -0
  59. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/feedback/__init__.py +0 -0
  60. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/feedback/tool.py +0 -0
  61. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/integrations/__init__.py +0 -0
  62. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/integrations/langgraph.py +0 -0
  63. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/local_search.py +0 -0
  64. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/py.typed +0 -0
  65. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/semantic_search.py +0 -0
  66. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/__init__.py +0 -0
  67. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/normalize.py +0 -0
  68. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/tfidf_index.py +0 -0
  69. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/conftest.py +0 -0
  70. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/mocks/serve.ts +0 -0
  71. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_feedback.py +0 -0
  72. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_integrations_langgraph.py +0 -0
  73. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_local_search.py +0 -0
  74. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_models.py +0 -0
  75. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_semantic_search.py +0 -0
  76. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_tfidf_index.py +0 -0
  77. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_tool_calling.py +0 -0
  78. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_toolset.py +0 -0
  79. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
  80. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
  81. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
  82. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
  83. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
  84. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
  85. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
  86. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
  87. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
  88. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
  89. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
  90. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
  91. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
  92. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
  93. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.envrc +0 -0
  94. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.git +0 -0
  95. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
  96. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
  97. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
  98. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
  99. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
  100. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
  101. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
  102. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
  103. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.gitignore +0 -0
  104. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
  105. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.mcp.json +0 -0
  106. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
  107. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
  108. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/AGENTS.md +0 -0
  109. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
  110. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/CLAUDE.md +0 -0
  111. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/LICENSE +0 -0
  112. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/README.md +0 -0
  113. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/README.md +0 -0
  114. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
  115. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
  116. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
  117. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
  118. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
  119. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
  120. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
  121. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
  122. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
  123. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
  124. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
  125. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
  126. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
  127. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/package.json +0 -0
  128. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
  129. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
  130. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
  131. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/flake.lock +0 -0
  132. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/flake.nix +0 -0
  133. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/knip.config.ts +0 -0
  134. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/lefthook.yaml +0 -0
  135. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
  136. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
  137. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
  138. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
  139. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
  140. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
  141. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
  142. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
  143. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/node.ts +0 -0
  144. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/package.json +0 -0
  145. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
  146. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
  147. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/consts.ts +0 -0
  148. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
  149. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/feedback.ts +0 -0
  150. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
  151. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/headers.ts +0 -0
  152. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/index.ts +0 -0
  153. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
  154. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
  155. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
  156. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
  157. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
  158. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
  159. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/schema.ts +0 -0
  160. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
  161. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
  162. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.ts +0 -0
  163. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
  164. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
  165. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
  166. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/types.ts +0 -0
  167. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
  168. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
  169. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
  170. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
  171. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
  172. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
  173. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
  174. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
  175. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
  176. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
  177. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
  178. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/tsconfig.json +0 -0
  179. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
  180. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/vitest.config.ts +0 -0
  181. {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.7.0"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.7.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.6.0...stackone-ai-v2.7.0) (2026-04-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * **search-optimization:** cache tool catalog and parallelize per-account MCP fetches ([#173](https://github.com/StackOneHQ/stackone-ai-python/issues/173)) ([cd635e6](https://github.com/StackOneHQ/stackone-ai-python/commit/cd635e65621e4e84da270a57e4e1453a3734ad95))
9
+
10
+ ## [2.6.0](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.5.1...stackone-ai-v2.6.0) (2026-04-07)
11
+
12
+
13
+ ### Features
14
+
15
+ * include available connectors in search/execute tool descriptions ([#165](https://github.com/StackOneHQ/stackone-ai-python/issues/165)) ([544f41e](https://github.com/StackOneHQ/stackone-ai-python/commit/544f41ef1340d11f58be1a34e627aa8e81f1102d))
16
+
3
17
  ## [2.5.1](https://github.com/StackOneHQ/stackone-ai-python/compare/stackone-ai-v2.5.0...stackone-ai-v2.5.1) (2026-03-26)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackone-ai
3
- Version: 2.5.1
3
+ Version: 2.7.0
4
4
  Summary: agents performing actions on your SaaS
5
5
  Author-email: StackOne <support@stackone.com>
6
6
  License-File: LICENSE
@@ -0,0 +1,134 @@
1
+ """Benchmark: measure SDK search latency with caching.
2
+
3
+ Runs fetch_tools, local (BM25+TF-IDF) search, and semantic search N times,
4
+ reports cold vs warm average latency and the speedup from caching.
5
+
6
+ Prerequisites:
7
+ - STACKONE_API_KEY environment variable
8
+ - STACKONE_ACCOUNT_ID environment variable
9
+
10
+ Run with:
11
+ uv run python examples/benchmark_search.py # default 100 iterations
12
+ uv run python examples/benchmark_search.py -n 50 # fewer for a quick check
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import os
19
+ import sys
20
+ import time
21
+
22
+ try:
23
+ from dotenv import load_dotenv
24
+
25
+ load_dotenv()
26
+ except ModuleNotFoundError:
27
+ pass
28
+
29
+ from stackone_ai import StackOneToolSet
30
+
31
+ QUERIES = [
32
+ "list events",
33
+ "cancel a meeting",
34
+ "send a message",
35
+ "get current user",
36
+ "list employees",
37
+ ]
38
+
39
+
40
+ def bench(fn, n: int) -> tuple[float, float, list[float]]:
41
+ """Run fn() n times. Return (cold, warm_avg, all_times)."""
42
+ times: list[float] = []
43
+ for _ in range(n):
44
+ t = time.perf_counter()
45
+ fn()
46
+ times.append(time.perf_counter() - t)
47
+
48
+ cold = times[0]
49
+ warm_times = times[1:]
50
+ warm_avg = sum(warm_times) / len(warm_times) if warm_times else cold
51
+ return cold, warm_avg, times
52
+
53
+
54
+ def fmt_ms(seconds: float) -> str:
55
+ return f"{seconds * 1000:8.1f}ms"
56
+
57
+
58
+ def main() -> int:
59
+ parser = argparse.ArgumentParser(description="Benchmark SDK search latency")
60
+ parser.add_argument(
61
+ "--iterations", "-n", type=int, default=100, help="iterations per benchmark (default 100)"
62
+ )
63
+ args = parser.parse_args()
64
+ n = args.iterations
65
+
66
+ api_key = os.getenv("STACKONE_API_KEY")
67
+ account_id = os.getenv("STACKONE_ACCOUNT_ID")
68
+
69
+ if not api_key:
70
+ print("Set STACKONE_API_KEY to run this benchmark.")
71
+ return 1
72
+ if not account_id:
73
+ print("Set STACKONE_ACCOUNT_ID to run this benchmark.")
74
+ return 1
75
+
76
+ print(f"Benchmarking with account {account_id[:8]}..., {n} iterations each\n")
77
+
78
+ ts = StackOneToolSet(
79
+ api_key=api_key,
80
+ account_id=account_id,
81
+ search={"method": "auto", "top_k": 5},
82
+ )
83
+
84
+ results: list[tuple[str, float, float, float]] = []
85
+ query_idx = 0
86
+
87
+ def next_query() -> str:
88
+ nonlocal query_idx
89
+ q = QUERIES[query_idx % len(QUERIES)]
90
+ query_idx += 1
91
+ return q
92
+
93
+ # --- 1. fetch_tools ---
94
+ print(f"[1/3] fetch_tools x{n} ...")
95
+ ts.clear_catalog_cache()
96
+ cold, warm_avg, _ = bench(lambda: ts.fetch_tools(), n)
97
+ speedup = cold / warm_avg if warm_avg > 0 else float("inf")
98
+ results.append(("fetch_tools", cold, warm_avg, speedup))
99
+ print(f" cold={fmt_ms(cold)} warm_avg={fmt_ms(warm_avg)} speedup={speedup:.0f}x")
100
+
101
+ # --- 2. local search (BM25 + TF-IDF) ---
102
+ print(f"[2/3] search_tools (local) x{n} ...")
103
+ ts.clear_catalog_cache()
104
+ query_idx = 0
105
+ cold, warm_avg, _ = bench(lambda: ts.search_tools(next_query(), search="local"), n)
106
+ speedup = cold / warm_avg if warm_avg > 0 else float("inf")
107
+ results.append(("search (local/BM25)", cold, warm_avg, speedup))
108
+ print(f" cold={fmt_ms(cold)} warm_avg={fmt_ms(warm_avg)} speedup={speedup:.0f}x")
109
+
110
+ # --- 3. semantic search (auto) ---
111
+ print(f"[3/3] search_tools (semantic/auto) x{n} ...")
112
+ ts.clear_catalog_cache()
113
+ query_idx = 0
114
+ cold, warm_avg, _ = bench(lambda: ts.search_tools(next_query(), search="auto"), n)
115
+ speedup = cold / warm_avg if warm_avg > 0 else float("inf")
116
+ results.append(("search (semantic)", cold, warm_avg, speedup))
117
+ print(f" cold={fmt_ms(cold)} warm_avg={fmt_ms(warm_avg)} speedup={speedup:.0f}x")
118
+
119
+ # --- Summary ---
120
+ print("\n" + "=" * 65)
121
+ print(f"{'Benchmark':<22} {'Cold':>10} {'Warm (avg)':>10} {'Speedup':>10}")
122
+ print("-" * 65)
123
+ for name, c, w, s in results:
124
+ print(f"{name:<22} {fmt_ms(c):>10} {fmt_ms(w):>10} {s:>9.0f}x")
125
+ print("=" * 65)
126
+
127
+ print(f"\nWarm = average of {n - 1} calls after the first (cold) call.")
128
+ print("Speedup = cold / warm_avg — shows the benefit of caching.\n")
129
+
130
+ return 0
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,66 @@
1
+ """Search tool patterns: callable wrapper and config overrides.
2
+
3
+ For semantic search basics, see semantic_search_example.py.
4
+ For full agent execution, see agent_tool_search.py.
5
+
6
+ Prerequisites:
7
+ - STACKONE_API_KEY environment variable
8
+ - STACKONE_ACCOUNT_ID environment variable
9
+
10
+ Run with:
11
+ uv run python examples/search_tool_example.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ try:
19
+ from dotenv import load_dotenv
20
+
21
+ load_dotenv()
22
+ except ModuleNotFoundError:
23
+ pass
24
+
25
+ from stackone_ai import StackOneToolSet
26
+
27
+
28
+ def main() -> None:
29
+ api_key = os.getenv("STACKONE_API_KEY")
30
+ account_id = os.getenv("STACKONE_ACCOUNT_ID")
31
+
32
+ if not api_key:
33
+ print("Set STACKONE_API_KEY to run this example.")
34
+ return
35
+ if not account_id:
36
+ print("Set STACKONE_ACCOUNT_ID to run this example.")
37
+ return
38
+
39
+ # --- Example 1: get_search_tool() callable ---
40
+ print("=== get_search_tool() callable ===\n")
41
+
42
+ toolset = StackOneToolSet(api_key=api_key, account_id=account_id, search={})
43
+ search_tool = toolset.get_search_tool()
44
+
45
+ queries = ["cancel an event", "list employees", "send a message"]
46
+ for query in queries:
47
+ tools = search_tool(query, top_k=3)
48
+ names = [t.name for t in tools]
49
+ print(f' "{query}" -> {", ".join(names) or "(none)"}')
50
+
51
+ # --- Example 2: Constructor top_k vs per-call override ---
52
+ print("\n=== Constructor top_k vs per-call override ===\n")
53
+
54
+ toolset_3 = StackOneToolSet(api_key=api_key, account_id=account_id, search={"top_k": 3})
55
+
56
+ query = "manage employee records"
57
+
58
+ tools_3 = toolset_3.search_tools(query)
59
+ print(f"Constructor top_k=3: got {len(tools_3)} tools")
60
+
61
+ tools_override = toolset_3.search_tools(query, top_k=10)
62
+ print(f"Per-call top_k=10 (overrides constructor 3): got {len(tools_override)} tools")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -133,7 +133,6 @@ def example_search_action_names():
133
133
  print(f"Top {len(results_limited)} matches from the full catalog:")
134
134
  for r in results_limited:
135
135
  print(f" [{r.similarity_score:.2f}] {r.id}")
136
- print(f" {r.description}")
137
136
  print()
138
137
 
139
138
  # Show filtering effect when account_ids are available
@@ -33,6 +33,8 @@ OPTIONAL_DEPENDENCIES = {
33
33
  "search_tool_example.py": ["mcp"],
34
34
  "semantic_search_example.py": ["mcp"],
35
35
  "mcp_server.py": ["mcp"],
36
+ "workday_integration.py": ["openai", "mcp"],
37
+ "benchmark_search.py": ["mcp"],
36
38
  }
37
39
 
38
40
 
@@ -0,0 +1,91 @@
1
+ """Workday integration: timeout and account scoping for slow providers.
2
+
3
+ Workday can take 10-15s to respond. This example shows how to configure
4
+ timeout and account_ids through the execute config.
5
+
6
+ Prerequisites:
7
+ - STACKONE_API_KEY environment variable
8
+ - STACKONE_ACCOUNT_ID environment variable (a Workday-connected account)
9
+ - OPENAI_API_KEY environment variable
10
+
11
+ Run with:
12
+ uv run python examples/workday_integration.py
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+
20
+ try:
21
+ from dotenv import load_dotenv
22
+
23
+ load_dotenv()
24
+ except ModuleNotFoundError:
25
+ pass
26
+
27
+ from openai import OpenAI
28
+
29
+ from stackone_ai import StackOneToolSet
30
+
31
+
32
+ def main() -> None:
33
+ api_key = os.getenv("STACKONE_API_KEY")
34
+ account_id = os.getenv("STACKONE_ACCOUNT_ID")
35
+
36
+ if not api_key:
37
+ print("Set STACKONE_API_KEY to run this example.")
38
+ return
39
+ if not account_id:
40
+ print("Set STACKONE_ACCOUNT_ID to run this example.")
41
+ return
42
+
43
+ # Timeout for slow providers, account_id for scoping
44
+ toolset = StackOneToolSet(
45
+ api_key=api_key,
46
+ account_id=account_id,
47
+ search={"method": "auto", "top_k": 5},
48
+ timeout=120,
49
+ )
50
+ client = OpenAI()
51
+
52
+ def run_agent(messages: list[dict], tools: list[dict], max_steps: int = 10) -> None:
53
+ """Simple agent loop: call LLM, execute tools, repeat."""
54
+ for _ in range(max_steps):
55
+ response = client.chat.completions.create(model="gpt-5.4", messages=messages, tools=tools)
56
+ choice = response.choices[0]
57
+
58
+ if not choice.message.tool_calls:
59
+ print(f"Answer: {choice.message.content}")
60
+ return
61
+
62
+ messages.append(choice.message.model_dump(exclude_none=True))
63
+ for tc in choice.message.tool_calls:
64
+ print(f" -> {tc.function.name}({tc.function.arguments[:80]})")
65
+ result = toolset.execute(tc.function.name, tc.function.arguments)
66
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)})
67
+
68
+ # --- Example 1: Search and execute mode ---
69
+ print("=== Search and execute mode ===\n")
70
+ run_agent(
71
+ messages=[
72
+ {"role": "system", "content": "Use tool_search to find tools, then tool_execute to run them."},
73
+ {"role": "user", "content": "List the first 5 employees."},
74
+ ],
75
+ tools=toolset.openai(mode="search_and_execute"),
76
+ )
77
+
78
+ # --- Example 2: Normal mode ---
79
+ print("\n=== Normal mode ===\n")
80
+ tools = toolset.fetch_tools(actions=["workday_*_employee*"])
81
+ if len(tools) == 0:
82
+ print("No Workday tools found for this account.")
83
+ else:
84
+ run_agent(
85
+ messages=[{"role": "user", "content": "List the first 5 employees."}],
86
+ tools=tools.to_openai(),
87
+ )
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackone-ai"
3
- version = "2.5.1"
3
+ version = "2.7.0"
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.5.1"
26
+ __version__ = "2.7.0"
@@ -65,6 +65,7 @@ class ExecuteConfig(BaseModel):
65
65
  parameter_locations: dict[str, ParameterLocation] = Field(
66
66
  default_factory=dict, description="Maps parameter names to their location in the request"
67
67
  )
68
+ timeout: float = Field(default=60.0, description="Request timeout in seconds")
68
69
 
69
70
 
70
71
  class ToolParameters(BaseModel):
@@ -249,7 +250,7 @@ class StackOneTool(BaseModel):
249
250
  if query_params:
250
251
  request_kwargs["params"] = query_params
251
252
 
252
- response = httpx.request(**request_kwargs)
253
+ response = httpx.request(**request_kwargs, timeout=self._execute_config.timeout)
253
254
  response_status = response.status_code
254
255
  response.raise_for_status()
255
256
 
@@ -59,7 +59,7 @@ class SearchConfig(TypedDict, total=False):
59
59
  class ExecuteToolsConfig(TypedDict, total=False):
60
60
  """Execution configuration for the StackOneToolSet constructor.
61
61
 
62
- Controls default account scoping for tool execution.
62
+ Controls default account scoping and timeout for tool execution.
63
63
 
64
64
  When set to ``None`` (default), no account scoping is applied.
65
65
  When provided, ``account_ids`` flow through to ``openai(mode="search_and_execute")``
@@ -69,6 +69,10 @@ class ExecuteToolsConfig(TypedDict, total=False):
69
69
  account_ids: list[str]
70
70
  """Account IDs to scope tool discovery and execution."""
71
71
 
72
+ timeout: float
73
+ """Request timeout in seconds. Default: 60. Can also be set as a top-level
74
+ constructor param which takes precedence."""
75
+
72
76
 
73
77
  _SEARCH_DEFAULT: SearchConfig = {"method": "auto"}
74
78
 
@@ -166,7 +170,6 @@ class _ExecuteTool(StackOneTool):
166
170
  """LLM-callable tool that executes a StackOne tool by name."""
167
171
 
168
172
  _toolset: Any = PrivateAttr(default=None)
169
- _cached_tools: Any = PrivateAttr(default=None)
170
173
 
171
174
  def execute(
172
175
  self, arguments: str | JsonDict | None = None, *, options: JsonDict | None = None
@@ -181,10 +184,8 @@ class _ExecuteTool(StackOneTool):
181
184
  parsed = _ExecuteInput(**raw_params)
182
185
  tool_name = parsed.tool_name
183
186
 
184
- if self._cached_tools is None:
185
- self._cached_tools = self._toolset.fetch_tools(account_ids=self._toolset._account_ids)
186
-
187
- target = self._cached_tools.get_tool(parsed.tool_name)
187
+ tools = self._toolset.fetch_tools(account_ids=self._toolset._account_ids)
188
+ target = tools.get_tool(parsed.tool_name)
188
189
 
189
190
  if target is None:
190
191
  return {
@@ -205,13 +206,14 @@ class _ExecuteTool(StackOneTool):
205
206
  return {"error": f"Invalid input: {exc}", "tool_name": tool_name}
206
207
 
207
208
 
208
- def _create_search_tool(api_key: str) -> _SearchTool:
209
+ def _create_search_tool(api_key: str, connectors: str = "") -> _SearchTool:
209
210
  name = "tool_search"
211
+ connector_line = f" Available connectors: {connectors}." if connectors else ""
210
212
  description = (
211
213
  "Search for available tools by describing what you need. "
212
214
  "Returns matching tool names, descriptions, and parameter schemas. "
213
215
  "Use the returned parameter schemas to know exactly what to pass "
214
- "when calling tool_execute."
216
+ f"when calling tool_execute.{connector_line}"
215
217
  )
216
218
  parameters = ToolParameters(
217
219
  type="object",
@@ -259,14 +261,14 @@ def _create_search_tool(api_key: str) -> _SearchTool:
259
261
  return tool
260
262
 
261
263
 
262
- def _create_execute_tool(api_key: str) -> _ExecuteTool:
264
+ def _create_execute_tool(api_key: str, connectors: str = "") -> _ExecuteTool:
263
265
  name = "tool_execute"
266
+ connector_line = f" Available connectors: {connectors}." if connectors else ""
264
267
  description = (
265
268
  "Execute a tool by name with the given parameters. "
266
269
  "Use tool_search first to find available tools. "
267
270
  "The parameters field must match the parameter schema returned "
268
- "by tool_search. Pass parameters as a nested object matching "
269
- "the schema structure."
271
+ f"by tool_search. Pass parameters as a nested object matching the schema structure.{connector_line}"
270
272
  )
271
273
  parameters = ToolParameters(
272
274
  type="object",
@@ -415,6 +417,7 @@ class _StackOneRpcTool(StackOneTool):
415
417
  api_key: str,
416
418
  base_url: str,
417
419
  account_id: str | None,
420
+ timeout: float = 60.0,
418
421
  ) -> None:
419
422
  execute_config = ExecuteConfig(
420
423
  method="POST",
@@ -423,6 +426,7 @@ class _StackOneRpcTool(StackOneTool):
423
426
  headers={},
424
427
  body_type="json",
425
428
  parameter_locations=dict(_RPC_PARAMETER_LOCATIONS),
429
+ timeout=timeout,
426
430
  )
427
431
  super().__init__(
428
432
  description=description,
@@ -555,6 +559,7 @@ class StackOneToolSet:
555
559
  base_url: str | None = None,
556
560
  search: SearchConfig | None = None,
557
561
  execute: ExecuteToolsConfig | None = None,
562
+ timeout: float | None = None,
558
563
  ) -> None:
559
564
  """Initialize StackOne tools with authentication
560
565
 
@@ -570,7 +575,10 @@ class StackOneToolSet:
570
575
  Per-call options always override these defaults.
571
576
  execute: Execution configuration. Controls default account scoping
572
577
  for tool execution. Pass ``{"account_ids": ["acc-1"]}`` to scope
573
- meta tools to specific accounts.
578
+ tools to specific accounts.
579
+ timeout: Request timeout in seconds for tool execution HTTP calls.
580
+ Default: 60. Takes precedence over ``execute.timeout`` if set.
581
+ Increase for slow providers (e.g. Workday).
574
582
 
575
583
  Raises:
576
584
  ToolsetConfigError: If no API key is provided or found in environment
@@ -584,11 +592,15 @@ class StackOneToolSet:
584
592
  self.api_key: str = api_key_value
585
593
  self.account_id = account_id
586
594
  self.base_url = base_url or DEFAULT_BASE_URL
587
- self._account_ids: list[str] = []
595
+ self._account_ids: list[str] = execute.get("account_ids", []) if execute else []
588
596
  self._semantic_client: SemanticSearchClient | None = None
589
597
  self._search_config: SearchConfig | None = search
590
598
  self._execute_config: ExecuteToolsConfig | None = execute
599
+ execute_timeout = execute.get("timeout") if execute else None
600
+ self._timeout: float = timeout if timeout is not None else (execute_timeout or 60.0)
591
601
  self._tools_cache: Tools | None = None
602
+ self._catalog_cache: dict[tuple[Any, ...], Tools] = {}
603
+ self._tool_index_cache: tuple[int, Any] | None = None
592
604
 
593
605
  def set_accounts(self, account_ids: list[str]) -> StackOneToolSet:
594
606
  """Set account IDs for filtering tools
@@ -600,8 +612,18 @@ class StackOneToolSet:
600
612
  This toolset instance for chaining
601
613
  """
602
614
  self._account_ids = account_ids
615
+ self.clear_catalog_cache()
603
616
  return self
604
617
 
618
+ def clear_catalog_cache(self) -> None:
619
+ """Invalidate cached tool catalog and local search index.
620
+
621
+ Call when linked accounts change outside of ``set_accounts`` or when
622
+ you need to force a fresh fetch from the StackOne MCP endpoint.
623
+ """
624
+ self._catalog_cache.clear()
625
+ self._tool_index_cache = None
626
+
605
627
  def get_search_tool(self, *, search: SearchMode | None = None) -> SearchTool:
606
628
  """Get a callable search tool that returns Tools collections.
607
629
 
@@ -645,10 +667,20 @@ class StackOneToolSet:
645
667
  if account_ids:
646
668
  self._account_ids = account_ids
647
669
 
648
- search_tool = _create_search_tool(self.api_key)
670
+ # Discover available connectors for dynamic descriptions
671
+ connectors_str = ""
672
+ try:
673
+ all_tools = self.fetch_tools(account_ids=self._account_ids)
674
+ connectors = sorted(all_tools.get_connectors())
675
+ if connectors:
676
+ connectors_str = ", ".join(connectors)
677
+ except Exception:
678
+ logger.debug("Could not discover connectors for tool descriptions")
679
+
680
+ search_tool = _create_search_tool(self.api_key, connectors=connectors_str)
649
681
  search_tool._toolset = self
650
682
 
651
- execute_tool = _create_execute_tool(self.api_key)
683
+ execute_tool = _create_execute_tool(self.api_key, connectors=connectors_str)
652
684
  execute_tool._toolset = self
653
685
 
654
686
  return Tools([search_tool, execute_tool])
@@ -779,7 +811,10 @@ class StackOneToolSet:
779
811
  if not available_connectors:
780
812
  return Tools([])
781
813
 
782
- index = ToolIndex(list(all_tools))
814
+ cache_key = id(all_tools)
815
+ if self._tool_index_cache is None or self._tool_index_cache[0] != cache_key:
816
+ self._tool_index_cache = (cache_key, ToolIndex(list(all_tools)))
817
+ index = self._tool_index_cache[1]
783
818
  results = index.search(
784
819
  query,
785
820
  limit=top_k if top_k is not None else 5,
@@ -1148,14 +1183,31 @@ class StackOneToolSet:
1148
1183
  else:
1149
1184
  account_scope = [None]
1150
1185
 
1186
+ cache_key = (
1187
+ tuple(sorted(account_scope, key=lambda a: (a is None, a))),
1188
+ tuple(sorted(p.lower() for p in providers)) if providers else None,
1189
+ tuple(sorted(actions)) if actions else None,
1190
+ )
1191
+ cached = self._catalog_cache.get(cache_key)
1192
+ if cached is not None:
1193
+ return cached
1194
+
1151
1195
  endpoint = f"{self.base_url.rstrip('/')}/mcp"
1152
- all_tools: list[StackOneTool] = []
1153
1196
 
1154
- for account in account_scope:
1197
+ def _fetch_for_account(account: str | None) -> list[StackOneTool]:
1155
1198
  headers = self._build_mcp_headers(account)
1156
1199
  catalog = _fetch_mcp_tools(endpoint, headers)
1157
- for tool_def in catalog:
1158
- all_tools.append(self._create_rpc_tool(tool_def, account))
1200
+ return [self._create_rpc_tool(tool_def, account) for tool_def in catalog]
1201
+
1202
+ all_tools: list[StackOneTool] = []
1203
+ if len(account_scope) == 1:
1204
+ all_tools.extend(_fetch_for_account(account_scope[0]))
1205
+ else:
1206
+ max_workers = min(len(account_scope), 10)
1207
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
1208
+ futures = [pool.submit(_fetch_for_account, acc) for acc in account_scope]
1209
+ for future in futures:
1210
+ all_tools.extend(future.result())
1159
1211
 
1160
1212
  if providers:
1161
1213
  all_tools = [tool for tool in all_tools if self._filter_by_provider(tool.name, providers)]
@@ -1163,7 +1215,9 @@ class StackOneToolSet:
1163
1215
  if actions:
1164
1216
  all_tools = [tool for tool in all_tools if self._filter_by_action(tool.name, actions)]
1165
1217
 
1166
- return Tools(all_tools)
1218
+ result = Tools(all_tools)
1219
+ self._catalog_cache[cache_key] = result
1220
+ return result
1167
1221
 
1168
1222
  except ToolsetError:
1169
1223
  raise
@@ -1192,6 +1246,7 @@ class StackOneToolSet:
1192
1246
  api_key=self.api_key,
1193
1247
  base_url=self.base_url,
1194
1248
  account_id=account_id,
1249
+ timeout=self._timeout,
1195
1250
  )
1196
1251
 
1197
1252
  def _normalize_schema_properties(self, schema: dict[str, Any]) -> dict[str, Any]:
@@ -268,7 +268,11 @@ class TestToolExecute:
268
268
 
269
269
  assert "error" in result
270
270
 
271
- def test_caches_fetched_tools(self):
271
+ def test_delegates_catalog_lookup_to_toolset(self):
272
+ # _ExecuteTool no longer holds a local cache; the toolset's catalog
273
+ # cache (see StackOneToolSet._catalog_cache) is the single source of
274
+ # truth. Verify execute always defers to the toolset so it benefits
275
+ # from that shared cache.
272
276
  toolset = MagicMock()
273
277
  toolset.api_key = "test-key"
274
278
  toolset._account_ids = []
@@ -286,7 +290,8 @@ class TestToolExecute:
286
290
  execute.execute({"tool_name": "test_tool"})
287
291
  execute.execute({"tool_name": "test_tool"})
288
292
 
289
- toolset.fetch_tools.assert_called_once()
293
+ assert toolset.fetch_tools.call_count == 2
294
+ toolset.fetch_tools.assert_called_with(account_ids=[])
290
295
 
291
296
  def test_passes_account_ids_from_toolset(self):
292
297
  toolset = MagicMock()