asi-telemetry 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devrithm Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: asi-telemetry
3
+ Version: 0.2.0
4
+ Summary: Observability and Distributed Tracing SDK for AI Agents and LLMs
5
+ Author: Devrithm Labs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Devrithm-Labs/ASI-Telemetry
8
+ Project-URL: Repository, https://github.com/Devrithm-Labs/ASI-Telemetry
9
+ Project-URL: Bug Tracker, https://github.com/Devrithm-Labs/ASI-Telemetry/issues
10
+ Keywords: telemetry,tracing,observability,ai-agents,llm,metrics,gemini,openai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # ASI Telemetry SDK
28
+
29
+ The official lightweight observability, runtime metrics, and distributed tracing SDK for AI agents, LLMs (Google Gemini, OpenAI, ASI-1, DeepSeek), and tool execution pipelines.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install asi-telemetry
35
+ ```
36
+
37
+ Zero external dependencies required (built using standard Python libraries).
38
+
39
+
40
+ ## Architecture & Features
41
+
42
+ - **Agent & Tool Performance Tracking (`MetricsTracker`)**:
43
+ - Measures execution latency, request counts, and success/error status.
44
+ - Asynchronously dispatches metrics to the ASI Telemetry backend and ClickHouse.
45
+ - Zero performance overhead; non-blocking background workers.
46
+ - Clean real-time terminal telemetry reporting.
47
+
48
+ - **Distributed LLM Tracing (`@traceable`)**:
49
+ - Trace multi-hop agent swarms and workflow chains with distributed context propagation (`trace_id`, `span_id`, `parent_id`).
50
+ - Supports both synchronous and asynchronous coroutine functions.
51
+ - Native auto-instrumentation for Google Gemini (`instrument_gemini`) and OpenAI/ASI (`instrument_openai` / `instrument_client`).
52
+ - Real-time token usage calculation and cost estimation per model.
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### 1. Monitoring Agent Tools & Functions
59
+
60
+ ```python
61
+ from asi_telemetry import MetricsTracker
62
+
63
+ tracker = MetricsTracker(agent_name="assistant")
64
+
65
+ @tracker.track(tool_name="weather_tool")
66
+ async def get_weather(city: str):
67
+ return f"Weather in {city}: 72°F Sunny"
68
+ ```
69
+
70
+ *(Note: `AgentMonitor` is retained as a backward-compatible alias for `MetricsTracker`)*
71
+
72
+ ### 2. Tracing LLMs and Multi-Agent Workflows
73
+
74
+ ```python
75
+ from asi_telemetry import traceable, instrument_gemini
76
+ from google import genai
77
+
78
+ client = genai.Client(api_key="YOUR_API_KEY")
79
+ instrument_gemini(client)
80
+
81
+ @traceable(name="generate_summary", run_type="llm")
82
+ def summarize(text: str):
83
+ response = client.models.generate_content(
84
+ model="gemini-2.5-flash",
85
+ contents=f"Summarize this: {text}"
86
+ )
87
+ return response.text
88
+ ```
89
+
90
+ ### 3. Distributed Context Propagation Across Swarms
91
+
92
+ ```python
93
+ from asi_telemetry import traceable, create_trace_context
94
+
95
+ @traceable(name="orchestrator", run_type="chain")
96
+ async def orchestrate():
97
+ # Generate trace context to pass across network boundaries
98
+ context = create_trace_context()
99
+ # Pass context={"trace_id": ..., "parent_id": ...} to sub-agents
100
+ ```
101
+
102
+ ### 4. Direct Cost Estimation
103
+
104
+ ```python
105
+ from asi_telemetry import calculate_cost
106
+
107
+ # Approximate cost in USD for a model call
108
+ cost = calculate_cost(model="gemini-3.8-flash", input_tokens=1200, output_tokens=350)
109
+ print(f"Cost: ${cost}")
110
+ ```
@@ -0,0 +1,84 @@
1
+ # ASI Telemetry SDK
2
+
3
+ The official lightweight observability, runtime metrics, and distributed tracing SDK for AI agents, LLMs (Google Gemini, OpenAI, ASI-1, DeepSeek), and tool execution pipelines.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install asi-telemetry
9
+ ```
10
+
11
+ Zero external dependencies required (built using standard Python libraries).
12
+
13
+
14
+ ## Architecture & Features
15
+
16
+ - **Agent & Tool Performance Tracking (`MetricsTracker`)**:
17
+ - Measures execution latency, request counts, and success/error status.
18
+ - Asynchronously dispatches metrics to the ASI Telemetry backend and ClickHouse.
19
+ - Zero performance overhead; non-blocking background workers.
20
+ - Clean real-time terminal telemetry reporting.
21
+
22
+ - **Distributed LLM Tracing (`@traceable`)**:
23
+ - Trace multi-hop agent swarms and workflow chains with distributed context propagation (`trace_id`, `span_id`, `parent_id`).
24
+ - Supports both synchronous and asynchronous coroutine functions.
25
+ - Native auto-instrumentation for Google Gemini (`instrument_gemini`) and OpenAI/ASI (`instrument_openai` / `instrument_client`).
26
+ - Real-time token usage calculation and cost estimation per model.
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ### 1. Monitoring Agent Tools & Functions
33
+
34
+ ```python
35
+ from asi_telemetry import MetricsTracker
36
+
37
+ tracker = MetricsTracker(agent_name="assistant")
38
+
39
+ @tracker.track(tool_name="weather_tool")
40
+ async def get_weather(city: str):
41
+ return f"Weather in {city}: 72°F Sunny"
42
+ ```
43
+
44
+ *(Note: `AgentMonitor` is retained as a backward-compatible alias for `MetricsTracker`)*
45
+
46
+ ### 2. Tracing LLMs and Multi-Agent Workflows
47
+
48
+ ```python
49
+ from asi_telemetry import traceable, instrument_gemini
50
+ from google import genai
51
+
52
+ client = genai.Client(api_key="YOUR_API_KEY")
53
+ instrument_gemini(client)
54
+
55
+ @traceable(name="generate_summary", run_type="llm")
56
+ def summarize(text: str):
57
+ response = client.models.generate_content(
58
+ model="gemini-2.5-flash",
59
+ contents=f"Summarize this: {text}"
60
+ )
61
+ return response.text
62
+ ```
63
+
64
+ ### 3. Distributed Context Propagation Across Swarms
65
+
66
+ ```python
67
+ from asi_telemetry import traceable, create_trace_context
68
+
69
+ @traceable(name="orchestrator", run_type="chain")
70
+ async def orchestrate():
71
+ # Generate trace context to pass across network boundaries
72
+ context = create_trace_context()
73
+ # Pass context={"trace_id": ..., "parent_id": ...} to sub-agents
74
+ ```
75
+
76
+ ### 4. Direct Cost Estimation
77
+
78
+ ```python
79
+ from asi_telemetry import calculate_cost
80
+
81
+ # Approximate cost in USD for a model call
82
+ cost = calculate_cost(model="gemini-3.8-flash", input_tokens=1200, output_tokens=350)
83
+ print(f"Cost: ${cost}")
84
+ ```
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "asi-telemetry"
7
+ version = "0.2.0"
8
+ description = "Observability and Distributed Tracing SDK for AI Agents and LLMs"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Devrithm Labs" }
14
+ ]
15
+ keywords = [
16
+ "telemetry",
17
+ "tracing",
18
+ "observability",
19
+ "ai-agents",
20
+ "llm",
21
+ "metrics",
22
+ "gemini",
23
+ "openai",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Software Development :: Libraries :: Python Modules",
36
+ "Topic :: System :: Monitoring",
37
+ ]
38
+ dependencies = []
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Devrithm-Labs/ASI-Telemetry"
42
+ Repository = "https://github.com/Devrithm-Labs/ASI-Telemetry"
43
+ "Bug Tracker" = "https://github.com/Devrithm-Labs/ASI-Telemetry/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+ if __name__ == "__main__":
4
+ setup()
@@ -0,0 +1,168 @@
1
+ import os
2
+ from typing import Any, Callable, Dict, Optional
3
+
4
+ from .metrics import MetricsTracker
5
+ from .tracer import (
6
+ traceable,
7
+ create_trace_context,
8
+ set_agent_info,
9
+ set_api_key,
10
+ )
11
+ from .instrumentation import (
12
+ instrument_gemini,
13
+ instrument_openai,
14
+ instrument_client,
15
+ instrument_llm_provider,
16
+ )
17
+ from .models import calculate_cost
18
+
19
+ __version__ = "0.2.0"
20
+
21
+
22
+ class ASI_Telemetry:
23
+ """
24
+ Unified ASI Telemetry client.
25
+
26
+ Usage:
27
+ from asi_telemetry import ASI_Telemetry
28
+
29
+ observe = ASI_Telemetry(
30
+ api_key="asi_live_...",
31
+ agent_name="assistant",
32
+ agent_address=agent.address,
33
+ )
34
+
35
+ @observe.track("handle_message")
36
+ @observe.traceable(name="handle_message", run_type="tool")
37
+ async def handle_message(ctx, sender, msg):
38
+ ...
39
+ """
40
+
41
+ DEFAULT_BACKEND_URL = "http://127.0.0.1:8080"
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str = "",
46
+ agent_name: str = "assistant",
47
+ agent_address: str = "",
48
+ backend_url: Optional[str] = None,
49
+ ):
50
+ self.api_key = (
51
+ api_key
52
+ or os.getenv("ASI_API_KEY")
53
+ or os.getenv("ASI_TELEMETRY_API_KEY")
54
+ or os.getenv("TELEMETRY_API_KEY")
55
+ or ""
56
+ )
57
+ self.backend_url = (
58
+ backend_url
59
+ or os.getenv("TELEMETRY_BACKEND_URL")
60
+ or self.DEFAULT_BACKEND_URL
61
+ )
62
+ self.agent_name = agent_name
63
+ self.agent_address = agent_address
64
+
65
+ # Initialize global environment & agent identities
66
+ if self.api_key:
67
+ set_api_key(self.api_key)
68
+ os.environ["ASI_API_KEY"] = self.api_key
69
+ if self.backend_url:
70
+ os.environ["TELEMETRY_BACKEND_URL"] = self.backend_url
71
+ os.environ["TELEMETRY_METRICS_URL"] = (
72
+ self.backend_url
73
+ if "/api/metrics" in self.backend_url or "/api/telemetry" in self.backend_url
74
+ else f"{self.backend_url.rstrip('/')}/api/metrics"
75
+ )
76
+ os.environ["TELEMETRY_TRACES_URL"] = (
77
+ self.backend_url
78
+ if "/api/llm/traces" in self.backend_url
79
+ else f"{self.backend_url.rstrip('/')}/api/llm/traces"
80
+ )
81
+
82
+ set_agent_info(agent_name=self.agent_name, agent_address=self.agent_address)
83
+
84
+ # Internal metrics tracker
85
+ self.metrics = MetricsTracker(
86
+ agent_name=self.agent_name,
87
+ agent_address=self.agent_address,
88
+ api_key=self.api_key,
89
+ backend_url=os.environ.get("TELEMETRY_METRICS_URL", self.backend_url),
90
+ )
91
+
92
+ def track(self, tool_name: Optional[Any] = None) -> Callable:
93
+ """Track function execution time, status, and agent performance metrics."""
94
+ return self.metrics.track(tool_name)
95
+
96
+ # Alias for track
97
+ tool_monitoring = track
98
+
99
+ def traceable(
100
+ self,
101
+ name: Optional[Any] = None,
102
+ run_type: str = "chain",
103
+ metadata: Optional[Dict[str, Any]] = None,
104
+ agent_address: Optional[str] = None,
105
+ ) -> Callable:
106
+ """Trace function execution, inputs, outputs, errors, latency, and tokens."""
107
+ return traceable(
108
+ name=name,
109
+ run_type=run_type,
110
+ metadata=metadata,
111
+ agent_address=agent_address or self.agent_address,
112
+ )
113
+
114
+ def create_trace_context(self) -> Dict[str, Optional[str]]:
115
+ """Create a distributed tracing context dictionary for inter-agent messages."""
116
+ return create_trace_context()
117
+
118
+ def instrument_gemini(self, client: Any) -> Any:
119
+ """Auto-instrument Google GenAI Gemini client."""
120
+ return instrument_gemini(client)
121
+
122
+ def instrument_openai(self, client: Any) -> Any:
123
+ """Auto-instrument OpenAI client."""
124
+ return instrument_openai(client)
125
+
126
+ def instrument_client(self, client: Any, provider: str = "custom") -> Any:
127
+ """Auto-instrument any LLM client."""
128
+ return instrument_client(client, provider)
129
+
130
+
131
+ # Aliases for backward compatibility
132
+ ASITelemetry = ASI_Telemetry
133
+
134
+
135
+ def init(
136
+ api_key: str = "",
137
+ agent_name: str = "assistant",
138
+ agent_address: str = "",
139
+ backend_url: Optional[str] = None,
140
+ ) -> ASI_Telemetry:
141
+ """Initialize the ASI Telemetry SDK with global client settings."""
142
+ return ASI_Telemetry(
143
+ api_key=api_key,
144
+ agent_name=agent_name,
145
+ agent_address=agent_address,
146
+ backend_url=backend_url,
147
+ )
148
+
149
+
150
+
151
+ __all__ = [
152
+ "__version__",
153
+ # Primary Client
154
+ "ASI_Telemetry",
155
+ "ASITelemetry",
156
+ # Core Entry Points
157
+ "init",
158
+ "traceable",
159
+ "create_trace_context",
160
+ "MetricsTracker",
161
+ # Instrumentations
162
+ "instrument_gemini",
163
+ "instrument_openai",
164
+ "instrument_client",
165
+ "instrument_llm_provider",
166
+ # Cost Utility
167
+ "calculate_cost",
168
+ ]
@@ -0,0 +1,114 @@
1
+ """
2
+ Client auto-instrumentation for Google Gemini, OpenAI, and compatible LLM providers.
3
+ """
4
+
5
+ import functools
6
+ from typing import Any, Optional
7
+ from .models import calculate_cost
8
+
9
+
10
+ def save_token_usage(response: Any, span: dict, model: Optional[str] = None) -> None:
11
+ """
12
+ Extracts token usage metadata from Gemini or OpenAI/ASI responses
13
+ and writes token counts and estimated costs into the span.
14
+ """
15
+ if not isinstance(span, dict):
16
+ return
17
+
18
+ # 1. Google Gemini style: response.usage_metadata
19
+ usage = getattr(response, "usage_metadata", None)
20
+ if usage:
21
+ input_tokens = getattr(usage, "prompt_token_count", 0) or 0
22
+ output_tokens = getattr(usage, "candidates_token_count", 0) or 0
23
+ total_tokens = getattr(usage, "total_token_count", 0) or (input_tokens + output_tokens)
24
+ resolved_model = model or span.get("metadata", {}).get("model", "gemini-3.8-flash")
25
+ else:
26
+ # 2. OpenAI / ASI style: response.usage
27
+ usage = getattr(response, "usage", None)
28
+ if not usage:
29
+ return # Not an LLM response with token usage
30
+ input_tokens = getattr(usage, "prompt_tokens", 0) or 0
31
+ output_tokens = getattr(usage, "completion_tokens", 0) or 0
32
+ total_tokens = getattr(usage, "total_tokens", 0) or (input_tokens + output_tokens)
33
+ resolved_model = model or getattr(response, "model", span.get("metadata", {}).get("model", "asi1-mini"))
34
+
35
+ span.setdefault("metadata", {})["model"] = resolved_model
36
+ span["tokens"] = {
37
+ "input": input_tokens,
38
+ "output": output_tokens,
39
+ "total": total_tokens,
40
+ }
41
+ span["cost"] = calculate_cost(resolved_model, input_tokens, output_tokens)
42
+
43
+
44
+ def instrument_gemini(client: Any) -> None:
45
+ """
46
+ Auto-instruments Google Gemini genai client (client.models.generate_content)
47
+ to automatically extract tokens and calculate costs for any enclosing span.
48
+ """
49
+ from .tracer import current_span
50
+
51
+ if not hasattr(client, "models") or not hasattr(client.models, "generate_content"):
52
+ return
53
+
54
+ original_generate = client.models.generate_content
55
+
56
+ @functools.wraps(original_generate)
57
+ def wrapped_generate(*args, **kwargs):
58
+ span = current_span.get()
59
+ model_name = kwargs.get("model", args[0] if args else "gemini-3.8-flash")
60
+ if span is not None:
61
+ span.setdefault("metadata", {})["model"] = model_name
62
+
63
+ response = original_generate(*args, **kwargs)
64
+
65
+ if span is not None:
66
+ save_token_usage(response, span, model=model_name)
67
+ return response
68
+
69
+ client.models.generate_content = wrapped_generate
70
+
71
+
72
+ def instrument_openai(client: Any) -> None:
73
+ """
74
+ Auto-instruments OpenAI / ASI client (client.chat.completions.create)
75
+ to capture token usage and calculate costs.
76
+ """
77
+ from .tracer import current_span
78
+
79
+ if not hasattr(client, "chat") or not hasattr(client.chat, "completions"):
80
+ return
81
+
82
+ original_create = client.chat.completions.create
83
+
84
+ @functools.wraps(original_create)
85
+ def wrapped_create(*args, **kwargs):
86
+ span = current_span.get()
87
+ model_name = kwargs.get("model", "asi1-mini")
88
+ if span is not None:
89
+ span.setdefault("metadata", {})["model"] = model_name
90
+
91
+ response = original_create(*args, **kwargs)
92
+
93
+ if span is not None:
94
+ save_token_usage(response, span, model=model_name)
95
+ return response
96
+
97
+ client.chat.completions.create = wrapped_create
98
+
99
+
100
+ def instrument_client(client: Any) -> None:
101
+ """
102
+ Automatically detects LLM client type (Google Gemini, OpenAI, or ASI)
103
+ and applies tracing instrumentation.
104
+ """
105
+ if hasattr(client, "models") and hasattr(client.models, "generate_content"):
106
+ instrument_gemini(client)
107
+ elif hasattr(client, "chat") and hasattr(client.chat, "completions"):
108
+ instrument_openai(client)
109
+
110
+
111
+ # Backward-compatible alias
112
+ instrument_llm_provider = instrument_client
113
+
114
+
@@ -0,0 +1,164 @@
1
+ """
2
+ Metrics tracking and runtime performance monitoring for AI agents and tools.
3
+ """
4
+
5
+ import asyncio
6
+ import inspect
7
+ import json
8
+ import os
9
+ import threading
10
+ import time
11
+ import urllib.request
12
+ from datetime import datetime, timezone
13
+ from functools import wraps
14
+ from typing import Any, Callable, Optional
15
+
16
+
17
+ class MetricsTracker:
18
+
19
+ def __init__(
20
+ self,
21
+ agent_name: str = "assistant",
22
+ agent_address: str = "",
23
+ api_key: str = "",
24
+ backend_url: str = "",
25
+ ):
26
+ self.agent_name = agent_name
27
+ self.agent_address = agent_address
28
+ self.api_key = api_key
29
+ self.backend_url = backend_url
30
+
31
+ # Sync process-level agent identity and API key with tracer
32
+ try:
33
+ from .tracer import set_agent_info, set_api_key
34
+ set_agent_info(
35
+ agent_name=agent_name,
36
+ agent_address=agent_address,
37
+ )
38
+ if self.api_key:
39
+ set_api_key(self.api_key)
40
+ except Exception:
41
+ pass
42
+
43
+ def _get_metrics_url(self) -> str:
44
+ url = (
45
+ self.backend_url
46
+ or os.getenv("TELEMETRY_METRICS_URL")
47
+ or os.getenv("TELEMETRY_BACKEND_URL")
48
+ or "http://127.0.0.1:8080"
49
+ )
50
+ if "/api/llm/traces" in url:
51
+ url = url.replace("/api/llm/traces", "/api/metrics")
52
+ elif not url.endswith("/api/metrics") and not url.endswith("/api/telemetry"):
53
+ url = f"{url.rstrip('/')}/api/metrics"
54
+ return url
55
+
56
+ def _dispatch_payload(self, payload: dict) -> None:
57
+ def _worker():
58
+ url = self._get_metrics_url()
59
+ try:
60
+ headers = {"Content-Type": "application/json"}
61
+ api_key = (
62
+ self.api_key
63
+ or os.getenv("ASI_API_KEY")
64
+ or os.getenv("ASI_TELEMETRY_API_KEY")
65
+ or os.getenv("TELEMETRY_API_KEY")
66
+ )
67
+ if not api_key:
68
+ try:
69
+ from .tracer import get_api_key
70
+ api_key = get_api_key()
71
+ except Exception:
72
+ pass
73
+
74
+ if api_key:
75
+ headers["x-api-key"] = api_key
76
+ headers["Authorization"] = f"Bearer {api_key}"
77
+ payload["api_key"] = api_key
78
+
79
+ data = json.dumps(payload, default=str).encode("utf-8")
80
+ req = urllib.request.Request(
81
+ url,
82
+ data=data,
83
+ headers=headers,
84
+ method="POST",
85
+ )
86
+ with urllib.request.urlopen(req, timeout=3.0):
87
+ pass
88
+ except Exception as e:
89
+ # Log when debugging is requested
90
+ if os.getenv("ASI_DEBUG") or os.getenv("TELEMETRY_DEBUG"):
91
+ print(f"[ASI-Telemetry Metrics Error] Failed to send metrics to {url}: {e}")
92
+
93
+ threading.Thread(target=_worker, daemon=True).start()
94
+
95
+ def track(self, tool_name: Optional[Any] = None) -> Callable:
96
+ """
97
+ Decorator to track function execution time, status, and agent metrics.
98
+ Can be used as:
99
+ @monitor.track
100
+ @monitor.track()
101
+ @monitor.track("custom_name")
102
+ Supports both sync and async functions.
103
+ """
104
+ def decorator(func: Callable) -> Callable:
105
+ target_name = (tool_name if isinstance(tool_name, str) else None) or func.__name__
106
+
107
+ if inspect.iscoroutinefunction(func):
108
+ @wraps(func)
109
+ async def async_wrapper(*args, **kwargs):
110
+ start_time = time.perf_counter()
111
+ status = "success"
112
+ try:
113
+ return await func(*args, **kwargs)
114
+ except asyncio.CancelledError:
115
+ status = "cancelled"
116
+ raise
117
+ except Exception:
118
+ status = "error"
119
+ raise
120
+ finally:
121
+ duration_ms = (time.perf_counter() - start_time) * 1000
122
+ payload = {
123
+ "agent_name": self.agent_name,
124
+ "agent_address": self.agent_address,
125
+ "func_name": target_name,
126
+ "status": status,
127
+ "response_time_ms": round(duration_ms, 2),
128
+ "timestamp": datetime.now(timezone.utc).isoformat(),
129
+ }
130
+ self._dispatch_payload(payload)
131
+
132
+ return async_wrapper
133
+ else:
134
+ @wraps(func)
135
+ def sync_wrapper(*args, **kwargs):
136
+ start_time = time.perf_counter()
137
+ status = "success"
138
+ try:
139
+ return func(*args, **kwargs)
140
+ except Exception:
141
+ status = "error"
142
+ raise
143
+ finally:
144
+ duration_ms = (time.perf_counter() - start_time) * 1000
145
+ payload = {
146
+ "agent_name": self.agent_name,
147
+ "agent_address": self.agent_address,
148
+ "func_name": target_name,
149
+ "status": status,
150
+ "response_time_ms": round(duration_ms, 2),
151
+ "timestamp": datetime.now(timezone.utc).isoformat(),
152
+ }
153
+ self._dispatch_payload(payload)
154
+
155
+ return sync_wrapper
156
+
157
+ if callable(tool_name):
158
+ func = tool_name
159
+ tool_name = None
160
+ return decorator(func)
161
+
162
+ return decorator
163
+
164
+
@@ -0,0 +1,30 @@
1
+ """
2
+ Data models, pricing tables, and schema constants for ASI Telemetry.
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+
7
+ # Price in USD per 1 million tokens
8
+ PRICE_PER_MILLION: Dict[str, Dict[str, float]] = {
9
+ "asi1-mini": {"input": 0.10, "output": 0.40},
10
+ "asi-mini": {"input": 0.10, "output": 0.40},
11
+ "asi1": {"input": 0.50, "output": 1.50},
12
+ "gemini-3.8-flash": {"input": 0.075, "output": 0.30},
13
+ "gemini-2.5-flash": {"input": 0.075, "output": 0.30},
14
+ "gemini-3.5-flash": {"input": 0.075, "output": 0.30},
15
+ "gemini-3.5-flash-lite": {"input": 0.0375, "output": 0.15}
16
+ }
17
+
18
+ DEFAULT_PRICE: Dict[str, float] = {"input": 0.10, "output": 0.40}
19
+
20
+
21
+ def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
22
+ """
23
+ Calculates estimated USD cost for a given model and token counts.
24
+ """
25
+ normalized_model = (model or "").replace("models/", "").lower()
26
+ price = PRICE_PER_MILLION.get(normalized_model, DEFAULT_PRICE)
27
+
28
+ input_cost = (input_tokens * price["input"]) / 1_000_000
29
+ output_cost = (output_tokens * price["output"]) / 1_000_000
30
+ return round(input_cost + output_cost, 6)
@@ -0,0 +1,250 @@
1
+ import functools
2
+ import inspect
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ import urllib.request
8
+ import uuid
9
+ from contextvars import ContextVar
10
+ from typing import Any, Callable, Dict, Optional, Tuple
11
+
12
+ from .instrumentation import save_token_usage
13
+
14
+ sdk_config: Dict[str, Any] = {
15
+ "api_key": os.getenv("ASI_API_KEY") or os.getenv("ASI_TELEMETRY_API_KEY") or os.getenv("TELEMETRY_API_KEY"),
16
+ }
17
+
18
+ current_agent: Dict[str, Optional[str]] = {
19
+ "agent_name": os.getenv("AGENT_NAME", "assistant"),
20
+ "agent_address": os.getenv("AGENT_ADDRESS", None),
21
+ }
22
+ CURRENT_AGENT_INFO = current_agent
23
+
24
+ active_span: ContextVar[Optional[Dict[str, Any]]] = ContextVar("active_span", default=None)
25
+ current_span = active_span
26
+
27
+
28
+ def set_api_key(api_key: str) -> None:
29
+ sdk_config["api_key"] = api_key
30
+ os.environ["ASI_API_KEY"] = api_key
31
+
32
+
33
+ def get_api_key() -> Optional[str]:
34
+ return sdk_config.get("api_key") or os.getenv("ASI_API_KEY") or os.getenv("ASI_TELEMETRY_API_KEY") or os.getenv("TELEMETRY_API_KEY")
35
+
36
+
37
+ def set_agent_info(agent_name: Optional[str] = None, agent_address: Optional[str] = None) -> None:
38
+ if agent_name:
39
+ current_agent["agent_name"] = agent_name
40
+ if agent_address:
41
+ current_agent["agent_address"] = agent_address
42
+
43
+
44
+ def get_agent_info() -> Dict[str, Optional[str]]:
45
+ return dict(current_agent)
46
+
47
+
48
+ def get_current_trace_id() -> Optional[str]:
49
+ span = active_span.get()
50
+ return span["trace_id"] if span else None
51
+
52
+
53
+ def get_current_span_id() -> Optional[str]:
54
+ span = active_span.get()
55
+ return span["span_id"] if span else None
56
+
57
+
58
+ def create_trace_context() -> Dict[str, Optional[str]]:
59
+ span = active_span.get()
60
+ if span:
61
+ return {"trace_id": span["trace_id"], "parent_id": span["span_id"]}
62
+ return {"trace_id": str(uuid.uuid4()), "parent_id": None}
63
+
64
+
65
+ def _extract_trace_context(args: tuple, kwargs: dict, metadata: Optional[Dict[str, Any]]) -> Tuple[Optional[str], Optional[str]]:
66
+ candidates = [metadata] + list(args) + list(kwargs.values())
67
+ for item in candidates:
68
+ ctx = getattr(item, "trace_context", None) or (item.get("trace_context") if isinstance(item, dict) else item)
69
+ if isinstance(ctx, dict) and ctx.get("trace_id"):
70
+ return ctx["trace_id"], ctx.get("parent_id") or ctx.get("span_id")
71
+ return None, None
72
+
73
+
74
+ def _dispatch_span(payload: Dict[str, Any]) -> None:
75
+ def _worker():
76
+ try:
77
+ url = os.getenv("TELEMETRY_TRACES_URL") or os.getenv("TELEMETRY_BACKEND_URL", "http://127.0.0.1:8080/api/llm/traces")
78
+ if "/api/metrics" in url:
79
+ url = url.replace("/api/metrics", "/api/llm/traces")
80
+
81
+ api_key = get_api_key()
82
+ headers = {"Content-Type": "application/json"}
83
+ if api_key:
84
+ headers["x-api-key"] = api_key
85
+ headers["Authorization"] = f"Bearer {api_key}"
86
+ payload["api_key"] = api_key
87
+
88
+ req = urllib.request.Request(
89
+ url,
90
+ data=json.dumps(payload, default=str).encode("utf-8"),
91
+ headers=headers,
92
+ method="POST",
93
+ )
94
+ with urllib.request.urlopen(req, timeout=2.0):
95
+ pass
96
+ except Exception:
97
+ pass
98
+
99
+ threading.Thread(target=_worker, daemon=True).start()
100
+
101
+
102
+ def start_span(
103
+ function_name: str,
104
+ run_type: str,
105
+ metadata: Optional[Dict[str, Any]],
106
+ args: tuple,
107
+ kwargs: dict,
108
+ agent_address: Optional[str] = None,
109
+ ) -> Tuple[Dict[str, Any], Any]:
110
+ parent_span = active_span.get()
111
+ incoming_trace_id, incoming_parent_id = _extract_trace_context(args, kwargs, metadata)
112
+
113
+ if incoming_trace_id:
114
+ trace_id = incoming_trace_id
115
+ parent_id = incoming_parent_id or (parent_span["span_id"] if parent_span else None)
116
+ elif parent_span:
117
+ trace_id = parent_span["trace_id"]
118
+ parent_id = parent_span["span_id"]
119
+ else:
120
+ trace_id = str(uuid.uuid4())
121
+ parent_id = None
122
+
123
+ is_parent_root = (parent_span is None)
124
+ agent_name = current_agent.get("agent_name", "assistant")
125
+ meta = dict(metadata or {})
126
+ meta["is_agent_root"] = is_parent_root
127
+ meta["agent_name"] = agent_name
128
+
129
+ addr = agent_address or meta.get("agent_address") or (parent_span.get("agent_address") if parent_span else None) or current_agent.get("agent_address")
130
+ span_id = str(uuid.uuid4())
131
+ inputs = kwargs if kwargs else (args[0] if len(args) == 1 else args)
132
+
133
+ span = {
134
+ "id": span_id,
135
+ "span_id": span_id,
136
+ "trace_id": trace_id,
137
+ "parent_id": parent_id,
138
+ "name": function_name,
139
+ "run_type": run_type,
140
+ "start_time": time.time(),
141
+ "inputs": inputs,
142
+ "outputs": None,
143
+ "error": None,
144
+ "tokens": {"input": 0, "output": 0, "total": 0},
145
+ "cost": 0.0,
146
+ "agent_name": agent_name,
147
+ "is_agent_root": is_parent_root,
148
+ "agent_address": addr,
149
+ "metadata": meta,
150
+ }
151
+
152
+ reset_token = active_span.set(span)
153
+ return span, reset_token
154
+
155
+
156
+ def end_span(
157
+ span: Dict[str, Any],
158
+ reset_token: Any,
159
+ result: Any = None,
160
+ error: Optional[Exception] = None,
161
+ ) -> None:
162
+ try:
163
+ span["end_time"] = time.time()
164
+ span["duration_ms"] = round((span["end_time"] - span["start_time"]) * 1000, 2)
165
+ if error is not None:
166
+ span["error"] = str(error)
167
+ else:
168
+ span["outputs"] = result
169
+ save_token_usage(result, span)
170
+ finally:
171
+ active_span.reset(reset_token)
172
+
173
+ in_val = span.get("inputs")
174
+ out_val = span.get("outputs")
175
+ meta = dict(span.get("metadata") or {})
176
+ addr = span.get("agent_address")
177
+ if addr:
178
+ meta["agent_address"] = addr
179
+ meta["agent_name"] = span.get("agent_name", "assistant")
180
+ meta["is_agent_root"] = span.get("is_agent_root", False)
181
+
182
+ payload = {
183
+ "id": span["span_id"],
184
+ "trace_id": span["trace_id"],
185
+ "span_id": span["span_id"],
186
+ "parent_id": span.get("parent_id"),
187
+ "name": span.get("name") or "agent_execution",
188
+ "run_type": span.get("run_type", "agent"),
189
+ "status": "error" if span.get("error") else "success",
190
+ "model": meta.get("model", "agent-tool"),
191
+ "input": json.dumps(in_val, default=str) if isinstance(in_val, (dict, list)) else str(in_val or ""),
192
+ "output": json.dumps(out_val, default=str) if isinstance(out_val, (dict, list)) else str(out_val or ""),
193
+ "error": str(span["error"]) if span.get("error") else None,
194
+ "latency_ms": span.get("duration_ms", 0.0),
195
+ "prompt_tokens": span.get("tokens", {}).get("input", 0),
196
+ "completion_tokens": span.get("tokens", {}).get("output", 0),
197
+ "total_tokens": span.get("tokens", {}).get("total", 0),
198
+ "cost_usd": span.get("cost", 0.0),
199
+ "agent_address": addr,
200
+ "agent_name": meta["agent_name"],
201
+ "is_agent_root": meta["is_agent_root"],
202
+ "metadata": meta,
203
+ }
204
+ _dispatch_span(payload)
205
+
206
+
207
+ def traceable(
208
+ name: Optional[Any] = None,
209
+ run_type: str = "chain",
210
+ metadata: Optional[Dict[str, Any]] = None,
211
+ agent_address: Optional[str] = None,
212
+ ) -> Callable:
213
+ def decorator(func: Callable) -> Callable:
214
+ fn_name = (name if isinstance(name, str) else None) or func.__name__
215
+
216
+ if inspect.iscoroutinefunction(func):
217
+ @functools.wraps(func)
218
+ async def async_wrapper(*args, **kwargs):
219
+ span, token = start_span(fn_name, run_type, metadata, args, kwargs, agent_address)
220
+ result, error = None, None
221
+ try:
222
+ result = await func(*args, **kwargs)
223
+ return result
224
+ except Exception as e:
225
+ error = e
226
+ raise
227
+ finally:
228
+ end_span(span, token, result=result, error=error)
229
+ return async_wrapper
230
+
231
+ @functools.wraps(func)
232
+ def sync_wrapper(*args, **kwargs):
233
+ span, token = start_span(fn_name, run_type, metadata, args, kwargs, agent_address)
234
+ result, error = None, None
235
+ try:
236
+ result = func(*args, **kwargs)
237
+ return result
238
+ except Exception as e:
239
+ error = e
240
+ raise
241
+ finally:
242
+ end_span(span, token, result=result, error=error)
243
+ return sync_wrapper
244
+
245
+ if callable(name):
246
+ func = name
247
+ name = None
248
+ return decorator(func)
249
+
250
+ return decorator
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: asi-telemetry
3
+ Version: 0.2.0
4
+ Summary: Observability and Distributed Tracing SDK for AI Agents and LLMs
5
+ Author: Devrithm Labs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Devrithm-Labs/ASI-Telemetry
8
+ Project-URL: Repository, https://github.com/Devrithm-Labs/ASI-Telemetry
9
+ Project-URL: Bug Tracker, https://github.com/Devrithm-Labs/ASI-Telemetry/issues
10
+ Keywords: telemetry,tracing,observability,ai-agents,llm,metrics,gemini,openai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # ASI Telemetry SDK
28
+
29
+ The official lightweight observability, runtime metrics, and distributed tracing SDK for AI agents, LLMs (Google Gemini, OpenAI, ASI-1, DeepSeek), and tool execution pipelines.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install asi-telemetry
35
+ ```
36
+
37
+ Zero external dependencies required (built using standard Python libraries).
38
+
39
+
40
+ ## Architecture & Features
41
+
42
+ - **Agent & Tool Performance Tracking (`MetricsTracker`)**:
43
+ - Measures execution latency, request counts, and success/error status.
44
+ - Asynchronously dispatches metrics to the ASI Telemetry backend and ClickHouse.
45
+ - Zero performance overhead; non-blocking background workers.
46
+ - Clean real-time terminal telemetry reporting.
47
+
48
+ - **Distributed LLM Tracing (`@traceable`)**:
49
+ - Trace multi-hop agent swarms and workflow chains with distributed context propagation (`trace_id`, `span_id`, `parent_id`).
50
+ - Supports both synchronous and asynchronous coroutine functions.
51
+ - Native auto-instrumentation for Google Gemini (`instrument_gemini`) and OpenAI/ASI (`instrument_openai` / `instrument_client`).
52
+ - Real-time token usage calculation and cost estimation per model.
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### 1. Monitoring Agent Tools & Functions
59
+
60
+ ```python
61
+ from asi_telemetry import MetricsTracker
62
+
63
+ tracker = MetricsTracker(agent_name="assistant")
64
+
65
+ @tracker.track(tool_name="weather_tool")
66
+ async def get_weather(city: str):
67
+ return f"Weather in {city}: 72°F Sunny"
68
+ ```
69
+
70
+ *(Note: `AgentMonitor` is retained as a backward-compatible alias for `MetricsTracker`)*
71
+
72
+ ### 2. Tracing LLMs and Multi-Agent Workflows
73
+
74
+ ```python
75
+ from asi_telemetry import traceable, instrument_gemini
76
+ from google import genai
77
+
78
+ client = genai.Client(api_key="YOUR_API_KEY")
79
+ instrument_gemini(client)
80
+
81
+ @traceable(name="generate_summary", run_type="llm")
82
+ def summarize(text: str):
83
+ response = client.models.generate_content(
84
+ model="gemini-2.5-flash",
85
+ contents=f"Summarize this: {text}"
86
+ )
87
+ return response.text
88
+ ```
89
+
90
+ ### 3. Distributed Context Propagation Across Swarms
91
+
92
+ ```python
93
+ from asi_telemetry import traceable, create_trace_context
94
+
95
+ @traceable(name="orchestrator", run_type="chain")
96
+ async def orchestrate():
97
+ # Generate trace context to pass across network boundaries
98
+ context = create_trace_context()
99
+ # Pass context={"trace_id": ..., "parent_id": ...} to sub-agents
100
+ ```
101
+
102
+ ### 4. Direct Cost Estimation
103
+
104
+ ```python
105
+ from asi_telemetry import calculate_cost
106
+
107
+ # Approximate cost in USD for a model call
108
+ cost = calculate_cost(model="gemini-3.8-flash", input_tokens=1200, output_tokens=350)
109
+ print(f"Cost: ${cost}")
110
+ ```
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ src/asi_telemetry/__init__.py
6
+ src/asi_telemetry/instrumentation.py
7
+ src/asi_telemetry/metrics.py
8
+ src/asi_telemetry/models.py
9
+ src/asi_telemetry/tracer.py
10
+ src/asi_telemetry.egg-info/PKG-INFO
11
+ src/asi_telemetry.egg-info/SOURCES.txt
12
+ src/asi_telemetry.egg-info/dependency_links.txt
13
+ src/asi_telemetry.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ asi_telemetry