pisama-auto 0.1.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,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ pip install -e ".[dev]"
27
+
28
+ - name: Lint
29
+ run: ruff check src/
30
+
31
+ - name: Test
32
+ run: pytest tests/ -q --tb=short
@@ -0,0 +1,114 @@
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
+ env/
28
+
29
+ # IDE
30
+ .idea/
31
+ .vscode/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # Testing
37
+ .pytest_cache/
38
+ .coverage
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ backend/backend/
43
+
44
+ # Generated test results and runtime data
45
+ detector_test_results_*.json
46
+ backend/data/fix_feedback.jsonl
47
+ backend/data/progress_log.jsonl
48
+ backend/data/mast_test_results*.json
49
+ backend/data/otel_test_results.json
50
+ backend/data/calibration_history.jsonl
51
+ backend/data/calibration_report.json
52
+ backend/data/baselines/
53
+ backend/app/data/
54
+
55
+ # Playwright
56
+ frontend/tests/auth/storage-state.json
57
+ frontend/test-results/
58
+ frontend/playwright-report/
59
+ frontend/playwright/.cache/
60
+
61
+ # Environment
62
+ .env
63
+ .env.*
64
+ !.env.example
65
+ !.env.test
66
+
67
+ # Vercel
68
+ .vercel
69
+
70
+ # Scraped traces (large data files) - only at root level
71
+ /traces/
72
+
73
+ # macOS
74
+ .DS_Store
75
+
76
+ # Secrets - never commit
77
+ deployment-env-vars.md
78
+ frontend/.env.local
79
+
80
+ # Secrets and credentials
81
+ *.pem
82
+ *.key
83
+ *.crt
84
+ credentials.json
85
+ *.secret
86
+
87
+ # Node (frontend)
88
+ node_modules/
89
+ .next/
90
+ out/
91
+
92
+ # Logs
93
+ *.log
94
+ logs/
95
+
96
+ # Local config
97
+ *.local
98
+ models/
99
+ _archived/
100
+
101
+ # Sensitive environment files (never commit)
102
+ frontend/.env.local
103
+ frontend/.env.vercel.production
104
+ .env.local
105
+
106
+ # Confidential docs (strategy, pricing, competitive intel)
107
+ docs-internal/
108
+
109
+ # Terraform
110
+ .terraform/
111
+ *.tfstate
112
+ *.tfstate.*
113
+ .terraform.lock.hcl
114
+ backend/models/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Pisama (tn-pisama)
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,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: pisama-auto
3
+ Version: 0.1.0
4
+ Summary: Zero-code auto-instrumentation for LLM applications — adds Pisama failure detection with one line
5
+ Project-URL: Homepage, https://github.com/tn-pisama/pisama-auto
6
+ Project-URL: Documentation, https://docs.pisama.ai/auto-instrumentation
7
+ Author-email: Pisama Team <team@pisama.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,auto-instrumentation,llm,monitoring,observability,opentelemetry
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT 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: opentelemetry-api>=1.20.0
20
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
21
+ Requires-Dist: opentelemetry-sdk>=1.20.0
22
+ Requires-Dist: wrapt>=1.15.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pisama-auto
26
+
27
+ Zero-code auto-instrumentation for LLM applications. Add Pisama failure detection with one line.
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ pip install pisama-auto
33
+ ```
34
+
35
+ ```python
36
+ import pisama_auto
37
+ pisama_auto.init() # traces locally; set PISAMA_API_KEY + PISAMA_ENDPOINT to export
38
+
39
+ # All subsequent LLM calls are automatically traced
40
+ import anthropic
41
+ client = anthropic.Anthropic()
42
+ response = client.messages.create(
43
+ model="claude-sonnet-4-6",
44
+ max_tokens=1024,
45
+ messages=[{"role": "user", "content": "Hello"}],
46
+ )
47
+ # ^ This call is automatically traced (and sent to Pisama if endpoint is configured)
48
+ ```
49
+
50
+ ## Supported Libraries
51
+
52
+ | Library | Status | What's Traced |
53
+ |---------|--------|---------------|
54
+ | `anthropic` | GA | `messages.create()`, `messages.stream()` |
55
+ | `openai` | GA | `chat.completions.create()` |
56
+
57
+ ## How It Works
58
+
59
+ 1. `pisama_auto.init()` sets up an OpenTelemetry tracer that exports to Pisama
60
+ 2. It then patches supported LLM libraries to emit spans with `gen_ai.*` semantic conventions
61
+ 3. Pisama's detection engine analyzes the traces for 44 failure modes
62
+ 4. Results appear in your Pisama dashboard
63
+
64
+ ## Configuration
65
+
66
+ ```python
67
+ pisama_auto.init(
68
+ api_key="ps_...", # or set PISAMA_API_KEY env var
69
+ endpoint="https://your-instance/api/v1/traces/ingest", # or PISAMA_ENDPOINT env var
70
+ service_name="my-agent", # OTEL service name
71
+ auto_patch=True, # auto-patch all detected libraries
72
+ )
73
+ ```
74
+
75
+ Without an endpoint, traces are generated locally but not exported. Set `PISAMA_ENDPOINT` to send them to the Pisama platform.
76
+
77
+ ## Selective Patching
78
+
79
+ ```python
80
+ import pisama_auto
81
+ pisama_auto.init(auto_patch=False) # don't auto-patch
82
+
83
+ from pisama_auto.patches import patch
84
+ patch("anthropic") # only patch anthropic
85
+ ```
@@ -0,0 +1,61 @@
1
+ # pisama-auto
2
+
3
+ Zero-code auto-instrumentation for LLM applications. Add Pisama failure detection with one line.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pip install pisama-auto
9
+ ```
10
+
11
+ ```python
12
+ import pisama_auto
13
+ pisama_auto.init() # traces locally; set PISAMA_API_KEY + PISAMA_ENDPOINT to export
14
+
15
+ # All subsequent LLM calls are automatically traced
16
+ import anthropic
17
+ client = anthropic.Anthropic()
18
+ response = client.messages.create(
19
+ model="claude-sonnet-4-6",
20
+ max_tokens=1024,
21
+ messages=[{"role": "user", "content": "Hello"}],
22
+ )
23
+ # ^ This call is automatically traced (and sent to Pisama if endpoint is configured)
24
+ ```
25
+
26
+ ## Supported Libraries
27
+
28
+ | Library | Status | What's Traced |
29
+ |---------|--------|---------------|
30
+ | `anthropic` | GA | `messages.create()`, `messages.stream()` |
31
+ | `openai` | GA | `chat.completions.create()` |
32
+
33
+ ## How It Works
34
+
35
+ 1. `pisama_auto.init()` sets up an OpenTelemetry tracer that exports to Pisama
36
+ 2. It then patches supported LLM libraries to emit spans with `gen_ai.*` semantic conventions
37
+ 3. Pisama's detection engine analyzes the traces for 44 failure modes
38
+ 4. Results appear in your Pisama dashboard
39
+
40
+ ## Configuration
41
+
42
+ ```python
43
+ pisama_auto.init(
44
+ api_key="ps_...", # or set PISAMA_API_KEY env var
45
+ endpoint="https://your-instance/api/v1/traces/ingest", # or PISAMA_ENDPOINT env var
46
+ service_name="my-agent", # OTEL service name
47
+ auto_patch=True, # auto-patch all detected libraries
48
+ )
49
+ ```
50
+
51
+ Without an endpoint, traces are generated locally but not exported. Set `PISAMA_ENDPOINT` to send them to the Pisama platform.
52
+
53
+ ## Selective Patching
54
+
55
+ ```python
56
+ import pisama_auto
57
+ pisama_auto.init(auto_patch=False) # don't auto-patch
58
+
59
+ from pisama_auto.patches import patch
60
+ patch("anthropic") # only patch anthropic
61
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pisama-auto"
7
+ version = "0.1.0"
8
+ description = "Zero-code auto-instrumentation for LLM applications — adds Pisama failure detection with one line"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Pisama Team", email = "team@pisama.ai" }
14
+ ]
15
+ keywords = [
16
+ "ai", "agents", "monitoring", "auto-instrumentation",
17
+ "opentelemetry", "observability", "llm"
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ ]
28
+ dependencies = [
29
+ "opentelemetry-api>=1.20.0",
30
+ "opentelemetry-sdk>=1.20.0",
31
+ "opentelemetry-exporter-otlp-proto-http>=1.20.0",
32
+ "wrapt>=1.15.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/tn-pisama/pisama-auto"
37
+ Documentation = "https://docs.pisama.ai/auto-instrumentation"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/pisama_auto"]
@@ -0,0 +1,79 @@
1
+ """Pisama Auto-Instrumentation.
2
+
3
+ Zero-code instrumentation for LLM applications.
4
+ Automatically patches supported libraries to emit OTEL traces
5
+ that Pisama can analyze for failure detection.
6
+
7
+ Usage:
8
+ import pisama_auto
9
+ pisama_auto.init(api_key="ps_...")
10
+
11
+ # All subsequent LLM calls are automatically traced
12
+ import anthropic
13
+ client = anthropic.Anthropic()
14
+ response = client.messages.create(...) # <-- automatically traced
15
+ """
16
+
17
+ import logging
18
+ from typing import Optional
19
+
20
+ logger = logging.getLogger("pisama_auto")
21
+
22
+ _initialized = False
23
+
24
+
25
+ def init(
26
+ api_key: Optional[str] = None,
27
+ endpoint: Optional[str] = None,
28
+ service_name: str = "pisama-auto",
29
+ auto_patch: bool = True,
30
+ ) -> None:
31
+ """Initialize Pisama auto-instrumentation.
32
+
33
+ Sets up OTEL tracing and patches supported LLM libraries to automatically
34
+ emit traces that Pisama can analyze.
35
+
36
+ Args:
37
+ api_key: Pisama API key (ps_...). Also reads PISAMA_API_KEY env var.
38
+ endpoint: Pisama OTEL ingestion endpoint. Also reads PISAMA_ENDPOINT env var.
39
+ If not set, traces are generated locally but not exported.
40
+ service_name: Service name for OTEL resource.
41
+ auto_patch: If True, automatically patch all detected libraries.
42
+ """
43
+ global _initialized
44
+ if _initialized:
45
+ logger.debug("Pisama auto-instrumentation already initialized")
46
+ return
47
+
48
+ import os
49
+ api_key = api_key or os.environ.get("PISAMA_API_KEY")
50
+ endpoint = endpoint or os.environ.get("PISAMA_ENDPOINT")
51
+
52
+ if not api_key:
53
+ logger.warning(
54
+ "No Pisama API key provided. Set PISAMA_API_KEY or pass api_key to init(). "
55
+ "Traces will be generated but not exported."
56
+ )
57
+
58
+ # Set up OTEL tracer
59
+ from ._tracer import setup_tracer
60
+ tracer_kwargs = {"api_key": api_key, "service_name": service_name}
61
+ if endpoint:
62
+ tracer_kwargs["endpoint"] = endpoint
63
+ setup_tracer(**tracer_kwargs)
64
+
65
+ # Auto-patch detected libraries
66
+ if auto_patch:
67
+ from .patches import patch_all
68
+ patched = patch_all()
69
+ if patched:
70
+ logger.info(f"Pisama: auto-instrumented {', '.join(patched)}")
71
+ else:
72
+ logger.info("Pisama: initialized (no patchable libraries detected yet)")
73
+
74
+ _initialized = True
75
+
76
+
77
+ def is_initialized() -> bool:
78
+ """Check if Pisama auto-instrumentation is initialized."""
79
+ return _initialized
@@ -0,0 +1,59 @@
1
+ """OTEL tracer setup for Pisama auto-instrumentation."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from opentelemetry import trace
7
+ from opentelemetry.sdk.trace import TracerProvider
8
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
9
+ from opentelemetry.sdk.resources import Resource
10
+
11
+ logger = logging.getLogger("pisama_auto")
12
+
13
+ _tracer: Optional[trace.Tracer] = None
14
+
15
+
16
+ def setup_tracer(
17
+ api_key: Optional[str] = None,
18
+ endpoint: str = "https://pisama-api.fly.dev/api/v1/traces/ingest",
19
+ service_name: str = "pisama-auto",
20
+ ) -> trace.Tracer:
21
+ """Set up the OTEL tracer with Pisama exporter."""
22
+ global _tracer
23
+
24
+ resource = Resource.create({
25
+ "service.name": service_name,
26
+ "pisama.sdk": "pisama-auto",
27
+ "pisama.sdk.version": "0.1.0",
28
+ })
29
+
30
+ provider = TracerProvider(resource=resource)
31
+
32
+ if api_key:
33
+ try:
34
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
35
+
36
+ exporter = OTLPSpanExporter(
37
+ endpoint=endpoint,
38
+ headers={"Authorization": f"Bearer {api_key}"},
39
+ )
40
+ provider.add_span_processor(BatchSpanProcessor(exporter))
41
+ logger.debug(f"Pisama OTEL exporter configured: {endpoint}")
42
+ except ImportError:
43
+ logger.warning("OTLP exporter not available, using console exporter")
44
+ provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
45
+ else:
46
+ # No API key — still trace locally for debugging
47
+ provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
48
+
49
+ trace.set_tracer_provider(provider)
50
+ _tracer = trace.get_tracer("pisama_auto", "0.1.0")
51
+ return _tracer
52
+
53
+
54
+ def get_tracer() -> trace.Tracer:
55
+ """Get the Pisama tracer. Sets up a default if not initialized."""
56
+ global _tracer
57
+ if _tracer is None:
58
+ _tracer = trace.get_tracer("pisama_auto", "0.1.0")
59
+ return _tracer
@@ -0,0 +1,70 @@
1
+ """Auto-patching for LLM libraries.
2
+
3
+ Detects installed libraries and patches them to emit OTEL spans
4
+ with gen_ai.* semantic conventions.
5
+ """
6
+
7
+ import importlib
8
+ import logging
9
+ from typing import List
10
+
11
+ logger = logging.getLogger("pisama_auto")
12
+
13
+ # Map of library name -> patch module
14
+ _PATCHABLE = {
15
+ "anthropic": ".anthropic_patch",
16
+ "openai": ".openai_patch",
17
+ }
18
+
19
+ _patched: List[str] = []
20
+
21
+
22
+ def patch_all() -> List[str]:
23
+ """Patch all detected LLM libraries.
24
+
25
+ Returns:
26
+ List of library names that were successfully patched.
27
+ """
28
+ for lib_name, patch_module in _PATCHABLE.items():
29
+ if lib_name in _patched:
30
+ continue
31
+ try:
32
+ importlib.import_module(lib_name)
33
+ except ImportError:
34
+ continue
35
+
36
+ try:
37
+ mod = importlib.import_module(patch_module, package="pisama_auto.patches")
38
+ mod.patch()
39
+ _patched.append(lib_name)
40
+ logger.debug(f"Patched {lib_name}")
41
+ except Exception as e:
42
+ logger.warning(f"Failed to patch {lib_name}: {e}")
43
+
44
+ return list(_patched)
45
+
46
+
47
+ def patch(library: str) -> bool:
48
+ """Patch a specific library.
49
+
50
+ Args:
51
+ library: Library name (anthropic, openai)
52
+
53
+ Returns:
54
+ True if patched successfully
55
+ """
56
+ if library in _patched:
57
+ return True
58
+
59
+ if library not in _PATCHABLE:
60
+ logger.warning(f"Unknown library: {library}. Supported: {list(_PATCHABLE.keys())}")
61
+ return False
62
+
63
+ try:
64
+ mod = importlib.import_module(_PATCHABLE[library], package="pisama_auto.patches")
65
+ mod.patch()
66
+ _patched.append(library)
67
+ return True
68
+ except Exception as e:
69
+ logger.warning(f"Failed to patch {library}: {e}")
70
+ return False
@@ -0,0 +1,154 @@
1
+ """Auto-instrumentation patch for the Anthropic Python SDK.
2
+
3
+ Wraps anthropic.Anthropic.messages.create() and .stream() to emit
4
+ OTEL spans with gen_ai.* semantic conventions.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ import wrapt
11
+ from opentelemetry.trace import StatusCode
12
+
13
+ logger = logging.getLogger("pisama_auto")
14
+
15
+ _original_create = None
16
+ _original_stream = None
17
+
18
+
19
+ def patch() -> None:
20
+ """Patch the Anthropic SDK to emit OTEL spans."""
21
+ import anthropic
22
+
23
+ messages_cls = anthropic.resources.Messages
24
+
25
+ global _original_create, _original_stream
26
+ _original_create = messages_cls.create
27
+ _original_stream = getattr(messages_cls, "stream", None)
28
+
29
+ wrapt.wrap_function_wrapper(messages_cls, "create", _traced_create)
30
+
31
+ if _original_stream:
32
+ wrapt.wrap_function_wrapper(messages_cls, "stream", _traced_stream)
33
+
34
+ logger.debug("Anthropic SDK patched")
35
+
36
+
37
+ def _traced_create(wrapped, instance, args, kwargs) -> Any:
38
+ """Wrapper for messages.create that adds OTEL tracing."""
39
+ from pisama_auto._tracer import get_tracer
40
+
41
+ tracer = get_tracer()
42
+ model = kwargs.get("model", "unknown")
43
+ span_name = f"gen_ai.chat {model}"
44
+
45
+ with tracer.start_as_current_span(span_name) as span:
46
+ # Set gen_ai.* attributes
47
+ span.set_attribute("gen_ai.system", "anthropic")
48
+ span.set_attribute("gen_ai.request.model", model)
49
+ span.set_attribute("gen_ai.operation.name", "chat")
50
+
51
+ max_tokens = kwargs.get("max_tokens")
52
+ if max_tokens:
53
+ span.set_attribute("gen_ai.request.max_tokens", max_tokens)
54
+
55
+ temperature = kwargs.get("temperature")
56
+ if temperature is not None:
57
+ span.set_attribute("gen_ai.request.temperature", temperature)
58
+
59
+ # Record input messages count
60
+ messages = kwargs.get("messages", [])
61
+ span.set_attribute("gen_ai.request.messages_count", len(messages))
62
+
63
+ system = kwargs.get("system")
64
+ if system:
65
+ span.set_attribute("gen_ai.request.has_system", True)
66
+
67
+ try:
68
+ response = wrapped(*args, **kwargs)
69
+
70
+ # Record response attributes
71
+ span.set_attribute("gen_ai.response.model", getattr(response, "model", model))
72
+ span.set_attribute("gen_ai.response.stop_reason", getattr(response, "stop_reason", ""))
73
+
74
+ usage = getattr(response, "usage", None)
75
+ if usage:
76
+ input_tokens = getattr(usage, "input_tokens", 0)
77
+ output_tokens = getattr(usage, "output_tokens", 0)
78
+ span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
79
+ span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
80
+ span.set_attribute("gen_ai.usage.total_tokens", input_tokens + output_tokens)
81
+
82
+ return response
83
+
84
+ except Exception as e:
85
+ span.set_attribute("error.type", type(e).__name__)
86
+ span.set_attribute("error.message", str(e)[:500])
87
+ span.set_status(StatusCode.ERROR, str(e)[:200])
88
+ raise
89
+
90
+
91
+ class _TracedStream:
92
+ """Wrapper that ends the OTEL span when the stream is consumed or closed."""
93
+
94
+ def __init__(self, stream, span):
95
+ self._stream = stream
96
+ self._span = span
97
+
98
+ def __enter__(self):
99
+ self._stream.__enter__()
100
+ return self
101
+
102
+ def __exit__(self, exc_type, exc_val, exc_tb):
103
+ try:
104
+ result = self._stream.__exit__(exc_type, exc_val, exc_tb)
105
+ if exc_type:
106
+ self._span.set_attribute("error.type", exc_type.__name__)
107
+ self._span.set_status(StatusCode.ERROR, str(exc_val)[:200])
108
+ return result
109
+ finally:
110
+ self._span.end()
111
+
112
+ def __iter__(self):
113
+ return self
114
+
115
+ def __next__(self):
116
+ try:
117
+ return next(self._stream)
118
+ except StopIteration:
119
+ self._span.end()
120
+ raise
121
+ except Exception as e:
122
+ self._span.set_attribute("error.type", type(e).__name__)
123
+ self._span.set_status(StatusCode.ERROR, str(e)[:200])
124
+ self._span.end()
125
+ raise
126
+
127
+ def __getattr__(self, name):
128
+ return getattr(self._stream, name)
129
+
130
+
131
+ def _traced_stream(wrapped, instance, args, kwargs) -> Any:
132
+ """Wrapper for messages.stream that adds OTEL tracing."""
133
+ from pisama_auto._tracer import get_tracer
134
+
135
+ tracer = get_tracer()
136
+ model = kwargs.get("model", "unknown")
137
+ span_name = f"gen_ai.chat.stream {model}"
138
+
139
+ span = tracer.start_span(span_name)
140
+ span.set_attribute("gen_ai.system", "anthropic")
141
+ span.set_attribute("gen_ai.request.model", model)
142
+ span.set_attribute("gen_ai.operation.name", "chat.stream")
143
+
144
+ messages = kwargs.get("messages", [])
145
+ span.set_attribute("gen_ai.request.messages_count", len(messages))
146
+
147
+ try:
148
+ result = wrapped(*args, **kwargs)
149
+ return _TracedStream(result, span)
150
+ except Exception as e:
151
+ span.set_attribute("error.type", type(e).__name__)
152
+ span.set_status(StatusCode.ERROR, str(e)[:200])
153
+ span.end()
154
+ raise
@@ -0,0 +1,83 @@
1
+ """Auto-instrumentation patch for the OpenAI Python SDK.
2
+
3
+ Wraps openai.ChatCompletion.create() and the v1+ client to emit
4
+ OTEL spans with gen_ai.* semantic conventions.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ import wrapt
11
+
12
+ logger = logging.getLogger("pisama_auto")
13
+
14
+
15
+ def patch() -> None:
16
+ """Patch the OpenAI SDK to emit OTEL spans."""
17
+ import openai
18
+
19
+ # OpenAI v1+ uses client.chat.completions.create()
20
+ if hasattr(openai, "resources"):
21
+ completions_cls = openai.resources.chat.Completions
22
+ wrapt.wrap_function_wrapper(completions_cls, "create", _traced_create)
23
+ logger.debug("OpenAI SDK v1+ patched")
24
+ else:
25
+ logger.warning("OpenAI SDK version not supported for auto-patching")
26
+
27
+
28
+ def _traced_create(wrapped, instance, args, kwargs) -> Any:
29
+ """Wrapper for chat.completions.create that adds OTEL tracing."""
30
+ from pisama_auto._tracer import get_tracer
31
+
32
+ tracer = get_tracer()
33
+ model = kwargs.get("model", "unknown")
34
+ span_name = f"gen_ai.chat {model}"
35
+
36
+ with tracer.start_as_current_span(span_name) as span:
37
+ span.set_attribute("gen_ai.system", "openai")
38
+ span.set_attribute("gen_ai.request.model", model)
39
+ span.set_attribute("gen_ai.operation.name", "chat")
40
+
41
+ max_tokens = kwargs.get("max_tokens")
42
+ if max_tokens:
43
+ span.set_attribute("gen_ai.request.max_tokens", max_tokens)
44
+
45
+ temperature = kwargs.get("temperature")
46
+ if temperature is not None:
47
+ span.set_attribute("gen_ai.request.temperature", temperature)
48
+
49
+ messages = kwargs.get("messages", [])
50
+ span.set_attribute("gen_ai.request.messages_count", len(messages))
51
+
52
+ stream = kwargs.get("stream", False)
53
+ if stream:
54
+ span.set_attribute("gen_ai.operation.name", "chat.stream")
55
+
56
+ try:
57
+ response = wrapped(*args, **kwargs)
58
+
59
+ if not stream and response:
60
+ resp_model = getattr(response, "model", model)
61
+ span.set_attribute("gen_ai.response.model", resp_model)
62
+
63
+ choices = getattr(response, "choices", [])
64
+ if choices:
65
+ finish_reason = getattr(choices[0], "finish_reason", "")
66
+ span.set_attribute("gen_ai.response.finish_reason", finish_reason or "")
67
+
68
+ usage = getattr(response, "usage", None)
69
+ if usage:
70
+ input_tokens = getattr(usage, "prompt_tokens", 0)
71
+ output_tokens = getattr(usage, "completion_tokens", 0)
72
+ span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
73
+ span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
74
+ span.set_attribute("gen_ai.usage.total_tokens", input_tokens + output_tokens)
75
+
76
+ return response
77
+
78
+ except Exception as e:
79
+ span.set_attribute("error.type", type(e).__name__)
80
+ span.set_attribute("error.message", str(e)[:500])
81
+ from opentelemetry.trace import StatusCode
82
+ span.set_status(StatusCode.ERROR, str(e)[:200])
83
+ raise