diff --git a/.env.example b/.env.example index c39f6fa9..a723cf46 100644 --- a/.env.example +++ b/.env.example @@ -60,3 +60,12 @@ RETRY_ATTEMPTS = 3 # number of editors NUM_EDITORS = 3 + +# Telemetry (Logfire) — disabled by default for local/tests +TELEMETRY_ENABLED=false +TELEMETRY_SERVICE_NAME=akd +TELEMETRY_ENVIRONMENT=local +TELEMETRY_SAMPLE_RATE=1.0 +TELEMETRY_REDACT_PARAMS=true +TELEMETRY_SEND_TO_CLOUD=false +LOGFIRE_TOKEN=pylf_ diff --git a/.gitignore b/.gitignore index 993e473f..d0d6600a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ .envrc.ci .env .worktrees/ +.logfire/ .deepeval/ searxng-docker/ data/cmr_collections_umm.json diff --git a/akd/_base/_base.py b/akd/_base/_base.py index c99ef025..fb406f2a 100644 --- a/akd/_base/_base.py +++ b/akd/_base/_base.py @@ -14,6 +14,7 @@ create_model, ) +from akd.observability import span_agent_run from akd.utils import get_model_fields, to_snake_case from .errors import HumanInputRequired, SchemaValidationError @@ -458,16 +459,19 @@ async def arun( logger.debug( f"Running {self.__class__.__name__} with params: {params}", ) - output = None - try: - output = await self._arun(params, **kwargs) - output = self._validate_output(output) - except HumanInputRequired: - logger.warning(f"{self.__class__.__name__}: HumanInputRequired (flow control)") - raise - except Exception as e: - logger.error(f"Error running {self.__class__.__name__}: {e}") - raise + run_context = kwargs.get("run_context") + run_id = getattr(run_context, "run_id", None) if run_context is not None else None + with span_agent_run(self.__class__.__name__, run_id=run_id): + output = None + try: + output = await self._arun(params, **kwargs) + output = self._validate_output(output) + except HumanInputRequired: + logger.warning(f"{self.__class__.__name__}: HumanInputRequired (flow control)") + raise + except Exception as e: + logger.error(f"Error running {self.__class__.__name__}: {e}") + raise return output @abstractmethod @@ -590,16 +594,19 @@ async def arun( logger.debug( f"Running {self.__class__.__name__} with params: {params}", ) - output = None - try: - output = await self._arun(params, **kwargs) - output = self._validate_output(output) - except HumanInputRequired: - logger.warning(f"{self.__class__.__name__}: HumanInputRequired (flow control)") - raise - except Exception as e: - logger.error(f"Error running {self.__class__.__name__}: {e}") - raise + run_context = kwargs.get("run_context") + run_id = getattr(run_context, "run_id", None) if run_context is not None else None + with span_agent_run(self.__class__.__name__, run_id=run_id): + output = None + try: + output = await self._arun(params, **kwargs) + output = self._validate_output(output) + except HumanInputRequired: + logger.warning(f"{self.__class__.__name__}: HumanInputRequired (flow control)") + raise + except Exception as e: + logger.error(f"Error running {self.__class__.__name__}: {e}") + raise return output @abstractmethod diff --git a/akd/_base/streaming.py b/akd/_base/streaming.py index 90be4d2c..29cc1f7b 100644 --- a/akd/_base/streaming.py +++ b/akd/_base/streaming.py @@ -10,6 +10,8 @@ from pydantic import BaseModel, ConfigDict, Field +from akd.observability import log_stream_event + from .structures import RunContext from .tool_calling import ToolCall, ToolResult @@ -390,6 +392,11 @@ async def astream( """ # Input validation (before any events are yielded) params = self._validate_input(params) + # Ensure run_context has run_id for trace correlation + if run_context is None: + run_context = RunContext() + if run_context.run_id is None: + run_context.run_id = uuid.uuid4().hex[:8] # Stream from internal implementation async for event in self._astream(params, run_context, **kwargs): @@ -406,6 +413,14 @@ async def astream( ) continue yield event + # Log lifecycle events only (skip STREAMING to avoid token volume) + if event.event_type != StreamEventType.STREAMING: + log_stream_event( + str(event.event_type), + run_id=run_context.run_id if run_context else None, + source=getattr(event, "source", None), + is_lifecycle=True, + ) async def _astream( self, diff --git a/akd/_base/tool_calling.py b/akd/_base/tool_calling.py index eb701220..c354571c 100644 --- a/akd/_base/tool_calling.py +++ b/akd/_base/tool_calling.py @@ -9,6 +9,8 @@ from loguru import logger from pydantic import BaseModel, Field +from akd.observability import span_tool_execution + if TYPE_CHECKING: from akd.tools._base import BaseTool @@ -101,24 +103,28 @@ async def _execute_tool( error=f"Unknown tool: {tool_call.tool_name}", ) - try: - input_obj = tool.input_schema(**tool_call.arguments) - result = await tool.arun(input_obj) - # Use mode='json' to ensure JSON-serializable types (HttpUrl → str, datetime → ISO string) - content = result.model_dump(mode="json") if hasattr(result, "model_dump") else result - return ToolResult( - tool_call_id=tool_call.tool_call_id, - tool_name=tool_call.tool_name, - content=content, - ) - except Exception as e: - logger.exception(f"Tool '{tool_call.tool_name}' failed with args {tool_call.arguments}") - return ToolResult( - tool_call_id=tool_call.tool_call_id, - tool_name=tool_call.tool_name, - content=None, - error=str(e), - ) + with span_tool_execution( + tool_call.tool_name, + tool_call.tool_call_id, + ): + try: + input_obj = tool.input_schema(**tool_call.arguments) + result = await tool.arun(input_obj) + # Use mode='json' to ensure JSON-serializable types (HttpUrl → str, datetime → ISO string) + content = result.model_dump(mode="json") if hasattr(result, "model_dump") else result + return ToolResult( + tool_call_id=tool_call.tool_call_id, + tool_name=tool_call.tool_name, + content=content, + ) + except Exception as e: + logger.exception(f"Tool '{tool_call.tool_name}' failed with args {tool_call.arguments}") + return ToolResult( + tool_call_id=tool_call.tool_call_id, + tool_name=tool_call.tool_name, + content=None, + error=str(e), + ) async def _execute_tools_parallel( self, diff --git a/akd/configs/project.py b/akd/configs/project.py index 0e3c257d..121c582d 100644 --- a/akd/configs/project.py +++ b/akd/configs/project.py @@ -46,6 +46,43 @@ class ModelConfigSettings(BaseSettings): default_no_answer: str = "Answer not found" +class TelemetrySettings(BaseSettings): + """Telemetry/observability settings (Logfire). Disabled by default for local and tests. + + Env vars: TELEMETRY_ENABLED, TELEMETRY_SERVICE_NAME, TELEMETRY_ENVIRONMENT, + TELEMETRY_SAMPLE_RATE, TELEMETRY_REDACT_PARAMS, TELEMETRY_SEND_TO_CLOUD. + """ + + enabled: bool = Field( + default=False, + description="Enable Logfire telemetry (spans, logs). Off by default for local/tests.", + ) + service_name: str = Field( + default="akd", + description="Service name sent to Logfire.", + ) + environment: str = Field( + default="local", + description="Environment label (local, dev, staging, production).", + ) + sample_rate: float = Field( + default=1.0, + ge=0.0, + le=1.0, + description="Fraction of traces to sample (0.0–1.0). Use <1.0 in high-volume.", + ) + redact_params: bool = Field( + default=True, + description="Redact input/params and large payloads in telemetry to avoid PII.", + ) + send_to_cloud: bool = Field( + default=False, + description="Send data to Logfire cloud. If False, console/file only.", + ) + + model_config = SettingsConfigDict(env_prefix="TELEMETRY_", extra="forbid") + + class GuardrailSettings(BaseModel): """Project-level guardrail defaults for the @guardrail decorator. @@ -81,6 +118,7 @@ class ProjectSettings(BaseSettings): env: Environment = Environment.LOCAL model_config_settings: ModelConfigSettings = ModelConfigSettings() guardrails: GuardrailSettings = GuardrailSettings() + telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings) model_config = SettingsConfigDict( env_file=(".env", ".env.prod"), diff --git a/akd/observability/__init__.py b/akd/observability/__init__.py new file mode 100644 index 00000000..7179b62a --- /dev/null +++ b/akd/observability/__init__.py @@ -0,0 +1,21 @@ +"""Observability (Logfire) bootstrap. Call init_observability() from script entrypoints.""" + +from __future__ import annotations + +from akd.observability._logfire import ( + init_observability, + is_observability_initialized, + log_stream_event, + redact_for_telemetry, + span_agent_run, + span_tool_execution, +) + +__all__ = [ + "init_observability", + "is_observability_initialized", + "log_stream_event", + "redact_for_telemetry", + "span_agent_run", + "span_tool_execution", +] diff --git a/akd/observability/_logfire.py b/akd/observability/_logfire.py new file mode 100644 index 00000000..cd64d807 --- /dev/null +++ b/akd/observability/_logfire.py @@ -0,0 +1,182 @@ +"""Central Logfire initialization and span helpers. No import-time side effects.""" + +from __future__ import annotations + +import os +import sys +import uuid +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from akd.configs.project import TelemetrySettings + +_initialized = False + +# Keys that should never be sent in telemetry (API keys, tokens, prompts) +_REDACT_KEYS = frozenset( + { + "api_key", + "token", + "password", + "secret", + "authorization", + "query", + "content", + "text", + "message", + "prompt", + "input", + "arguments", + } +) +_MAX_PAYLOAD_CHARS = 500 + + +def _is_test_run() -> bool: + """Return True if we are running under pytest (telemetry should be disabled).""" + return "pytest" in sys.modules or os.environ.get("PYTEST_CURRENT_TEST") is not None + + +def redact_for_telemetry( + data: dict[str, Any], + *, + redact_params: bool = True, + max_value_chars: int = _MAX_PAYLOAD_CHARS, +) -> dict[str, Any]: + """Return a copy of data safe for telemetry: sensitive keys redacted, large values truncated.""" + if not data: + return {} + out: dict[str, Any] = {} + for k, v in data.items(): + key_lower = k.lower() + if redact_params and any(s in key_lower for s in _REDACT_KEYS): + out[k] = "[redacted]" + continue + if isinstance(v, str) and len(v) > max_value_chars: + out[k] = v[:max_value_chars] + "..." + elif isinstance(v, dict): + out[k] = redact_for_telemetry(v, redact_params=redact_params, max_value_chars=max_value_chars) + else: + out[k] = v + return out + + +def is_observability_initialized() -> bool: + """Return True if Logfire was configured (init_observability succeeded).""" + return _initialized + + +@contextmanager +def _noop_context(): + """No-op context manager when telemetry is disabled.""" + yield + + +@contextmanager +def span_agent_run( + agent_name: str, + run_id: str | None = None, + *, + params_safe: dict[str, Any] | None = None, +): + """Context manager for an agent/tool run span. No-op when telemetry not initialized.""" + if not _initialized: + yield + return + import logfire + + rid = run_id or str(uuid.uuid4())[:8] + attrs: dict[str, Any] = {"agent": agent_name, "run_id": rid} + if params_safe is not None: + attrs["params_keys"] = list(params_safe.keys()) if isinstance(params_safe, dict) else [] + with logfire.span("akd.agent.run", **attrs): + yield + + +@contextmanager +def span_tool_execution( + tool_name: str, + tool_call_id: str, + *, + run_id: str | None = None, +): + """Context manager for a tool execution span. No-op when telemetry not initialized.""" + if not _initialized: + yield + return + import logfire + + attrs: dict[str, Any] = {"tool_name": tool_name, "tool_call_id": tool_call_id} + if run_id is not None: + attrs["run_id"] = run_id + with logfire.span("akd.tool.execute", **attrs): + yield + + +def log_stream_event( + event_type: str, + *, + run_id: str | None = None, + source: str | None = None, + is_lifecycle: bool = True, +) -> None: + """Log a streaming lifecycle event. No-op when telemetry not initialized. + + Use for STARTING, COMPLETED, FAILED, TOOL_CALLING, TOOL_RESULT, HUMAN_INPUT_REQUIRED. + Do not call for every STREAMING token (high volume). HUMAN_INPUT_REQUIRED is + logged as lifecycle, not error. + """ + if not _initialized: + return + import logfire + + attrs: dict[str, Any] = {"event_type": event_type, "is_lifecycle": is_lifecycle} + if run_id is not None: + attrs["run_id"] = run_id + if source is not None: + attrs["source"] = source + logfire.info("akd.stream.event", **attrs) + + +def init_observability( + settings: TelemetrySettings | None = None, +) -> bool: + """Initialize Logfire once. Safe to call from script entrypoints. + + If telemetry is disabled or logfire is not installed, returns False and does nothing. + Idempotent: only the first successful call configures Logfire. + + Args: + settings: Telemetry settings. Defaults to get_project_settings().telemetry. + + Returns: + True if Logfire was configured, False otherwise. + """ + global _initialized + if _initialized: + return True + + if settings is None: + from akd.configs.project import get_project_settings + + settings = get_project_settings().telemetry + + if not settings.enabled: + return False + if _is_test_run(): + return False + + try: + import logfire + except ImportError: + return False + + logfire.configure( + service_name=settings.service_name, + environment=settings.environment, + send_to_logfire=settings.send_to_cloud, + trace_sample_rate=settings.sample_rate, + ) + _initialized = True + return True diff --git a/akd/tools/search/searxng.py b/akd/tools/search/searxng.py index c9386fa9..1027086d 100644 --- a/akd/tools/search/searxng.py +++ b/akd/tools/search/searxng.py @@ -148,6 +148,12 @@ async def _fetch_search_results( raw_results = data.get("results", []) # Convert to SearchResultItem objects + def url_or_none(v: str | None) -> str | None: + """Return None for empty or whitespace-only strings so AnyUrl accepts it.""" + if v is None or (isinstance(v, str) and not v.strip()): + return None + return v + search_results = [] for result in raw_results: # Handle DOI normalization @@ -159,13 +165,16 @@ async def _fetch_search_results( doi = str(doi) result["doi"] = doi + raw_url = url_or_none(result.pop("url", None)) + if raw_url is None: + continue # skip results with no valid URL (required field) search_results.append( SearchResultItem( - url=result.pop("url", None), + url=raw_url, title=result.pop("title", "Untitled") or "", content=result.pop("content", "") or "", query=query, - pdf_url=result.pop("pdf_url", None), + pdf_url=url_or_none(result.pop("pdf_url", None)), category=result.pop("category", None), doi=result.pop("doi", None), published_date=result.pop("publishedDate", None), diff --git a/pyproject.toml b/pyproject.toml index 271f8ce4..0c44bbd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ dependencies = [ "litellm>=1.81.0", "dateparser>=1.2.2", "pyzmq>=27.0.0", + "logfire>=4.25.0", + "docling>=2.70.0", ] [project.urls] @@ -103,7 +105,7 @@ dev = [ "pytest-cov>=6.0.0", "pytest-xdist>=3.5.0", "pre-commit>=4.2.0", - "memray>=1.18.0", + "memray>=1.18.0; sys_platform != 'win32'", "scalene>=1.5.55", ] local = [ @@ -118,6 +120,9 @@ ml = [ "docling>=2.37.0", "deepeval>=3.4.0", ] +telemetry = [ + "logfire>=0.54.0", +] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["F841"] diff --git a/scripts/demo_deep_search.py b/scripts/demo_deep_search.py index 482abc46..43c87463 100644 --- a/scripts/demo_deep_search.py +++ b/scripts/demo_deep_search.py @@ -19,6 +19,7 @@ project_root = Path(__file__).parent sys.path.insert(0, str(project_root)) +from akd.observability import init_observability from akd.agents.search import ( DeepLitSearchAgent, DeepLitSearchAgentConfig, @@ -266,6 +267,7 @@ async def main(): print("=" * 80) if __name__ == "__main__": + init_observability() try: asyncio.run(main()) except KeyboardInterrupt: diff --git a/scripts/demo_planner.py b/scripts/demo_planner.py index 672ec905..536899b0 100644 --- a/scripts/demo_planner.py +++ b/scripts/demo_planner.py @@ -22,6 +22,7 @@ # Add the project root to the path sys.path.insert(0, str(Path(__file__).parent.parent)) +from akd.observability import init_observability from akd.planner.llm_planner import create_planner, quick_plan from akd.planner.registry import get_agent_registry @@ -523,6 +524,7 @@ async def _quick_plan(): if __name__ == "__main__": + init_observability() # Set up logging logger.remove() logger.add(sys.stderr, level="INFO") diff --git a/scripts/minimal_planner.py b/scripts/minimal_planner.py index 5ee62eba..fa155829 100644 --- a/scripts/minimal_planner.py +++ b/scripts/minimal_planner.py @@ -12,6 +12,7 @@ from datetime import datetime import sys +from akd.observability import init_observability from akd.planner.llm_planner import create_planner @@ -57,6 +58,7 @@ async def run_planner_chat(initial_request: str): if __name__ == "__main__": + init_observability() # Get initial research goal from user # get as argument initial_request = sys.argv[1] diff --git a/scripts/run_lit_agent.py b/scripts/run_lit_agent.py index f84b798e..23a8c672 100644 --- a/scripts/run_lit_agent.py +++ b/scripts/run_lit_agent.py @@ -5,7 +5,7 @@ from loguru import logger -# Removed unused imports +from akd.observability import init_observability from akd.agents.search import ControlledSearchAgent, LitSearchAgentInputSchema # Removed unused import @@ -13,7 +13,7 @@ async def main(args): - # Removed unused variable assignments + init_observability() # Use the new ControlledAgenticLitSearchAgent with proper configuration from akd.agents.search import ControlledSearchAgentConfig diff --git a/tests/observability/test_logfire.py b/tests/observability/test_logfire.py new file mode 100644 index 00000000..4844dcd7 --- /dev/null +++ b/tests/observability/test_logfire.py @@ -0,0 +1,58 @@ +"""Tests for observability (Logfire) integration: disabled in tests, redaction.""" + +import pytest + +from akd.observability import ( + init_observability, + is_observability_initialized, + redact_for_telemetry, + span_agent_run, + span_tool_execution, +) + + +class TestInitObservability: + """Telemetry is disabled during pytest to avoid flakiness and test data leakage.""" + + def test_init_returns_false_under_pytest(self): + """When running under pytest, init_observability does not enable Logfire.""" + result = init_observability() + assert result is False + assert is_observability_initialized() is False + + def test_spans_are_no_op_when_not_initialized(self): + """Span context managers are no-op when telemetry not initialized.""" + with span_agent_run("TestAgent", run_id="r1"): + pass + with span_tool_execution("test_tool", "tc1"): + pass + + +class TestRedactForTelemetry: + """Redaction prevents PII/sensitive data from being sent to telemetry.""" + + def test_redacts_sensitive_keys(self): + out = redact_for_telemetry( + {"api_key": "secret123", "query": "user prompt", "count": 5}, + redact_params=True, + ) + assert out["api_key"] == "[redacted]" + assert out["query"] == "[redacted]" + assert out["count"] == 5 + + def test_truncates_large_values(self): + long_str = "x" * 600 + out = redact_for_telemetry({"key": long_str}, redact_params=False) + assert len(out["key"]) < 600 + assert out["key"].endswith("...") + + def test_nested_redaction(self): + out = redact_for_telemetry( + {"nested": {"password": "pwd", "ok": 1}}, + redact_params=True, + ) + assert out["nested"]["password"] == "[redacted]" + assert out["nested"]["ok"] == 1 + + def test_empty_dict(self): + assert redact_for_telemetry({}) == {} diff --git a/uv.lock b/uv.lock index 965e4e27..35b31d24 100644 --- a/uv.lock +++ b/uv.lock @@ -157,6 +157,7 @@ source = { editable = "." } dependencies = [ { name = "crawl4ai" }, { name = "dateparser" }, + { name = "docling" }, { name = "duckduckgo-search" }, { name = "gdown" }, { name = "httpx" }, @@ -169,6 +170,7 @@ dependencies = [ { name = "langchain-openai" }, { name = "langgraph" }, { name = "litellm" }, + { name = "logfire" }, { name = "loguru" }, { name = "lxml-html-clean" }, { name = "markdownify" }, @@ -190,7 +192,7 @@ dependencies = [ [package.optional-dependencies] dev = [ - { name = "memray" }, + { name = "memray", marker = "sys_platform != 'win32'" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -210,12 +212,16 @@ ml = [ { name = "pandas" }, { name = "sentence-transformers" }, ] +telemetry = [ + { name = "logfire" }, +] [package.metadata] requires-dist = [ { name = "crawl4ai", specifier = ">=0.7.6" }, { name = "dateparser", specifier = ">=1.2.2" }, { name = "deepeval", marker = "extra == 'ml'", specifier = ">=3.4.0" }, + { name = "docling", specifier = ">=2.70.0" }, { name = "docling", marker = "extra == 'ml'", specifier = ">=2.37.0" }, { name = "duckduckgo-search", specifier = ">=8.0.1" }, { name = "gdown", specifier = ">=5.2.0" }, @@ -232,11 +238,13 @@ requires-dist = [ { name = "langchain-openai", specifier = ">=0.3.27" }, { name = "langgraph", specifier = "==1.0.3" }, { name = "litellm", specifier = ">=1.81.0" }, + { name = "logfire", specifier = ">=4.25.0" }, + { name = "logfire", marker = "extra == 'telemetry'", specifier = ">=0.54.0" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "lxml-html-clean", specifier = ">=0.4.1" }, { name = "marimo", marker = "extra == 'local'", specifier = "==0.15.0" }, { name = "markdownify", specifier = ">=1.1.0" }, - { name = "memray", marker = "extra == 'dev'", specifier = ">=1.18.0" }, + { name = "memray", marker = "sys_platform != 'win32' and extra == 'dev'", specifier = ">=1.18.0" }, { name = "ollama", specifier = ">=0.5.1" }, { name = "openai", specifier = ">=1.75.0" }, { name = "pandas", marker = "extra == 'ml'", specifier = ">=2.3.1" }, @@ -260,7 +268,7 @@ requires-dist = [ { name = "undetected-chromedriver", specifier = ">=3.5.5" }, { name = "wikipedia", specifier = ">=1.4.0" }, ] -provides-extras = ["dev", "local", "ml"] +provides-extras = ["dev", "local", "ml", "telemetry"] [[package]] name = "alphashape" @@ -2700,6 +2708,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/62/d3f53c665261fdd5bb2401246e005a4ea8194ad1c4d8c663318ae3d638bf/litellm-1.81.3-py3-none-any.whl", hash = "sha256:3f60fd8b727587952ad3dd18b68f5fed538d6f43d15bb0356f4c3a11bccb2b92", size = 11946995, upload-time = "2026-01-25T02:45:55.887Z" }, ] +[[package]] +name = "logfire" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/374fc0e6ebe95209414cf743cc693f4ff2ad391fd0712445ed1f63245395/logfire-4.25.0.tar.gz", hash = "sha256:f9a6bf6d40fd3e2c2a86a364617246cadecbde620b4ecccb17c499140f1ebc13", size = 1049745, upload-time = "2026-02-19T15:27:28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/cc/a3eb3a5fff27a6bfe2f626624c7c781322151f3228d4ea98c31003dc2d4c/logfire-4.25.0-py3-none-any.whl", hash = "sha256:1865b832e08c58a3fb0d21b24460ee9c6cbeff12db6038c508fb966699ce81c2", size = 298186, upload-time = "2026-02-19T15:27:23.324Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -3699,6 +3725,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.39.1" @@ -6040,6 +6099,8 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" },