magic-lab-observer 0.1.1__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,68 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+ *~
34
+
35
+ # Testing
36
+ .coverage
37
+ htmlcov/
38
+ .pytest_cache/
39
+ .mypy_cache/
40
+
41
+ # Node.js
42
+ node_modules/
43
+ npm-debug.log*
44
+ yarn-debug.log*
45
+ yarn-error.log*
46
+
47
+ # Build
48
+ dist/
49
+ build/
50
+ *.tsbuildinfo
51
+
52
+ # Environment
53
+ .env
54
+ .env.local
55
+ .env.*.local
56
+
57
+ # Podman
58
+ podman-compose.override.yml
59
+
60
+ # Database
61
+ *.db
62
+ *.sqlite3
63
+ .observer-state/
64
+
65
+ # OS
66
+ .DS_Store
67
+ Thumbs.db
68
+
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: magic-lab-observer
3
+ Version: 0.1.1
4
+ Summary: Python SDK for LLM Observatory - Open-source LLM observability
5
+ Project-URL: Homepage, https://github.com/Magic-Lab-Studio/observer
6
+ Project-URL: Repository, https://github.com/Magic-Lab-Studio/observer.git
7
+ Project-URL: Issues, https://github.com/Magic-Lab-Studio/observer/issues
8
+ Author: LLM Observatory Contributors
9
+ License-Expression: Apache-2.0
10
+ Keywords: anthropic,llm,observability,openai,tracing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27.0
20
+ Provides-Extra: all
21
+ Requires-Dist: anthropic>=0.20.0; extra == 'all'
22
+ Requires-Dist: langchain-core>=0.2.0; extra == 'all'
23
+ Requires-Dist: openai>=1.0.0; extra == 'all'
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.10; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Provides-Extra: langchain
32
+ Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
33
+ Provides-Extra: openai
34
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # LLM Observatory Python SDK
38
+
39
+ Open-source LLM observability SDK for tracing and monitoring AI applications.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install magic-lab-observer
45
+ ```
46
+
47
+ With optional integrations:
48
+
49
+ ```bash
50
+ pip install magic-lab-observer[openai,anthropic,langchain]
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ The distribution name is `magic-lab-observer`; the stable Python import remains
56
+ `llm_observatory`.
57
+
58
+ ```python
59
+ from llm_observatory import instrument, trace
60
+
61
+ # Auto-instrument LLM libraries
62
+ instrument(openai=True, anthropic=True)
63
+
64
+ # Or manually trace functions
65
+ @trace(name="summarize")
66
+ def summarize(text: str):
67
+ return openai.chat.completions.create(
68
+ model="gpt-4",
69
+ messages=[{"role": "user", "content": text}]
70
+ )
71
+ ```
72
+
73
+ ## License
74
+
75
+ Apache License 2.0
@@ -0,0 +1,39 @@
1
+ # LLM Observatory Python SDK
2
+
3
+ Open-source LLM observability SDK for tracing and monitoring AI applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install magic-lab-observer
9
+ ```
10
+
11
+ With optional integrations:
12
+
13
+ ```bash
14
+ pip install magic-lab-observer[openai,anthropic,langchain]
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ The distribution name is `magic-lab-observer`; the stable Python import remains
20
+ `llm_observatory`.
21
+
22
+ ```python
23
+ from llm_observatory import instrument, trace
24
+
25
+ # Auto-instrument LLM libraries
26
+ instrument(openai=True, anthropic=True)
27
+
28
+ # Or manually trace functions
29
+ @trace(name="summarize")
30
+ def summarize(text: str):
31
+ return openai.chat.completions.create(
32
+ model="gpt-4",
33
+ messages=[{"role": "user", "content": text}]
34
+ )
35
+ ```
36
+
37
+ ## License
38
+
39
+ Apache License 2.0
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "magic-lab-observer"
7
+ version = "0.1.1"
8
+ description = "Python SDK for LLM Observatory - Open-source LLM observability"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "LLM Observatory Contributors" }
14
+ ]
15
+ keywords = ["llm", "observability", "tracing", "openai", "anthropic"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+
26
+ dependencies = [
27
+ "httpx>=0.27.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Magic-Lab-Studio/observer"
32
+ Repository = "https://github.com/Magic-Lab-Studio/observer.git"
33
+ Issues = "https://github.com/Magic-Lab-Studio/observer/issues"
34
+
35
+ [project.optional-dependencies]
36
+ openai = ["openai>=1.0.0"]
37
+ anthropic = ["anthropic>=0.20.0"]
38
+ langchain = ["langchain-core>=0.2.0"]
39
+ all = [
40
+ "openai>=1.0.0",
41
+ "anthropic>=0.20.0",
42
+ "langchain-core>=0.2.0",
43
+ ]
44
+ dev = [
45
+ "pytest>=8.0",
46
+ "pytest-cov>=5.0",
47
+ "ruff>=0.5",
48
+ "mypy>=1.10",
49
+ ]
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/llm_observatory"]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+ target-version = "py310"
57
+
58
+ [tool.ruff.lint]
59
+ select = ["E", "F", "I", "N", "W", "UP"]
60
+
61
+ [tool.mypy]
62
+ python_version = "3.10"
63
+ strict = true
@@ -0,0 +1,32 @@
1
+ """LLM Observatory Python SDK - Open-source LLM observability."""
2
+
3
+ from llm_observatory.exporters.otlp import OTLPExporter
4
+ from llm_observatory.instrumentors import instrument, uninstrument
5
+ from llm_observatory.pricing import MODEL_PRICING, calculate_cost, get_model_pricing
6
+ from llm_observatory.tracer import (
7
+ Span,
8
+ SpanStatus,
9
+ TokenUsage,
10
+ Tracer,
11
+ get_tracer,
12
+ set_tracer,
13
+ trace,
14
+ )
15
+
16
+ __version__ = "0.1.1"
17
+
18
+ __all__ = [
19
+ "Tracer",
20
+ "Span",
21
+ "SpanStatus",
22
+ "TokenUsage",
23
+ "trace",
24
+ "get_tracer",
25
+ "set_tracer",
26
+ "instrument",
27
+ "uninstrument",
28
+ "calculate_cost",
29
+ "get_model_pricing",
30
+ "MODEL_PRICING",
31
+ "OTLPExporter",
32
+ ]
@@ -0,0 +1,23 @@
1
+ """Exporters for sending traces to backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+
9
+ class BaseExporter(ABC):
10
+ """Base class for trace exporters."""
11
+
12
+ @abstractmethod
13
+ def export(self, spans: list[dict[str, Any]]) -> None:
14
+ """Export spans to the backend.
15
+
16
+ Args:
17
+ spans: List of span dictionaries to export.
18
+ """
19
+ ...
20
+
21
+ def flush(self) -> None:
22
+ """Flush any buffered spans."""
23
+ pass
@@ -0,0 +1,90 @@
1
+ """OTLP HTTP exporter for sending traces to observatory backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from llm_observatory.exporters import BaseExporter
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class OTLPExporter(BaseExporter):
16
+ """Exports traces to an OTLP-compatible backend via HTTP."""
17
+
18
+ def __init__(
19
+ self,
20
+ endpoint: str = "http://localhost:8000",
21
+ api_key: str | None = None,
22
+ timeout: float = 10.0,
23
+ max_retries: int = 3,
24
+ ) -> None:
25
+ """Initialize the exporter.
26
+
27
+ Args:
28
+ endpoint: Backend endpoint URL.
29
+ api_key: Optional API key for authentication.
30
+ timeout: Request timeout in seconds.
31
+ max_retries: Maximum number of retries on failure.
32
+ """
33
+ self.endpoint = endpoint.rstrip("/")
34
+ self.api_key = api_key
35
+ self.timeout = timeout
36
+ self.max_retries = max_retries
37
+ self._client = httpx.Client(timeout=timeout)
38
+
39
+ def export(self, spans: list[dict[str, Any]]) -> None:
40
+ """Export spans to the backend.
41
+
42
+ Args:
43
+ spans: List of span dictionaries to export.
44
+ """
45
+ headers = {"Content-Type": "application/json"}
46
+ if self.api_key:
47
+ headers["Authorization"] = f"Bearer {self.api_key}"
48
+
49
+ for attempt in range(self.max_retries):
50
+ try:
51
+ response = self._client.post(
52
+ f"{self.endpoint}/v1/traces/batch",
53
+ json={"spans": spans},
54
+ headers=headers,
55
+ )
56
+ response.raise_for_status()
57
+ logger.debug(f"Exported {len(spans)} spans successfully")
58
+ return
59
+ except httpx.HTTPStatusError as e:
60
+ logger.warning(
61
+ f"Export failed (attempt {attempt + 1}/{self.max_retries}): "
62
+ f"HTTP {e.response.status_code} - {e.response.text}"
63
+ )
64
+ if attempt == self.max_retries - 1:
65
+ msg = (
66
+ f"Export failed after {self.max_retries} attempts, "
67
+ f"dropping {len(spans)} spans"
68
+ )
69
+ logger.error(msg)
70
+ except httpx.HTTPError as e:
71
+ logger.warning(
72
+ f"Export failed (attempt {attempt + 1}/{self.max_retries}): {e}"
73
+ )
74
+ if attempt == self.max_retries - 1:
75
+ msg = (
76
+ f"Export failed after {self.max_retries} attempts, "
77
+ f"dropping {len(spans)} spans"
78
+ )
79
+ logger.error(msg)
80
+
81
+ def flush(self) -> None:
82
+ """Flush any buffered spans."""
83
+ self._client.close()
84
+
85
+ def __del__(self) -> None:
86
+ """Clean up the client."""
87
+ try:
88
+ self._client.close()
89
+ except Exception:
90
+ pass
@@ -0,0 +1,57 @@
1
+ """Auto-instrumentation for LLM libraries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ # Store instrumentor instances for uninstrument()
8
+ _active_instrumentors: list[Any] = []
9
+
10
+
11
+ def instrument(
12
+ openai: bool = False,
13
+ anthropic: bool = False,
14
+ langchain: bool = False,
15
+ **kwargs: Any,
16
+ ) -> None:
17
+ """Auto-instrument LLM libraries.
18
+
19
+ Args:
20
+ openai: Enable OpenAI instrumentation.
21
+ anthropic: Enable Anthropic instrumentation.
22
+ langchain: Enable LangChain instrumentation.
23
+ **kwargs: Additional configuration options.
24
+ """
25
+ if openai:
26
+ from llm_observatory.instrumentors.openai import OpenAIInstrumentor
27
+
28
+ inst = OpenAIInstrumentor()
29
+ inst.instrument()
30
+ _active_instrumentors.append(inst)
31
+
32
+ if anthropic:
33
+ from llm_observatory.instrumentors.anthropic import AnthropicInstrumentor
34
+
35
+ inst = AnthropicInstrumentor()
36
+ inst.instrument()
37
+ _active_instrumentors.append(inst)
38
+
39
+ if langchain:
40
+ try:
41
+ from llm_observatory.instrumentors.langchain import LangChainInstrumentor
42
+
43
+ inst = LangChainInstrumentor()
44
+ inst.instrument()
45
+ _active_instrumentors.append(inst)
46
+ except ImportError:
47
+ pass
48
+
49
+
50
+ def uninstrument() -> None:
51
+ """Uninstrument all active instrumentors."""
52
+ for inst in _active_instrumentors:
53
+ try:
54
+ inst.uninstrument()
55
+ except Exception:
56
+ pass
57
+ _active_instrumentors.clear()
@@ -0,0 +1,135 @@
1
+ """Anthropic auto-instrumentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from llm_observatory.pricing import calculate_cost
8
+ from llm_observatory.tracer import Span, SpanStatus, get_tracer
9
+
10
+
11
+ class AnthropicInstrumentor:
12
+ """Instruments Anthropic API calls to capture traces."""
13
+
14
+ def __init__(self) -> None:
15
+ self._original_sync_create: Any | None = None
16
+ self._original_async_create: Any | None = None
17
+
18
+ def instrument(self) -> None:
19
+ """Patch Anthropic to capture traces."""
20
+ try:
21
+ import anthropic
22
+
23
+ # Patch sync messages
24
+ self._original_sync_create = anthropic.resources.messages.Messages.create
25
+ anthropic.resources.messages.Messages.create = self._patched_sync_create
26
+
27
+ # Patch async messages
28
+ self._original_async_create = (
29
+ anthropic.resources.messages.AsyncMessages.create
30
+ )
31
+ anthropic.resources.messages.AsyncMessages.create = (
32
+ self._patched_async_create
33
+ )
34
+ except ImportError:
35
+ pass
36
+
37
+ def _build_span(
38
+ self, model: str, messages: list[dict[str, Any]]
39
+ ) -> tuple[Span, Any]:
40
+ """Build a span for an Anthropic call."""
41
+ tracer = get_tracer()
42
+ span = tracer.start_span(
43
+ name="anthropic.messages.create",
44
+ span_type="llm",
45
+ )
46
+ span.set_input({"model": model, "messages": messages})
47
+ span.set_attribute("gen_ai.system", "anthropic")
48
+ span.set_attribute("gen_ai.request.model", model)
49
+ return span, tracer
50
+
51
+ def _finish_span(self, span: Span, result: Any, model: str) -> None:
52
+ """Extract data from result and finish span."""
53
+ try:
54
+ # Extract token usage
55
+ if hasattr(result, "usage"):
56
+ input_tokens = getattr(result.usage, "input_tokens", 0) or 0
57
+ output_tokens = getattr(result.usage, "output_tokens", 0) or 0
58
+ span.set_token_usage(input_tokens, output_tokens)
59
+
60
+ # Calculate cost
61
+ cost = calculate_cost(model, input_tokens, output_tokens)
62
+ if cost is not None:
63
+ span.set_cost(cost)
64
+
65
+ # Extract output
66
+ if hasattr(result, "content") and result.content:
67
+ content_blocks = []
68
+ for block in result.content:
69
+ block_data = {}
70
+ if hasattr(block, "type"):
71
+ block_data["type"] = block.type
72
+ if hasattr(block, "text"):
73
+ block_data["text"] = block.text
74
+ content_blocks.append(block_data)
75
+ span.set_output({"content": content_blocks})
76
+
77
+ span.set_attribute("response_id", getattr(result, "id", None))
78
+ span.set_status(SpanStatus.OK)
79
+
80
+ except Exception as e:
81
+ span.set_status(SpanStatus.ERROR)
82
+ span.set_attribute("error.message", str(e))
83
+
84
+ finally:
85
+ span.end()
86
+
87
+ def _patched_sync_create(self, *args: Any, **kwargs: Any) -> Any:
88
+ """Patched sync create method."""
89
+ model = kwargs.get("model", "unknown")
90
+ messages = kwargs.get("messages", [])
91
+
92
+ span, _ = self._build_span(model, messages)
93
+
94
+ try:
95
+ result = self._original_sync_create(*args, **kwargs)
96
+ self._finish_span(span, result, model)
97
+ return result
98
+ except Exception as e:
99
+ span.set_status(SpanStatus.ERROR)
100
+ span.set_attribute("error.message", str(e))
101
+ span.end()
102
+ raise
103
+
104
+ async def _patched_async_create(self, *args: Any, **kwargs: Any) -> Any:
105
+ """Patched async create method."""
106
+ model = kwargs.get("model", "unknown")
107
+ messages = kwargs.get("messages", [])
108
+
109
+ span, _ = self._build_span(model, messages)
110
+
111
+ try:
112
+ result = await self._original_async_create(*args, **kwargs)
113
+ self._finish_span(span, result, model)
114
+ return result
115
+ except Exception as e:
116
+ span.set_status(SpanStatus.ERROR)
117
+ span.set_attribute("error.message", str(e))
118
+ span.end()
119
+ raise
120
+
121
+ def uninstrument(self) -> None:
122
+ """Remove instrumentation."""
123
+ try:
124
+ import anthropic
125
+
126
+ if self._original_sync_create is not None:
127
+ anthropic.resources.messages.Messages.create = (
128
+ self._original_sync_create
129
+ )
130
+ if self._original_async_create is not None:
131
+ anthropic.resources.messages.AsyncMessages.create = (
132
+ self._original_async_create
133
+ )
134
+ except ImportError:
135
+ pass