mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-07 23:11:51 +00:00
Compare commits
7 Commits
main
...
feat/telem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1b0ca1124 | ||
|
|
36070166cf | ||
|
|
75da3625fc | ||
|
|
59c932cd68 | ||
|
|
96e40ed3a4 | ||
|
|
73bfae300d | ||
|
|
9a67ecf63b |
@@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true'
|
||||
| نعم | بيانات دورة حياة المهمة | تشمل: أوقات الإنشاء وبدء/انتهاء التنفيذ، معرّفات الطاقم والمهمة. مخزنة كنطاقات مع طوابع زمنية. لا بيانات شخصية. |
|
||||
| نعم | سمات LLM | تشمل: الاسم، model_name، model، top_k، temperature، واسم فئة LLM. كلها بيانات تقنية غير شخصية. |
|
||||
| نعم | محاولة نشر الطاقم باستخدام CLI الخاص بـ CrewAI | تشمل: حقيقة إجراء النشر ومعرّف الطاقم، وما إذا كان يحاول سحب السجلات، لا بيانات أخرى. |
|
||||
| نعم | بيئة التنفيذ | تشمل: مساعد البرمجة بالذكاء الاصطناعي الذي يشغّل العملية إن وُجد (واحد من قائمة ثابتة مثل `claude_code` أو `codex` أو `cursor` أو `unknown`)، ومكان تشغيل العملية (واحد من قائمة ثابتة مثل `ci` أو `container` أو `serverless` أو `interactive`)، و`project_id` من ملف `pyproject.toml` عند ضبطه. يتحقق الاكتشاف فقط مما إذا كانت متغيرات البيئة المعروفة مضبوطة، ولا يقرأ قيمها أبدًا. لا بيانات شخصية. |
|
||||
| لا | بيانات الوكيل الموسّعة | تشمل: وصف الهدف، نص الخلفية، معرّف ملف موجهات i18n. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في حقول النص. |
|
||||
| لا | معلومات المهمة التفصيلية | تشمل: وصف المهمة، وصف المخرجات المتوقعة، مراجع السياق. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في هذه الحقول. |
|
||||
| لا | معلومات البيئة | تشمل: المنصة، الإصدار، النظام، الإصدار، وعدد وحدات المعالجة المركزية. مثال: 'Windows 10'، 'x86_64'. لا بيانات شخصية. |
|
||||
|
||||
@@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true'
|
||||
| Yes | Task Lifecycle Data | Includes: creation and execution start/end times, crew and task identifiers. Stored as spans with timestamps. No personal data. |
|
||||
| Yes | LLM Attributes | Includes: name, model_name, model, top_k, temperature, and class name of the LLM. All technical, non-personal data. |
|
||||
| Yes | Crew Deployment attempt using crewAI CLI | Includes: The fact a deploy is being made and crew id, and if it's trying to pull logs, no other data. |
|
||||
| Yes | Execution Environment | Includes: which AI coding assistant is running the process, if any (one of a fixed list such as `claude_code`, `codex`, `cursor`, or `unknown`), where the process runs (one of a fixed list such as `ci`, `container`, `serverless`, `interactive`), and the `project_id` from your `pyproject.toml` when one is configured. Detection reads only whether known environment variables are set, never their values. No personal data. |
|
||||
| No | Agent's Expanded Data | Includes: goal description, backstory text, i18n prompt file identifier. Users should ensure no personal info is included in text fields. |
|
||||
| No | Detailed Task Information | Includes: task description, expected output description, context references. Users should ensure no personal info is included in these fields. |
|
||||
| No | Environment Information | Includes: platform, release, system, version, and CPU count. Example: 'Windows 10', 'x86_64'. No personal data. |
|
||||
|
||||
@@ -45,6 +45,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true'
|
||||
| 예 | 작업 라이프사이클 데이터 | 생성 및 실행 시작/종료 시각, crew 및 작업 식별자가 포함됩니다. 타임스탬프를 포함한 span으로 저장됩니다. 개인 정보 없음. |
|
||||
| 예 | LLM 속성 | LLM의 이름, model_name, 모델, top_k, temperature 및 클래스명이 포함됩니다. 모두 기술적이고 비개인 정보입니다. |
|
||||
| 예 | crewAI CLI를 통한 Crew 배포 시도 | 배포가 시도되고 있고 crew id가 포함되며, 로그를 가져오려고 하는 경우에만 해당. 다른 데이터 없음. |
|
||||
| 예 | 실행 환경 | 포함: 프로세스를 실행 중인 AI 코딩 어시스턴트(있는 경우, `claude_code`, `codex`, `cursor`, `unknown` 등 고정 목록 중 하나), 프로세스가 실행되는 위치(`ci`, `container`, `serverless`, `interactive` 등 고정 목록 중 하나), 그리고 `pyproject.toml`에 설정된 경우 `project_id`. 감지는 알려진 환경 변수의 설정 여부만 확인하며 값은 읽지 않음. 개인 데이터 없음. |
|
||||
| 아니오 | 에이전트 확장 데이터 | 목표 설명, 배경 이야기 텍스트, i18n 프롬프트 파일 식별자가 포함됩니다. 사용자들은 텍스트 필드에 개인 정보가 포함되지 않도록 해야 합니다. |
|
||||
| 아니오 | 상세 작업 정보 | 작업 설명, 예상 출력 설명, 컨텍스트 참조가 포함됩니다. 사용자들은 이러한 필드에 개인 정보가 포함되지 않도록 해야 합니다. |
|
||||
| 아니오 | 환경 정보 | 플랫폼, 릴리즈, 시스템, 버전, CPU 개수가 포함됩니다. 예: 'Windows 10', 'x86_64'. 개인 정보 없음. |
|
||||
|
||||
@@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true'
|
||||
| Sim | Dados do Ciclo de Vida da Tarefa | Inclui: horários de criação, início/fim de execução, identificadores de crew e tarefa. Armazenado como spans com timestamps. Sem dados pessoais. |
|
||||
| Sim | Atributos do LLM | Inclui: nome, model_name, model, top_k, temperatura e nome da classe do LLM. Todos técnicos, sem dados pessoais. |
|
||||
| Sim | Tentativa de Deploy do Crew pelo CLI do crewAI | Inclui: O fato de um deploy estar sendo realizado e o crew id, e se está tentando buscar logs, sem mais dados. |
|
||||
| Sim | Ambiente de Execução | Inclui: qual assistente de código com IA está executando o processo, se houver (um de uma lista fixa como `claude_code`, `codex`, `cursor` ou `unknown`), onde o processo é executado (um de uma lista fixa como `ci`, `container`, `serverless`, `interactive`) e o `project_id` do seu `pyproject.toml` quando houver um configurado. A detecção lê apenas se variáveis de ambiente conhecidas estão definidas, nunca seus valores. Sem dados pessoais. |
|
||||
| Não | Dados Expandidos do Agente | Inclui: descrição do objetivo, texto da história, identificador de arquivo i18n prompt. Usuários devem garantir que não haja info pessoal nesses campos de texto. |
|
||||
| Não | Informações Detalhadas da Tarefa | Inclui: descrição da tarefa, descrição do resultado esperado, referências de contexto. Usuários devem garantir que não haja info pessoal nessas áreas. |
|
||||
| Não | Informações de Ambiente | Inclui: plataforma, release, sistema, versão e quantidade de CPUs. Exemplo: 'Windows 10', 'x86_64'. Sem dados pessoais. |
|
||||
|
||||
2578
lib/crewai/src/crewai/new_agent/executor.py
Normal file
2578
lib/crewai/src/crewai/new_agent/executor.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import threading
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import weakref
|
||||
|
||||
from crewai_core.project import get_project_id
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||
@@ -54,6 +55,7 @@ from crewai.telemetry.utils import (
|
||||
add_crew_attributes,
|
||||
close_span,
|
||||
detect_coding_agent,
|
||||
detect_runtime_context,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N_DEFAULT
|
||||
from crewai.utilities.logger_utils import suppress_warnings
|
||||
@@ -172,6 +174,7 @@ class Telemetry:
|
||||
# Weak so instrumented apps' providers are not kept alive by telemetry.
|
||||
self._common_attributes_providers: weakref.WeakSet[Any] = weakref.WeakSet()
|
||||
self._common_attributes_lock = threading.Lock()
|
||||
self._common_attributes: dict[str, str] | None = None
|
||||
|
||||
if self._is_telemetry_disabled():
|
||||
return
|
||||
@@ -203,6 +206,39 @@ class Telemetry:
|
||||
raise
|
||||
self.ready = False
|
||||
|
||||
def _common_span_attributes(self) -> dict[str, str]:
|
||||
"""Build the attributes every span carries, once per process.
|
||||
|
||||
Memoized because it is computed per provider and reads the project's
|
||||
``pyproject.toml``, and because a process cannot change which
|
||||
assistant, runtime, or project it belongs to partway through.
|
||||
|
||||
Returns:
|
||||
Attributes to stamp on every span. ``project_id`` is omitted for
|
||||
projects that do not declare one.
|
||||
"""
|
||||
if self._common_attributes is not None:
|
||||
return self._common_attributes
|
||||
|
||||
attributes = {
|
||||
"coding_agent": detect_coding_agent(),
|
||||
"runtime_context": detect_runtime_context(),
|
||||
}
|
||||
|
||||
try:
|
||||
# Read-only: minting an id belongs to the CLI commands a user
|
||||
# invoked, not to a library call during execution.
|
||||
project_id = get_project_id()
|
||||
except Exception as e: # Telemetry must never break execution.
|
||||
logger.debug(f"Failed to read project id: {e}")
|
||||
project_id = None
|
||||
|
||||
if project_id:
|
||||
attributes["project_id"] = project_id
|
||||
|
||||
self._common_attributes = attributes
|
||||
return attributes
|
||||
|
||||
def _attach_common_attributes(self, provider: Any) -> None:
|
||||
"""Attach process-wide attributes to every span a provider emits.
|
||||
|
||||
@@ -230,9 +266,7 @@ class Telemetry:
|
||||
if provider in self._common_attributes_providers:
|
||||
return
|
||||
add_span_processor(
|
||||
CommonAttributesSpanProcessor(
|
||||
{"coding_agent": detect_coding_agent()}
|
||||
)
|
||||
CommonAttributesSpanProcessor(self._common_span_attributes())
|
||||
)
|
||||
self._common_attributes_providers.add(provider)
|
||||
except Exception as e: # Telemetry must never break execution.
|
||||
|
||||
@@ -12,7 +12,11 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from opentelemetry.trace import Span, Status, StatusCode
|
||||
|
||||
from crewai.utilities.constants import CODING_AGENT_ENV_MARKERS
|
||||
from crewai.utilities.constants import (
|
||||
CODING_AGENT_ENV_MARKERS,
|
||||
GENERIC_AGENT_ENV_VARS,
|
||||
RUNTIME_CONTEXT_ENV_MARKERS,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -20,23 +24,33 @@ if TYPE_CHECKING:
|
||||
from crewai.task import Task
|
||||
|
||||
|
||||
# Editors whose integrated terminal implies a human is likely present. Used only
|
||||
# as a weaker fallback when no explicit coding-agent marker is found.
|
||||
# Editors whose integrated terminal a process can be launched from. Matched on
|
||||
# an exact value rather than presence, since these variables name the terminal.
|
||||
_EDITOR_TERM_MARKERS: Final[tuple[tuple[str, str, str], ...]] = (
|
||||
("TERM_PROGRAM", "vscode", "vscode_terminal"),
|
||||
("TERMINAL_EMULATOR", "JetBrains-JediTerm", "jetbrains_terminal"),
|
||||
)
|
||||
|
||||
_FALLBACK_AGENT_NAMES: Final[tuple[str, ...]] = ("non_interactive", "unknown")
|
||||
# Marks a process running inside a container when no more specific runtime
|
||||
# marker is present.
|
||||
_DOCKER_ENV_PATH: Final[str] = "/.dockerenv"
|
||||
|
||||
_UNKNOWN: Final[str] = "unknown"
|
||||
_OTHER: Final[str] = "other"
|
||||
|
||||
# The complete set of values detect_coding_agent() can ever return. Every value
|
||||
# is a literal from CODING_AGENT_ENV_MARKERS or this module, which is what makes
|
||||
# the function structurally incapable of emitting PII: no environment value,
|
||||
# path, hostname, or user-supplied string can reach the return value.
|
||||
KNOWN_CODING_AGENTS: Final[frozenset[str]] = frozenset(
|
||||
[name for name, _ in CODING_AGENT_ENV_MARKERS]
|
||||
[name for name, _ in CODING_AGENT_ENV_MARKERS] + [_OTHER, _UNKNOWN]
|
||||
)
|
||||
|
||||
# The same guarantee for detect_runtime_context(): a closed set of literals.
|
||||
KNOWN_RUNTIME_CONTEXTS: Final[frozenset[str]] = frozenset(
|
||||
[name for name, _ in RUNTIME_CONTEXT_ENV_MARKERS]
|
||||
+ [name for _, _, name in _EDITOR_TERM_MARKERS]
|
||||
+ list(_FALLBACK_AGENT_NAMES)
|
||||
+ ["interactive", "non_interactive", _UNKNOWN]
|
||||
)
|
||||
|
||||
|
||||
@@ -57,27 +71,65 @@ def detect_coding_agent() -> str:
|
||||
integrated terminal, so a result names the environment the process is
|
||||
running *under*, not proof that an agent authored the code.
|
||||
|
||||
Answers only *which assistant*. Where the process runs is
|
||||
:func:`detect_runtime_context`, so an "unknown" here is a genuine gap in
|
||||
the marker table rather than a run that never had an assistant to find.
|
||||
|
||||
Returns:
|
||||
A normalized assistant name (e.g. "claude_code", "cursor", "codex"),
|
||||
an editor terminal hint (e.g. "vscode_terminal"), "non_interactive"
|
||||
when no marker is found and there is no TTY, or "unknown" otherwise.
|
||||
The result is always a member of KNOWN_CODING_AGENTS.
|
||||
A normalized assistant name (e.g. "claude_code", "cursor", "codex"), or
|
||||
"unknown" when no marker matches. The result is always a member of
|
||||
KNOWN_CODING_AGENTS.
|
||||
"""
|
||||
for agent_name, env_vars in CODING_AGENT_ENV_MARKERS:
|
||||
if any(os.environ.get(env_var) for env_var in env_vars):
|
||||
return agent_name
|
||||
|
||||
for env_var, expected, agent_name in _EDITOR_TERM_MARKERS:
|
||||
# Checked last and by presence: the cross-vendor marker establishes that an
|
||||
# assistant is present without naming one, and an empty value still says so.
|
||||
if any(env_var in os.environ for env_var in GENERIC_AGENT_ENV_VARS):
|
||||
return _OTHER
|
||||
|
||||
return _UNKNOWN
|
||||
|
||||
|
||||
def detect_runtime_context() -> str:
|
||||
"""Best-effort detection of where this process is running.
|
||||
|
||||
Separate from :func:`detect_coding_agent` because the two answer different
|
||||
questions. Automated runs -- CI, containers, serverless -- have no
|
||||
assistant to detect, and reporting them in the assistant field made an
|
||||
unrecognized assistant indistinguishable from a run that could never have
|
||||
had one.
|
||||
|
||||
Precedence runs most specific first: CI and hosted IDEs typically run
|
||||
inside containers, so a bare container match only applies once the more
|
||||
specific markers have been ruled out.
|
||||
|
||||
Returns:
|
||||
One of the runtime names (e.g. "ci", "serverless", "container",
|
||||
"notebook", "hosted_ide"), an editor terminal (e.g. "vscode_terminal"),
|
||||
"interactive" or "non_interactive" when only a TTY check applies, or
|
||||
"unknown" when that check cannot be made. The result is always a member
|
||||
of KNOWN_RUNTIME_CONTEXTS.
|
||||
"""
|
||||
# Presence, not truthiness: a platform that exports an empty CI= is still
|
||||
# CI, unlike the assistant markers where an empty value means the tool set
|
||||
# a placeholder rather than claiming the session.
|
||||
for context_name, env_vars in RUNTIME_CONTEXT_ENV_MARKERS:
|
||||
if any(env_var in os.environ for env_var in env_vars):
|
||||
return context_name
|
||||
|
||||
for env_var, expected, context_name in _EDITOR_TERM_MARKERS:
|
||||
if os.environ.get(env_var) == expected:
|
||||
return agent_name
|
||||
return context_name
|
||||
|
||||
if os.path.exists(_DOCKER_ENV_PATH):
|
||||
return "container"
|
||||
|
||||
try:
|
||||
if not sys.stdout.isatty():
|
||||
return "non_interactive"
|
||||
return "interactive" if sys.stdout.isatty() else "non_interactive"
|
||||
except (AttributeError, ValueError, OSError):
|
||||
return "unknown"
|
||||
|
||||
return "unknown"
|
||||
return _UNKNOWN
|
||||
|
||||
|
||||
def add_agent_fingerprint_to_span(
|
||||
|
||||
@@ -12,15 +12,30 @@ from pydantic_core import CoreSchema
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ANTIGRAVITY_ENV_VARS",
|
||||
"AUGMENT_ENV_VARS",
|
||||
"CC_ENV_VAR",
|
||||
"CC_ENV_VARS",
|
||||
"CI_ENV_VARS",
|
||||
"CLINE_ENV_VARS",
|
||||
"CODEX_ENV_VARS",
|
||||
"CODING_AGENT_ENV_MARKERS",
|
||||
"CONTAINER_ENV_VARS",
|
||||
"CREWAI_TRAINED_AGENTS_FILE_ENV",
|
||||
"CURSOR_ENV_VARS",
|
||||
"EMITTER_COLOR",
|
||||
"GEMINI_CLI_ENV_VARS",
|
||||
"GENERIC_AGENT_ENV_VARS",
|
||||
"HOSTED_IDE_ENV_VARS",
|
||||
"JUNIE_ENV_VARS",
|
||||
"KNOWLEDGE_DIRECTORY",
|
||||
"MAX_FILE_NAME_LENGTH",
|
||||
"NOTEBOOK_ENV_VARS",
|
||||
"NOT_SPECIFIED",
|
||||
"OPENCODE_ENV_VARS",
|
||||
"PAAS_ENV_VARS",
|
||||
"RUNTIME_CONTEXT_ENV_MARKERS",
|
||||
"SERVERLESS_ENV_VARS",
|
||||
"TRAINED_AGENTS_DATA_FILE",
|
||||
"TRAINING_DATA_FILE",
|
||||
]
|
||||
@@ -35,6 +50,7 @@ CODEX_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED",
|
||||
"CODEX_THREAD_ID",
|
||||
)
|
||||
CC_ENV_VARS: Final[tuple[str, ...]] = (CC_ENV_VAR, "CLAUDE_CODE")
|
||||
CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CURSOR_AGENT",
|
||||
"CURSOR_EXTENSION_HOST_ROLE",
|
||||
@@ -42,6 +58,20 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CURSOR_TRACE_ID",
|
||||
"CURSOR_WORKSPACE_LABEL",
|
||||
)
|
||||
ANTIGRAVITY_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"ANTIGRAVITY_AGENT",
|
||||
"ANTIGRAVITY_CLI_ALIAS",
|
||||
)
|
||||
AUGMENT_ENV_VARS: Final[tuple[str, ...]] = ("AUGMENT_AGENT",)
|
||||
CLINE_ENV_VARS: Final[tuple[str, ...]] = ("CLINE_ACTIVE",)
|
||||
GEMINI_CLI_ENV_VARS: Final[tuple[str, ...]] = ("GEMINI_CLI",)
|
||||
JUNIE_ENV_VARS: Final[tuple[str, ...]] = ("JUNIE_DATA", "JUNIE_SHIM_PATH")
|
||||
OPENCODE_ENV_VARS: Final[tuple[str, ...]] = ("OPENCODE", "OPENCODE_CLIENT")
|
||||
|
||||
# Proposed cross-vendor marker (agentsmd/agents.md#136). Checked last and
|
||||
# reported as "other": it says an assistant is present without naming one, and
|
||||
# reading its value to find out would put an arbitrary string in telemetry.
|
||||
GENERIC_AGENT_ENV_VARS: Final[tuple[str, ...]] = ("AI_AGENT",)
|
||||
|
||||
# Ordered (name, env vars) pairs for identifying the AI coding assistant a
|
||||
# process is running under. Reuses the sets above and keeps the same precedence
|
||||
@@ -62,13 +92,99 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
# a leftover config value would mislabel ordinary human executions.
|
||||
#
|
||||
# Extend the shared sets above rather than adding a parallel tuple here, so both
|
||||
# detection paths pick the new markers up together.
|
||||
# detection paths pick the new markers up together: ``get_env_context()`` walks
|
||||
# this same table for its precedence and emits ``DefaultEnvEvent`` for the
|
||||
# assistants that have no event class of their own.
|
||||
#
|
||||
# Markers below the first three were taken from the published detection matrix
|
||||
# at vercel/detect-agent (agents.json), cross-checked against the proposal in
|
||||
# agentsmd/agents.md#136 and microsoft/vscode#311734. Rule 2 excluded several
|
||||
# entries those sources list: Goose's ``GOOSE_PROVIDER`` and Copilot's
|
||||
# ``COPILOT_MODEL`` / ``COPILOT_GITHUB_TOKEN`` are user configuration, and a
|
||||
# committed ``.env`` carrying one would relabel every ordinary run. Replit's
|
||||
# ``REPL_ID`` is a hosted environment rather than an assistant, so it stays in
|
||||
# HOSTED_IDE_ENV_VARS.
|
||||
#
|
||||
# The assistants that spawn inside another editor's terminal are ordered ahead
|
||||
# of Cursor for the same reason Codex is: CURSOR_* is set for every integrated
|
||||
# terminal, so checking Cursor first would mask anything running inside it.
|
||||
CODING_AGENT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = (
|
||||
("claude_code", (CC_ENV_VAR,)),
|
||||
("claude_code", CC_ENV_VARS),
|
||||
("codex", CODEX_ENV_VARS),
|
||||
("cline", CLINE_ENV_VARS),
|
||||
("gemini_cli", GEMINI_CLI_ENV_VARS),
|
||||
("augment", AUGMENT_ENV_VARS),
|
||||
("opencode", OPENCODE_ENV_VARS),
|
||||
("antigravity", ANTIGRAVITY_ENV_VARS),
|
||||
("junie", JUNIE_ENV_VARS),
|
||||
("cursor", CURSOR_ENV_VARS),
|
||||
)
|
||||
|
||||
# Markers for *where* a process runs, kept separate from which assistant is
|
||||
# driving it. The two answer different questions: a scheduled container run has
|
||||
# no assistant to detect, and folding it into the assistant field made "no
|
||||
# marker found" and "no assistant possible" indistinguishable.
|
||||
#
|
||||
# These are published platform contracts rather than per-tool observations, so
|
||||
# they do not need the case-by-case verification the assistant table requires.
|
||||
# Presence is all that is checked; no value is ever read.
|
||||
CI_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"APPVEYOR",
|
||||
"BITBUCKET_BUILD_NUMBER",
|
||||
"BUILDKITE",
|
||||
"CI",
|
||||
"CIRCLECI",
|
||||
"DRONE",
|
||||
"GITHUB_ACTIONS",
|
||||
"GITLAB_CI",
|
||||
"JENKINS_URL",
|
||||
"TEAMCITY_VERSION",
|
||||
"TF_BUILD",
|
||||
"TRAVIS",
|
||||
)
|
||||
SERVERLESS_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"AWS_LAMBDA_FUNCTION_NAME",
|
||||
"FUNCTIONS_EXTENSION_VERSION",
|
||||
"FUNCTIONS_WORKER_RUNTIME",
|
||||
"FUNCTION_TARGET",
|
||||
"K_SERVICE",
|
||||
"VERCEL",
|
||||
)
|
||||
# Managed application platforms, kept apart from serverless: their markers are
|
||||
# set for long-lived containers rather than per-invocation functions, and
|
||||
# checking them under "serverless" would have claimed every Heroku dyno and
|
||||
# Azure App Service instance before the container check could see them.
|
||||
#
|
||||
# Azure Functions run on the App Service host and inherit WEBSITE_INSTANCE_ID,
|
||||
# so they would land here despite being serverless. The FUNCTIONS_* markers
|
||||
# above are checked first to keep them out.
|
||||
PAAS_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"DYNO",
|
||||
"WEBSITE_INSTANCE_ID",
|
||||
)
|
||||
HOSTED_IDE_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CODESPACES",
|
||||
"GITPOD_WORKSPACE_ID",
|
||||
"REPL_ID",
|
||||
)
|
||||
NOTEBOOK_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"COLAB_RELEASE_TAG",
|
||||
"JPY_PARENT_PID",
|
||||
)
|
||||
CONTAINER_ENV_VARS: Final[tuple[str, ...]] = ("KUBERNETES_SERVICE_HOST",)
|
||||
|
||||
# Ordered most specific first. CI jobs and hosted IDEs usually run inside
|
||||
# containers, so a bare container match is only meaningful once the others have
|
||||
# been ruled out.
|
||||
RUNTIME_CONTEXT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = (
|
||||
("ci", CI_ENV_VARS),
|
||||
("serverless", SERVERLESS_ENV_VARS),
|
||||
("paas", PAAS_ENV_VARS),
|
||||
("hosted_ide", HOSTED_IDE_ENV_VARS),
|
||||
("notebook", NOTEBOOK_ENV_VARS),
|
||||
("container", CONTAINER_ENV_VARS),
|
||||
)
|
||||
|
||||
|
||||
class _NotSpecified:
|
||||
"""Sentinel class to detect when no value has been explicitly provided.
|
||||
|
||||
@@ -8,20 +8,21 @@ from crewai.events.types.env_events import (
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.utilities.constants import CC_ENV_VAR, CODEX_ENV_VARS, CURSOR_ENV_VARS
|
||||
from crewai.utilities.constants import CODING_AGENT_ENV_MARKERS
|
||||
|
||||
|
||||
_env_context_emitted: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||
"_env_context_emitted", default=False
|
||||
)
|
||||
|
||||
|
||||
def _is_codex_env() -> bool:
|
||||
return any(os.environ.get(var) for var in CODEX_ENV_VARS)
|
||||
|
||||
|
||||
def _is_cursor_env() -> bool:
|
||||
return any(os.environ.get(var) for var in CURSOR_ENV_VARS)
|
||||
# Assistants with an event of their own. Anything else in the shared table is
|
||||
# detected with the same precedence but reported as DefaultEnvEvent, so the two
|
||||
# detection paths can never disagree about which assistant is present.
|
||||
_AGENT_EVENTS = {
|
||||
"claude_code": CCEnvEvent,
|
||||
"codex": CodexEnvEvent,
|
||||
"cursor": CursorEnvEvent,
|
||||
}
|
||||
|
||||
|
||||
def get_env_context() -> None:
|
||||
@@ -29,11 +30,13 @@ def get_env_context() -> None:
|
||||
return
|
||||
_env_context_emitted.set(True)
|
||||
|
||||
if os.environ.get(CC_ENV_VAR):
|
||||
crewai_event_bus.emit(None, CCEnvEvent())
|
||||
elif _is_codex_env():
|
||||
crewai_event_bus.emit(None, CodexEnvEvent())
|
||||
elif _is_cursor_env():
|
||||
crewai_event_bus.emit(None, CursorEnvEvent())
|
||||
else:
|
||||
crewai_event_bus.emit(None, DefaultEnvEvent())
|
||||
# Walks the shared table rather than restating precedence: hard-coding the
|
||||
# order here let the two paths drift, so a marker added for telemetry was
|
||||
# invisible to this one.
|
||||
for agent_name, env_vars in CODING_AGENT_ENV_MARKERS:
|
||||
if any(os.environ.get(var) for var in env_vars):
|
||||
event = _AGENT_EVENTS.get(agent_name, DefaultEnvEvent)
|
||||
crewai_event_bus.emit(None, event())
|
||||
return
|
||||
|
||||
crewai_event_bus.emit(None, DefaultEnvEvent())
|
||||
|
||||
@@ -5,20 +5,47 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.telemetry.utils import KNOWN_CODING_AGENTS, detect_coding_agent
|
||||
from crewai.telemetry.utils import (
|
||||
KNOWN_CODING_AGENTS,
|
||||
KNOWN_RUNTIME_CONTEXTS,
|
||||
detect_coding_agent,
|
||||
detect_runtime_context,
|
||||
)
|
||||
from crewai.utilities.constants import (
|
||||
ANTIGRAVITY_ENV_VARS,
|
||||
AUGMENT_ENV_VARS,
|
||||
CC_ENV_VAR,
|
||||
CC_ENV_VARS,
|
||||
CLINE_ENV_VARS,
|
||||
CODEX_ENV_VARS,
|
||||
CODING_AGENT_ENV_MARKERS,
|
||||
CURSOR_ENV_VARS,
|
||||
GEMINI_CLI_ENV_VARS,
|
||||
GENERIC_AGENT_ENV_VARS,
|
||||
JUNIE_ENV_VARS,
|
||||
OPENCODE_ENV_VARS,
|
||||
RUNTIME_CONTEXT_ENV_MARKERS,
|
||||
)
|
||||
|
||||
|
||||
# Derived from the shared table rather than restated, so adding an assistant
|
||||
# there cannot leave these tests silently checking a stale marker set.
|
||||
ALL_MARKERS = tuple(
|
||||
var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars
|
||||
) + ("TERM_PROGRAM", "TERMINAL_EMULATOR")
|
||||
# Derived from the shared tables rather than restated, so adding an assistant
|
||||
# or runtime there cannot leave these tests silently checking a stale set.
|
||||
RUNTIME_MARKERS = tuple(
|
||||
var for _, env_vars in RUNTIME_CONTEXT_ENV_MARKERS for var in env_vars
|
||||
)
|
||||
|
||||
ALL_MARKERS = (
|
||||
tuple(var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars)
|
||||
+ RUNTIME_MARKERS
|
||||
+ GENERIC_AGENT_ENV_VARS
|
||||
+ ("TERM_PROGRAM", "TERMINAL_EMULATOR")
|
||||
)
|
||||
|
||||
EVERY_RUNTIME_CASE = [
|
||||
(var, context)
|
||||
for context, env_vars in RUNTIME_CONTEXT_ENV_MARKERS
|
||||
for var in env_vars
|
||||
]
|
||||
|
||||
EVERY_MARKER_CASE = [
|
||||
(var, agent) for agent, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars
|
||||
@@ -33,6 +60,18 @@ def clean_env(monkeypatch):
|
||||
return monkeypatch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def otel_enabled(monkeypatch):
|
||||
"""Let the SDK build real spans for tests that assert on exported ones.
|
||||
|
||||
The suite runs with OTEL_SDK_DISABLED set, which makes TracerProvider hand
|
||||
out no-op tracers. An export-based assertion then sees zero spans rather
|
||||
than a failed attribute, so it fails only when it happens to run before a
|
||||
test whose fixture flips the variable - which random ordering decides.
|
||||
"""
|
||||
monkeypatch.setenv("OTEL_SDK_DISABLED", "false")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_telemetry(monkeypatch):
|
||||
"""Build a fresh Telemetry without touching the process-wide singleton.
|
||||
@@ -143,15 +182,33 @@ def test_precedence_matches_get_env_context(clean_env):
|
||||
assert detect_coding_agent() == expected, markers
|
||||
|
||||
|
||||
def test_config_style_variables_are_not_used_as_markers():
|
||||
@pytest.mark.parametrize(
|
||||
"config_var",
|
||||
[
|
||||
"AIDER_MODEL",
|
||||
"COPILOT_GITHUB_TOKEN",
|
||||
"COPILOT_MODEL",
|
||||
"GOOSE_PROVIDER",
|
||||
],
|
||||
)
|
||||
def test_config_style_variables_are_not_used_as_markers(config_var):
|
||||
"""Persistent user config must never be treated as a session marker.
|
||||
|
||||
crewai loads dotenv files on normal runs, so a committed AIDER_MODEL or
|
||||
similar would mislabel ordinary human executions.
|
||||
GOOSE_PROVIDER would mislabel ordinary human executions. Published
|
||||
detection matrices list several of these; they are deliberately excluded
|
||||
here rather than copied wholesale.
|
||||
"""
|
||||
all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
|
||||
|
||||
assert "AIDER_MODEL" not in all_vars
|
||||
assert config_var not in all_vars
|
||||
|
||||
|
||||
def test_hosted_environments_are_not_reported_as_assistants():
|
||||
"""REPL_ID marks a hosted environment, not an assistant driving the run."""
|
||||
all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
|
||||
|
||||
assert "REPL_ID" not in all_vars
|
||||
|
||||
|
||||
def test_every_marker_comes_from_a_verified_set():
|
||||
@@ -162,15 +219,63 @@ def test_every_marker_comes_from_a_verified_set():
|
||||
Adding an assistant means extending the canonical sets, which keeps both
|
||||
detection paths in sync.
|
||||
"""
|
||||
verified = {CC_ENV_VAR, *CODEX_ENV_VARS, *CURSOR_ENV_VARS}
|
||||
verified = {
|
||||
*ANTIGRAVITY_ENV_VARS,
|
||||
*AUGMENT_ENV_VARS,
|
||||
*CC_ENV_VARS,
|
||||
*CLINE_ENV_VARS,
|
||||
*CODEX_ENV_VARS,
|
||||
*CURSOR_ENV_VARS,
|
||||
*GEMINI_CLI_ENV_VARS,
|
||||
*JUNIE_ENV_VARS,
|
||||
*OPENCODE_ENV_VARS,
|
||||
}
|
||||
declared = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
|
||||
|
||||
assert declared == verified, (
|
||||
"markers must come from CC_ENV_VAR / CODEX_ENV_VARS / CURSOR_ENV_VARS; "
|
||||
"markers must come from the canonical per-assistant sets; "
|
||||
f"unverified names present: {sorted(declared - verified)}"
|
||||
)
|
||||
|
||||
|
||||
def test_generic_marker_is_the_last_resort(clean_env):
|
||||
"""AI_AGENT says an assistant is present without naming which one.
|
||||
|
||||
A named marker must win, so the generic entry cannot mask a specific one.
|
||||
"""
|
||||
clean_env.setenv("AI_AGENT", "1")
|
||||
assert detect_coding_agent() == "other"
|
||||
|
||||
clean_env.setenv("CLINE_ACTIVE", "true")
|
||||
assert detect_coding_agent() == "cline"
|
||||
|
||||
|
||||
def test_generic_marker_value_is_never_reported(clean_env):
|
||||
"""Its value is an arbitrary vendor string, so it is never read."""
|
||||
clean_env.setenv("AI_AGENT", "some-unreleased-tool/2.0")
|
||||
|
||||
assert detect_coding_agent() == "other"
|
||||
|
||||
|
||||
def test_terminal_bound_assistants_outrank_cursor(clean_env):
|
||||
"""CURSOR_* is set for every integrated terminal.
|
||||
|
||||
Checking Cursor first would report cursor for anything spawned inside it,
|
||||
the same trap Codex already had to be ordered around.
|
||||
"""
|
||||
clean_env.setenv("CURSOR_TRACE_ID", "t-1")
|
||||
|
||||
for marker, expected in (
|
||||
("CLINE_ACTIVE", "cline"),
|
||||
("GEMINI_CLI", "gemini_cli"),
|
||||
("AUGMENT_AGENT", "augment"),
|
||||
("OPENCODE_CLIENT", "opencode"),
|
||||
):
|
||||
clean_env.setenv(marker, "1")
|
||||
assert detect_coding_agent() == expected, marker
|
||||
clean_env.delenv(marker)
|
||||
|
||||
|
||||
def test_concurrent_attach_registers_the_processor_once(isolated_telemetry, clean_env):
|
||||
"""Check-then-act on the provider set must be locked.
|
||||
|
||||
@@ -216,16 +321,16 @@ def test_concurrent_attach_registers_the_processor_once(isolated_telemetry, clea
|
||||
|
||||
def test_editor_terminal_requires_exact_value(clean_env):
|
||||
clean_env.setenv("TERM_PROGRAM", "vscode")
|
||||
assert detect_coding_agent() == "vscode_terminal"
|
||||
assert detect_runtime_context() == "vscode_terminal"
|
||||
|
||||
clean_env.setenv("TERM_PROGRAM", "iTerm.app")
|
||||
assert detect_coding_agent() != "vscode_terminal"
|
||||
assert detect_runtime_context() != "vscode_terminal"
|
||||
|
||||
|
||||
def test_explicit_agent_marker_wins_over_editor_terminal(clean_env):
|
||||
def test_editor_terminal_is_not_reported_as_an_assistant(clean_env):
|
||||
"""An editor's terminal says where a process runs, not who drove it."""
|
||||
clean_env.setenv("TERM_PROGRAM", "vscode")
|
||||
clean_env.setenv("CLAUDECODE", "1")
|
||||
assert detect_coding_agent() == "claude_code"
|
||||
assert detect_coding_agent() == "unknown"
|
||||
|
||||
|
||||
def test_empty_marker_value_is_ignored(clean_env):
|
||||
@@ -233,12 +338,61 @@ def test_empty_marker_value_is_ignored(clean_env):
|
||||
assert detect_coding_agent() != "claude_code"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("env_var", "expected"), EVERY_RUNTIME_CASE)
|
||||
def test_detects_every_runtime_marker(clean_env, env_var, expected):
|
||||
"""Every runtime marker must map to its context."""
|
||||
clean_env.setenv(env_var, "1")
|
||||
assert detect_runtime_context() == expected
|
||||
|
||||
|
||||
def test_runtime_precedence_prefers_the_most_specific(clean_env):
|
||||
"""CI and hosted IDEs usually run in containers; the specific one wins."""
|
||||
clean_env.setenv("KUBERNETES_SERVICE_HOST", "10.0.0.1")
|
||||
assert detect_runtime_context() == "container"
|
||||
|
||||
clean_env.setenv("GITHUB_ACTIONS", "true")
|
||||
assert detect_runtime_context() == "ci"
|
||||
|
||||
|
||||
def test_an_automated_run_still_reports_an_unknown_assistant(clean_env):
|
||||
"""The split must keep the two fields independent.
|
||||
|
||||
A CI run has no assistant to find, which is different from failing to
|
||||
recognize one - the reason they no longer share a field.
|
||||
"""
|
||||
clean_env.setenv("CI", "true")
|
||||
assert detect_runtime_context() == "ci"
|
||||
assert detect_coding_agent() == "unknown"
|
||||
|
||||
|
||||
def test_assistant_and_runtime_are_reported_together(clean_env):
|
||||
"""An assistant inside CI must not mask either signal."""
|
||||
clean_env.setenv("CI", "true")
|
||||
clean_env.setenv("CLAUDECODE", "1")
|
||||
assert detect_coding_agent() == "claude_code"
|
||||
assert detect_runtime_context() == "ci"
|
||||
|
||||
|
||||
def test_falls_back_to_non_interactive_without_tty(clean_env, monkeypatch):
|
||||
monkeypatch.setattr("os.path.exists", lambda path: False)
|
||||
monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: False})())
|
||||
assert detect_coding_agent() == "non_interactive"
|
||||
assert detect_runtime_context() == "non_interactive"
|
||||
|
||||
|
||||
def test_falls_back_to_unknown_with_tty(clean_env, monkeypatch):
|
||||
def test_falls_back_to_interactive_with_tty(clean_env, monkeypatch):
|
||||
monkeypatch.setattr("os.path.exists", lambda path: False)
|
||||
monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: True})())
|
||||
assert detect_runtime_context() == "interactive"
|
||||
|
||||
|
||||
def test_dockerenv_marks_a_container(clean_env, monkeypatch):
|
||||
"""The container check is the last resort before the TTY fallback."""
|
||||
monkeypatch.setattr("os.path.exists", lambda path: path == "/.dockerenv")
|
||||
assert detect_runtime_context() == "container"
|
||||
|
||||
|
||||
def test_unmatched_assistant_is_unknown(clean_env, monkeypatch):
|
||||
"""No marker means a gap in the table, reported as unknown."""
|
||||
monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: True})())
|
||||
assert detect_coding_agent() == "unknown"
|
||||
|
||||
@@ -255,8 +409,9 @@ def test_handles_broken_stdout(clean_env, monkeypatch):
|
||||
def isatty(self):
|
||||
raise ValueError("detached")
|
||||
|
||||
monkeypatch.setattr("os.path.exists", lambda path: False)
|
||||
monkeypatch.setattr("sys.stdout", BrokenStdout())
|
||||
assert detect_coding_agent() == "unknown"
|
||||
assert detect_runtime_context() == "unknown"
|
||||
|
||||
|
||||
def test_result_is_always_a_known_literal(clean_env):
|
||||
@@ -269,20 +424,23 @@ def test_result_is_always_a_known_literal(clean_env):
|
||||
|
||||
for var in ALL_MARKERS:
|
||||
clean_env.setenv(var, sensitive)
|
||||
result = detect_coding_agent()
|
||||
assert result in KNOWN_CODING_AGENTS
|
||||
assert sensitive not in result
|
||||
agent = detect_coding_agent()
|
||||
context = detect_runtime_context()
|
||||
assert agent in KNOWN_CODING_AGENTS
|
||||
assert context in KNOWN_RUNTIME_CONTEXTS
|
||||
assert sensitive not in agent
|
||||
assert sensitive not in context
|
||||
clean_env.delenv(var, raising=False)
|
||||
|
||||
|
||||
def test_known_agents_contains_no_pii_shaped_values():
|
||||
"""Every possible emitted value is a short, opaque identifier."""
|
||||
for name in KNOWN_CODING_AGENTS:
|
||||
for name in KNOWN_CODING_AGENTS | KNOWN_RUNTIME_CONTEXTS:
|
||||
assert name.replace("_", "").isalnum(), name
|
||||
assert len(name) <= 32, name
|
||||
|
||||
|
||||
def test_coding_agent_lands_on_every_exported_span(clean_env):
|
||||
def test_coding_agent_lands_on_every_exported_span(clean_env, otel_enabled):
|
||||
"""End-to-end: the attribute must appear as a *span attribute* on any span.
|
||||
|
||||
It cannot be a Resource attribute - the ingestion pipeline preserves only
|
||||
@@ -319,7 +477,7 @@ def test_coding_agent_lands_on_every_exported_span(clean_env):
|
||||
assert "coding_agent" not in exported[0].resource.attributes
|
||||
|
||||
|
||||
def test_common_attributes_processor_never_breaks_span_creation(clean_env):
|
||||
def test_common_attributes_processor_never_breaks_span_creation(clean_env, otel_enabled):
|
||||
"""A failure applying attributes must not propagate into user execution."""
|
||||
from crewai.telemetry.telemetry import CommonAttributesSpanProcessor
|
||||
|
||||
@@ -348,7 +506,7 @@ def test_coding_agent_span_emits_once(isolated_telemetry, clean_env, monkeypatch
|
||||
|
||||
|
||||
def test_attribute_survives_an_externally_installed_provider(
|
||||
isolated_telemetry, clean_env
|
||||
isolated_telemetry, clean_env, otel_enabled
|
||||
):
|
||||
"""Spans must keep coding_agent when the app installs its own provider.
|
||||
|
||||
@@ -402,3 +560,219 @@ def test_attaching_to_a_provider_without_processors_is_safe(isolated_telemetry):
|
||||
telemetry = isolated_telemetry()
|
||||
|
||||
telemetry._attach_common_attributes(object())
|
||||
|
||||
|
||||
def _common_attributes(monkeypatch, project_id=None):
|
||||
"""Build the process-wide span attributes with a stubbed project id."""
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai.telemetry.telemetry.get_project_id", lambda *a, **k: project_id
|
||||
)
|
||||
telemetry = Telemetry.__new__(Telemetry)
|
||||
telemetry._common_attributes = None
|
||||
return telemetry._common_span_attributes()
|
||||
|
||||
|
||||
def test_common_attributes_carry_agent_and_runtime(clean_env, monkeypatch):
|
||||
"""Both fields ride on every span, independently of each other."""
|
||||
clean_env.setenv("CLAUDECODE", "1")
|
||||
clean_env.setenv("GITHUB_ACTIONS", "true")
|
||||
|
||||
attributes = _common_attributes(monkeypatch)
|
||||
|
||||
assert attributes["coding_agent"] == "claude_code"
|
||||
assert attributes["runtime_context"] == "ci"
|
||||
|
||||
|
||||
def test_common_attributes_include_project_id_when_declared(clean_env, monkeypatch):
|
||||
attributes = _common_attributes(monkeypatch, project_id="proj-123")
|
||||
|
||||
assert attributes["project_id"] == "proj-123"
|
||||
|
||||
|
||||
def test_project_id_is_omitted_when_absent(clean_env, monkeypatch):
|
||||
"""Projects without an id must not report a placeholder."""
|
||||
attributes = _common_attributes(monkeypatch, project_id=None)
|
||||
|
||||
assert "project_id" not in attributes
|
||||
|
||||
|
||||
def test_project_id_lookup_never_breaks_telemetry(clean_env, monkeypatch):
|
||||
"""A failed lookup degrades to omitting the attribute."""
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
|
||||
def boom(*args, **kwargs):
|
||||
raise OSError("unreadable")
|
||||
|
||||
monkeypatch.setattr("crewai.telemetry.telemetry.get_project_id", boom)
|
||||
telemetry = Telemetry.__new__(Telemetry)
|
||||
telemetry._common_attributes = None
|
||||
|
||||
attributes = telemetry._common_span_attributes()
|
||||
|
||||
assert "project_id" not in attributes
|
||||
assert "coding_agent" in attributes
|
||||
|
||||
|
||||
def test_common_attributes_are_computed_once(clean_env, monkeypatch):
|
||||
"""The project file must not be re-read for each provider."""
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
|
||||
calls = []
|
||||
|
||||
def counting_get_project_id(*args, **kwargs):
|
||||
calls.append(1)
|
||||
return "proj-123"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai.telemetry.telemetry.get_project_id", counting_get_project_id
|
||||
)
|
||||
telemetry = Telemetry.__new__(Telemetry)
|
||||
telemetry._common_attributes = None
|
||||
|
||||
first = telemetry._common_span_attributes()
|
||||
second = telemetry._common_span_attributes()
|
||||
|
||||
assert first is second
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_all_common_attributes_land_on_exported_spans(clean_env, monkeypatch, otel_enabled):
|
||||
"""End-to-end: every common attribute survives onto arbitrary spans."""
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
||||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
||||
InMemorySpanExporter,
|
||||
)
|
||||
|
||||
from crewai.telemetry.telemetry import CommonAttributesSpanProcessor
|
||||
|
||||
clean_env.setenv("CLAUDECODE", "1")
|
||||
clean_env.setenv("CI", "true")
|
||||
attributes = _common_attributes(monkeypatch, project_id="proj-123")
|
||||
|
||||
exporter = InMemorySpanExporter()
|
||||
provider = TracerProvider()
|
||||
provider.add_span_processor(CommonAttributesSpanProcessor(attributes))
|
||||
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
||||
|
||||
provider.get_tracer("test").start_span("Feature Usage").end()
|
||||
provider.force_flush()
|
||||
|
||||
exported = dict(exporter.get_finished_spans()[0].attributes)
|
||||
assert exported["coding_agent"] == "claude_code"
|
||||
assert exported["runtime_context"] == "ci"
|
||||
assert exported["project_id"] == "proj-123"
|
||||
|
||||
|
||||
def test_runtime_markers_are_detected_by_presence(clean_env, monkeypatch):
|
||||
"""An empty value still means the platform set the marker.
|
||||
|
||||
Some platforms export a bare `CI=`; truthiness checks would drop those
|
||||
runs to the TTY fallback and mislabel them as ordinary local executions.
|
||||
"""
|
||||
monkeypatch.setattr("os.path.exists", lambda path: False)
|
||||
clean_env.setenv("CI", "")
|
||||
|
||||
assert detect_runtime_context() == "ci"
|
||||
|
||||
|
||||
def test_managed_platforms_are_not_reported_as_serverless(clean_env):
|
||||
"""Long-lived managed platforms must not claim the serverless label.
|
||||
|
||||
DYNO and WEBSITE_INSTANCE_ID mark Heroku dynos and Azure App Service
|
||||
instances, which are containers rather than per-invocation functions.
|
||||
"""
|
||||
clean_env.setenv("DYNO", "web.1")
|
||||
assert detect_runtime_context() == "paas"
|
||||
|
||||
clean_env.delenv("DYNO")
|
||||
clean_env.setenv("WEBSITE_INSTANCE_ID", "abc123")
|
||||
assert detect_runtime_context() == "paas"
|
||||
|
||||
|
||||
def test_serverless_markers_still_win_over_paas(clean_env):
|
||||
clean_env.setenv("DYNO", "web.1")
|
||||
clean_env.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
||||
|
||||
assert detect_runtime_context() == "serverless"
|
||||
|
||||
|
||||
def test_generic_marker_is_detected_by_presence(clean_env):
|
||||
"""An empty AI_AGENT still says an assistant is present.
|
||||
|
||||
The named markers keep truthiness, where an empty value means the tool set
|
||||
a placeholder rather than claiming the session.
|
||||
"""
|
||||
clean_env.setenv("AI_AGENT", "")
|
||||
|
||||
assert detect_coding_agent() == "other"
|
||||
|
||||
|
||||
def test_azure_functions_are_not_reported_as_paas(clean_env):
|
||||
"""Azure Functions run on the App Service host and inherit its marker."""
|
||||
clean_env.setenv("WEBSITE_INSTANCE_ID", "abc123")
|
||||
clean_env.setenv("FUNCTIONS_WORKER_RUNTIME", "python")
|
||||
|
||||
assert detect_runtime_context() == "serverless"
|
||||
|
||||
|
||||
def test_env_context_precedence_matches_the_shared_table(clean_env):
|
||||
"""Both detection paths must agree on which assistant is present.
|
||||
|
||||
get_env_context previously restated precedence, so a marker added for
|
||||
telemetry was invisible here and the two disagreed.
|
||||
"""
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.utilities import env as env_module
|
||||
|
||||
agent_to_event = {
|
||||
"claude_code": CCEnvEvent,
|
||||
"codex": CodexEnvEvent,
|
||||
"cursor": CursorEnvEvent,
|
||||
}
|
||||
|
||||
for agent, env_vars in CODING_AGENT_ENV_MARKERS:
|
||||
for var in env_vars:
|
||||
for other in ALL_MARKERS:
|
||||
clean_env.delenv(other, raising=False)
|
||||
clean_env.setenv(var, "1")
|
||||
|
||||
emitted: list[type] = []
|
||||
clean_env.setattr(
|
||||
env_module.crewai_event_bus,
|
||||
"emit",
|
||||
lambda _source, event, sink=emitted: sink.append(type(event)),
|
||||
)
|
||||
env_module._env_context_emitted.set(False)
|
||||
env_module.get_env_context()
|
||||
|
||||
assert detect_coding_agent() == agent, var
|
||||
assert emitted[0] is agent_to_event.get(agent, DefaultEnvEvent), var
|
||||
|
||||
|
||||
def test_assistant_inside_cursor_agrees_across_both_paths(clean_env):
|
||||
"""Cursor sets CURSOR_* in every terminal, including for other assistants."""
|
||||
from crewai.events.types.env_events import DefaultEnvEvent
|
||||
from crewai.utilities import env as env_module
|
||||
|
||||
clean_env.setenv("CURSOR_TRACE_ID", "t-1")
|
||||
clean_env.setenv("CLINE_ACTIVE", "true")
|
||||
|
||||
emitted: list[type] = []
|
||||
clean_env.setattr(
|
||||
env_module.crewai_event_bus,
|
||||
"emit",
|
||||
lambda _source, event, sink=emitted: sink.append(type(event)),
|
||||
)
|
||||
env_module._env_context_emitted.set(False)
|
||||
env_module.get_env_context()
|
||||
|
||||
assert detect_coding_agent() == "cline"
|
||||
assert emitted[0] is DefaultEnvEvent
|
||||
|
||||
@@ -172,7 +172,7 @@ info = "Commits must follow Conventional Commits 1.0.0."
|
||||
[tool.uv]
|
||||
exclude-newer = "3 days"
|
||||
# These security fixes are newer than the global supply-chain cutoff.
|
||||
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z", gitpython = "2026-07-27T00:00:00Z" }
|
||||
exclude-newer-package = { pypdf = "2026-08-07T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z", gitpython = "2026-08-05T00:00:00Z" }
|
||||
|
||||
# composio-core pins rich<14 but textual requires rich>=14.
|
||||
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
|
||||
@@ -186,6 +186,9 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T
|
||||
# cryptography <50.0.0 has GHSA-g6cj-pr64-35w5 (PKCS#7 Bleichenbacher oracle); force 50.0.0+.
|
||||
# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p.
|
||||
# pypdf <6.14.2 has GHSA-jm82-fx9c-mx94 and GHSA-5qjq-93h5-hrgp/GHSA-55h5-xmcq-c37v/GHSA-g867-7843-wf8q/GHSA-5xf7-4p34-54qr; force 6.14.2+.
|
||||
# pypdf <6.15.0 has GHSA-fwg2-594c-jp42 and GHSA-fp3f-mc75-235c (unbounded runtime/memory on large content
|
||||
# and /ToUnicode streams); force 6.15.0+. Its exclude-newer-package cutoff is bumped to 2026-08-07 to admit
|
||||
# that release.
|
||||
# uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+.
|
||||
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
|
||||
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
|
||||
@@ -194,7 +197,11 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T
|
||||
# gitpython 3.1.52 has GHSA-3rp5-jjmw-4wv2, GHSA-fjr4-x663-mwxc, GHSA-6p8h-3wgx-97gf, and GHSA-r9mr-m37c-5fr3; force 3.1.55+.
|
||||
# gitpython <3.1.56 has GHSA-p538-c434-8v24 (arbitrary file truncation via `git rev-list --output` argument
|
||||
# injection) and <3.1.57 has GHSA-3f7w-8rr8-f37f (unguarded git option forwarding in IndexFile.checkout and
|
||||
# TagReference); force 3.1.57+. Its exclude-newer-package cutoff is bumped to 2026-07-27 to admit that release.
|
||||
# TagReference); force 3.1.57+.
|
||||
# gitpython <3.1.58 has GHSA-9rj7-rf2p-w77r, GHSA-4gmw-gg2m-w46p, GHSA-hh9p-6wh2-4mfc, GHSA-wvpp-8hx9-p66j and
|
||||
# GHSA-jm78-9fvv-mhgr (further unguarded git option forwarding in Repo.init, read-tree and git-config, plus
|
||||
# arbitrary file read via --pathspec-from-file); force 3.1.58+. Its exclude-newer-package cutoff is bumped to
|
||||
# 2026-08-05 to admit that release.
|
||||
# pyasn1 <0.6.4 has GHSA-8ppf-4f7h-5ppj and GHSA-hm4w-wwcw-mr6r; force 0.6.4+.
|
||||
# urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+.
|
||||
# langsmith <0.8.18 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure)
|
||||
@@ -231,10 +238,10 @@ override-dependencies = [
|
||||
"urllib3>=2.7.0",
|
||||
"transformers>=5.4.0; python_version >= '3.10'",
|
||||
"cryptography>=50.0.0",
|
||||
"pypdf>=6.14.2,<7",
|
||||
"pypdf>=6.15.0,<7",
|
||||
"uv>=0.11.15,<1",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"gitpython>=3.1.57,<4",
|
||||
"gitpython>=3.1.58,<4",
|
||||
"pyasn1>=0.6.4",
|
||||
"langsmith>=0.8.18,<1",
|
||||
"authlib>=1.6.12",
|
||||
|
||||
114
uv.lock
generated
114
uv.lock
generated
@@ -19,8 +19,8 @@ exclude-newer-span = "P3D"
|
||||
[options.exclude-newer-package]
|
||||
msgpack = "2026-06-20T00:00:00Z"
|
||||
langsmith = "2026-06-20T00:00:00Z"
|
||||
gitpython = "2026-07-27T00:00:00Z"
|
||||
pypdf = "2026-06-24T00:00:00Z"
|
||||
gitpython = "2026-08-05T00:00:00Z"
|
||||
pypdf = "2026-08-07T00:00:00Z"
|
||||
pydantic-settings = "2026-06-20T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
@@ -37,7 +37,7 @@ overrides = [
|
||||
{ name = "authlib", specifier = ">=1.6.12" },
|
||||
{ name = "cryptography", specifier = ">=50.0.0" },
|
||||
{ name = "docling-core", extras = ["chunking"], specifier = ">=2.74.1" },
|
||||
{ name = "gitpython", specifier = ">=3.1.57,<4" },
|
||||
{ name = "gitpython", specifier = ">=3.1.58,<4" },
|
||||
{ name = "h2", specifier = ">=4.4.1" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3,<2" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2,<2" },
|
||||
@@ -51,7 +51,7 @@ overrides = [
|
||||
{ name = "pip", specifier = ">=26.1.2" },
|
||||
{ name = "pyasn1", specifier = ">=0.6.4" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.14.2" },
|
||||
{ name = "pypdf", specifier = ">=6.14.2,<7" },
|
||||
{ name = "pypdf", specifier = ">=6.15.0,<7" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "setuptools", specifier = ">=83.0.0" },
|
||||
@@ -1057,7 +1057,7 @@ name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "humanfriendly" },
|
||||
{ name = "humanfriendly", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
|
||||
wheels = [
|
||||
@@ -1155,7 +1155,7 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
|
||||
wheels = [
|
||||
@@ -1230,7 +1230,7 @@ resolution-markers = [
|
||||
"python_full_version == '3.11.*' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||
wheels = [
|
||||
@@ -1884,34 +1884,34 @@ wheels = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
cudart = [
|
||||
{ name = "nvidia-cuda-runtime" },
|
||||
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft" },
|
||||
{ name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile" },
|
||||
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti" },
|
||||
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand" },
|
||||
{ name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver" },
|
||||
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx" },
|
||||
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2436,7 +2436,7 @@ name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
@@ -2772,14 +2772,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gitpython"
|
||||
version = "3.1.57"
|
||||
version = "3.1.58"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "gitdb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22", size = 228498, upload-time = "2026-08-04T15:05:49.47Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f", size = 220183, upload-time = "2026-08-04T15:05:48.025Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3025,8 +3025,8 @@ name = "grpcio-health-checking"
|
||||
version = "1.71.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "protobuf", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/86/20994347ef36b7626fb74539f13128100dd8b7eaac67efc063264e6cdc80/grpcio_health_checking-1.71.2.tar.gz", hash = "sha256:1c21ece88c641932f432b573ef504b20603bdf030ad4e1ec35dd7fdb4ea02637", size = 16770, upload-time = "2025-06-28T04:24:08.768Z" }
|
||||
wheels = [
|
||||
@@ -3230,7 +3230,7 @@ name = "humanfriendly"
|
||||
version = "10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
|
||||
wheels = [
|
||||
@@ -4467,10 +4467,10 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "jsonref" },
|
||||
{ name = "mcp", extra = ["ws"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "jsonref", marker = "python_full_version < '3.12'" },
|
||||
{ name = "mcp", extra = ["ws"], marker = "python_full_version < '3.12'" },
|
||||
{ name = "pydantic", marker = "python_full_version < '3.12'" },
|
||||
{ name = "python-dotenv", marker = "python_full_version < '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/28/64fc666fa5d86bb1b048c167975d4ea19210f9f8571b64b26563739774ac/mcpadapt-0.1.19.tar.gz", hash = "sha256:dfab84fc75cc84a49a40bd61079773b1faf840227b74b82c71a7755b9c1957c5", size = 4227721, upload-time = "2025-10-16T07:11:56.736Z" }
|
||||
wheels = [
|
||||
@@ -4488,10 +4488,10 @@ resolution-markers = [
|
||||
"python_full_version == '3.12.*' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "jsonref" },
|
||||
{ name = "mcp", extra = ["ws"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "jsonref", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "mcp", extra = ["ws"], marker = "python_full_version >= '3.12'" },
|
||||
{ name = "pydantic", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "python-dotenv", marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/71/1bbbe157e55d30ab4a74fa878f6942cc0586e9820f03e03451a3d2297e9b/mcpadapt-0.1.20.tar.gz", hash = "sha256:4047c0da61e481dd0673a48936a427da9e6547c6cf0d580ff4e4761dcf058ed1", size = 4203656, upload-time = "2025-10-24T15:35:02.135Z" }
|
||||
wheels = [
|
||||
@@ -5507,12 +5507,12 @@ name = "onnxruntime"
|
||||
version = "1.23.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coloredlogs" },
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sympy" },
|
||||
{ name = "coloredlogs", marker = "python_full_version < '3.11'" },
|
||||
{ name = "flatbuffers", marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sympy", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" },
|
||||
@@ -7187,14 +7187,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.14.2"
|
||||
version = "6.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8160,7 +8160,7 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
||||
wheels = [
|
||||
@@ -8224,7 +8224,7 @@ resolution-markers = [
|
||||
"python_full_version == '3.11.*' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
@@ -9855,13 +9855,13 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "authlib" },
|
||||
{ name = "deprecation" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "grpcio-health-checking" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "validators" },
|
||||
{ name = "authlib", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "deprecation", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "grpcio-health-checking", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "httpx", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "pydantic", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
{ name = "validators", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/b9/7b9e05cf923743aa1479afcd85c48ebca82d031c3c3a5d02b1b3fcb52eb9/weaviate_client-4.16.2.tar.gz", hash = "sha256:eb7107a3221a5ad68d604cafc65195bd925a9709512ea0b6fe0dd212b0678fab", size = 681321, upload-time = "2025-07-22T09:10:48.79Z" }
|
||||
wheels = [
|
||||
@@ -9880,13 +9880,13 @@ resolution-markers = [
|
||||
"python_full_version == '3.11.*' and platform_machine == 's390x'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "authlib" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "validators" },
|
||||
{ name = "authlib", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "grpcio", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "httpx", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "packaging", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "protobuf", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "pydantic", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
{ name = "validators", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/b8/103f3aaa246d4e932f4cfeb846e51436966f2aeedf60c2665a3fc51a975a/weaviate_client-4.21.3.tar.gz", hash = "sha256:d7b1f2b0cecbc747e9427f4e3b9463cdfee090746bfbbd40e59cfa25ea2afd4a", size = 847895, upload-time = "2026-06-02T13:03:51.598Z" }
|
||||
wheels = [
|
||||
|
||||
Reference in New Issue
Block a user