observo-ai 0.1.2__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.
- observo_ai-0.1.2/PKG-INFO +33 -0
- observo_ai-0.1.2/observo/__init__.py +239 -0
- observo_ai-0.1.2/observo/_buffer.py +57 -0
- observo_ai-0.1.2/observo/_state.py +19 -0
- observo_ai-0.1.2/observo/auto.py +119 -0
- observo_ai-0.1.2/observo/cli.py +61 -0
- observo_ai-0.1.2/observo/client.py +203 -0
- observo_ai-0.1.2/observo/context.py +151 -0
- observo_ai-0.1.2/observo/decorators.py +394 -0
- observo_ai-0.1.2/observo/types.py +72 -0
- observo_ai-0.1.2/observo_ai.egg-info/PKG-INFO +33 -0
- observo_ai-0.1.2/observo_ai.egg-info/SOURCES.txt +17 -0
- observo_ai-0.1.2/observo_ai.egg-info/dependency_links.txt +1 -0
- observo_ai-0.1.2/observo_ai.egg-info/entry_points.txt +2 -0
- observo_ai-0.1.2/observo_ai.egg-info/requires.txt +12 -0
- observo_ai-0.1.2/observo_ai.egg-info/top_level.txt +1 -0
- observo_ai-0.1.2/pyproject.toml +68 -0
- observo_ai-0.1.2/setup.cfg +4 -0
- observo_ai-0.1.2/tests/test_sdk.py +291 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: observo-ai
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for the Observo AI Observability & Evaluation Platform
|
|
5
|
+
Author-email: Observo <sdk@observo.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://observo.dev
|
|
8
|
+
Project-URL: Documentation, https://docs.observo.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/yourusername/observo-platform
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/observo-platform/issues
|
|
11
|
+
Keywords: llm,observability,ai,monitoring,evaluation,tracing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
31
|
+
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observo SDK ā Public API
|
|
3
|
+
|
|
4
|
+
Install: pip install observo-ai
|
|
5
|
+
Docs: https://docs.observo.dev
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import observo
|
|
9
|
+
|
|
10
|
+
# 1. Initialize once (e.g. in app startup)
|
|
11
|
+
observo.init(api_key="obs_live_...", environment="production")
|
|
12
|
+
|
|
13
|
+
# 2a. Decorator ā automatic tracing (zero code change)
|
|
14
|
+
@observo.trace
|
|
15
|
+
def ask_llm(prompt: str) -> str:
|
|
16
|
+
return my_llm.generate(prompt)
|
|
17
|
+
|
|
18
|
+
# 2b. Decorator with metadata
|
|
19
|
+
@observo.trace(model="gpt-4o", provider="openai")
|
|
20
|
+
async def ask_gpt(prompt: str) -> str:
|
|
21
|
+
return await openai_client.chat(prompt)
|
|
22
|
+
|
|
23
|
+
# 2c. Context manager ā manual control
|
|
24
|
+
with observo.span("rag_pipeline") as span:
|
|
25
|
+
context = retrieve_docs(question)
|
|
26
|
+
answer = llm.generate(question)
|
|
27
|
+
span.set_input(question)
|
|
28
|
+
span.set_output(answer)
|
|
29
|
+
span.set_ground_truth("Expected answer") # optional
|
|
30
|
+
span.set_metadata(model="gemini-1.5-flash")
|
|
31
|
+
|
|
32
|
+
# 2d. Manual logging
|
|
33
|
+
observo.log(
|
|
34
|
+
input="What is Python?",
|
|
35
|
+
output="Python is a programming language.",
|
|
36
|
+
model="gpt-4o",
|
|
37
|
+
provider="openai",
|
|
38
|
+
latency_ms=320,
|
|
39
|
+
)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import atexit
|
|
45
|
+
import warnings
|
|
46
|
+
from typing import Any, Optional
|
|
47
|
+
|
|
48
|
+
from . import _state
|
|
49
|
+
from .client import ObservoClient
|
|
50
|
+
from .context import ObservoSpan
|
|
51
|
+
from .decorators import trace
|
|
52
|
+
from .types import ObservoConfig, TraceData
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
__all__ = ["init", "trace", "span", "log", "flush", "shutdown", "__version__"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def init(
|
|
59
|
+
api_key: Optional[str] = None,
|
|
60
|
+
*,
|
|
61
|
+
api_url: Optional[str] = None,
|
|
62
|
+
environment: Optional[str] = None,
|
|
63
|
+
flush_interval_sec: float = 1.0,
|
|
64
|
+
max_buffer_size: int = 1000,
|
|
65
|
+
max_batch_size: int = 100,
|
|
66
|
+
timeout_sec: float = 5.0,
|
|
67
|
+
max_retries: int = 3,
|
|
68
|
+
debug: bool = False,
|
|
69
|
+
auto_patch: bool = True,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Initialize the Observo SDK.
|
|
73
|
+
Call this once at application startup before any @trace decorators fire.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
api_key: Your Observo API key (starts with obs_live_...)
|
|
77
|
+
api_url: Observo API base URL (default: localhost for dev)
|
|
78
|
+
environment: Deployment environment tag (production/staging/development)
|
|
79
|
+
flush_interval_sec: How often to batch-send traces (default: 1s)
|
|
80
|
+
max_buffer_size: Max traces to hold in memory if API is down (default: 1000)
|
|
81
|
+
max_batch_size: Max traces per HTTP request (default: 100)
|
|
82
|
+
timeout_sec: HTTP request timeout (default: 5s)
|
|
83
|
+
max_retries: Retry attempts on failure (default: 3)
|
|
84
|
+
debug: Log SDK internals to stdout (default: False)
|
|
85
|
+
auto_patch: Enable auto-instrumentation (default: True)
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
observo.init(
|
|
89
|
+
api_key="obs_live_ab12_xxxxxxxxxxxxx",
|
|
90
|
+
api_url="https://observo-demo.onrender.com",
|
|
91
|
+
environment="production",
|
|
92
|
+
auto_patch=True, # New: Zero-config auto instrumentation
|
|
93
|
+
)
|
|
94
|
+
"""
|
|
95
|
+
import os
|
|
96
|
+
|
|
97
|
+
# 1. Resolve configuration from args or environment
|
|
98
|
+
resolved_api_key = api_key or os.environ.get("OBSERVO_API_KEY")
|
|
99
|
+
env_url = os.environ.get("OBSERVO_API_URL", "http://localhost:8080")
|
|
100
|
+
resolved_api_url = api_url or env_url
|
|
101
|
+
|
|
102
|
+
# Strip trailing /v1 if present to prevent double-prefixing in client.py
|
|
103
|
+
resolved_api_url = resolved_api_url.rstrip("/")
|
|
104
|
+
if resolved_api_url.endswith("/v1"):
|
|
105
|
+
resolved_api_url = resolved_api_url[:-3]
|
|
106
|
+
|
|
107
|
+
resolved_env = environment or os.environ.get("OBSERVO_ENV", "production")
|
|
108
|
+
|
|
109
|
+
if not resolved_api_key:
|
|
110
|
+
raise ValueError("observo.init() requires an api_key. Provide it as an argument or via OBSERVO_API_KEY environment variable.")
|
|
111
|
+
|
|
112
|
+
if not resolved_api_key.startswith("obs_"):
|
|
113
|
+
warnings.warn(
|
|
114
|
+
"API key does not start with 'obs_' ā double-check your key.",
|
|
115
|
+
stacklevel=2,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
config = ObservoConfig(
|
|
119
|
+
api_key=resolved_api_key,
|
|
120
|
+
api_url=resolved_api_url.rstrip("/"),
|
|
121
|
+
environment=resolved_env,
|
|
122
|
+
flush_interval_sec=flush_interval_sec,
|
|
123
|
+
max_buffer_size=max_buffer_size,
|
|
124
|
+
max_batch_size=max_batch_size,
|
|
125
|
+
timeout_sec=timeout_sec,
|
|
126
|
+
max_retries=max_retries,
|
|
127
|
+
debug=debug,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Shutdown existing client if re-initializing
|
|
131
|
+
if _state.client is not None:
|
|
132
|
+
_state.client.shutdown()
|
|
133
|
+
|
|
134
|
+
_state.client = ObservoClient(config)
|
|
135
|
+
|
|
136
|
+
# Register automatic shutdown on process exit
|
|
137
|
+
atexit.register(shutdown)
|
|
138
|
+
|
|
139
|
+
# āā Zero-Config Auto-Instrumentation āā
|
|
140
|
+
if auto_patch:
|
|
141
|
+
try:
|
|
142
|
+
from . import auto
|
|
143
|
+
auto.install()
|
|
144
|
+
except Exception as e:
|
|
145
|
+
if debug:
|
|
146
|
+
print(f"ā ļø Observo: Auto-instrumentation failed: {e}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def span(name: str = "llm_call") -> ObservoSpan:
|
|
150
|
+
"""
|
|
151
|
+
Create a trace span for manual instrumentation.
|
|
152
|
+
|
|
153
|
+
Usage (sync):
|
|
154
|
+
with observo.span("rag_pipeline") as span:
|
|
155
|
+
answer = llm.generate(question)
|
|
156
|
+
span.set_input(question)
|
|
157
|
+
span.set_output(answer)
|
|
158
|
+
|
|
159
|
+
Usage (async):
|
|
160
|
+
async with observo.span("async_pipeline") as span:
|
|
161
|
+
answer = await llm.generate(question)
|
|
162
|
+
span.set_input(question).set_output(answer)
|
|
163
|
+
"""
|
|
164
|
+
return ObservoSpan(name)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def log(
|
|
168
|
+
input: str,
|
|
169
|
+
output: str,
|
|
170
|
+
*,
|
|
171
|
+
model: Optional[str] = None,
|
|
172
|
+
provider: Optional[str] = None,
|
|
173
|
+
environment: Optional[str] = None,
|
|
174
|
+
latency_ms: Optional[int] = None,
|
|
175
|
+
input_tokens: Optional[int] = None,
|
|
176
|
+
output_tokens: Optional[int] = None,
|
|
177
|
+
ground_truth: Optional[str] = None,
|
|
178
|
+
**metadata: Any,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Manually log a single LLM trace.
|
|
182
|
+
Use when you want full control without decorators or context managers.
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
observo.log(
|
|
186
|
+
input="Translate to French: Hello",
|
|
187
|
+
output="Bonjour",
|
|
188
|
+
model="gpt-4o",
|
|
189
|
+
provider="openai",
|
|
190
|
+
latency_ms=280,
|
|
191
|
+
ground_truth="Bonjour",
|
|
192
|
+
)
|
|
193
|
+
"""
|
|
194
|
+
if _state.client is None:
|
|
195
|
+
warnings.warn(
|
|
196
|
+
"observo.log() called before observo.init() ā trace dropped.",
|
|
197
|
+
stacklevel=2,
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
trace_data = TraceData(
|
|
203
|
+
input=input,
|
|
204
|
+
output=output,
|
|
205
|
+
model=model,
|
|
206
|
+
provider=provider,
|
|
207
|
+
environment=environment or _state.client._config.environment,
|
|
208
|
+
latency_ms=latency_ms,
|
|
209
|
+
input_tokens=input_tokens,
|
|
210
|
+
output_tokens=output_tokens,
|
|
211
|
+
ground_truth=ground_truth,
|
|
212
|
+
metadata=dict(metadata),
|
|
213
|
+
)
|
|
214
|
+
_state.client.enqueue(trace_data)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass # Never crash the caller
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def flush() -> None:
|
|
220
|
+
"""
|
|
221
|
+
Force an immediate flush of all buffered traces.
|
|
222
|
+
Useful in tests or before shutting down your application.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
observo.flush() # ensure all traces are sent before exit
|
|
226
|
+
"""
|
|
227
|
+
if _state.client:
|
|
228
|
+
_state.client.flush()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def shutdown() -> None:
|
|
232
|
+
"""
|
|
233
|
+
Gracefully shut down the SDK.
|
|
234
|
+
Flushes remaining traces and stops the background thread.
|
|
235
|
+
Called automatically on process exit via atexit.
|
|
236
|
+
"""
|
|
237
|
+
if _state.client:
|
|
238
|
+
_state.client.shutdown()
|
|
239
|
+
_state.client = None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observo SDK ā In-Memory Trace Buffer
|
|
3
|
+
Thread-safe queue that holds traces when the API is temporarily unreachable.
|
|
4
|
+
|
|
5
|
+
NFR: Durability ā traces are never lost due to temporary API outages.
|
|
6
|
+
The buffer holds up to max_buffer_size traces in memory.
|
|
7
|
+
When the API recovers, the background flusher drains the buffer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from collections import deque
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
from .types import TraceData
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TraceBuffer:
|
|
20
|
+
"""
|
|
21
|
+
Thread-safe FIFO buffer for traces.
|
|
22
|
+
|
|
23
|
+
- Acts as a safety net when the Observo API is temporarily unreachable
|
|
24
|
+
- maxlen ensures memory usage is bounded (oldest traces dropped if full)
|
|
25
|
+
- Lock ensures thread safety across the decorator thread + flush thread
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, max_size: int = 1000) -> None:
|
|
29
|
+
self._buffer: deque[TraceData] = deque(maxlen=max_size)
|
|
30
|
+
self._lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
def add(self, trace: TraceData) -> None:
|
|
33
|
+
"""Add a trace to the buffer. Thread-safe."""
|
|
34
|
+
with self._lock:
|
|
35
|
+
self._buffer.append(trace)
|
|
36
|
+
|
|
37
|
+
def drain(self, batch_size: int = 100) -> List[TraceData]:
|
|
38
|
+
"""
|
|
39
|
+
Remove and return up to batch_size traces from the front.
|
|
40
|
+
Thread-safe. Returns empty list if buffer is empty.
|
|
41
|
+
"""
|
|
42
|
+
with self._lock:
|
|
43
|
+
batch: List[TraceData] = []
|
|
44
|
+
for _ in range(min(batch_size, len(self._buffer))):
|
|
45
|
+
batch.append(self._buffer.popleft())
|
|
46
|
+
return batch
|
|
47
|
+
|
|
48
|
+
def size(self) -> int:
|
|
49
|
+
"""Current number of traces in the buffer."""
|
|
50
|
+
return len(self._buffer)
|
|
51
|
+
|
|
52
|
+
def is_empty(self) -> bool:
|
|
53
|
+
return len(self._buffer) == 0
|
|
54
|
+
|
|
55
|
+
def clear(self) -> None:
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._buffer.clear()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observo SDK ā Global State
|
|
3
|
+
Holds the singleton client instance created by observo.init().
|
|
4
|
+
Kept in a separate module to avoid circular imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import ObservoClient
|
|
12
|
+
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
|
|
15
|
+
# The singleton client ā None until observo.init() is called
|
|
16
|
+
client: "ObservoClient | None" = None
|
|
17
|
+
|
|
18
|
+
# Context variable to prevent double-logging during auto-instrumentation
|
|
19
|
+
is_tracing: ContextVar[bool] = ContextVar("is_tracing", default=False)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
import functools
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
# Set up a silent logger for auto-instrumentation
|
|
9
|
+
logger = logging.getLogger("observo.auto")
|
|
10
|
+
|
|
11
|
+
# Guard flag to prevent duplicate install() runs
|
|
12
|
+
_installed = False
|
|
13
|
+
_message_printed = False
|
|
14
|
+
|
|
15
|
+
def _get_dashboard_url():
|
|
16
|
+
"""Returns the local or production dashboard URL."""
|
|
17
|
+
base = os.environ.get("OBSERVO_DASHBOARD_URL", "http://localhost:3000")
|
|
18
|
+
project_id = os.environ.get("OBSERVO_PROJECT_ID", "default")
|
|
19
|
+
return f"{base}/traces"
|
|
20
|
+
|
|
21
|
+
def _print_wow_message():
|
|
22
|
+
"""Prints a single, clean startup banner to the developer's console."""
|
|
23
|
+
global _message_printed
|
|
24
|
+
if _message_printed:
|
|
25
|
+
return
|
|
26
|
+
_message_printed = True
|
|
27
|
+
|
|
28
|
+
url = _get_dashboard_url()
|
|
29
|
+
PURPLE = "\033[95m"
|
|
30
|
+
CYAN = "\033[96m"
|
|
31
|
+
BOLD = "\033[1m"
|
|
32
|
+
DIM = "\033[2m"
|
|
33
|
+
END = "\033[0m"
|
|
34
|
+
|
|
35
|
+
print(f"\n{PURPLE}{BOLD}š Observo:{END} {CYAN}Auto-tracing active.{END}")
|
|
36
|
+
print(f" {DIM}Dashboard:{END} {BOLD}{url}{END}\n")
|
|
37
|
+
|
|
38
|
+
def _wrap_llm_method(original_method: Callable, provider: str) -> Callable:
|
|
39
|
+
"""
|
|
40
|
+
Wraps an LLM call method (like create) to automatically trace it.
|
|
41
|
+
"""
|
|
42
|
+
from .decorators import trace
|
|
43
|
+
|
|
44
|
+
# Pre-configure the trace with provider info
|
|
45
|
+
return trace(provider=provider)(original_method)
|
|
46
|
+
|
|
47
|
+
def patch_openai():
|
|
48
|
+
"""Detects and patches OpenAI client methods."""
|
|
49
|
+
try:
|
|
50
|
+
import openai
|
|
51
|
+
# Modern OpenAI SDK (v1+)
|
|
52
|
+
try:
|
|
53
|
+
from openai.resources.chat.completions import Completions, AsyncCompletions
|
|
54
|
+
|
|
55
|
+
# Patch Sync
|
|
56
|
+
if not hasattr(Completions.create, "__observo_patched__"):
|
|
57
|
+
original = Completions.create
|
|
58
|
+
Completions.create = _wrap_llm_method(original, "OpenAI")
|
|
59
|
+
Completions.create.__observo_patched__ = True
|
|
60
|
+
|
|
61
|
+
# Patch Async
|
|
62
|
+
if not hasattr(AsyncCompletions.create, "__observo_patched__"):
|
|
63
|
+
original = AsyncCompletions.create
|
|
64
|
+
AsyncCompletions.create = _wrap_llm_method(original, "OpenAI")
|
|
65
|
+
AsyncCompletions.create.__observo_patched__ = True
|
|
66
|
+
|
|
67
|
+
except (ImportError, AttributeError):
|
|
68
|
+
pass # Likely an older version of OpenAI or different structure
|
|
69
|
+
|
|
70
|
+
except ImportError:
|
|
71
|
+
pass # OpenAI not used
|
|
72
|
+
|
|
73
|
+
def patch_groq():
|
|
74
|
+
"""Detects and patches Groq client methods."""
|
|
75
|
+
try:
|
|
76
|
+
import groq
|
|
77
|
+
from groq.resources.chat.completions import Completions, AsyncCompletions
|
|
78
|
+
|
|
79
|
+
if not hasattr(Completions.create, "__observo_patched__"):
|
|
80
|
+
original = Completions.create
|
|
81
|
+
Completions.create = _wrap_llm_method(original, "Groq")
|
|
82
|
+
Completions.create.__observo_patched__ = True
|
|
83
|
+
|
|
84
|
+
if not hasattr(AsyncCompletions.create, "__observo_patched__"):
|
|
85
|
+
original = AsyncCompletions.create
|
|
86
|
+
AsyncCompletions.create = _wrap_llm_method(original, "Groq")
|
|
87
|
+
AsyncCompletions.create.__observo_patched__ = True
|
|
88
|
+
|
|
89
|
+
except ImportError:
|
|
90
|
+
pass # Groq not used
|
|
91
|
+
|
|
92
|
+
def install():
|
|
93
|
+
"""Entry point to start auto-instrumentation. Fail-safe. Only runs once."""
|
|
94
|
+
global _installed
|
|
95
|
+
if _installed:
|
|
96
|
+
return
|
|
97
|
+
_installed = True
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# 1. Initialize the Client (Background Sender) if not already done
|
|
101
|
+
from . import _state, init
|
|
102
|
+
if _state.client is None:
|
|
103
|
+
init(debug=os.environ.get("OBSERVO_DEBUG") == "true", auto_patch=False)
|
|
104
|
+
|
|
105
|
+
# 2. Apply patches
|
|
106
|
+
patched = []
|
|
107
|
+
patch_openai()
|
|
108
|
+
patch_groq()
|
|
109
|
+
|
|
110
|
+
# 3. Print the startup banner (once)
|
|
111
|
+
if os.environ.get("OBSERVO_SILENT") != "true":
|
|
112
|
+
_print_wow_message()
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
# Fail silently to ensure the user's app never crashes
|
|
116
|
+
logger.warning(f"Observo: Auto-instrumentation failed (silent): {e}")
|
|
117
|
+
|
|
118
|
+
# If imported via 'import observo.auto', it runs immediately
|
|
119
|
+
install()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import runpy
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="observo-run",
|
|
9
|
+
description="Observo Zero-Touch Instrumentation: Run your AI app with automatic tracing."
|
|
10
|
+
)
|
|
11
|
+
parser.add_argument("script", help="The python script to run (e.g., main.py)")
|
|
12
|
+
parser.add_argument("args", nargs=argparse.REMAINDER, help="Arguments for your script")
|
|
13
|
+
|
|
14
|
+
if len(sys.argv) < 2:
|
|
15
|
+
parser.print_help()
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
# 1. Activate Auto-Instrumentation
|
|
21
|
+
try:
|
|
22
|
+
try:
|
|
23
|
+
import dotenv
|
|
24
|
+
dotenv.load_dotenv()
|
|
25
|
+
except ImportError:
|
|
26
|
+
pass # python-dotenv not installed, proceed without it
|
|
27
|
+
|
|
28
|
+
import observo.auto
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"ā ļø [Observo] Auto-instrumentation failed to start: {e}")
|
|
31
|
+
|
|
32
|
+
# 2. Prepare the environment for the user's script
|
|
33
|
+
script_path = args.script
|
|
34
|
+
|
|
35
|
+
# Handle redundant 'python' argument if user still types it
|
|
36
|
+
if (script_path == "python" or script_path == "python3") and args.args:
|
|
37
|
+
script_path = args.args[0]
|
|
38
|
+
args.args = args.args[1:]
|
|
39
|
+
|
|
40
|
+
if not os.path.exists(script_path):
|
|
41
|
+
print(f"ā [Observo] Script not found: {script_path}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
# Update sys.argv so the user's script thinks it was run normally
|
|
45
|
+
sys.argv = [script_path] + args.args
|
|
46
|
+
|
|
47
|
+
# 3. RUN the user's script
|
|
48
|
+
print(f"⨠[Observo] Running {script_path}...")
|
|
49
|
+
print("ā" * 40)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# run_path executes the script as if it were the main module
|
|
53
|
+
runpy.run_path(script_path, run_name="__main__")
|
|
54
|
+
except SystemExit as e:
|
|
55
|
+
sys.exit(e.code)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"\nā [Observo] App crashed during execution:")
|
|
58
|
+
raise e
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|