lanternfish 0.3.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.
- lanternfish-0.3.0/PKG-INFO +137 -0
- lanternfish-0.3.0/README.md +98 -0
- lanternfish-0.3.0/__init__.py +61 -0
- lanternfish-0.3.0/configuration.py +81 -0
- lanternfish-0.3.0/context.py +67 -0
- lanternfish-0.3.0/decorators.py +222 -0
- lanternfish-0.3.0/deployments.py +69 -0
- lanternfish-0.3.0/errors.py +9 -0
- lanternfish-0.3.0/exporters.py +34 -0
- lanternfish-0.3.0/instrument.py +181 -0
- lanternfish-0.3.0/integrations/__init__.py +61 -0
- lanternfish-0.3.0/integrations/anthropic.py +20 -0
- lanternfish-0.3.0/integrations/autogen.py +75 -0
- lanternfish-0.3.0/integrations/crewai.py +17 -0
- lanternfish-0.3.0/integrations/gemini.py +24 -0
- lanternfish-0.3.0/integrations/groq.py +21 -0
- lanternfish-0.3.0/integrations/haystack.py +65 -0
- lanternfish-0.3.0/integrations/http.py +129 -0
- lanternfish-0.3.0/integrations/instrumentor.py +24 -0
- lanternfish-0.3.0/integrations/langchain.py +21 -0
- lanternfish-0.3.0/integrations/llamaindex.py +21 -0
- lanternfish-0.3.0/integrations/mcp.py +208 -0
- lanternfish-0.3.0/integrations/ollama.py +73 -0
- lanternfish-0.3.0/integrations/openai.py +17 -0
- lanternfish-0.3.0/integrations/openai_agents.py +78 -0
- lanternfish-0.3.0/integrations/pydantic_ai.py +66 -0
- lanternfish-0.3.0/integrations/redis.py +296 -0
- lanternfish-0.3.0/integrations/sql.py +300 -0
- lanternfish-0.3.0/json_exporter.py +170 -0
- lanternfish-0.3.0/lanternfish.egg-info/PKG-INFO +137 -0
- lanternfish-0.3.0/lanternfish.egg-info/SOURCES.txt +73 -0
- lanternfish-0.3.0/lanternfish.egg-info/dependency_links.txt +1 -0
- lanternfish-0.3.0/lanternfish.egg-info/requires.txt +31 -0
- lanternfish-0.3.0/lanternfish.egg-info/top_level.txt +1 -0
- lanternfish-0.3.0/providers.py +14 -0
- lanternfish-0.3.0/py.typed +0 -0
- lanternfish-0.3.0/pyproject.toml +57 -0
- lanternfish-0.3.0/semantic.py +794 -0
- lanternfish-0.3.0/setup.cfg +4 -0
- lanternfish-0.3.0/span_processor.py +69 -0
- lanternfish-0.3.0/tests/test_batch_export.py +102 -0
- lanternfish-0.3.0/tests/test_configuration.py +93 -0
- lanternfish-0.3.0/tests/test_deployments.py +60 -0
- lanternfish-0.3.0/tests/test_instrument_end_to_end.py +210 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lanternfish
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Lanternfish SDK: trace AI agent runs, decisions and releases with OpenTelemetry.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://github.com/TriTetra/agentlens-sdk/tree/master/lanternfish#readme
|
|
7
|
+
Project-URL: Source, https://github.com/TriTetra/agentlens-sdk
|
|
8
|
+
Keywords: ai-agents,observability,opentelemetry,llm,tracing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: opentelemetry-api<2,>=1.25.0
|
|
18
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.25.0
|
|
19
|
+
Requires-Dist: requests<3,>=2.31.0
|
|
20
|
+
Provides-Extra: openai
|
|
21
|
+
Requires-Dist: opentelemetry-instrumentation-openai<1,>=0.40.0; extra == "openai"
|
|
22
|
+
Provides-Extra: anthropic
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic<1,>=0.40.0; extra == "anthropic"
|
|
24
|
+
Provides-Extra: gemini
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-google-generativeai<1,>=0.40.0; extra == "gemini"
|
|
26
|
+
Provides-Extra: groq
|
|
27
|
+
Requires-Dist: opentelemetry-instrumentation-groq<1,>=0.40.0; extra == "groq"
|
|
28
|
+
Provides-Extra: langchain
|
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-langchain<1,>=0.40.0; extra == "langchain"
|
|
30
|
+
Provides-Extra: crewai
|
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-crewai<1,>=0.40.0; extra == "crewai"
|
|
32
|
+
Provides-Extra: llamaindex
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex<1,>=0.40.0; extra == "llamaindex"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: lanternfish[anthropic,crewai,gemini,groq,langchain,llamaindex,openai]; extra == "all"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
39
|
+
|
|
40
|
+
# Lanternfish Python SDK
|
|
41
|
+
|
|
42
|
+
Record what your AI agent does: its LLM calls, its own steps, the decisions it makes and the release it runs on.
|
|
43
|
+
Lanternfish compares those runs over time and tells you what changed.
|
|
44
|
+
|
|
45
|
+
Built on OpenTelemetry. Works next to an existing OpenTelemetry or Logfire setup.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install "lanternfish[openai]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use the extra for each provider you call: `openai`, `anthropic`, `gemini`, `groq`, `langchain`, `crewai`,
|
|
54
|
+
`llamaindex`, or `all`. Requires Python 3.11+.
|
|
55
|
+
|
|
56
|
+
## Start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export LANTERNFISH_API_KEY="lfsk_..." # Settings → API keys
|
|
60
|
+
export LANTERNFISH_ENDPOINT="https://your-lanternfish-endpoint"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import lanternfish
|
|
65
|
+
|
|
66
|
+
lanternfish.instrument(agent="travel-agent")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
From here on, calls to installed providers are recorded. The SDK never picks a destination by itself: without an
|
|
70
|
+
endpoint it stops with an error that says what to set.
|
|
71
|
+
|
|
72
|
+
## Record your own steps and decisions
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import lanternfish
|
|
76
|
+
|
|
77
|
+
@lanternfish.trace # records arguments; capture_args=False for private data
|
|
78
|
+
def search_flights(destination: str) -> list[str]:
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
with lanternfish.set_user("user_42", session_id="sess_9"): # removed again when the block ends
|
|
82
|
+
with lanternfish.span("book-trip"):
|
|
83
|
+
options = search_flights("Lisbon")
|
|
84
|
+
with lanternfish.reasoning(name="pick_flight") as r:
|
|
85
|
+
r.consider_option("KLM 1724", score=0.8, reason="Arrives before the meeting")
|
|
86
|
+
r.decide("KLM 1724", confidence=0.8, reason="Arrives before the meeting")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A step that raises is marked failed, with the exception type and message.
|
|
90
|
+
|
|
91
|
+
## Releases
|
|
92
|
+
|
|
93
|
+
Every span carries the release it ran on: `release=` if you pass it, otherwise `LANTERNFISH_RELEASE`, otherwise the
|
|
94
|
+
commit your CI or host exposes (`GITHUB_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, …). To mark the moment a
|
|
95
|
+
release went live, call this from your deploy step:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
lanternfish.record_deployment("v1.8.2", metadata={"commit": "a1b2c3d", "environment": "production"})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Options
|
|
102
|
+
|
|
103
|
+
| `instrument()` argument | If omitted | |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `api_key` | `LANTERNFISH_API_KEY` | Project API key, sent only in the `x-api-key` header |
|
|
106
|
+
| `endpoint` | `LANTERNFISH_ENDPOINT` | Your Lanternfish API base URL |
|
|
107
|
+
| `agent` | — | Agent name on every run |
|
|
108
|
+
| `release` | `LANTERNFISH_RELEASE` or CI commit | Version of the running code |
|
|
109
|
+
| `environment` | `LANTERNFISH_ENVIRONMENT` | For example `production` |
|
|
110
|
+
| `include` | `[]` | Also record `"http"`, `"sql"`, `"redis"` calls |
|
|
111
|
+
| `capture_content` | `True` | Record prompt and completion text of LLM calls |
|
|
112
|
+
|
|
113
|
+
## What is recorded
|
|
114
|
+
|
|
115
|
+
- **LLM calls:** model, tokens, duration, errors and, unless `capture_content=False`, the prompt and completion text.
|
|
116
|
+
- **HTTP** (only with `include=["http"]`): method, host, path and status. Never bodies, headers or query strings.
|
|
117
|
+
- **SQL** (only with `include=["sql"]`): the statement with literals replaced by `?`. Never parameter values.
|
|
118
|
+
|
|
119
|
+
## Short scripts and serverless
|
|
120
|
+
|
|
121
|
+
Spans are sent in the background in batches. Call `lanternfish.flush()` before a short script or a serverless handler
|
|
122
|
+
returns; `lanternfish.shutdown()` runs on process exit by itself.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import logging
|
|
128
|
+
logging.basicConfig(level="INFO")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `lanternfish` logger names the endpoint, lists the integrations it turned on, and logs every rejected batch with
|
|
132
|
+
the reason (invalid key, plan limit reached, endpoint unreachable). Sending never raises into your code; a temporary
|
|
133
|
+
error is retried once.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Lanternfish Python SDK
|
|
2
|
+
|
|
3
|
+
Record what your AI agent does: its LLM calls, its own steps, the decisions it makes and the release it runs on.
|
|
4
|
+
Lanternfish compares those runs over time and tells you what changed.
|
|
5
|
+
|
|
6
|
+
Built on OpenTelemetry. Works next to an existing OpenTelemetry or Logfire setup.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install "lanternfish[openai]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use the extra for each provider you call: `openai`, `anthropic`, `gemini`, `groq`, `langchain`, `crewai`,
|
|
15
|
+
`llamaindex`, or `all`. Requires Python 3.11+.
|
|
16
|
+
|
|
17
|
+
## Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export LANTERNFISH_API_KEY="lfsk_..." # Settings → API keys
|
|
21
|
+
export LANTERNFISH_ENDPOINT="https://your-lanternfish-endpoint"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import lanternfish
|
|
26
|
+
|
|
27
|
+
lanternfish.instrument(agent="travel-agent")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From here on, calls to installed providers are recorded. The SDK never picks a destination by itself: without an
|
|
31
|
+
endpoint it stops with an error that says what to set.
|
|
32
|
+
|
|
33
|
+
## Record your own steps and decisions
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import lanternfish
|
|
37
|
+
|
|
38
|
+
@lanternfish.trace # records arguments; capture_args=False for private data
|
|
39
|
+
def search_flights(destination: str) -> list[str]:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
with lanternfish.set_user("user_42", session_id="sess_9"): # removed again when the block ends
|
|
43
|
+
with lanternfish.span("book-trip"):
|
|
44
|
+
options = search_flights("Lisbon")
|
|
45
|
+
with lanternfish.reasoning(name="pick_flight") as r:
|
|
46
|
+
r.consider_option("KLM 1724", score=0.8, reason="Arrives before the meeting")
|
|
47
|
+
r.decide("KLM 1724", confidence=0.8, reason="Arrives before the meeting")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A step that raises is marked failed, with the exception type and message.
|
|
51
|
+
|
|
52
|
+
## Releases
|
|
53
|
+
|
|
54
|
+
Every span carries the release it ran on: `release=` if you pass it, otherwise `LANTERNFISH_RELEASE`, otherwise the
|
|
55
|
+
commit your CI or host exposes (`GITHUB_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, …). To mark the moment a
|
|
56
|
+
release went live, call this from your deploy step:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
lanternfish.record_deployment("v1.8.2", metadata={"commit": "a1b2c3d", "environment": "production"})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
| `instrument()` argument | If omitted | |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `api_key` | `LANTERNFISH_API_KEY` | Project API key, sent only in the `x-api-key` header |
|
|
67
|
+
| `endpoint` | `LANTERNFISH_ENDPOINT` | Your Lanternfish API base URL |
|
|
68
|
+
| `agent` | — | Agent name on every run |
|
|
69
|
+
| `release` | `LANTERNFISH_RELEASE` or CI commit | Version of the running code |
|
|
70
|
+
| `environment` | `LANTERNFISH_ENVIRONMENT` | For example `production` |
|
|
71
|
+
| `include` | `[]` | Also record `"http"`, `"sql"`, `"redis"` calls |
|
|
72
|
+
| `capture_content` | `True` | Record prompt and completion text of LLM calls |
|
|
73
|
+
|
|
74
|
+
## What is recorded
|
|
75
|
+
|
|
76
|
+
- **LLM calls:** model, tokens, duration, errors and, unless `capture_content=False`, the prompt and completion text.
|
|
77
|
+
- **HTTP** (only with `include=["http"]`): method, host, path and status. Never bodies, headers or query strings.
|
|
78
|
+
- **SQL** (only with `include=["sql"]`): the statement with literals replaced by `?`. Never parameter values.
|
|
79
|
+
|
|
80
|
+
## Short scripts and serverless
|
|
81
|
+
|
|
82
|
+
Spans are sent in the background in batches. Call `lanternfish.flush()` before a short script or a serverless handler
|
|
83
|
+
returns; `lanternfish.shutdown()` runs on process exit by itself.
|
|
84
|
+
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import logging
|
|
89
|
+
logging.basicConfig(level="INFO")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The `lanternfish` logger names the endpoint, lists the integrations it turned on, and logs every rejected batch with
|
|
93
|
+
the reason (invalid key, plan limit reached, endpoint unreachable). Sending never raises into your code; a temporary
|
|
94
|
+
error is retried once.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# __init__.py
|
|
2
|
+
# Görev: Lanternfish SDK'nin public API'sini dışa aktarmak
|
|
3
|
+
# Yapmaz: Instrumentation mantığı içermez
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from lanternfish.context import UserScope, set_user
|
|
8
|
+
from lanternfish.decorators import span, span_context, trace
|
|
9
|
+
from lanternfish.deployments import record_deployment
|
|
10
|
+
from lanternfish.errors import LanternfishConfigurationError, LanternfishDeploymentError
|
|
11
|
+
from lanternfish.instrument import flush, instrument, shutdown
|
|
12
|
+
from lanternfish.semantic import (
|
|
13
|
+
confidence_tracking,
|
|
14
|
+
context_retrieval,
|
|
15
|
+
decision,
|
|
16
|
+
memory_access,
|
|
17
|
+
planning,
|
|
18
|
+
reasoning,
|
|
19
|
+
reflection,
|
|
20
|
+
set_planning_attribute,
|
|
21
|
+
set_reasoning_attribute,
|
|
22
|
+
set_reflection_attribute,
|
|
23
|
+
set_verification_attribute,
|
|
24
|
+
tool_usage,
|
|
25
|
+
verification,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Sürüm tek kaynaktan, pyproject'ten okunur; kaynak ağaçtan çalışırken
|
|
29
|
+
# paket kurulu olmayabilir.
|
|
30
|
+
try:
|
|
31
|
+
__version__ = version("lanternfish")
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
__version__ = "0.0.0+local"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"__version__",
|
|
37
|
+
"instrument",
|
|
38
|
+
"flush",
|
|
39
|
+
"shutdown",
|
|
40
|
+
"UserScope",
|
|
41
|
+
"LanternfishConfigurationError",
|
|
42
|
+
"LanternfishDeploymentError",
|
|
43
|
+
"record_deployment",
|
|
44
|
+
"set_user",
|
|
45
|
+
"trace",
|
|
46
|
+
"span",
|
|
47
|
+
"span_context",
|
|
48
|
+
"planning",
|
|
49
|
+
"reasoning",
|
|
50
|
+
"reflection",
|
|
51
|
+
"verification",
|
|
52
|
+
"tool_usage",
|
|
53
|
+
"memory_access",
|
|
54
|
+
"context_retrieval",
|
|
55
|
+
"confidence_tracking",
|
|
56
|
+
"decision",
|
|
57
|
+
"set_planning_attribute",
|
|
58
|
+
"set_reasoning_attribute",
|
|
59
|
+
"set_reflection_attribute",
|
|
60
|
+
"set_verification_attribute",
|
|
61
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK yapılandırmasının çözümü.
|
|
3
|
+
|
|
4
|
+
Görev: Endpoint, API anahtarı, sürüm (release) ve ortam değerlerini argümandan ya da ortam değişkeninden okur ve
|
|
5
|
+
doğrular. Sürüm verilmezse yaygın CI/hosting değişkenlerinden (Git commit) bulur.
|
|
6
|
+
Yapmaz: Ağa çıkmaz, provider kurmaz, varsayılan bir Cloud adresi seçmez.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
|
|
12
|
+
from lanternfish.errors import LanternfishConfigurationError
|
|
13
|
+
|
|
14
|
+
# Sürümü kendiliğinden bulmak için okunan değişkenler; ilk dolu olan kazanır. Platformların kendi adları.
|
|
15
|
+
RELEASE_ENV_VARS = (
|
|
16
|
+
"LANTERNFISH_RELEASE",
|
|
17
|
+
"GIT_COMMIT_SHA",
|
|
18
|
+
"GITHUB_SHA",
|
|
19
|
+
"VERCEL_GIT_COMMIT_SHA",
|
|
20
|
+
"RENDER_GIT_COMMIT",
|
|
21
|
+
"RAILWAY_GIT_COMMIT_SHA",
|
|
22
|
+
"HEROKU_SLUG_COMMIT",
|
|
23
|
+
"SOURCE_COMMIT",
|
|
24
|
+
)
|
|
25
|
+
SHORT_SHA_LENGTH = 12
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_endpoint(endpoint: str | None) -> str:
|
|
29
|
+
"""API taban adresini argümandan veya LANTERNFISH_ENDPOINT'ten çözer ve doğrular.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
LanternfishConfigurationError: Adres yoksa, mutlak http(s) adresi değilse veya sorgu içeriyorsa.
|
|
33
|
+
"""
|
|
34
|
+
value = (endpoint or os.getenv("LANTERNFISH_ENDPOINT", "")).strip().rstrip("/")
|
|
35
|
+
if not value:
|
|
36
|
+
raise LanternfishConfigurationError(
|
|
37
|
+
"Lanternfish endpoint is required. Pass endpoint=... or set LANTERNFISH_ENDPOINT. "
|
|
38
|
+
"The SDK never selects a destination automatically."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parsed = urlsplit(value)
|
|
42
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
43
|
+
raise LanternfishConfigurationError("Lanternfish endpoint must be an absolute http:// or https:// URL.")
|
|
44
|
+
if parsed.query or parsed.fragment:
|
|
45
|
+
raise LanternfishConfigurationError("Lanternfish endpoint must not contain a query string or fragment.")
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_api_key(api_key: str | None) -> str:
|
|
50
|
+
"""API anahtarını argümandan veya LANTERNFISH_API_KEY'den çözer.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
LanternfishConfigurationError: Anahtar yoksa.
|
|
54
|
+
"""
|
|
55
|
+
value = (api_key or os.getenv("LANTERNFISH_API_KEY", "")).strip()
|
|
56
|
+
if not value:
|
|
57
|
+
raise LanternfishConfigurationError(
|
|
58
|
+
"Lanternfish API key is required. Pass api_key=... or set LANTERNFISH_API_KEY. "
|
|
59
|
+
"Create one in Settings → API keys."
|
|
60
|
+
)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_release(release: str | None) -> str | None:
|
|
65
|
+
"""Çalışan kodun sürümünü döndürür: argüman, LANTERNFISH_RELEASE ya da CI'ın commit değişkeni.
|
|
66
|
+
|
|
67
|
+
Commit SHA'sı ilk 12 karaktere kısaltılır; sürüm adı (ör. "v1.8.2") olduğu gibi kalır.
|
|
68
|
+
"""
|
|
69
|
+
if release and release.strip():
|
|
70
|
+
return release.strip()
|
|
71
|
+
for name in RELEASE_ENV_VARS:
|
|
72
|
+
value = os.getenv(name, "").strip()
|
|
73
|
+
if value:
|
|
74
|
+
return value if name == "LANTERNFISH_RELEASE" else value[:SHORT_SHA_LENGTH]
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_environment(environment: str | None) -> str | None:
|
|
79
|
+
"""Ortam adını (ör. "production") argümandan veya LANTERNFISH_ENVIRONMENT'tan döndürür."""
|
|
80
|
+
value = (environment or os.getenv("LANTERNFISH_ENVIRONMENT", "")).strip()
|
|
81
|
+
return value or None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Son kullanıcı kimliği.
|
|
3
|
+
|
|
4
|
+
Görev: Agent'ı kullanan son kullanıcının kimliğini, oturumunu ve etiketlerini bağlama koyar; bundan sonra açılan her
|
|
5
|
+
span bu bilgiyi taşır. `with` ile kullanılırsa blok bitince eski bağlama döner.
|
|
6
|
+
Yapmaz: Span açmaz veya göndermez; değeri span'e damgalamak span_processor.py'nin işidir.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
|
|
11
|
+
from opentelemetry import context
|
|
12
|
+
|
|
13
|
+
from lanternfish.span_processor import SESSION_ID_KEY, TAGS_KEY, USER_ID_KEY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserScope:
|
|
17
|
+
"""`set_user()` sonucunun kapsamı: `with` bloğundan çıkınca kimliği bağlamdan kaldırır."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, token: object) -> None:
|
|
20
|
+
"""Kapsamı, bağlama eklenen değerin geri alma anahtarıyla kurar."""
|
|
21
|
+
self._token = token
|
|
22
|
+
self._closed = False
|
|
23
|
+
|
|
24
|
+
def __enter__(self) -> "UserScope":
|
|
25
|
+
"""Kimlik zaten bağlamda; bloğa olduğu gibi girer."""
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(
|
|
29
|
+
self,
|
|
30
|
+
exc_type: type[BaseException] | None,
|
|
31
|
+
exc: BaseException | None,
|
|
32
|
+
traceback: TracebackType | None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Bloktan çıkarken kimliği bağlamdan kaldırır."""
|
|
35
|
+
self.close()
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
"""Kimliği bağlamdan kaldırır; ikinci çağrı etkisizdir."""
|
|
39
|
+
if not self._closed:
|
|
40
|
+
context.detach(self._token) # type: ignore[arg-type]
|
|
41
|
+
self._closed = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_user(user_id: str, session_id: str | None = None, tags: dict[str, str] | None = None) -> UserScope:
|
|
45
|
+
"""Bundan sonra açılan span'lere son kullanıcı kimliğini ekler.
|
|
46
|
+
|
|
47
|
+
Web sunucusunda istek başına `with` ile kullanın; böylece kimlik bir sonraki isteğe taşınmaz::
|
|
48
|
+
|
|
49
|
+
with lanternfish.set_user("user_42", session_id="sess_9"):
|
|
50
|
+
run_agent(question)
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
user_id: Son kullanıcının kimliği.
|
|
54
|
+
session_id: Sohbet veya oturum kimliği.
|
|
55
|
+
tags: Run'ı süzmek için düz etiketler.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
`with` ile kullanılabilen ya da `close()` ile kapatılabilen kapsam.
|
|
59
|
+
"""
|
|
60
|
+
ctx = context.get_current()
|
|
61
|
+
if user_id:
|
|
62
|
+
ctx = context.set_value(USER_ID_KEY, user_id, ctx)
|
|
63
|
+
if session_id:
|
|
64
|
+
ctx = context.set_value(SESSION_ID_KEY, session_id, ctx)
|
|
65
|
+
if tags:
|
|
66
|
+
ctx = context.set_value(TAGS_KEY, tags, ctx)
|
|
67
|
+
return UserScope(context.attach(ctx))
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# decorators.py
|
|
2
|
+
# Gorev: @trace ve @span decorator + span() context manager ile manual instrumentation
|
|
3
|
+
# Yapmaz: TracerProvider veya exporter olusturmaz; instrument() ile aktif provider kullanilir
|
|
4
|
+
#
|
|
5
|
+
# Kullanim:
|
|
6
|
+
# @trace -> span adi = fn adi, parametreler otomatik yakalanir
|
|
7
|
+
# @trace(name="x") -> custom span adi
|
|
8
|
+
# @span -> span adi = fn adi, parametreler yakalanmaz
|
|
9
|
+
# @span("x") / @span(name="x") -> custom span adi, decorator
|
|
10
|
+
# with span("x") as s: -> context manager, manuel attribute
|
|
11
|
+
# with span_context("x") as s: -> span_context alias'i
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import inspect
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from typing import Any, Callable, Generator
|
|
17
|
+
|
|
18
|
+
from opentelemetry import trace as otel_trace
|
|
19
|
+
from opentelemetry.trace import StatusCode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── @trace ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
def trace(func: Callable | None = None, *, name: str | None = None, capture_args: bool = True) -> Any:
|
|
25
|
+
"""Custom fonksiyonlari OpenTelemetry span olarak izler.
|
|
26
|
+
|
|
27
|
+
Parametreler otomatik olarak manual.param.<isim> attribute'i olur.
|
|
28
|
+
Exception durumunda span status ERROR olarak isaretlenir.
|
|
29
|
+
|
|
30
|
+
Iki kullanim sekli:
|
|
31
|
+
@trace -> span adi fonksiyon adindan alinir
|
|
32
|
+
@trace(name="custom_name") -> span adi manuel verilir
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
func: Direkt decorator olarak kullanildiginda gelen fonksiyon.
|
|
36
|
+
name: Opsiyonel custom span adi.
|
|
37
|
+
capture_args: False ise parametre degerleri span'e yazilmaz (kisisel veri tasiyan fonksiyonlar icin).
|
|
38
|
+
"""
|
|
39
|
+
def decorator(fn: Callable) -> Callable:
|
|
40
|
+
span_name = name or fn.__name__
|
|
41
|
+
|
|
42
|
+
if inspect.iscoroutinefunction(fn):
|
|
43
|
+
@functools.wraps(fn)
|
|
44
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
45
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
46
|
+
with tracer.start_as_current_span(span_name) as s:
|
|
47
|
+
if capture_args:
|
|
48
|
+
_add_params(s, fn, args, kwargs)
|
|
49
|
+
try:
|
|
50
|
+
result = await fn(*args, **kwargs)
|
|
51
|
+
s.set_status(StatusCode.OK)
|
|
52
|
+
return result
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
s.set_status(StatusCode.ERROR, str(exc))
|
|
55
|
+
s.record_exception(exc)
|
|
56
|
+
raise
|
|
57
|
+
return async_wrapper
|
|
58
|
+
else:
|
|
59
|
+
@functools.wraps(fn)
|
|
60
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
61
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
62
|
+
with tracer.start_as_current_span(span_name) as s:
|
|
63
|
+
if capture_args:
|
|
64
|
+
_add_params(s, fn, args, kwargs)
|
|
65
|
+
try:
|
|
66
|
+
result = fn(*args, **kwargs)
|
|
67
|
+
s.set_status(StatusCode.OK)
|
|
68
|
+
return result
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
s.set_status(StatusCode.ERROR, str(exc))
|
|
71
|
+
s.record_exception(exc)
|
|
72
|
+
raise
|
|
73
|
+
return sync_wrapper
|
|
74
|
+
|
|
75
|
+
if func is None:
|
|
76
|
+
return decorator
|
|
77
|
+
return decorator(func)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── @span / span() context manager ────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
class _SpanProxy:
|
|
83
|
+
"""span("name") cagrisi icin dual-use proxy.
|
|
84
|
+
|
|
85
|
+
Hem @span("name") decorator hem with span("name") as s: context manager
|
|
86
|
+
olarak kullanilabilir. Parametreler otomatik yakalanmaz; caller s.set_attribute()
|
|
87
|
+
ile manuel ekler.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, span_name: str) -> None:
|
|
91
|
+
self._span_name = span_name
|
|
92
|
+
self._cm: Any = None
|
|
93
|
+
|
|
94
|
+
def __call__(self, fn: Callable) -> Callable:
|
|
95
|
+
"""@span("name") seklinde decorator olarak kullanildiginda."""
|
|
96
|
+
return _make_span_wrapper(fn, self._span_name)
|
|
97
|
+
|
|
98
|
+
def __enter__(self) -> Any:
|
|
99
|
+
"""with span("name") as s: seklinde context manager olarak kullanildiginda."""
|
|
100
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
101
|
+
self._cm = tracer.start_as_current_span(self._span_name)
|
|
102
|
+
s = self._cm.__enter__()
|
|
103
|
+
self._span_ref = s
|
|
104
|
+
return s
|
|
105
|
+
|
|
106
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
|
|
107
|
+
if exc_val is not None:
|
|
108
|
+
self._span_ref.set_status(StatusCode.ERROR, str(exc_val))
|
|
109
|
+
self._span_ref.record_exception(exc_val)
|
|
110
|
+
else:
|
|
111
|
+
self._span_ref.set_status(StatusCode.OK)
|
|
112
|
+
return self._cm.__exit__(exc_type, exc_val, exc_tb)
|
|
113
|
+
|
|
114
|
+
# async context manager destegi
|
|
115
|
+
async def __aenter__(self) -> Any:
|
|
116
|
+
return self.__enter__()
|
|
117
|
+
|
|
118
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
|
|
119
|
+
return self.__exit__(exc_type, exc_val, exc_tb)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def span(func_or_name: Callable | str | None = None, *, name: str | None = None) -> Any:
|
|
123
|
+
"""Kod blogunu veya fonksiyonu izler. Hafif versiyon (@trace'in aksine parametre yakalamaz).
|
|
124
|
+
|
|
125
|
+
Uc kullanim sekli:
|
|
126
|
+
@span -> decorator, span adi = fn adi
|
|
127
|
+
@span("custom") / @span(name=..) -> decorator, custom span adi
|
|
128
|
+
with span("name") as s: -> context manager, s.set_attribute() ile manuel attr
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
func_or_name: Fonksiyon (@span direkt), string (span adi) veya None.
|
|
132
|
+
name: Keyword-only custom span adi.
|
|
133
|
+
"""
|
|
134
|
+
# Durum 1: @span (parametresiz, direkt fonksiyon uzerinde)
|
|
135
|
+
if callable(func_or_name):
|
|
136
|
+
return _make_span_wrapper(func_or_name, func_or_name.__name__)
|
|
137
|
+
|
|
138
|
+
# Durum 2: span("name") veya span(name="name") -> _SpanProxy doner
|
|
139
|
+
actual_name: str = (
|
|
140
|
+
func_or_name if isinstance(func_or_name, str)
|
|
141
|
+
else name or "span"
|
|
142
|
+
)
|
|
143
|
+
return _SpanProxy(actual_name)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@contextmanager
|
|
147
|
+
def span_context(name: str) -> Generator[Any, None, None]:
|
|
148
|
+
"""Context manager olarak span olusturur.
|
|
149
|
+
|
|
150
|
+
Kullanim:
|
|
151
|
+
with span_context("operation") as s:
|
|
152
|
+
s.set_attribute("key", "value")
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: Span adi.
|
|
156
|
+
|
|
157
|
+
Yields:
|
|
158
|
+
Aktif OpenTelemetry span.
|
|
159
|
+
"""
|
|
160
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
161
|
+
with tracer.start_as_current_span(name) as s:
|
|
162
|
+
try:
|
|
163
|
+
yield s
|
|
164
|
+
s.set_status(StatusCode.OK)
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
s.set_status(StatusCode.ERROR, str(exc))
|
|
167
|
+
s.record_exception(exc)
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Internal helpers ──────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def _make_span_wrapper(fn: Callable, span_name: str) -> Callable:
|
|
174
|
+
"""Sync veya async fonksiyon icin span wrapper olusturur (parametresiz)."""
|
|
175
|
+
if inspect.iscoroutinefunction(fn):
|
|
176
|
+
@functools.wraps(fn)
|
|
177
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
178
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
179
|
+
with tracer.start_as_current_span(span_name) as s:
|
|
180
|
+
try:
|
|
181
|
+
result = await fn(*args, **kwargs)
|
|
182
|
+
s.set_status(StatusCode.OK)
|
|
183
|
+
return result
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
s.set_status(StatusCode.ERROR, str(exc))
|
|
186
|
+
s.record_exception(exc)
|
|
187
|
+
raise
|
|
188
|
+
return async_wrapper
|
|
189
|
+
else:
|
|
190
|
+
@functools.wraps(fn)
|
|
191
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
192
|
+
tracer = otel_trace.get_tracer("lanternfish.manual")
|
|
193
|
+
with tracer.start_as_current_span(span_name) as s:
|
|
194
|
+
try:
|
|
195
|
+
result = fn(*args, **kwargs)
|
|
196
|
+
s.set_status(StatusCode.OK)
|
|
197
|
+
return result
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
s.set_status(StatusCode.ERROR, str(exc))
|
|
200
|
+
s.record_exception(exc)
|
|
201
|
+
raise
|
|
202
|
+
return sync_wrapper
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _add_params(s: Any, fn: Callable, args: tuple, kwargs: dict) -> None:
|
|
206
|
+
"""Fonksiyon parametrelerini span attribute olarak ekler (trace icin).
|
|
207
|
+
|
|
208
|
+
self ve cls atlanir. 500 karakteri asan degerler kisaltilir.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
sig = inspect.signature(fn)
|
|
212
|
+
bound = sig.bind(*args, **kwargs)
|
|
213
|
+
bound.apply_defaults()
|
|
214
|
+
for param_name, param_value in bound.arguments.items():
|
|
215
|
+
if param_name in ("self", "cls"):
|
|
216
|
+
continue
|
|
217
|
+
value_str = str(param_value)
|
|
218
|
+
if len(value_str) > 500:
|
|
219
|
+
value_str = value_str[:500] + "... [truncated]"
|
|
220
|
+
s.set_attribute(f"manual.param.{param_name}", value_str)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|