diff --git a/.github/workflows/loongsuite_lint_0.yml b/.github/workflows/loongsuite_lint_0.yml index 188456007..b9295fd61 100644 --- a/.github/workflows/loongsuite_lint_0.yml +++ b/.github/workflows/loongsuite_lint_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}] + [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-deepagents", "package": "loongsuite-instrumentation-deepagents", "tox_env": "lint-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/.github/workflows/loongsuite_test_0.yml b/.github/workflows/loongsuite_test_0.yml index 2fe2e26bb..f1724ccdf 100644 --- a/.github/workflows/loongsuite_test_0.yml +++ b/.github/workflows/loongsuite_test_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 Ubuntu"}, {"name": "py39-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 Ubuntu"}, {"name": "py310-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 Ubuntu"}, {"name": "py311-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 Ubuntu"}, {"name": "py312-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 Ubuntu"}, {"name": "py313-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 Ubuntu"}, {"name": "py314-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 Ubuntu"}, {"name": "pypy3-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 Ubuntu"}, {"name": "py311-test-detect-loongsuite-changes_ubuntu-latest", "os": "ubuntu-latest", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 Ubuntu"}] + [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 Ubuntu"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-deepagents_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-deepagents_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-deepagents_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-deepagents_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 Ubuntu"}, {"name": "py39-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 Ubuntu"}, {"name": "py310-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 Ubuntu"}, {"name": "py311-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 Ubuntu"}, {"name": "py312-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 Ubuntu"}, {"name": "py313-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 Ubuntu"}, {"name": "py314-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 Ubuntu"}, {"name": "pypy3-test-util-genai_ubuntu-latest", "os": "ubuntu-latest", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 Ubuntu"}, {"name": "py311-test-detect-loongsuite-changes_ubuntu-latest", "os": "ubuntu-latest", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-litellm_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-crewai_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 Ubuntu"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 Ubuntu"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 Ubuntu"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 Ubuntu"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_ubuntu-latest", "os": "ubuntu-latest", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 Ubuntu"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/instrumentation-loongsuite/README.md b/instrumentation-loongsuite/README.md index 0816ea65c..8983cd484 100644 --- a/instrumentation-loongsuite/README.md +++ b/instrumentation-loongsuite/README.md @@ -6,6 +6,7 @@ | [loongsuite-instrumentation-claude-agent-sdk](./loongsuite-instrumentation-claude-agent-sdk) | claude-agent-sdk >= 0.1.0 | No | development | [loongsuite-instrumentation-crewai](./loongsuite-instrumentation-crewai) | crewai >= 0.80.0 | No | development | [loongsuite-instrumentation-dashscope](./loongsuite-instrumentation-dashscope) | dashscope >= 1.0.0 | No | development +| [loongsuite-instrumentation-deepagents](./loongsuite-instrumentation-deepagents) | deepagents >= 0.6.0, < 0.7.0 | Yes | development | [loongsuite-instrumentation-dify](./loongsuite-instrumentation-dify) | dify | No | development | [loongsuite-instrumentation-google-adk](./loongsuite-instrumentation-google-adk) | google-adk >= 0.1.0 | No | development | [loongsuite-instrumentation-hermes-agent](./loongsuite-instrumentation-hermes-agent) | openai >= 1.0.0 | No | development @@ -15,4 +16,4 @@ | [loongsuite-instrumentation-mcp](./loongsuite-instrumentation-mcp) | mcp >= 1.3.0, <= 1.25.0 | No | development | [loongsuite-instrumentation-mem0](./loongsuite-instrumentation-mem0) | mem0ai >= 1.0.0, < 2.0.0 | No | development | [loongsuite-instrumentation-qwen-agent](./loongsuite-instrumentation-qwen-agent) | qwen-agent >= 0.0.20 | No | development -| [loongsuite-instrumentation-qwenpaw](./loongsuite-instrumentation-qwenpaw) | qwenpaw >= 1.1.0; copaw >= 0.1.0, <= 1.0.2 (legacy) | No | development \ No newline at end of file +| [loongsuite-instrumentation-qwenpaw](./loongsuite-instrumentation-qwenpaw) | qwenpaw >= 1.1.0; copaw >= 0.1.0, <= 1.0.2 (legacy) | No | development diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md new file mode 100644 index 000000000..4d6c67264 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +## Version 0.6.0.dev + +### Added + +- Initial implementation of DeepAgents instrumentation. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/README.md b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/README.md new file mode 100644 index 000000000..4a5bc310e --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/README.md @@ -0,0 +1,31 @@ +# LoongSuite deepagents Instrumentation + +This package adds the deepagents-specific telemetry that is not already +covered by `loongsuite-instrumentation-langchain` and +`loongsuite-instrumentation-langgraph`. + +It intentionally adds only three integration points: + +- wraps `deepagents.graph.create_deep_agent` and the returned graph instance's + `invoke`, `ainvoke`, `stream`, and `astream` methods to create the outer + `ENTRY` span; +- injects a sidecar LangChain callback handler that enriches existing + LoongSuite LangChain spans with deepagents framework and SubAgent metadata; +- installs a `SpanProcessor` that emits the `genai_calls_*` and + `genai_llm_*` metrics from completed GenAI spans. + +The `task` tool remains a `TOOL` span and is marked with +`gen_ai.tool.type=agent`. The SubAgent itself is represented by the nested +`AGENT` span emitted by the LangChain/LangGraph instrumentation. + +## Local Install + +Install the shared GenAI utility from the same source tree first, then install +the dependent LangChain, LangGraph, and deepagents instrumentations: + +```bash +pip install -e ./util/opentelemetry-util-genai +pip install -e ./instrumentation-loongsuite/loongsuite-instrumentation-langchain +pip install -e ./instrumentation-loongsuite/loongsuite-instrumentation-langgraph +pip install -e ./instrumentation-loongsuite/loongsuite-instrumentation-deepagents +``` diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/pyproject.toml b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/pyproject.toml new file mode 100644 index 000000000..5ecd5776d --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "loongsuite-instrumentation-deepagents" +dynamic = ["version"] +description = "LoongSuite deepagents instrumentation" +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.10" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "loongsuite-instrumentation-langchain", + "loongsuite-instrumentation-langgraph", + "opentelemetry-api ~= 1.37", + "opentelemetry-instrumentation >= 0.58b0", + "opentelemetry-sdk ~= 1.37", + "opentelemetry-semantic-conventions >= 0.58b0", + "opentelemetry-util-genai", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [ + "deepagents >= 0.6.0, < 0.7.0", +] + +[project.entry-points.opentelemetry_instrumentor] +deepagents = "opentelemetry.instrumentation.deepagents:DeepAgentsInstrumentor" + +[project.urls] +Homepage = "https://github.com/alibaba/loongsuite-python-agent/tree/main/instrumentation-loongsuite/loongsuite-instrumentation-deepagents" +Repository = "https://github.com/alibaba/loongsuite-python-agent" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/deepagents/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "src", + "tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/__init__.py new file mode 100644 index 000000000..1b04be8b0 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/__init__.py @@ -0,0 +1,135 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LoongSuite instrumentation for langchain-ai deepagents.""" + +from __future__ import annotations + +import logging +from importlib import import_module +from typing import Any, Collection + +from opentelemetry import metrics, trace +from opentelemetry.instrumentation.deepagents.internal._enricher import ( + install_enricher_callback, + uninstall_enricher_callback, +) +from opentelemetry.instrumentation.deepagents.internal._entry_patch import ( + instrument_entry_patch, + uninstrument_entry_patch, +) +from opentelemetry.instrumentation.deepagents.internal._metrics_processor import ( # noqa: E501 + install_metrics_processor, + shutdown_metrics_processors, +) +from opentelemetry.instrumentation.deepagents.package import _instruments +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler + +__all__ = ["DeepAgentsInstrumentor"] + +_logger = logging.getLogger(__name__) + + +def _instrument_dependency( + module_name: str, + class_name: str, + **kwargs: Any, +) -> None: + """Instrument a required base package when it is installed.""" + try: + module = import_module(module_name) + except ModuleNotFoundError as exc: + if exc.name == module_name or ( + exc.name is not None and module_name.startswith(f"{exc.name}.") + ): + _logger.warning( + "deepagents instrumentation requires %s; continuing with " + "ENTRY/metrics only.", + module_name, + ) + return + raise + + instrumentor_type = getattr(module, class_name, None) + if instrumentor_type is None: + _logger.warning( + "deepagents instrumentation could not find %s.%s", + module_name, + class_name, + ) + return + + instrumentor = instrumentor_type() + if instrumentor.is_instrumented_by_opentelemetry: + return + instrumentor.instrument(**kwargs) + + +class DeepAgentsInstrumentor(BaseInstrumentor): + """Instrumentation for deepagents. + + The plugin is intentionally additive: LangChain and LangGraph keep owning + AGENT/CHAIN/STEP/LLM/TOOL spans, while this plugin contributes the ENTRY + wrapper, a sidecar metadata enricher, and GenAI metrics from finished spans. + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Any) -> None: + tracer_provider = kwargs.get("tracer_provider") + meter_provider = kwargs.get("meter_provider") + logger_provider = kwargs.get("logger_provider") + + _instrument_dependency( + "opentelemetry.instrumentation.langchain", + "LangChainInstrumentor", + tracer_provider=tracer_provider, + meter_provider=meter_provider, + logger_provider=logger_provider, + ) + _instrument_dependency( + "opentelemetry.instrumentation.langgraph", + "LangGraphInstrumentor", + tracer_provider=tracer_provider, + meter_provider=meter_provider, + logger_provider=logger_provider, + ) + + handler = ExtendedTelemetryHandler( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + logger_provider=logger_provider, + ) + instrument_entry_patch(handler) + install_enricher_callback() + + if tracer_provider is None: + tracer_provider = trace.get_tracer_provider() + if meter_provider is None: + _logger.warning( + "deepagents instrumentation meter_provider was not supplied; " + "using the global MeterProvider for GenAI metrics." + ) + meter_provider = metrics.get_meter_provider() + install_metrics_processor( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + + def _uninstrument(self, **kwargs: Any) -> None: + uninstrument_entry_patch() + uninstall_enricher_callback() + shutdown_metrics_processors() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/__init__.py new file mode 100644 index 000000000..f79f9d155 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/__init__.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_attributes.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_attributes.py new file mode 100644 index 000000000..d02099736 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_attributes.py @@ -0,0 +1,110 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""Shared constants for deepagents instrumentation.""" + +from __future__ import annotations + +FRAMEWORK_NAME = "deepagents" + +GEN_AI_AGENT_DESCRIPTION = "gen_ai.agent.description" +GEN_AI_AGENT_NAME = "gen_ai.agent.name" +GEN_AI_AGENT_TYPE = "gen_ai.agent.type" +GEN_AI_FRAMEWORK = "gen_ai.framework" +GEN_AI_FRAMEWORK_VERSION = "gen_ai.framework.version" +GEN_AI_OPERATION_NAME = "gen_ai.operation.name" +GEN_AI_REQUEST_MODEL = "gen_ai.request.model" +GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" +GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" +GEN_AI_SESSION_ID = "gen_ai.session.id" +GEN_AI_SPAN_KIND = "gen_ai.span.kind" +GEN_AI_TOOL_NAME = "gen_ai.tool.name" +GEN_AI_TOOL_TYPE = "gen_ai.tool.type" +GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = ( + "gen_ai.usage.cache_creation.input_tokens" +) +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = ( + "gen_ai.usage.cache_read.input_tokens" +) +GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" +GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" +GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + +SPAN_KIND_AGENT = "AGENT" +SPAN_KIND_CHAIN = "CHAIN" +SPAN_KIND_EMBEDDING = "EMBEDDING" +SPAN_KIND_ENTRY = "ENTRY" +SPAN_KIND_LLM = "LLM" +SPAN_KIND_RERANKER = "RERANKER" +SPAN_KIND_RETRIEVER = "RETRIEVER" +SPAN_KIND_STEP = "STEP" +SPAN_KIND_TASK = "TASK" +SPAN_KIND_TOOL = "TOOL" + +ENTRY_PARENT_KINDS = {SPAN_KIND_ENTRY, SPAN_KIND_AGENT} +GENAI_SPAN_KINDS = { + SPAN_KIND_AGENT, + SPAN_KIND_CHAIN, + SPAN_KIND_EMBEDDING, + SPAN_KIND_ENTRY, + SPAN_KIND_LLM, + SPAN_KIND_RERANKER, + SPAN_KIND_RETRIEVER, + SPAN_KIND_STEP, + SPAN_KIND_TASK, + SPAN_KIND_TOOL, +} + +METADATA_LS_INTEGRATION = "ls_integration" +METADATA_LS_AGENT_TYPE = "ls_agent_type" +METADATA_LC_AGENT_NAME = "lc_agent_name" +METADATA_VERSIONS = "versions" +METADATA_DEEPAGENTS_VERSION = "deepagents" +METADATA_SUBAGENT_DESCRIPTION = "loongsuite_deepagents_subagent_description" + +SUBAGENT_TYPE = "subagent" +TASK_TOOL_NAME = "task" +TOOL_TYPE_AGENT = "agent" + +GRAPH_ATTR = "_loongsuite_deepagents_graph" +GRAPH_VERSION_ATTR = "_loongsuite_deepagents_version" +GRAPH_METADATA_ATTR = "_loongsuite_deepagents_metadata" +GRAPH_REGISTRY_ATTR = "_loongsuite_deepagents_subagent_registry" +GRAPH_ORIGINAL_METHODS_ATTR = "_loongsuite_deepagents_original_methods" +GRAPH_METHODS_WRAPPED_ATTR = "_loongsuite_deepagents_methods_wrapped" +LANGGRAPH_REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" + +CREATE_DEEP_AGENT_MODULE = "deepagents.graph" +CREATE_DEEP_AGENT_NAME = "create_deep_agent" +BUILD_TASK_TOOL_MODULE = "deepagents.middleware.subagents" +BUILD_TASK_TOOL_NAME = "_build_task_tool" + +METRIC_CALLS_COUNT = "genai_calls_count" +METRIC_CALLS_DURATION_SECONDS = "genai_calls_duration_seconds" +METRIC_CALLS_ERROR_COUNT = "genai_calls_error_count" +METRIC_CALLS_SLOW_COUNT = "genai_calls_slow_count" +METRIC_LLM_FIRST_TOKEN_SECONDS = "genai_llm_first_token_seconds" +METRIC_LLM_USAGE_TOKENS = "genai_llm_usage_tokens" + +DEFAULT_SLOW_THRESHOLDS_SECONDS = { + SPAN_KIND_ENTRY: 60.0, + SPAN_KIND_AGENT: 30.0, + SPAN_KIND_CHAIN: 10.0, + SPAN_KIND_STEP: 10.0, + SPAN_KIND_LLM: 10.0, + SPAN_KIND_TOOL: 10.0, + SPAN_KIND_RETRIEVER: 5.0, + SPAN_KIND_RERANKER: 5.0, + SPAN_KIND_EMBEDDING: 5.0, + SPAN_KIND_TASK: 30.0, +} + +USAGE_TOKEN_ATTRIBUTES = { + "input": GEN_AI_USAGE_INPUT_TOKENS, + "output": GEN_AI_USAGE_OUTPUT_TOKENS, + "total": GEN_AI_USAGE_TOTAL_TOKENS, + "cache_creation": GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + "cache_read": GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, +} diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_enricher.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_enricher.py new file mode 100644 index 000000000..1ba55f29e --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_enricher.py @@ -0,0 +1,239 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""LangChain callback sidecar that enriches existing LoongSuite spans.""" + +from __future__ import annotations + +import logging +from collections.abc import Mapping +from contextlib import suppress +from importlib import import_module +from typing import Any, Callable + +from wrapt import wrap_function_wrapper + +from opentelemetry import trace +from opentelemetry.instrumentation.utils import unwrap + +from ._attributes import ( + FRAMEWORK_NAME, + GEN_AI_AGENT_DESCRIPTION, + GEN_AI_AGENT_NAME, + GEN_AI_AGENT_TYPE, + GEN_AI_FRAMEWORK, + GEN_AI_FRAMEWORK_VERSION, + GEN_AI_TOOL_NAME, + GEN_AI_TOOL_TYPE, + METADATA_DEEPAGENTS_VERSION, + METADATA_LC_AGENT_NAME, + METADATA_LS_AGENT_TYPE, + METADATA_LS_INTEGRATION, + METADATA_VERSIONS, + SUBAGENT_TYPE, + TASK_TOOL_NAME, + TOOL_TYPE_AGENT, +) +from ._utils import ( + current_subagent_registry, + obj_get, + safe_set_attribute, +) + +try: + from langchain_core.callbacks import BaseCallbackHandler +except ModuleNotFoundError: + BaseCallbackHandler = object # type: ignore[assignment,misc] + +_logger = logging.getLogger(__name__) +_is_enricher_patched = False +_handler: "DeepAgentsEnricherCallbackHandler | None" = None + + +class DeepAgentsEnricherCallbackHandler(BaseCallbackHandler): # type: ignore[misc,valid-type] + """Sidecar callback that writes deepagents metadata to active spans.""" + + def on_chain_start( + self, + serialized: dict[str, Any], + inputs: dict[str, Any], + *, + run_id: Any, + parent_run_id: Any | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + del serialized, inputs, parent_run_id, tags, kwargs + self._enrich_agent_or_chain(metadata or {}, run_id=run_id) + + async def on_chain_start_async( + self, + serialized: dict[str, Any], + inputs: dict[str, Any], + *, + run_id: Any, + parent_run_id: Any | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + del serialized, inputs, parent_run_id, tags, kwargs + self._enrich_agent_or_chain(metadata or {}, run_id=run_id) + + def on_tool_start( + self, + serialized: dict[str, Any], + input_str: str, + *, + run_id: Any, + parent_run_id: Any | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + del input_str, run_id, parent_run_id, tags, metadata + self._enrich_tool(serialized, kwargs) + + async def on_tool_start_async( + self, + serialized: dict[str, Any], + input_str: str, + *, + run_id: Any, + parent_run_id: Any | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + del input_str, run_id, parent_run_id, tags, metadata + self._enrich_tool(serialized, kwargs) + + def _enrich_agent_or_chain( + self, + metadata: Mapping[str, Any], + *, + run_id: Any, + ) -> None: + if obj_get(metadata, METADATA_LS_INTEGRATION) != FRAMEWORK_NAME: + return + + current_span = _span_for_run(run_id) or trace.get_current_span() + safe_set_attribute(current_span, GEN_AI_FRAMEWORK, FRAMEWORK_NAME) + version = _version_from_metadata(metadata) + if version: + safe_set_attribute(current_span, GEN_AI_FRAMEWORK_VERSION, version) + + agent_name = obj_get(metadata, METADATA_LC_AGENT_NAME) + if agent_name: + safe_set_attribute(current_span, GEN_AI_AGENT_NAME, str(agent_name)) + + if _is_subagent_metadata(metadata, agent_name): + safe_set_attribute(current_span, GEN_AI_AGENT_TYPE, SUBAGENT_TYPE) + description = current_subagent_registry().get(str(agent_name)) + if description: + safe_set_attribute( + current_span, + GEN_AI_AGENT_DESCRIPTION, + description, + ) + + def _enrich_tool( + self, + serialized: Mapping[str, Any] | None, + kwargs: Mapping[str, Any], + ) -> None: + name = ( + obj_get(serialized or {}, "name") + or obj_get(kwargs, "name") + or obj_get(obj_get(serialized or {}, "kwargs", {}), "name") + ) + if name != TASK_TOOL_NAME: + return + current_span = trace.get_current_span() + safe_set_attribute(current_span, GEN_AI_TOOL_NAME, TASK_TOOL_NAME) + safe_set_attribute(current_span, GEN_AI_TOOL_TYPE, TOOL_TYPE_AGENT) + + +def install_enricher_callback() -> None: + global _handler, _is_enricher_patched # noqa: PLW0603 + if _is_enricher_patched: + return + + try: + import_module("langchain_core.callbacks") + except ModuleNotFoundError as exc: + if exc.name == "langchain_core": + _logger.warning( + "langchain_core is not installed; deepagents enricher skipped." + ) + return + raise + + _handler = DeepAgentsEnricherCallbackHandler() + wrap_function_wrapper( + module="langchain_core.callbacks", + name="BaseCallbackManager.__init__", + wrapper=_BaseCallbackManagerInit(_handler), + ) + _is_enricher_patched = True + + +def uninstall_enricher_callback() -> None: + global _handler, _is_enricher_patched # noqa: PLW0603 + if not _is_enricher_patched: + _handler = None + return + with suppress(Exception): + import langchain_core.callbacks # noqa: PLC0415 + + unwrap(langchain_core.callbacks.BaseCallbackManager, "__init__") + _handler = None + _is_enricher_patched = False + + +class _BaseCallbackManagerInit: + __slots__ = ("_handler",) + + def __init__(self, handler: DeepAgentsEnricherCallbackHandler) -> None: + self._handler = handler + + def __call__( + self, + wrapped: Callable[..., None], + instance: Any, + args: Any, + kwargs: Any, + ) -> None: + wrapped(*args, **kwargs) + for handler in getattr(instance, "inheritable_handlers", ()): + if isinstance(handler, DeepAgentsEnricherCallbackHandler): + return + instance.add_handler(self._handler, True) + + +def _version_from_metadata(metadata: Mapping[str, Any]) -> str | None: + versions = obj_get(metadata, METADATA_VERSIONS, {}) + version = obj_get(versions, METADATA_DEEPAGENTS_VERSION) + return str(version) if version else None + + +def _is_subagent_metadata( + metadata: Mapping[str, Any], + agent_name: Any, +) -> bool: + if obj_get(metadata, METADATA_LS_AGENT_TYPE) == SUBAGENT_TYPE: + return True + return bool(agent_name and str(agent_name) in current_subagent_registry()) + + +def _span_for_run(run_id: Any) -> Any: + try: + from opentelemetry.instrumentation.langchain.internal._tracer import ( # noqa: PLC0415 + get_otel_span_for_run, + ) + except Exception: # noqa: BLE001 + return None + return get_otel_span_for_run(run_id) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_entry_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_entry_patch.py new file mode 100644 index 000000000..af07d59d9 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_entry_patch.py @@ -0,0 +1,570 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""ENTRY span patch for ``deepagents.graph.create_deep_agent``.""" + +from __future__ import annotations + +import logging +import sys +from collections.abc import AsyncIterator, Iterator, Mapping +from contextlib import suppress +from importlib import import_module +from typing import Any, Callable + +from wrapt import wrap_function_wrapper + +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler +from opentelemetry.util.genai.extended_types import EntryInvocation +from opentelemetry.util.genai.types import Error + +from ._attributes import ( + BUILD_TASK_TOOL_MODULE, + BUILD_TASK_TOOL_NAME, + CREATE_DEEP_AGENT_MODULE, + CREATE_DEEP_AGENT_NAME, + FRAMEWORK_NAME, + GEN_AI_AGENT_NAME, + GEN_AI_FRAMEWORK, + GEN_AI_FRAMEWORK_VERSION, + GEN_AI_OPERATION_NAME, + GEN_AI_SPAN_KIND, + GRAPH_ATTR, + GRAPH_METADATA_ATTR, + GRAPH_METHODS_WRAPPED_ATTR, + GRAPH_ORIGINAL_METHODS_ATTR, + GRAPH_REGISTRY_ATTR, + GRAPH_VERSION_ATTR, + LANGGRAPH_REACT_AGENT_METADATA_KEY, + METADATA_LS_AGENT_TYPE, + METADATA_SUBAGENT_DESCRIPTION, + SPAN_KIND_ENTRY, + SUBAGENT_TYPE, +) +from ._utils import ( + active_span_is_entry_or_agent, + config_from_call, + config_with_langgraph_react_metadata, + create_graph_metadata, + detect_deepagents_version, + entry_attributes, + extract_subagent_registry, + inject_langgraph_react_metadata, + input_messages_from_value, + input_value_from_call, + output_messages_from_value, + prime_entry_span, + reset_current_subagent_registry, + root_agent_name, + safe_set_attribute, + session_id_from_config, + set_current_subagent_registry, +) + +_logger = logging.getLogger(__name__) +_handler: ExtendedTelemetryHandler | None = None +_is_entry_patched = False +_TOP_LEVEL_MODULE = "deepagents" +_MISSING = object() +_top_level_original: Any = _MISSING +_top_level_patched = False +_is_subagent_task_patched = False + + +def instrument_entry_patch(handler: ExtendedTelemetryHandler) -> None: + """Patch ``create_deep_agent`` and retain the util-genai handler.""" + global _handler, _is_entry_patched # noqa: PLW0603 + _handler = handler + if _is_entry_patched: + return + try: + wrap_function_wrapper( + CREATE_DEEP_AGENT_MODULE, + CREATE_DEEP_AGENT_NAME, + _create_deep_agent_wrapper, + ) + except ModuleNotFoundError as exc: + if exc.name == "deepagents" or exc.name == CREATE_DEEP_AGENT_MODULE: + _logger.warning( + "deepagents is not installed; create_deep_agent ENTRY patch skipped." + ) + return + raise + except AttributeError: + _logger.warning( + "%s.%s not found; ENTRY patch skipped.", + CREATE_DEEP_AGENT_MODULE, + CREATE_DEEP_AGENT_NAME, + ) + return + _sync_top_level_create_deep_agent() + _instrument_subagent_task_tool() + _is_entry_patched = True + + +def uninstrument_entry_patch() -> None: + """Remove the create_deep_agent patch. + + Graph instances returned while instrumented keep their instance-level + wrappers. They are disabled by losing the global handler and are not + reachable from here without retaining application object references. + """ + global _handler, _is_entry_patched # noqa: PLW0603 + _handler = None + if not _is_entry_patched: + return + with suppress(Exception): + module = import_module(CREATE_DEEP_AGENT_MODULE) + unwrap(module, CREATE_DEEP_AGENT_NAME) + _uninstrument_subagent_task_tool() + _restore_top_level_create_deep_agent() + _is_entry_patched = False + + +def _sync_top_level_create_deep_agent() -> None: + """Point ``deepagents.create_deep_agent`` at the wrapped graph export.""" + global _top_level_original, _top_level_patched # noqa: PLW0603 + top_level_module = sys.modules.get(_TOP_LEVEL_MODULE) + graph_module = sys.modules.get(CREATE_DEEP_AGENT_MODULE) + if top_level_module is None or graph_module is None: + return + + _top_level_original = getattr( + top_level_module, + CREATE_DEEP_AGENT_NAME, + _MISSING, + ) + wrapped_create_deep_agent = getattr(graph_module, CREATE_DEEP_AGENT_NAME, None) + if wrapped_create_deep_agent is None: + return + try: + setattr(top_level_module, CREATE_DEEP_AGENT_NAME, wrapped_create_deep_agent) + except Exception: # noqa: BLE001 + _logger.debug("Failed to sync deepagents top-level export", exc_info=True) + return + _top_level_patched = True + + +def _restore_top_level_create_deep_agent() -> None: + """Restore the top-level ``create_deep_agent`` export after unwrap.""" + global _top_level_original, _top_level_patched # noqa: PLW0603 + if not _top_level_patched: + return + top_level_module = sys.modules.get(_TOP_LEVEL_MODULE) + if top_level_module is None: + _top_level_original = _MISSING + _top_level_patched = False + return + try: + if _top_level_original is _MISSING: + delattr(top_level_module, CREATE_DEEP_AGENT_NAME) + else: + setattr(top_level_module, CREATE_DEEP_AGENT_NAME, _top_level_original) + except Exception: # noqa: BLE001 + _logger.debug("Failed to restore deepagents top-level export", exc_info=True) + finally: + _top_level_original = _MISSING + _top_level_patched = False + + +def _create_deep_agent_wrapper( + wrapped: Callable[..., Any], + _instance: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + graph = wrapped(*args, **kwargs) + metadata = create_graph_metadata(graph, name=kwargs.get("name")) + registry = extract_subagent_registry(kwargs.get("subagents")) + _mark_graph(graph, metadata, registry) + _wrap_graph_methods(graph, metadata, registry) + return graph + + +def _instrument_subagent_task_tool() -> None: + """Patch deepagents subagent task construction to mark nested graphs.""" + global _is_subagent_task_patched # noqa: PLW0603 + if _is_subagent_task_patched: + return + try: + wrap_function_wrapper( + BUILD_TASK_TOOL_MODULE, + BUILD_TASK_TOOL_NAME, + _build_task_tool_wrapper, + ) + except ModuleNotFoundError as exc: + if exc.name == "deepagents" or exc.name == BUILD_TASK_TOOL_MODULE: + _logger.debug( + "deepagents subagent middleware is not installed; " + "SubAgent task patch skipped." + ) + return + raise + except AttributeError: + _logger.debug( + "%s.%s not found; SubAgent task patch skipped.", + BUILD_TASK_TOOL_MODULE, + BUILD_TASK_TOOL_NAME, + ) + return + _is_subagent_task_patched = True + + +def _uninstrument_subagent_task_tool() -> None: + global _is_subagent_task_patched # noqa: PLW0603 + if not _is_subagent_task_patched: + return + with suppress(Exception): + module = import_module(BUILD_TASK_TOOL_MODULE) + unwrap(module, BUILD_TASK_TOOL_NAME) + _is_subagent_task_patched = False + + +def _build_task_tool_wrapper( + wrapped: Callable[..., Any], + _instance: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + _mark_subagent_specs(_subagent_specs_from_call(args, kwargs)) + return wrapped(*args, **kwargs) + + +def _subagent_specs_from_call( + args: tuple[Any, ...], + kwargs: Mapping[str, Any], +) -> Any: + if args: + return args[0] + return kwargs.get("subagents") + + +def _mark_subagent_specs(subagents: Any) -> None: + for spec in subagents or (): + try: + name = spec.get("name") if isinstance(spec, dict) else None + description = ( + spec.get("description") if isinstance(spec, dict) else None + ) + runnable = spec.get("runnable") if isinstance(spec, dict) else None + if not name or runnable is None: + continue + spec["runnable"] = _mark_subagent_runnable( + runnable, + name=str(name), + description=str(description) if description else None, + ) + except Exception: # noqa: BLE001 + _logger.debug("Failed to mark deepagents SubAgent graph", exc_info=True) + + +def _mark_subagent_runnable( + runnable: Any, + *, + name: str, + description: str | None, +) -> Any: + metadata = create_graph_metadata(runnable, name=name) + metadata.setdefault(METADATA_LS_AGENT_TYPE, SUBAGENT_TYPE) + metadata.setdefault(LANGGRAPH_REACT_AGENT_METADATA_KEY, True) + if description: + metadata.setdefault(METADATA_SUBAGENT_DESCRIPTION, description) + + registry = {name: description} if description else {} + _mark_graph(runnable, metadata, registry) + proxy = _SubagentRunnableProxy(runnable, metadata) + _mark_graph(proxy, metadata, registry) + return proxy + + +class _SubagentRunnableProxy: + """Proxy that injects deepagents metadata before nested SubAgent calls.""" + + __slots__ = ("_metadata", "_runnable") + + def __init__(self, runnable: Any, metadata: Mapping[str, Any]) -> None: + self._runnable = runnable + self._metadata = dict(metadata) + + def __getattr__(self, name: str) -> Any: + return getattr(self._runnable, name) + + def invoke(self, value: Any, config: Any = None, **kwargs: Any) -> Any: + return self._runnable.invoke( + value, + config_with_langgraph_react_metadata(config, self._metadata), + **kwargs, + ) + + async def ainvoke( + self, + value: Any, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self._runnable.ainvoke( + value, + config_with_langgraph_react_metadata(config, self._metadata), + **kwargs, + ) + + +def _mark_graph( + graph: Any, + metadata: dict[str, Any], + registry: dict[str, str], +) -> None: + version = detect_deepagents_version(metadata) + with suppress(Exception): + setattr(graph, GRAPH_ATTR, True) + setattr(graph, LANGGRAPH_REACT_AGENT_METADATA_KEY, True) + setattr(graph, GRAPH_METADATA_ATTR, metadata) + setattr(graph, GRAPH_REGISTRY_ATTR, registry) + if version: + setattr(graph, GRAPH_VERSION_ATTR, version) + + +def _wrap_graph_methods( + graph: Any, + metadata: dict[str, Any], + registry: dict[str, str], +) -> None: + if getattr(graph, GRAPH_METHODS_WRAPPED_ATTR, False): + return + + originals: dict[str, Any] = {} + for method_name in ("invoke", "ainvoke", "stream", "astream"): + original = getattr(graph, method_name, None) + if original is None: + continue + originals[method_name] = original + wrapper = _make_method_wrapper( + graph=graph, + method_name=method_name, + original=original, + metadata=metadata, + registry=registry, + ) + try: + setattr(graph, method_name, wrapper) + except Exception: # noqa: BLE001 + _logger.debug( + "Failed to wrap deepagents graph method %s", method_name, exc_info=True + ) + + with suppress(Exception): + setattr(graph, GRAPH_ORIGINAL_METHODS_ATTR, originals) + setattr(graph, GRAPH_METHODS_WRAPPED_ATTR, True) + + +def _make_method_wrapper( + *, + graph: Any, + method_name: str, + original: Callable[..., Any], + metadata: dict[str, Any], + registry: dict[str, str], +) -> Callable[..., Any]: + if method_name == "ainvoke": + + async def ainvoke_wrapper(*args: Any, **kwargs: Any) -> Any: + return await _call_async_with_entry( + graph, method_name, original, metadata, registry, args, kwargs + ) + + return ainvoke_wrapper + + if method_name == "stream": + + def stream_wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]: + yield from _call_stream_with_entry( + graph, method_name, original, metadata, registry, args, kwargs + ) + + return stream_wrapper + + if method_name == "astream": + + async def astream_wrapper(*args: Any, **kwargs: Any) -> AsyncIterator[Any]: + async for chunk in _call_astream_with_entry( + graph, method_name, original, metadata, registry, args, kwargs + ): + yield chunk + + return astream_wrapper + + def invoke_wrapper(*args: Any, **kwargs: Any) -> Any: + return _call_sync_with_entry( + graph, method_name, original, metadata, registry, args, kwargs + ) + + return invoke_wrapper + + +def _create_entry_invocation( + graph: Any, + method_name: str, + metadata: dict[str, Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> EntryInvocation: + config = config_from_call(args, kwargs) + invocation = EntryInvocation( + session_id=session_id_from_config(config), + input_messages=input_messages_from_value(input_value_from_call(args, kwargs)), + attributes=entry_attributes(method_name=method_name, metadata=metadata), + ) + agent_name = root_agent_name(graph, metadata) + if agent_name: + invocation.attributes[GEN_AI_AGENT_NAME] = agent_name + return invocation + + +def _start_entry( + graph: Any, + method_name: str, + metadata: dict[str, Any], + registry: dict[str, str], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> tuple[EntryInvocation | None, Any]: + if _handler is None or active_span_is_entry_or_agent(): + return None, None + + invocation = _create_entry_invocation(graph, method_name, metadata, args, kwargs) + token = set_current_subagent_registry(registry) + _handler.start_entry(invocation) + prime_entry_span(invocation.span, method_name=method_name, metadata=metadata) + _rename_entry_span(invocation, graph, method_name, metadata) + return invocation, token + + +def _rename_entry_span( + invocation: EntryInvocation, + graph: Any, + method_name: str, + metadata: dict[str, Any], +) -> None: + span = invocation.span + if span is None: + return + version = detect_deepagents_version(metadata) + with suppress(Exception): + span.update_name(f"deepagents.{method_name}") + safe_set_attribute(span, GEN_AI_SPAN_KIND, SPAN_KIND_ENTRY) + safe_set_attribute(span, GEN_AI_OPERATION_NAME, method_name) + safe_set_attribute(span, GEN_AI_FRAMEWORK, FRAMEWORK_NAME) + safe_set_attribute(span, GEN_AI_FRAMEWORK_VERSION, version) + safe_set_attribute(span, GEN_AI_AGENT_NAME, root_agent_name(graph, metadata)) + + +def _finish_entry( + invocation: EntryInvocation | None, + token: Any, + result: Any = None, + exc: Exception | None = None, +) -> None: + if token is not None: + reset_current_subagent_registry(token) + if invocation is None or _handler is None: + return + if exc is None: + invocation.output_messages = output_messages_from_value(result) + _handler.stop_entry(invocation) + return + _handler.fail_entry(invocation, Error(message=str(exc), type=type(exc))) + + +def _call_sync_with_entry( + graph: Any, + method_name: str, + original: Callable[..., Any], + metadata: dict[str, Any], + registry: dict[str, str], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + invocation, token = _start_entry( + graph, method_name, metadata, registry, args, kwargs + ) + args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata) + try: + result = original(*args, **kwargs) + except Exception as exc: + _finish_entry(invocation, token, exc=exc) + raise + _finish_entry(invocation, token, result=result) + return result + + +async def _call_async_with_entry( + graph: Any, + method_name: str, + original: Callable[..., Any], + metadata: dict[str, Any], + registry: dict[str, str], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + invocation, token = _start_entry( + graph, method_name, metadata, registry, args, kwargs + ) + args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata) + try: + result = await original(*args, **kwargs) + except Exception as exc: + _finish_entry(invocation, token, exc=exc) + raise + _finish_entry(invocation, token, result=result) + return result + + +def _call_stream_with_entry( + graph: Any, + method_name: str, + original: Callable[..., Any], + metadata: dict[str, Any], + registry: dict[str, str], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Iterator[Any]: + invocation, token = _start_entry( + graph, method_name, metadata, registry, args, kwargs + ) + args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata) + last_chunk = None + try: + for chunk in original(*args, **kwargs): + last_chunk = chunk + yield chunk + except Exception as exc: + _finish_entry(invocation, token, exc=exc) + raise + _finish_entry(invocation, token, result=last_chunk) + + +async def _call_astream_with_entry( + graph: Any, + method_name: str, + original: Callable[..., Any], + metadata: dict[str, Any], + registry: dict[str, str], + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> AsyncIterator[Any]: + invocation, token = _start_entry( + graph, method_name, metadata, registry, args, kwargs + ) + args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata) + last_chunk = None + try: + async for chunk in original(*args, **kwargs): + last_chunk = chunk + yield chunk + except Exception as exc: + _finish_entry(invocation, token, exc=exc) + raise + _finish_entry(invocation, token, result=last_chunk) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_metrics_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_metrics_processor.py new file mode 100644 index 000000000..e80da66c6 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_metrics_processor.py @@ -0,0 +1,195 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""SpanProcessor-backed GenAI metrics for deepagents deployments.""" + +from __future__ import annotations + +import logging +import os +from typing import Any + +from opentelemetry import metrics +from opentelemetry.instrumentation.deepagents.version import __version__ +from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider +from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, TracerProvider +from opentelemetry.trace import StatusCode + +from ._attributes import ( + DEFAULT_SLOW_THRESHOLDS_SECONDS, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, + GEN_AI_SPAN_KIND, + GENAI_SPAN_KINDS, + METRIC_CALLS_COUNT, + METRIC_CALLS_DURATION_SECONDS, + METRIC_CALLS_ERROR_COUNT, + METRIC_CALLS_SLOW_COUNT, + METRIC_LLM_FIRST_TOKEN_SECONDS, + METRIC_LLM_USAGE_TOKENS, + SPAN_KIND_LLM, + USAGE_TOKEN_ATTRIBUTES, +) + +_logger = logging.getLogger(__name__) +_processors_by_provider_id: dict[int, "DeepAgentMetricsSpanProcessor"] = {} + + +class DeepAgentMetricsSpanProcessor(SpanProcessor): + """Record LoongSuite GenAI metrics from completed GenAI spans.""" + + def __init__(self, meter_provider: Any = None) -> None: + super().__init__() + meter = metrics.get_meter( + __name__, + __version__, + meter_provider=meter_provider, + ) + self._calls_count = meter.create_counter(METRIC_CALLS_COUNT) + self._calls_duration = meter.create_histogram( + METRIC_CALLS_DURATION_SECONDS, + unit="s", + ) + self._calls_error_count = meter.create_counter(METRIC_CALLS_ERROR_COUNT) + self._calls_slow_count = meter.create_counter(METRIC_CALLS_SLOW_COUNT) + self._llm_first_token = meter.create_histogram( + METRIC_LLM_FIRST_TOKEN_SECONDS, + unit="s", + ) + self._llm_usage_tokens = meter.create_counter(METRIC_LLM_USAGE_TOKENS) + self._thresholds = _load_slow_thresholds() + self._enabled = True + + def on_start(self, span: Any, parent_context: Any = None) -> None: + del span, parent_context + + def on_end(self, span: ReadableSpan) -> None: + if not self._enabled: + return + attributes = span.attributes or {} + span_kind = attributes.get(GEN_AI_SPAN_KIND) + if span_kind not in GENAI_SPAN_KINDS: + return + + labels = { + "spanKind": str(span_kind), + "modelName": _model_name(attributes), + } + duration = _duration_seconds(span) + + self._calls_count.add(1, labels) + if duration is not None: + self._calls_duration.record(duration, labels) + threshold = self._thresholds.get(str(span_kind)) + if threshold is not None and duration > threshold: + self._calls_slow_count.add(1, labels) + if getattr(span.status, "status_code", None) == StatusCode.ERROR: + self._calls_error_count.add(1, labels) + + if span_kind != SPAN_KIND_LLM: + return + + ttft_ns = _to_float(attributes.get(GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN)) + if ttft_ns is not None: + self._llm_first_token.record(ttft_ns / 1_000_000_000, labels) + + for usage_type, attribute_name in USAGE_TOKEN_ATTRIBUTES.items(): + token_count = _to_float(attributes.get(attribute_name)) + if token_count is None: + continue + usage_labels = { + "spanKind": SPAN_KIND_LLM, + "modelName": labels["modelName"], + "usageType": usage_type, + } + self._llm_usage_tokens.add(token_count, usage_labels) + + def shutdown(self) -> None: + self._enabled = False + + def force_flush(self, timeout_millis: int = 30000) -> bool: + del timeout_millis + return True + + +def install_metrics_processor( + *, + tracer_provider: Any, + meter_provider: Any = None, +) -> None: + if not isinstance(tracer_provider, TracerProvider): + _logger.warning( + "deepagents metrics require an SDK TracerProvider; metrics skipped." + ) + return + if meter_provider is None: + _logger.warning( + "deepagents metrics meter_provider was not supplied; using the " + "global MeterProvider." + ) + meter_provider = metrics.get_meter_provider() + if not isinstance(meter_provider, SDKMeterProvider): + _logger.warning( + "deepagents metrics MeterProvider is %s, not an SDK MeterProvider; " + "metrics may be no-op.", + type(meter_provider).__name__, + ) + provider_id = id(tracer_provider) + if provider_id in _processors_by_provider_id: + return + processor = DeepAgentMetricsSpanProcessor(meter_provider=meter_provider) + tracer_provider.add_span_processor(processor) + _processors_by_provider_id[provider_id] = processor + + +def shutdown_metrics_processors() -> None: + for processor in list(_processors_by_provider_id.values()): + processor.shutdown() + _processors_by_provider_id.clear() + + +def _duration_seconds(span: ReadableSpan) -> float | None: + if span.start_time is None or span.end_time is None: + return None + return max((span.end_time - span.start_time) / 1_000_000_000, 0.0) + + +def _model_name(attributes: Any) -> str: + return str( + attributes.get(GEN_AI_REQUEST_MODEL) + or attributes.get(GEN_AI_RESPONSE_MODEL) + or "" + ) + + +def _to_float(value: Any) -> float | None: + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _load_slow_thresholds() -> dict[str, float]: + thresholds = dict(DEFAULT_SLOW_THRESHOLDS_SECONDS) + for span_kind, default in DEFAULT_SLOW_THRESHOLDS_SECONDS.items(): + env_name = f"LOONGSUITE_DEEPAGENTS_SLOW_THRESHOLD_{span_kind}_MS" + raw_value = os.getenv(env_name) + if raw_value is None: + thresholds[span_kind] = default + continue + try: + thresholds[span_kind] = float(raw_value) / 1000.0 + except ValueError: + _logger.warning( + "Invalid %s=%r; using default %.3fs", + env_name, + raw_value, + default, + ) + thresholds[span_kind] = default + return thresholds diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_utils.py new file mode 100644 index 000000000..d72239093 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_utils.py @@ -0,0 +1,431 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""Utility helpers for deepagents telemetry.""" + +from __future__ import annotations + +import contextvars +import json +import logging +from collections.abc import Mapping +from importlib import import_module +from typing import Any + +from opentelemetry import trace +from opentelemetry.trace import Span +from opentelemetry.util.genai.types import ( + InputMessage, + OutputMessage, + Text, + ToolCall, + ToolCallResponse, +) + +try: + from langchain_core.runnables.config import ensure_config as _ensure_config +except ModuleNotFoundError: + _ensure_config = None # type: ignore[assignment] + +from ._attributes import ( + ENTRY_PARENT_KINDS, + FRAMEWORK_NAME, + GEN_AI_AGENT_NAME, + GEN_AI_FRAMEWORK, + GEN_AI_FRAMEWORK_VERSION, + GEN_AI_OPERATION_NAME, + GEN_AI_SPAN_KIND, + GRAPH_METADATA_ATTR, + LANGGRAPH_REACT_AGENT_METADATA_KEY, + METADATA_DEEPAGENTS_VERSION, + METADATA_LC_AGENT_NAME, + METADATA_LS_AGENT_TYPE, + METADATA_LS_INTEGRATION, + METADATA_SUBAGENT_DESCRIPTION, + METADATA_VERSIONS, + SPAN_KIND_ENTRY, +) + +_logger = logging.getLogger(__name__) +_CURRENT_SUBAGENT_REGISTRY: contextvars.ContextVar[dict[str, str] | None] = ( + contextvars.ContextVar( + "opentelemetry_deepagents_subagent_registry", + default=None, + ) +) + + +def obj_get(value: Any, field: str, default: Any = None) -> Any: + if isinstance(value, Mapping): + return value.get(field, default) + return getattr(value, field, default) + + +def set_current_subagent_registry(registry: dict[str, str] | None): + return _CURRENT_SUBAGENT_REGISTRY.set(registry or None) + + +def reset_current_subagent_registry(token: Any) -> None: + _CURRENT_SUBAGENT_REGISTRY.reset(token) + + +def current_subagent_registry() -> dict[str, str]: + return _CURRENT_SUBAGENT_REGISTRY.get() or {} + + +def detect_deepagents_version(metadata: Mapping[str, Any] | None = None) -> str: + versions = obj_get(metadata or {}, METADATA_VERSIONS, {}) + version = obj_get(versions, METADATA_DEEPAGENTS_VERSION) + if version: + return str(version) + try: + return str(getattr(import_module("deepagents"), "__version__")) + except Exception: # noqa: BLE001 + return "" + + +def graph_metadata(graph: Any, fallback: Mapping[str, Any] | None = None) -> dict[str, Any]: + existing = getattr(graph, GRAPH_METADATA_ATTR, None) + if isinstance(existing, Mapping): + return dict(existing) + + config = getattr(graph, "config", None) + metadata = obj_get(config or {}, "metadata", None) + if isinstance(metadata, Mapping): + return dict(metadata) + + return dict(fallback or {}) + + +def root_agent_name(graph: Any, metadata: Mapping[str, Any] | None = None) -> str | None: + name = obj_get(metadata or {}, METADATA_LC_AGENT_NAME) + if name: + return str(name) + config = getattr(graph, "config", None) + metadata_from_config = obj_get(config or {}, "metadata", {}) + name = obj_get(metadata_from_config, METADATA_LC_AGENT_NAME) + if name: + return str(name) + name = getattr(graph, "name", None) + return str(name) if name else None + + +def create_graph_metadata( + graph: Any, + *, + name: Any = None, +) -> dict[str, Any]: + metadata = graph_metadata(graph) + metadata.setdefault(METADATA_LS_INTEGRATION, FRAMEWORK_NAME) + versions = dict(obj_get(metadata, METADATA_VERSIONS, {}) or {}) + version = detect_deepagents_version(metadata) + if version: + versions.setdefault(METADATA_DEEPAGENTS_VERSION, version) + if versions: + metadata[METADATA_VERSIONS] = versions + if name is not None: + metadata.setdefault(METADATA_LC_AGENT_NAME, str(name)) + return metadata + + +def extract_subagent_registry( + subagents: Any, +) -> dict[str, str]: + registry: dict[str, str] = {} + for spec in subagents or (): + name = obj_get(spec, "name") + description = obj_get(spec, "description") + if name and description: + registry[str(name)] = str(description) + return registry + + +def active_span_is_entry_or_agent() -> bool: + current_span = trace.get_current_span() + attributes = getattr(current_span, "attributes", None) + if isinstance(attributes, Mapping): + if attributes.get(GEN_AI_SPAN_KIND) in ENTRY_PARENT_KINDS: + return True + + # util-genai sets the span kind on finish. The name check prevents nested + # ENTRY creation while an unfinished util-genai ENTRY/AGENT span is active. + span_name = str(getattr(current_span, "name", "") or "") + return span_name.startswith(("enter_ai_application_system", "invoke_agent")) + + +def safe_set_attribute(span: Any, key: str, value: Any) -> None: + if value is None: + return + setter = getattr(span, "set_attribute", None) + if setter is None: + return + try: + setter(key, value) + except Exception: # noqa: BLE001 + _logger.debug("Failed to set span attribute %s", key, exc_info=True) + + +def prime_entry_span( + span: Span | None, + *, + method_name: str, + metadata: Mapping[str, Any], +) -> None: + if span is None: + return + version = detect_deepagents_version(metadata) + safe_set_attribute(span, GEN_AI_SPAN_KIND, SPAN_KIND_ENTRY) + safe_set_attribute(span, GEN_AI_OPERATION_NAME, method_name) + safe_set_attribute(span, GEN_AI_FRAMEWORK, FRAMEWORK_NAME) + if version: + safe_set_attribute(span, GEN_AI_FRAMEWORK_VERSION, version) + safe_set_attribute(span, GEN_AI_AGENT_NAME, obj_get(metadata, METADATA_LC_AGENT_NAME)) + + +def entry_attributes( + *, + method_name: str, + metadata: Mapping[str, Any], +) -> dict[str, Any]: + attributes: dict[str, Any] = { + GEN_AI_OPERATION_NAME: method_name, + GEN_AI_FRAMEWORK: FRAMEWORK_NAME, + } + version = detect_deepagents_version(metadata) + if version: + attributes[GEN_AI_FRAMEWORK_VERSION] = version + agent_name = obj_get(metadata, METADATA_LC_AGENT_NAME) + if agent_name: + attributes[GEN_AI_AGENT_NAME] = str(agent_name) + return attributes + + +def config_from_call(args: tuple[Any, ...], kwargs: Mapping[str, Any]) -> Any: + if len(args) > 1: + return args[1] + return kwargs.get("config") + + +def _merge_deepagents_metadata( + target: dict[str, Any], + source: Mapping[str, Any] | None, +) -> None: + if not source: + return + for key, value in source.items(): + if key == METADATA_VERSIONS and isinstance(value, Mapping): + versions = dict(obj_get(target, METADATA_VERSIONS, {}) or {}) + for version_key, version_value in value.items(): + versions.setdefault(version_key, version_value) + if versions: + target[METADATA_VERSIONS] = versions + continue + if key == METADATA_SUBAGENT_DESCRIPTION and value: + target[key] = value + continue + if key in { + METADATA_LS_INTEGRATION, + METADATA_LS_AGENT_TYPE, + METADATA_LC_AGENT_NAME, + METADATA_SUBAGENT_DESCRIPTION, + }: + target[key] = value + continue + target.setdefault(key, value) + + +def config_with_langgraph_react_metadata( + config: Any, + metadata: Mapping[str, Any] | None = None, +) -> Any: + if _ensure_config is None: + ensured = config or {} + else: + try: + ensured = _ensure_config(config) + except Exception: # noqa: BLE001 + ensured = config or {} + + if not isinstance(ensured, Mapping): + return ensured + + updated = dict(ensured) + updated_metadata = dict(obj_get(updated, "metadata", {}) or {}) + _merge_deepagents_metadata(updated_metadata, metadata) + updated_metadata.setdefault(LANGGRAPH_REACT_AGENT_METADATA_KEY, True) + updated["metadata"] = updated_metadata + return updated + + +def inject_langgraph_react_metadata( + args: tuple[Any, ...], + kwargs: Mapping[str, Any], + metadata: Mapping[str, Any] | None = None, +) -> tuple[tuple[Any, ...], dict[str, Any]]: + updated_kwargs = dict(kwargs) + if len(args) > 1: + config = config_with_langgraph_react_metadata(args[1], metadata) + return (args[0], config) + args[2:], updated_kwargs + + updated_kwargs["config"] = config_with_langgraph_react_metadata( + updated_kwargs.get("config"), + metadata, + ) + return args, updated_kwargs + + +def session_id_from_config(config: Any) -> str | None: + configurable = obj_get(config or {}, "configurable", {}) + thread_id = obj_get(configurable or {}, "thread_id") + return str(thread_id) if thread_id is not None else None + + +def input_value_from_call(args: tuple[Any, ...], kwargs: Mapping[str, Any]) -> Any: + if args: + return args[0] + if "input" in kwargs: + return kwargs["input"] + return None + + +def _flatten_content(value: Any) -> str | None: + if value is None: + return None + if isinstance(value, str): + return value if value.strip() else None + if isinstance(value, list): + parts = [] + for item in value: + text = obj_get(item, "text") + content = obj_get(item, "content") + if text: + parts.append(str(text)) + elif content: + parts.append(str(content)) + return "\n".join(parts).strip() or None + return str(value) + + +def _tool_call_parts(message: Any) -> list[ToolCall]: + parts: list[ToolCall] = [] + for tool_call in obj_get(message, "tool_calls", []) or []: + function = obj_get(tool_call, "function", {}) + name = obj_get(tool_call, "name") or obj_get(function, "name") + if not name: + continue + arguments = ( + obj_get(tool_call, "args") + if obj_get(tool_call, "args") is not None + else obj_get(function, "arguments", obj_get(tool_call, "arguments", {})) + ) + parts.append( + ToolCall( + id=obj_get(tool_call, "id"), + name=str(name), + arguments=arguments, + ) + ) + return parts + + +def _message_role(message: Any) -> str | None: + role = obj_get(message, "role") or obj_get(message, "type") + if role == "human": + return "user" + if role == "ai": + return "assistant" + if role == "function": + return "tool" + return str(role) if role else None + + +def _message_to_input(message: Any) -> InputMessage | None: + role = _message_role(message) + if not role: + return None + + if role == "tool": + return InputMessage( + role="tool", + parts=[ + ToolCallResponse( + id=obj_get(message, "tool_call_id"), + response=_flatten_content(obj_get(message, "content")), + ) + ], + ) + + parts: list[Any] = [] + content = _flatten_content(obj_get(message, "content", message)) + if content: + parts.append(Text(content=content)) + parts.extend(_tool_call_parts(message)) + return InputMessage(role=role, parts=parts) if parts else None + + +def _message_to_output(message: Any) -> OutputMessage | None: + input_message = _message_to_input(message) + if input_message is None: + return None + finish_reason = obj_get(message, "finish_reason") or obj_get( + obj_get(message, "response_metadata", {}), + "finish_reason", + ) + return OutputMessage( + role=input_message.role, + parts=input_message.parts, + finish_reason=str(finish_reason or "stop"), + ) + + +def _messages_from_state(value: Any) -> list[Any]: + if isinstance(value, Mapping) and "messages" in value: + messages = value.get("messages") + if isinstance(messages, list): + return messages + return [messages] + if isinstance(value, list): + return value + if value is None: + return [] + return [{"role": "user", "content": _safe_json(value)}] + + +def input_messages_from_value(value: Any) -> list[InputMessage]: + messages = [] + for message in _messages_from_state(value): + converted = _message_to_input(message) + if converted is not None: + messages.append(converted) + return messages + + +def output_messages_from_value(value: Any) -> list[OutputMessage]: + messages = _messages_from_state(value) + if messages: + messages = [messages[-1]] + converted_messages = [] + for message in messages: + converted = _message_to_output(message) + if converted is not None: + converted_messages.append(converted) + if converted_messages: + return converted_messages + if value is None: + return [] + return [ + OutputMessage( + role="assistant", + parts=[Text(content=_safe_json(value))], + finish_reason="stop", + ) + ] + + +def _safe_json(value: Any) -> str: + try: + return json.dumps(value, default=str, ensure_ascii=False) + except Exception: # noqa: BLE001 + return str(value) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/package.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/package.py new file mode 100644 index 000000000..64051c459 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/package.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_instruments = ("deepagents >= 0.6.0, < 0.7.0",) + +_supports_metrics = True diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/version.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/version.py new file mode 100644 index 000000000..14eb7863c --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.6.0.dev" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/conftest.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/conftest.py new file mode 100644 index 000000000..62dc44b1d --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/conftest.py @@ -0,0 +1,61 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +import pytest + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = Path(__file__).resolve().parents[3] + +for path in ( + PACKAGE_ROOT / "src", + REPO_ROOT / "util" / "opentelemetry-util-genai" / "src", +): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from opentelemetry.sdk.metrics import MeterProvider # noqa: E402 +from opentelemetry.sdk.metrics.export import InMemoryMetricReader # noqa: E402 +from opentelemetry.sdk.trace import TracerProvider # noqa: E402 +from opentelemetry.sdk.trace.export import SimpleSpanProcessor # noqa: E402 +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( # noqa: E402 + InMemorySpanExporter, +) + + +def pytest_configure() -> None: + os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai_latest_experimental" + os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( + "SPAN_ONLY" + ) + + +@pytest.fixture(name="span_exporter") +def fixture_span_exporter(): + exporter = InMemorySpanExporter() + yield exporter + + +@pytest.fixture(name="tracer_provider") +def fixture_tracer_provider(span_exporter): + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + return provider + + +@pytest.fixture(name="metric_reader") +def fixture_metric_reader(): + return InMemoryMetricReader() + + +@pytest.fixture(name="meter_provider") +def fixture_meter_provider(metric_reader): + return MeterProvider(metric_readers=[metric_reader]) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test-requirements.txt b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test-requirements.txt new file mode 100644 index 000000000..f02f0785f --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test-requirements.txt @@ -0,0 +1,12 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +pytest + +-e opentelemetry-instrumentation +-e util/opentelemetry-util-genai +-e instrumentation-loongsuite/loongsuite-instrumentation-langchain +-e instrumentation-loongsuite/loongsuite-instrumentation-langgraph +-e instrumentation-loongsuite/loongsuite-instrumentation-deepagents diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_enricher.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_enricher.py new file mode 100644 index 000000000..46bc3e2c7 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_enricher.py @@ -0,0 +1,101 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from __future__ import annotations + +from uuid import uuid4 + +from opentelemetry.instrumentation.deepagents.internal import _enricher +from opentelemetry.instrumentation.deepagents.internal._enricher import ( + DeepAgentsEnricherCallbackHandler, +) +from opentelemetry.instrumentation.deepagents.internal._utils import ( + reset_current_subagent_registry, + set_current_subagent_registry, +) + + +def test_enricher_sets_framework_and_subagent_attributes( + tracer_provider, + span_exporter, +): + handler = DeepAgentsEnricherCallbackHandler() + tracer = tracer_provider.get_tracer(__name__) + token = set_current_subagent_registry({"researcher": "Research agent"}) + + with tracer.start_as_current_span("invoke_agent researcher"): + handler.on_chain_start( + {}, + {}, + run_id=uuid4(), + metadata={ + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.2"}, + "lc_agent_name": "researcher", + "ls_agent_type": "subagent", + }, + ) + + reset_current_subagent_registry(token) + [span] = span_exporter.get_finished_spans() + attributes = span.attributes + assert attributes["gen_ai.framework"] == "deepagents" + assert attributes["gen_ai.framework.version"] == "0.6.2" + assert attributes["gen_ai.agent.name"] == "researcher" + assert attributes["gen_ai.agent.type"] == "subagent" + assert attributes["gen_ai.agent.description"] == "Research agent" + + +def test_enricher_marks_task_tool_as_agent_tool( + tracer_provider, + span_exporter, +): + handler = DeepAgentsEnricherCallbackHandler() + tracer = tracer_provider.get_tracer(__name__) + + with tracer.start_as_current_span("execute_tool task"): + handler.on_tool_start({"name": "task"}, "", run_id=uuid4()) + + [span] = span_exporter.get_finished_spans() + assert span.attributes["gen_ai.tool.name"] == "task" + assert span.attributes["gen_ai.tool.type"] == "agent" + + +def test_enricher_targets_loongsuite_run_span_and_uses_registry_fallback( + tracer_provider, + span_exporter, + monkeypatch, +): + handler = DeepAgentsEnricherCallbackHandler() + tracer = tracer_provider.get_tracer(__name__) + run_id = uuid4() + token = set_current_subagent_registry({"researcher": "Research agent"}) + run_span = tracer.start_span("invoke_agent researcher") + + monkeypatch.setattr( + _enricher, + "_span_for_run", + lambda seen_run_id: run_span if seen_run_id == run_id else None, + ) + + with tracer.start_as_current_span("current chain"): + handler.on_chain_start( + {}, + {}, + run_id=run_id, + metadata={ + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.2"}, + "lc_agent_name": "researcher", + }, + ) + + run_span.end() + reset_current_subagent_registry(token) + spans = {span.name: span for span in span_exporter.get_finished_spans()} + run_attributes = spans["invoke_agent researcher"].attributes + assert run_attributes["gen_ai.agent.type"] == "subagent" + assert run_attributes["gen_ai.agent.description"] == "Research agent" + assert "gen_ai.agent.type" not in spans["current chain"].attributes diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_entry_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_entry_patch.py new file mode 100644 index 000000000..9ce12e7ed --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_entry_patch.py @@ -0,0 +1,229 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from __future__ import annotations + +import sys +import types + +import pytest + +from opentelemetry.instrumentation.deepagents.internal._entry_patch import ( + instrument_entry_patch, + uninstrument_entry_patch, +) +from opentelemetry.trace import get_tracer +from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler + + +class FakeGraph: + def __init__(self, name: str = "supervisor") -> None: + self.name = name + self.last_config = None + self.config = { + "metadata": { + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.2"}, + "lc_agent_name": name, + } + } + + def with_config(self, config): + bound = FakeGraph(self.name) + bound.bound = self + bound.config = config + return bound + + def invoke(self, value, config=None): + self.last_config = config + return {"messages": [*value["messages"], {"role": "assistant", "content": "done"}]} + + async def ainvoke(self, value, config=None): + self.last_config = config + return {"messages": [*value["messages"], {"role": "assistant", "content": "done"}]} + + def stream(self, value, config=None): + self.last_config = config + yield {"messages": [*value["messages"], {"role": "assistant", "content": "part"}]} + yield {"messages": [*value["messages"], {"role": "assistant", "content": "done"}]} + + async def astream(self, value, config=None): + self.last_config = config + yield {"messages": [*value["messages"], {"role": "assistant", "content": "done"}]} + + +@pytest.fixture(name="fake_deepagents_graph") +def fixture_fake_deepagents_graph(monkeypatch): + deepagents_module = types.ModuleType("deepagents") + deepagents_module.__path__ = [] + deepagents_module.__version__ = "0.6.2" + graph_module = types.ModuleType("deepagents.graph") + middleware_module = types.ModuleType("deepagents.middleware") + middleware_module.__path__ = [] + subagents_module = types.ModuleType("deepagents.middleware.subagents") + + def create_deep_agent(*, name="supervisor", subagents=None): + del subagents + return FakeGraph(name) + + def _build_task_tool(subagents, task_description=None): + del task_description + return [spec["runnable"] for spec in subagents] + + graph_module.create_deep_agent = create_deep_agent + subagents_module._build_task_tool = _build_task_tool + deepagents_module.create_deep_agent = create_deep_agent + deepagents_module.graph = graph_module + deepagents_module.middleware = middleware_module + middleware_module.subagents = subagents_module + monkeypatch.setitem(sys.modules, "deepagents", deepagents_module) + monkeypatch.setitem(sys.modules, "deepagents.graph", graph_module) + monkeypatch.setitem(sys.modules, "deepagents.middleware", middleware_module) + monkeypatch.setitem( + sys.modules, + "deepagents.middleware.subagents", + subagents_module, + ) + try: + yield graph_module + finally: + uninstrument_entry_patch() + + +def _entry_spans(span_exporter): + return [ + span + for span in span_exporter.get_finished_spans() + if span.attributes.get("gen_ai.span.kind") == "ENTRY" + ] + + +def test_invoke_creates_one_deepagents_entry_span( + fake_deepagents_graph, + tracer_provider, + span_exporter, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + instrument_entry_patch(handler) + + graph = fake_deepagents_graph.create_deep_agent( + name="supervisor", + subagents=[ + {"name": "researcher", "description": "Research agent"}, + ], + ) + result = graph.invoke( + {"messages": [{"role": "user", "content": "hi"}]}, + {"configurable": {"thread_id": "thread-1"}}, + ) + + assert result["messages"][-1]["content"] == "done" + assert getattr(graph, "_loongsuite_react_agent") is True + assert graph.last_config["metadata"]["_loongsuite_react_agent"] is True + assert graph.last_config["metadata"]["ls_integration"] == "deepagents" + assert graph.last_config["metadata"]["versions"]["deepagents"] == "0.6.2" + assert graph.last_config["metadata"]["lc_agent_name"] == "supervisor" + [entry_span] = _entry_spans(span_exporter) + attributes = entry_span.attributes + assert attributes["gen_ai.operation.name"] == "invoke" + assert attributes["gen_ai.framework"] == "deepagents" + assert attributes["gen_ai.framework.version"] == "0.6.2" + assert attributes["gen_ai.agent.name"] == "supervisor" + assert attributes["gen_ai.session.id"] == "thread-1" + + +def test_top_level_create_deep_agent_export_is_wrapped_and_restored( + fake_deepagents_graph, + tracer_provider, + span_exporter, +): + import deepagents # noqa: PLC0415 + + original_top_level = deepagents.create_deep_agent + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + instrument_entry_patch(handler) + + from deepagents import create_deep_agent # noqa: PLC0415 + + assert create_deep_agent is deepagents.create_deep_agent + assert create_deep_agent is fake_deepagents_graph.create_deep_agent + assert create_deep_agent is not original_top_level + + graph = create_deep_agent(name="supervisor") + graph.invoke({"messages": [{"role": "user", "content": "hi"}]}) + + [entry_span] = _entry_spans(span_exporter) + assert entry_span.attributes["gen_ai.operation.name"] == "invoke" + + uninstrument_entry_patch() + assert deepagents.create_deep_agent is original_top_level + + +def test_entry_is_skipped_inside_existing_agent_span( + fake_deepagents_graph, + tracer_provider, + span_exporter, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + instrument_entry_patch(handler) + graph = fake_deepagents_graph.create_deep_agent(name="supervisor") + tracer = get_tracer(__name__, tracer_provider=tracer_provider) + + with tracer.start_as_current_span("invoke_agent parent") as span: + span.set_attribute("gen_ai.span.kind", "AGENT") + graph.invoke({"messages": [{"role": "user", "content": "nested"}]}) + + assert _entry_spans(span_exporter) == [] + + +def test_stream_keeps_entry_open_until_iteration_finishes( + fake_deepagents_graph, + tracer_provider, + span_exporter, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + instrument_entry_patch(handler) + graph = fake_deepagents_graph.create_deep_agent(name="supervisor") + + chunks = list(graph.stream({"messages": [{"role": "user", "content": "hi"}]})) + + assert chunks[-1]["messages"][-1]["content"] == "done" + [entry_span] = _entry_spans(span_exporter) + assert entry_span.attributes["gen_ai.operation.name"] == "stream" + + +def test_subagent_task_tool_marks_nested_graph_metadata( + fake_deepagents_graph, + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + instrument_entry_patch(handler) + subagents_module = sys.modules["deepagents.middleware.subagents"] + runnable = FakeGraph("researcher") + + [bound] = subagents_module._build_task_tool( + [ + { + "name": "researcher", + "description": "Research agent", + "runnable": runnable, + } + ] + ) + + bound.invoke({"messages": [{"role": "user", "content": "hi"}]}) + + assert getattr(runnable, "_loongsuite_react_agent") is True + assert getattr(bound, "_loongsuite_react_agent") is True + metadata = runnable.last_config["metadata"] + assert metadata["_loongsuite_react_agent"] is True + assert metadata["ls_integration"] == "deepagents" + assert metadata["versions"]["deepagents"] == "0.6.2" + assert metadata["lc_agent_name"] == "researcher" + assert metadata["ls_agent_type"] == "subagent" + assert ( + metadata["loongsuite_deepagents_subagent_description"] + == "Research agent" + ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_metrics.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_metrics.py new file mode 100644 index 000000000..474f12afc --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_metrics.py @@ -0,0 +1,98 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from __future__ import annotations + +import logging + +from opentelemetry.instrumentation.deepagents import DeepAgentsInstrumentor +from opentelemetry.instrumentation.deepagents.internal._metrics_processor import ( + DeepAgentMetricsSpanProcessor, + install_metrics_processor, + shutdown_metrics_processors, +) + + +def _metric_names(metric_reader): + metric_reader.collect() + metrics_data = metric_reader.get_metrics_data() + names = set() + for resource_metrics in metrics_data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + names.add(metric.name) + return names + + +def test_metrics_processor_records_genai_call_and_llm_usage( + tracer_provider, + meter_provider, + metric_reader, +): + tracer_provider.add_span_processor( + DeepAgentMetricsSpanProcessor(meter_provider=meter_provider) + ) + tracer = tracer_provider.get_tracer(__name__) + + with tracer.start_as_current_span("chat qwen3.6-plus") as span: + span.set_attribute("gen_ai.span.kind", "LLM") + span.set_attribute("gen_ai.request.model", "qwen3.6-plus") + span.set_attribute("gen_ai.usage.input_tokens", 11) + span.set_attribute("gen_ai.usage.output_tokens", 7) + span.set_attribute("gen_ai.usage.total_tokens", 18) + span.set_attribute("gen_ai.response.time_to_first_token", 250_000_000) + + names = _metric_names(metric_reader) + assert "genai_calls_count" in names + assert "genai_calls_duration_seconds" in names + assert "genai_llm_usage_tokens" in names + assert "genai_llm_first_token_seconds" in names + + +def test_instrumentor_warns_when_meter_provider_defaults_to_global( + tracer_provider, + caplog, +): + instrumentor = DeepAgentsInstrumentor() + caplog.set_level(logging.WARNING) + + instrumentor._instrument(tracer_provider=tracer_provider) + try: + assert "meter_provider was not supplied" in caplog.text + assert "metrics may be no-op" in caplog.text + finally: + instrumentor._uninstrument() + + +def test_metrics_processor_warns_when_meter_provider_is_missing( + tracer_provider, + caplog, +): + caplog.set_level(logging.WARNING) + + install_metrics_processor(tracer_provider=tracer_provider) + try: + assert "deepagents metrics meter_provider was not supplied" in caplog.text + assert "metrics may be no-op" in caplog.text + finally: + shutdown_metrics_processors() + + +def test_metrics_processor_accepts_sdk_meter_provider_without_warning( + tracer_provider, + meter_provider, + caplog, +): + caplog.set_level(logging.WARNING) + + install_metrics_processor( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + try: + assert "meter_provider was not supplied" not in caplog.text + assert "metrics may be no-op" not in caplog.text + finally: + shutdown_metrics_processors() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py index 830159f5c..ccbeb9ed8 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py @@ -40,10 +40,12 @@ import logging import timeit +from collections.abc import Mapping from dataclasses import dataclass from threading import RLock from typing import Any, Literal, Optional from uuid import UUID +from weakref import WeakSet from langchain_core.tracers.base import BaseTracer from langchain_core.tracers.schemas import Run @@ -77,6 +79,7 @@ Span, SpanKind, StatusCode, + get_current_span, get_tracer, set_span_in_context, ) @@ -102,6 +105,22 @@ ) logger = logging.getLogger(__name__) +_TRACER_INSTANCES: WeakSet["LoongsuiteTracer"] = WeakSet() +_DEEPAGENTS_FRAMEWORK_NAME = "deepagents" +_DEEPAGENTS_METADATA_INTEGRATION = "ls_integration" +_DEEPAGENTS_METADATA_VERSIONS = "versions" +_DEEPAGENTS_METADATA_AGENT_NAME = "lc_agent_name" +_DEEPAGENTS_METADATA_AGENT_TYPE = "ls_agent_type" +_DEEPAGENTS_METADATA_SUBAGENT_DESCRIPTION = ( + "loongsuite_deepagents_subagent_description" +) +_DEEPAGENTS_SUBAGENT_TYPE = "subagent" +_GEN_AI_FRAMEWORK = "gen_ai.framework" +_GEN_AI_FRAMEWORK_VERSION = "gen_ai.framework.version" +_GEN_AI_AGENT_NAME = "gen_ai.agent.name" +_GEN_AI_AGENT_TYPE = "gen_ai.agent.type" +_GEN_AI_AGENT_DESCRIPTION = "gen_ai.agent.description" +_LANGGRAPH_RUN_NAME = "LangGraph" # --------------------------------------------------------------------------- # _RunData — per-run bookkeeping @@ -115,6 +134,9 @@ class _RunData: span: Span | None = None context: Context | None = None invocation: Any = None + parent_run_id: UUID | None = None + tool_name: str | None = None + tool_call_id: str | None = None context_token: object | None = None # only used for Chain attach/detach # Agent run only: ReAct Step state react_round: int = 0 @@ -141,6 +163,11 @@ def _should_capture_chain_content() -> bool: return False +def _span_start_context(parent_ctx: Context | None) -> Context: + """Use an empty context when no LangChain parent was resolved.""" + return parent_ctx if parent_ctx is not None else Context() + + # --------------------------------------------------------------------------- # LoongsuiteTracer # --------------------------------------------------------------------------- @@ -181,6 +208,7 @@ def __init__( # Don't use super().run_map because it will lead to unexpected behavior when multiple tracers are used. self.run_map = dict(self.run_map) self.run_map_lock = RLock() + _TRACER_INSTANCES.add(self) def _persist_run(self, run: Run) -> None: pass @@ -191,6 +219,10 @@ def _persist_run(self, run: Run) -> None: def _get_parent_context(self, run: Run) -> Context | None: """Return the stored context of the parent run, or *None*.""" + task_tool_ctx = self._get_deepagents_subagent_task_context(run) + if task_tool_ctx is not None: + return task_tool_ctx + parent_id = getattr(run, "parent_run_id", None) if parent_id: with self._lock: @@ -199,6 +231,40 @@ def _get_parent_context(self, run: Run) -> Context | None: return rd.context return None + def get_otel_span_for_run(self, run_id: UUID | str) -> Span | None: + """Return the active OTel span created for a LangChain run.""" + normalized_run_id = _normalize_run_id(run_id) + if normalized_run_id is None: + return None + with self._lock: + rd = self._runs.get(normalized_run_id) + return rd.span if rd is not None else None + + def _get_deepagents_subagent_task_context( + self, run: Run + ) -> Context | None: + if not _is_deepagents_subagent_candidate(run): + return None + + current_span = get_current_span() + if _is_task_tool_span(current_span): + return set_span_in_context(current_span) + + parent_id = getattr(run, "parent_run_id", None) + with self._lock: + run_data = list(self._runs.values()) + latest_task_context: Context | None = None + for rd in reversed(run_data): + if rd.run_kind != "tool" or rd.tool_name != "task": + continue + if latest_task_context is None: + latest_task_context = rd.context + if parent_id is not None and rd.parent_run_id != parent_id: + continue + if rd.context is not None: + return rd.context + return latest_task_context + # ------------------------------------------------------------------ # _start_trace / _end_trace # ------------------------------------------------------------------ @@ -266,7 +332,10 @@ def _handle_llm_start(self, run: Run) -> None: tool_defs = _extract_tool_definitions(run) if tool_defs: invocation.tool_definitions = tool_defs - self._handler.start_llm(invocation, context=parent_ctx) + self._handler.start_llm( + invocation, + context=_span_start_context(parent_ctx), + ) rd = _RunData( run_kind="llm", span=invocation.span, @@ -274,6 +343,7 @@ def _handle_llm_start(self, run: Run) -> None: if invocation.span else None, invocation=invocation, + parent_run_id=getattr(run, "parent_run_id", None), ) with self._lock: self._runs[run.id] = rd @@ -338,6 +408,10 @@ def _handle_langgraph_chain_start(self, run: Run) -> None: * **Otherwise** → top-level graph → create Agent span. """ parent_id = getattr(run, "parent_run_id", None) + if _is_deepagents_subagent_root(run): + self._start_agent(run) + return + with self._lock: parent_rd = self._runs.get(parent_id) if parent_id else None @@ -360,7 +434,7 @@ def _resolve_langgraph_agent_name(self, run: Run) -> str: over the generic default. """ name = run.name or "" - if not _has_langgraph_react_metadata(run) or name != "LangGraph": + if not _has_langgraph_react_metadata(run) or name != _LANGGRAPH_RUN_NAME: return name parent_id = getattr(run, "parent_run_id", None) @@ -409,7 +483,11 @@ def _start_agent(self, run: Run) -> None: agent_name=agent_name, input_messages=input_messages, ) - self._handler.start_invoke_agent(invocation, context=parent_ctx) + self._handler.start_invoke_agent( + invocation, + context=_span_start_context(parent_ctx), + ) + _enrich_deepagents_agent_span(invocation.span, run) rd = _RunData( run_kind="agent", span=invocation.span, @@ -417,6 +495,7 @@ def _start_agent(self, run: Run) -> None: if invocation.span else None, invocation=invocation, + parent_run_id=getattr(run, "parent_run_id", None), is_langgraph_react=_has_langgraph_react_metadata(run), ) with self._lock: @@ -427,7 +506,7 @@ def _start_chain(self, run: Run) -> None: span = self._tracer.start_span( name=f"chain {run.name}", kind=SpanKind.INTERNAL, - context=parent_ctx, + context=_span_start_context(parent_ctx), ) span.set_attribute(GEN_AI_OPERATION_NAME, "chain") @@ -455,6 +534,7 @@ def _start_chain(self, run: Run) -> None: span=span, context=ctx, context_token=token, + parent_run_id=parent_id, inside_langgraph_react=inside_lg, ) with self._lock: @@ -572,7 +652,10 @@ def _on_tool_start(self, run: Run) -> None: tool_call_arguments=input_str, tool_call_id=tool_call_id, ) - self._handler.start_execute_tool(invocation, context=parent_ctx) + self._handler.start_execute_tool( + invocation, + context=_span_start_context(parent_ctx), + ) rd = _RunData( run_kind="tool", span=invocation.span, @@ -580,6 +663,9 @@ def _on_tool_start(self, run: Run) -> None: if invocation.span else None, invocation=invocation, + parent_run_id=getattr(run, "parent_run_id", None), + tool_name=run.name, + tool_call_id=tool_call_id, ) with self._lock: self._runs[run.id] = rd @@ -630,7 +716,10 @@ def _on_retriever_start(self, run: Run) -> None: query = inputs.get("query") or "" invocation = RetrievalInvocation(query=query) - self._handler.start_retrieval(invocation, context=parent_ctx) + self._handler.start_retrieval( + invocation, + context=_span_start_context(parent_ctx), + ) rd = _RunData( run_kind="retriever", span=invocation.span, @@ -638,6 +727,7 @@ def _on_retriever_start(self, run: Run) -> None: if invocation.span else None, invocation=invocation, + parent_run_id=getattr(run, "parent_run_id", None), ) with self._lock: self._runs[run.id] = rd @@ -776,6 +866,98 @@ def __copy__(self) -> LoongsuiteTracer: return self +def _normalize_run_id(run_id: UUID | str | Any) -> UUID | None: + if isinstance(run_id, UUID): + return run_id + try: + return UUID(str(run_id)) + except (TypeError, ValueError): + return None + + +def get_otel_span_for_run(run_id: UUID | str | Any) -> Span | None: + """Return the active OTel span for a LangChain run across tracers.""" + for tracer in list(_TRACER_INSTANCES): + span = tracer.get_otel_span_for_run(run_id) + if span is not None: + return span + return None + + +def _is_task_tool_span(span: Span | None) -> bool: + attributes = getattr(span, "attributes", None) + if not isinstance(attributes, Mapping): + return False + return ( + attributes.get(GEN_AI_SPAN_KIND) == "TOOL" + and attributes.get("gen_ai.tool.name") == "task" + ) + + +def _is_deepagents_subagent_candidate(run: Run) -> bool: + """Detect deepagents subagent roots, preserving explicit metadata first.""" + metadata = getattr(run, "metadata", None) or {} + if not isinstance(metadata, Mapping): + return False + if metadata.get("ls_agent_type") == "subagent": + return True + if getattr(run, "name", None) != _LANGGRAPH_RUN_NAME: + return False + return ( + metadata.get("ls_integration") == "deepagents" + and _is_task_tool_span(get_current_span()) + ) + + +def _enrich_deepagents_agent_span(span: Span | None, run: Run) -> None: + """Write deepagents attributes when LangGraph metadata marks an agent run.""" + if span is None: + return + metadata = getattr(run, "metadata", None) or {} + if not isinstance(metadata, Mapping): + return + if not _is_deepagents_metadata(metadata): + return + + span.set_attribute(_GEN_AI_FRAMEWORK, _DEEPAGENTS_FRAMEWORK_NAME) + versions = metadata.get(_DEEPAGENTS_METADATA_VERSIONS) + if isinstance(versions, Mapping): + version = versions.get(_DEEPAGENTS_FRAMEWORK_NAME) + if version: + span.set_attribute(_GEN_AI_FRAMEWORK_VERSION, str(version)) + + agent_name = metadata.get(_DEEPAGENTS_METADATA_AGENT_NAME) + if agent_name: + span.set_attribute(_GEN_AI_AGENT_NAME, str(agent_name)) + + agent_type = metadata.get(_DEEPAGENTS_METADATA_AGENT_TYPE) + if agent_type: + span.set_attribute(_GEN_AI_AGENT_TYPE, str(agent_type)) + + description = metadata.get(_DEEPAGENTS_METADATA_SUBAGENT_DESCRIPTION) + if description: + span.set_attribute(_GEN_AI_AGENT_DESCRIPTION, str(description)) + + +def _is_deepagents_metadata(metadata: Mapping[str, Any]) -> bool: + if metadata.get(_DEEPAGENTS_METADATA_INTEGRATION) == _DEEPAGENTS_FRAMEWORK_NAME: + return True + if metadata.get(_DEEPAGENTS_METADATA_AGENT_TYPE) == _DEEPAGENTS_SUBAGENT_TYPE: + return True + versions = metadata.get(_DEEPAGENTS_METADATA_VERSIONS) + return isinstance(versions, Mapping) and _DEEPAGENTS_FRAMEWORK_NAME in versions + + +def _is_deepagents_subagent_root(run: Run) -> bool: + metadata = getattr(run, "metadata", None) or {} + if not isinstance(metadata, Mapping): + return False + if metadata.get(_DEEPAGENTS_METADATA_AGENT_TYPE) != _DEEPAGENTS_SUBAGENT_TYPE: + return False + agent_name = metadata.get(_DEEPAGENTS_METADATA_AGENT_NAME) + return bool(agent_name and getattr(run, "name", None) == agent_name) + + # --------------------------------------------------------------------------- # LangGraph message helpers (module-level, used by _start_agent / _stop_agent) # --------------------------------------------------------------------------- diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py index 57a63de30..c5d973892 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py @@ -16,6 +16,7 @@ import json import logging +from collections.abc import Mapping from typing import Any from opentelemetry.util.genai.extended_types import RetrievalDocument @@ -115,21 +116,18 @@ def _extract_tool_definitions(run: Any) -> list[FunctionToolDefinition]: tool_definitions: list[FunctionToolDefinition] = [] tools: list[Any] = [] - params = _extract_invocation_params(run) - if params and "tools" in params: - raw = params["tools"] - if isinstance(raw, list): - tools = raw - elif hasattr(raw, "__iter__") and not isinstance(raw, (str, dict)): - tools = list(raw) - - if not tools: - inputs = getattr(run, "inputs", None) or {} - raw = inputs.get("tools") - if isinstance(raw, list): - tools = raw - elif hasattr(raw, "__iter__") and not isinstance(raw, (str, dict)): - tools = list(raw) + for source in ( + _extract_invocation_params(run), + getattr(run, "inputs", None) or {}, + getattr(run, "extra", None) or {}, + getattr(run, "serialized", None) or {}, + ): + for raw in _iter_tool_definition_values(source): + tools = _coerce_tool_list(raw) + if tools: + break + if tools: + break for tool in tools: if isinstance(tool, FunctionToolDefinition): @@ -169,6 +167,73 @@ def _extract_tool_definitions(run: Any) -> list[FunctionToolDefinition]: return tool_definitions +def _iter_tool_definition_values( + value: Any, + *, + _depth: int = 0, + _seen: set[int] | None = None, +): + if _depth > 4 or value is None: + return + if _seen is None: + _seen = set() + value_id = id(value) + if value_id in _seen: + return + _seen.add(value_id) + + if isinstance(value, Mapping): + for key in ("tools", "functions"): + if key in value: + yield value[key] + for key in ( + "kwargs", + "model_kwargs", + "invocation_params", + "extra_body", + "config", + "configurable", + "metadata", + "bound", + ): + if key in value: + yield from _iter_tool_definition_values( + value[key], + _depth=_depth + 1, + _seen=_seen, + ) + return + + for attr in ( + "tools", + "functions", + "kwargs", + "model_kwargs", + "invocation_params", + "extra_body", + ): + if hasattr(value, attr): + yield from _iter_tool_definition_values( + getattr(value, attr), + _depth=_depth + 1, + _seen=_seen, + ) + + +def _coerce_tool_list(raw: Any) -> list[Any]: + if isinstance(raw, list): + return raw + if isinstance(raw, tuple): + return list(raw) + if isinstance(raw, Mapping): + if "function" in raw or "name" in raw: + return [raw] + return [] + if hasattr(raw, "__iter__") and not isinstance(raw, (str, bytes)): + return list(raw) + return [] + + # --------------------------------------------------------------------------- # LangChain message ↔ util-genai message conversion # --------------------------------------------------------------------------- diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py index 00da288db..56ee64760 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py @@ -14,17 +14,32 @@ """Tests for Agent span creation — verifying AGENT_RUN_NAMES detection.""" +from uuid import uuid4 + +from opentelemetry.instrumentation.langchain.internal._tracer import ( + LoongsuiteTracer, + _is_deepagents_subagent_candidate, + _RunData, +) from opentelemetry.instrumentation.langchain.internal._utils import ( AGENT_RUN_NAMES, _is_agent_run, ) +from opentelemetry.trace import get_current_span, set_span_in_context +from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler class _FakeRun: """Minimal stub that looks like a langchain Run for unit tests.""" - def __init__(self, name: str): + def __init__(self, name: str, **kwargs): + self.id = kwargs.get("id", uuid4()) self.name = name + self.parent_run_id = kwargs.get("parent_run_id") + self.metadata = kwargs.get("metadata", {}) + self.inputs = kwargs.get("inputs", {}) + self.outputs = kwargs.get("outputs", {}) + self.extra = kwargs.get("extra", {}) class TestAgentDetection: @@ -51,3 +66,280 @@ def test_none_name_not_detected(self): def test_agent_run_names_immutable(self): assert isinstance(AGENT_RUN_NAMES, frozenset) + + +def test_deepagents_subagent_prefers_current_task_tool_parent( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + + with otel_tracer.start_as_current_span("execute_tool task") as tool_span: + tool_span.set_attribute("gen_ai.span.kind", "TOOL") + tool_span.set_attribute("gen_ai.tool.name", "task") + run = _FakeRun( + "LangGraph", + parent_run_id=uuid4(), + metadata={ + "ls_integration": "deepagents", + "lc_agent_name": "researcher", + }, + ) + + context = tracer._get_parent_context(run) + + assert get_current_span(context) is tool_span + + +def test_deepagents_task_tool_parent_ignores_non_langgraph_runs( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + + with otel_tracer.start_as_current_span("execute_tool task") as tool_span: + tool_span.set_attribute("gen_ai.span.kind", "TOOL") + tool_span.set_attribute("gen_ai.tool.name", "task") + run = _FakeRun( + "RetrievalQA", + parent_run_id=uuid4(), + metadata={ + "ls_integration": "deepagents", + "lc_agent_name": "researcher", + }, + ) + + context = tracer._get_parent_context(run) + + assert context is None + + +def test_parentless_chain_does_not_inherit_ambient_context( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + + with otel_tracer.start_as_current_span("ambient") as ambient_span: + run = _FakeRun("RetrievalQA") + + tracer._on_chain_start(run) + try: + span = tracer._runs[run.id].span + assert span.parent is None + assert span.context.trace_id != ambient_span.context.trace_id + finally: + tracer._on_chain_end(run) + + +def test_parentless_handler_spans_do_not_inherit_ambient_context( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + cases = ( + (tracer._handle_llm_start, _FakeRun("ChatOpenAI")), + ( + tracer._start_agent, + _FakeRun("AgentExecutor", inputs={"input": "hello"}), + ), + (tracer._on_tool_start, _FakeRun("search", inputs={"input": "q"})), + ( + tracer._on_retriever_start, + _FakeRun("retriever", inputs={"query": "q"}), + ), + ) + + for start, run in cases: + with otel_tracer.start_as_current_span("ambient") as ambient_span: + start(run) + try: + span = tracer._runs[run.id].span + assert span.parent is None + assert span.context.trace_id != ambient_span.context.trace_id + finally: + tracer._runs.pop(run.id).span.end() + + +def test_deepagents_explicit_subagent_metadata_bypasses_langgraph_name(): + run = _FakeRun( + "CustomAgent", + metadata={ + "ls_integration": "deepagents", + "ls_agent_type": "subagent", + }, + ) + + assert _is_deepagents_subagent_candidate(run) + + +def test_deepagents_subagent_falls_back_to_active_task_tool_run( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + parent_run_id = uuid4() + + tool_span = otel_tracer.start_span("execute_tool task") + try: + tracer._runs[uuid4()] = _RunData( + run_kind="tool", + span=tool_span, + context=set_span_in_context(tool_span), + parent_run_id=parent_run_id, + tool_name="task", + ) + run = _FakeRun( + "LangGraph", + parent_run_id=parent_run_id, + metadata={ + "ls_integration": "deepagents", + "lc_agent_name": "researcher", + "ls_agent_type": "subagent", + }, + ) + + context = tracer._get_parent_context(run) + finally: + tool_span.end() + + assert get_current_span(context) is tool_span + + +def test_deepagents_subagent_falls_back_to_latest_task_tool_when_parent_differs( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + otel_tracer = tracer_provider.get_tracer(__name__) + + tool_span = otel_tracer.start_span("execute_tool task") + try: + tracer._runs[uuid4()] = _RunData( + run_kind="tool", + span=tool_span, + context=set_span_in_context(tool_span), + parent_run_id=uuid4(), + tool_name="task", + ) + run = _FakeRun( + "LangGraph", + parent_run_id=uuid4(), + metadata={ + "ls_integration": "deepagents", + "lc_agent_name": "researcher", + "ls_agent_type": "subagent", + }, + ) + + context = tracer._get_parent_context(run) + finally: + tool_span.end() + + assert get_current_span(context) is tool_span + + +def test_start_agent_writes_deepagents_framework_attributes( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + run = _FakeRun( + "LangGraph", + metadata={ + "_loongsuite_react_agent": True, + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.3"}, + "lc_agent_name": "deep_agent", + }, + ) + + tracer._start_agent(run) + try: + span = tracer._runs[run.id].span + attributes = span.attributes + finally: + tracer._runs.pop(run.id).span.end() + + assert attributes["gen_ai.framework"] == "deepagents" + assert attributes["gen_ai.framework.version"] == "0.6.3" + assert attributes["gen_ai.agent.name"] == "deep_agent" + + +def test_start_agent_writes_deepagents_subagent_attributes( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + run = _FakeRun( + "LangGraph", + metadata={ + "_loongsuite_react_agent": True, + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.3"}, + "lc_agent_name": "country-researcher", + "ls_agent_type": "subagent", + "loongsuite_deepagents_subagent_description": ( + "Research country facts" + ), + }, + ) + + tracer._start_agent(run) + try: + span = tracer._runs[run.id].span + attributes = span.attributes + finally: + tracer._runs.pop(run.id).span.end() + + assert attributes["gen_ai.framework"] == "deepagents" + assert attributes["gen_ai.framework.version"] == "0.6.3" + assert attributes["gen_ai.agent.name"] == "country-researcher" + assert attributes["gen_ai.agent.type"] == "subagent" + assert ( + attributes["gen_ai.agent.description"] + == "Research country facts" + ) + + +def test_langgraph_router_treats_deepagents_subagent_root_as_agent( + tracer_provider, +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer(handler, tracer_provider=tracer_provider) + parent_run_id = uuid4() + tracer._runs[parent_run_id] = _RunData( + run_kind="chain", + inside_langgraph_react=True, + ) + run = _FakeRun( + "country-researcher", + parent_run_id=parent_run_id, + metadata={ + "_loongsuite_react_agent": True, + "ls_integration": "deepagents", + "versions": {"deepagents": "0.6.3"}, + "lc_agent_name": "country-researcher", + "ls_agent_type": "subagent", + "loongsuite_deepagents_subagent_description": ( + "Research country facts" + ), + }, + ) + + tracer._on_chain_start(run) + try: + run_data = tracer._runs[run.id] + attributes = run_data.span.attributes + finally: + tracer._runs.pop(run.id).span.end() + tracer._runs.pop(parent_run_id) + + assert run_data.run_kind == "agent" + assert attributes["gen_ai.framework"] == "deepagents" + assert attributes["gen_ai.agent.type"] == "subagent" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_data_extraction.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_data_extraction.py index cc1cace4e..fc4a674ec 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_data_extraction.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_data_extraction.py @@ -211,6 +211,51 @@ def test_from_inputs(self): assert len(result) == 1 assert result[0].name == "calculator" + def test_from_nested_invocation_params_kwargs(self): + """Tools nested under kwargs are used by LangChain v1 wrap_model_call.""" + run = _FakeRun( + extra={ + "invocation_params": { + "kwargs": { + "tools": [ + { + "type": "function", + "function": { + "name": "task", + "description": "Invoke subagent", + "parameters": {"type": "object"}, + }, + }, + ] + } + } + } + ) + result = _extract_tool_definitions(run) + assert len(result) == 1 + assert result[0].name == "task" + + def test_from_nested_model_kwargs(self): + """Tools nested under model_kwargs are discovered as a fallback.""" + run = _FakeRun( + extra={ + "invocation_params": { + "model_kwargs": { + "tools": [ + { + "name": "write_file", + "description": "Write a file", + "parameters": {"type": "object"}, + }, + ] + } + } + } + ) + result = _extract_tool_definitions(run) + assert len(result) == 1 + assert result[0].name == "write_file" + def test_empty_when_no_tools(self): run = _FakeRun(extra={}, inputs={}) assert _extract_tool_definitions(run) == [] diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 08ef3fcea..af449f35e 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -48,6 +48,10 @@ envlist = py3{9,10,11,12,13}-test-loongsuite-instrumentation-qwen-agent-{oldest,latest} lint-loongsuite-instrumentation-qwen-agent + ; loongsuite-instrumentation-deepagents + py3{10,11,12,13}-test-loongsuite-instrumentation-deepagents + lint-loongsuite-instrumentation-deepagents + ; ; loongsuite-instrumentation-mcp ; py3{9,10,11,12,13}-test-loongsuite-instrumentation-mcp ; lint-loongsuite-instrumentation-mcp @@ -134,6 +138,9 @@ deps = qwen-agent-latest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests/requirements.latest.txt lint-loongsuite-instrumentation-qwen-agent: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests/requirements.oldest.txt + loongsuite-instrumentation-deepagents: {[testenv]test_deps} + loongsuite-instrumentation-deepagents: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test-requirements.txt + mcp: {[testenv]test_deps} mcp: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-mcp/test-requirements.txt @@ -203,6 +210,9 @@ commands = test-loongsuite-instrumentation-qwen-agent: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests {posargs} lint-loongsuite-instrumentation-qwen-agent: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent + test-loongsuite-instrumentation-deepagents: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests {posargs} + lint-loongsuite-instrumentation-deepagents: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents + test-loongsuite-instrumentation-mcp: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-mcp/tests {posargs} lint-loongsuite-instrumentation-mcp: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-mcp