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.
- stackone_ai-2.7.0/.release-please-manifest.json +3 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/CHANGELOG.md +14 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/PKG-INFO +1 -1
- stackone_ai-2.7.0/examples/benchmark_search.py +134 -0
- stackone_ai-2.7.0/examples/search_tool_example.py +66 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/semantic_search_example.py +0 -1
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/test_examples.py +2 -0
- stackone_ai-2.7.0/examples/workday_integration.py +91 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/pyproject.toml +1 -1
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/__init__.py +1 -1
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/models.py +2 -1
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/toolset.py +76 -21
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_agent_tools.py +7 -2
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_fetch_tools.py +222 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/uv.lock +1 -1
- stackone_ai-2.5.1/.release-please-manifest.json +0 -3
- stackone_ai-2.5.1/examples/search_tool_example.py +0 -314
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/examples-standards.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/nix-workflow.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/no-relative-imports.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/package-installation.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/release-please-standards.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.claude/rules/uv-scripts.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/examples-standards.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/no-relative-imports.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/package-installation.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/release-please-standards.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.cursor/rules/uv-scripts.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.envrc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/dependabot.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/nix-flake-update.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitignore +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitleaks.toml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.gitmodules +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.mcp.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/.release-please-config.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/CLAUDE.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/LICENSE +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/README.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/agent_tool_search.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/crewai_integration.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/file_uploads.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/index.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/langchain_integration.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/openai_integration.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/examples/stackone_account_ids.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/flake.lock +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/flake.nix +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/justfile +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/constants.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/feedback/__init__.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/feedback/tool.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/integrations/__init__.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/integrations/langgraph.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/local_search.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/py.typed +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/semantic_search.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/__init__.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/normalize.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/stackone_ai/utils/tfidf_index.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/conftest.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/mocks/serve.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_feedback.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_integrations_langgraph.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_local_search.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_models.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_semantic_search.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_tfidf_index.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_tool_calling.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/tests/test_toolset.py +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/development-workflow.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/file-operations.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/git-workflow.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/pnpm-usage.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/typescript-patterns.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/rules/typescript-testing.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.claude/skills/orama-integration/SKILL.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/development-workflow.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/file-operations.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/git-workflow.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/orama-integration.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/pnpm-usage.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/typescript-patterns.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.cursor/rules/typescript-testing.mdc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.envrc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.git +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/CODEOWNERS +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/actions/setup-nix/action.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/check-title.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/ci.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/claude.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/dry-publish.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/nix-flake.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.github/workflows/release.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.gitignore +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.gitleaks.toml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.mcp.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.oxfmtrc.jsonc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/.oxlintrc.jsonc +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/AGENTS.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/CHANGELOG.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/CLAUDE.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/LICENSE +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/README.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/README.md +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/ai-sdk-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/anthropic-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/claude-agent-sdk-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools-debug.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/fetch-tools.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/meta-tools.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-integration.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-responses-integration.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/openai-responses-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/package.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tanstack-ai-integration.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/examples/tsconfig.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/flake.lock +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/flake.nix +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/knip.config.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/lefthook.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.example-api.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.mcp.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.openai.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.stackone-ai.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.stackone-rpc.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/handlers.utils.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/mcp-server.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/mocks/node.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/package.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/pnpm-lock.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/pnpm-workspace.yaml +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/consts.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/feedback.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/feedback.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/headers.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/headers.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/index.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/mcp-client.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/mcp-client.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/requestBuilder.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/requestBuilder.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/rpc-client.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/rpc-client.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/schema.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.test-d.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/tool.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.test-d.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/toolsets.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/types.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/array.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/array.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone-api.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/error-stackone.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/tfidf-index.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/tfidf-index.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/try-import.test.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/try-import.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/src/utils/type.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/tsconfig.json +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/tsdown.config.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/vitest.config.ts +0 -0
- {stackone_ai-2.5.1 → stackone_ai-2.7.0}/vendor/stackone-ai-node/vitest.setup.ts +0 -0
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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()
|
|
@@ -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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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()
|