memtrace-ai 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memtrace_ai-0.2.0/PKG-INFO +137 -0
- memtrace_ai-0.2.0/README.md +73 -0
- memtrace_ai-0.2.0/memtrace/__init__.py +31 -0
- memtrace_ai-0.2.0/memtrace/_version.py +1 -0
- memtrace_ai-0.2.0/memtrace/adapters/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/asgi/__init__.py +3 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/asgi/prompt_override.py +39 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/auto_instrumentation.py +22 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/langchain/__init__.py +5 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/langchain/auto.py +43 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/langchain/callback.py +142 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/langchain/mapping.py +110 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/langchain/prompt_middleware.py +45 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/manual/__init__.py +5 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/manual/llm.py +47 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/manual/retrieval.py +14 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/manual/step.py +81 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/openai_agents/__init__.py +4 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/openai_agents/auto.py +35 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/openai_agents/callback.py +193 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/pydantic_ai/__init__.py +3 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/pydantic_ai/auto.py +42 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/pydantic_ai/mcp.py +56 -0
- memtrace_ai-0.2.0/memtrace/adapters/inbound/pydantic_ai/normalization.py +48 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/file_prompt_cache.py +49 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/http/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/http/eval_api_client.py +204 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/http/feedback_client.py +45 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/http/prompt_client.py +138 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/llm/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/llm/anthropic_client.py +43 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/noop.py +37 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/attribute_sanitizer.py +29 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/exporter_factory.py +40 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/factory.py +74 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/sanitizing_exporter.py +80 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/session_processor.py +36 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/span_adapter.py +70 -0
- memtrace_ai-0.2.0/memtrace/adapters/outbound/otel/switchable_provider.py +55 -0
- memtrace_ai-0.2.0/memtrace/application/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/application/context.py +47 -0
- memtrace_ai-0.2.0/memtrace/application/eval_ports.py +160 -0
- memtrace_ai-0.2.0/memtrace/application/experiment_runner.py +188 -0
- memtrace_ai-0.2.0/memtrace/application/judges/__init__.py +5 -0
- memtrace_ai-0.2.0/memtrace/application/judges/base.py +122 -0
- memtrace_ai-0.2.0/memtrace/application/judges/correctness.py +27 -0
- memtrace_ai-0.2.0/memtrace/application/judges/faithfulness.py +34 -0
- memtrace_ai-0.2.0/memtrace/application/ports.py +38 -0
- memtrace_ai-0.2.0/memtrace/application/prompt_fix.py +113 -0
- memtrace_ai-0.2.0/memtrace/application/prompt_override.py +27 -0
- memtrace_ai-0.2.0/memtrace/application/prompt_ports.py +48 -0
- memtrace_ai-0.2.0/memtrace/application/prompt_registry.py +378 -0
- memtrace_ai-0.2.0/memtrace/application/redteam/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/application/redteam/attacks.py +68 -0
- memtrace_ai-0.2.0/memtrace/application/redteam/judges.py +84 -0
- memtrace_ai-0.2.0/memtrace/application/redteam/runner.py +33 -0
- memtrace_ai-0.2.0/memtrace/application/retrieval_capture.py +31 -0
- memtrace_ai-0.2.0/memtrace/application/retrieval_metrics.py +98 -0
- memtrace_ai-0.2.0/memtrace/application/run_lifecycle.py +94 -0
- memtrace_ai-0.2.0/memtrace/application/run_registry.py +50 -0
- memtrace_ai-0.2.0/memtrace/application/security/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/application/security/detectors.py +106 -0
- memtrace_ai-0.2.0/memtrace/application/security/policy.py +61 -0
- memtrace_ai-0.2.0/memtrace/application/security/recorder.py +40 -0
- memtrace_ai-0.2.0/memtrace/application/span_normalization.py +35 -0
- memtrace_ai-0.2.0/memtrace/application/tracing_service.py +223 -0
- memtrace_ai-0.2.0/memtrace/config.py +194 -0
- memtrace_ai-0.2.0/memtrace/dependency_container.py +194 -0
- memtrace_ai-0.2.0/memtrace/domain/__init__.py +0 -0
- memtrace_ai-0.2.0/memtrace/domain/attributes.py +129 -0
- memtrace_ai-0.2.0/memtrace/domain/evaluation.py +167 -0
- memtrace_ai-0.2.0/memtrace/domain/model.py +64 -0
- memtrace_ai-0.2.0/memtrace/domain/prompt.py +98 -0
- memtrace_ai-0.2.0/memtrace/domain/redaction.py +138 -0
- memtrace_ai-0.2.0/memtrace/domain/security.py +160 -0
- memtrace_ai-0.2.0/memtrace/domain/semconv.py +39 -0
- memtrace_ai-0.2.0/memtrace/domain/serialization.py +101 -0
- memtrace_ai-0.2.0/memtrace/eval.py +188 -0
- memtrace_ai-0.2.0/memtrace/eval_judges.py +21 -0
- memtrace_ai-0.2.0/memtrace/langchain.py +12 -0
- memtrace_ai-0.2.0/memtrace/openai_agents.py +8 -0
- memtrace_ai-0.2.0/memtrace/pii.py +149 -0
- memtrace_ai-0.2.0/memtrace/prompts.py +151 -0
- memtrace_ai-0.2.0/memtrace/py.typed +0 -0
- memtrace_ai-0.2.0/memtrace/pydantic_ai.py +8 -0
- memtrace_ai-0.2.0/memtrace/redteam.py +108 -0
- memtrace_ai-0.2.0/memtrace/security.py +88 -0
- memtrace_ai-0.2.0/memtrace/user_feedback.py +82 -0
- memtrace_ai-0.2.0/memtrace_ai.egg-info/PKG-INFO +137 -0
- memtrace_ai-0.2.0/memtrace_ai.egg-info/SOURCES.txt +129 -0
- memtrace_ai-0.2.0/memtrace_ai.egg-info/dependency_links.txt +1 -0
- memtrace_ai-0.2.0/memtrace_ai.egg-info/requires.txt +58 -0
- memtrace_ai-0.2.0/memtrace_ai.egg-info/top_level.txt +1 -0
- memtrace_ai-0.2.0/pyproject.toml +121 -0
- memtrace_ai-0.2.0/setup.cfg +4 -0
- memtrace_ai-0.2.0/tests/test_architecture.py +35 -0
- memtrace_ai-0.2.0/tests/test_auto_instrumentation.py +163 -0
- memtrace_ai-0.2.0/tests/test_concurrency.py +41 -0
- memtrace_ai-0.2.0/tests/test_config.py +75 -0
- memtrace_ai-0.2.0/tests/test_conformance.py +242 -0
- memtrace_ai-0.2.0/tests/test_container.py +59 -0
- memtrace_ai-0.2.0/tests/test_decorators.py +151 -0
- memtrace_ai-0.2.0/tests/test_domain.py +40 -0
- memtrace_ai-0.2.0/tests/test_eval_builtins.py +46 -0
- memtrace_ai-0.2.0/tests/test_eval_http_adapter.py +208 -0
- memtrace_ai-0.2.0/tests/test_experiment_runner.py +360 -0
- memtrace_ai-0.2.0/tests/test_integration.py +66 -0
- memtrace_ai-0.2.0/tests/test_langchain.py +124 -0
- memtrace_ai-0.2.0/tests/test_langgraph.py +50 -0
- memtrace_ai-0.2.0/tests/test_llm_judge.py +142 -0
- memtrace_ai-0.2.0/tests/test_openai_agents.py +150 -0
- memtrace_ai-0.2.0/tests/test_otel_adapters.py +69 -0
- memtrace_ai-0.2.0/tests/test_pii.py +165 -0
- memtrace_ai-0.2.0/tests/test_prompt_adapters.py +249 -0
- memtrace_ai-0.2.0/tests/test_prompt_fix.py +192 -0
- memtrace_ai-0.2.0/tests/test_prompt_frameworks.py +65 -0
- memtrace_ai-0.2.0/tests/test_prompt_langchain.py +142 -0
- memtrace_ai-0.2.0/tests/test_prompt_override.py +314 -0
- memtrace_ai-0.2.0/tests/test_prompts.py +453 -0
- memtrace_ai-0.2.0/tests/test_pydantic_ai_mcp.py +87 -0
- memtrace_ai-0.2.0/tests/test_redaction.py +86 -0
- memtrace_ai-0.2.0/tests/test_redteam.py +80 -0
- memtrace_ai-0.2.0/tests/test_retrieval_metrics.py +76 -0
- memtrace_ai-0.2.0/tests/test_sanitizing_exporter.py +211 -0
- memtrace_ai-0.2.0/tests/test_security.py +159 -0
- memtrace_ai-0.2.0/tests/test_span_normalization.py +90 -0
- memtrace_ai-0.2.0/tests/test_tracing_service.py +149 -0
- memtrace_ai-0.2.0/tests/test_user_feedback.py +63 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memtrace-ai
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: OpenTelemetry-based tracing and observability SDK for AI agents
|
|
5
|
+
Author: MemTrace Authors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/martia-es/memtrace
|
|
8
|
+
Project-URL: Documentation, https://github.com/martia-es/memtrace/tree/main/docs-site
|
|
9
|
+
Project-URL: Changelog, https://github.com/martia-es/memtrace/blob/main/sdk/python/CHANGELOG.md
|
|
10
|
+
Keywords: observability,tracing,opentelemetry,llm,ai-agents,langchain,pydantic-ai,openai-agents
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
26
|
+
Provides-Extra: grpc
|
|
27
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "grpc"
|
|
28
|
+
Provides-Extra: http
|
|
29
|
+
Provides-Extra: langchain
|
|
30
|
+
Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
|
|
31
|
+
Requires-Dist: langchain>=1.0.0; python_version >= "3.10" and extra == "langchain"
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-langchain<1,>=0.45.0; extra == "langchain"
|
|
33
|
+
Requires-Dist: wrapt<2; python_version < "3.10" and extra == "langchain"
|
|
34
|
+
Provides-Extra: pydantic-ai
|
|
35
|
+
Requires-Dist: pydantic-ai-slim>=1.0.0; extra == "pydantic-ai"
|
|
36
|
+
Provides-Extra: openai-agents
|
|
37
|
+
Requires-Dist: openai-agents>=0.2.0; extra == "openai-agents"
|
|
38
|
+
Provides-Extra: pii
|
|
39
|
+
Requires-Dist: presidio-analyzer>=2.2.35; extra == "pii"
|
|
40
|
+
Requires-Dist: presidio-anonymizer>=2.2.35; extra == "pii"
|
|
41
|
+
Requires-Dist: spacy>=3.7; extra == "pii"
|
|
42
|
+
Provides-Extra: security
|
|
43
|
+
Requires-Dist: detect-secrets>=1.5; extra == "security"
|
|
44
|
+
Requires-Dist: presidio-analyzer>=2.2.35; extra == "security"
|
|
45
|
+
Provides-Extra: eval
|
|
46
|
+
Requires-Dist: httpx>=0.27; extra == "eval"
|
|
47
|
+
Requires-Dist: anthropic>=0.30; extra == "eval"
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
51
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
52
|
+
Requires-Dist: langchain-core>=0.1.0; extra == "dev"
|
|
53
|
+
Requires-Dist: langchain>=1.0.0; python_version >= "3.10" and extra == "dev"
|
|
54
|
+
Requires-Dist: opentelemetry-instrumentation-langchain<1,>=0.45.0; extra == "dev"
|
|
55
|
+
Requires-Dist: wrapt<2; python_version < "3.10" and extra == "dev"
|
|
56
|
+
Requires-Dist: pydantic-ai-slim>=1.0.0; python_version >= "3.10" and extra == "dev"
|
|
57
|
+
Requires-Dist: openai-agents>=0.2.0; extra == "dev"
|
|
58
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
59
|
+
Requires-Dist: anthropic>=0.30; extra == "dev"
|
|
60
|
+
Requires-Dist: detect-secrets>=1.5; extra == "dev"
|
|
61
|
+
Requires-Dist: presidio-analyzer>=2.2.35; extra == "dev"
|
|
62
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
63
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
64
|
+
|
|
65
|
+
# memtrace-ai (Python SDK)
|
|
66
|
+
|
|
67
|
+
Instruments AI agents and exports traces over OTLP (OpenTelemetry GenAI conventions). It does not know the storage: it only talks to the OTel Collector.
|
|
68
|
+
|
|
69
|
+
> The package is named `memtrace-ai` (the Python import is `memtrace`). It is **not on PyPI yet**: install it from this repository.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install -e sdk/python # from the repository root; PyPI publication is pending
|
|
73
|
+
# extras (pip install -e "sdk/python[eval]"): grpc (OTLP/gRPC), langchain, eval, pydantic-ai, pii, dev
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from memtrace import init_tracer, trace_step, trace_llm_call, shutdown
|
|
78
|
+
|
|
79
|
+
init_tracer(service_name="my-agent") # or MEMTRACE_* environment variables
|
|
80
|
+
|
|
81
|
+
@trace_step(name="search", step_type="tool")
|
|
82
|
+
def search(q: str) -> str: ...
|
|
83
|
+
|
|
84
|
+
# LangChain / LangGraph - Option 1: manual callback handler
|
|
85
|
+
from memtrace.langchain import MemTraceCallbackHandler
|
|
86
|
+
chain.invoke(x, config={"callbacks": [MemTraceCallbackHandler()]})
|
|
87
|
+
|
|
88
|
+
# LangChain / LangGraph - Option 2: automatic (pip install 'memtrace-ai[langchain]')
|
|
89
|
+
# from memtrace.langchain import enable_langchain_instrumentation
|
|
90
|
+
# enable_langchain_instrumentation() # once, after init_tracer(); do not combine with option 1
|
|
91
|
+
|
|
92
|
+
# Pydantic AI - automatic (pip install 'memtrace-ai[pydantic-ai]')
|
|
93
|
+
# from memtrace.pydantic_ai import enable_pydantic_ai_instrumentation
|
|
94
|
+
# enable_pydantic_ai_instrumentation()
|
|
95
|
+
|
|
96
|
+
shutdown() # optional: the OTel SDK also flushes at exit; use flush() in short scripts / serverless
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Error policy**: the tracing calls never raise into your code. The only exceptions are the `enable_*_instrumentation()` functions, which raise `ImportError` if their extra is not installed.
|
|
100
|
+
|
|
101
|
+
**Conversations**: wrap each turn with `with memtrace.session("conversation-id"):` (or pass `thread_id` / `session_id` / `conversation_id` in LangChain's `metadata`) and the dashboard groups the traces into one conversation. The id must be stable across turns, unique per conversation and free of personal data (it shows up in URLs). Without it, every turn is a standalone trace. `session()` also applies to spans emitted by auto-instrumented libraries.
|
|
102
|
+
|
|
103
|
+
Spans created inside a `@trace_step` (or inside `with memtrace.session("id")`) hang from it, including those of the LangChain handler and of auto-instrumented libraries.
|
|
104
|
+
|
|
105
|
+
`trace_step`, `trace_step_context` and `trace_llm_call` accept `service=` (the value returned by `init_tracer`) to use an explicit tracer instead of the global one.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
| Variable | Default | |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `MEMTRACE_ENABLED` | `true` | `false` disables everything (no-op) |
|
|
112
|
+
| `MEMTRACE_SERVICE_NAME` | `default-agent` | `service.name` |
|
|
113
|
+
| `MEMTRACE_SERVICE_VERSION`, `MEMTRACE_ENVIRONMENT` | - | Resource attributes |
|
|
114
|
+
| `MEMTRACE_OTLP_ENDPOINT` | `http://localhost:4318` (http) / `:4317` (grpc) | any OTLP endpoint |
|
|
115
|
+
| `MEMTRACE_OTLP_PROTOCOL` | `http/protobuf` | or `grpc` (extra `grpc`) |
|
|
116
|
+
| `MEMTRACE_OTLP_HEADERS` | - | `k1=v1,k2=v2` |
|
|
117
|
+
| `MEMTRACE_CAPTURE_CONTENT` | `false` | store prompts/completions/arguments (ADR-004); also gates the auto-instrumentations |
|
|
118
|
+
| `MEMTRACE_MAX_CONTENT_LENGTH` | `16384` | truncation of captured content |
|
|
119
|
+
| `MEMTRACE_REDACT_KEYS` | - | extra key names to mask, on top of the built-in list (ADR-021) |
|
|
120
|
+
| `MEMTRACE_SPAN_TTL_SECONDS` | `3600` | closes orphaned spans |
|
|
121
|
+
| `MEMTRACE_MAX_ACTIVE_RUNS` | `10000` | cap on in-flight spans |
|
|
122
|
+
| `MEMTRACE_EXPORT_TIMEOUT_MS`, `MEMTRACE_BATCH_MAX_QUEUE_SIZE`, `MEMTRACE_BATCH_SCHEDULE_DELAY_MS`, `MEMTRACE_BATCH_MAX_EXPORT_SIZE` | 5000 / 2048 / 5000 / 512 | `BatchSpanProcessor` |
|
|
123
|
+
|
|
124
|
+
**Redaction**: secrets are masked in every exported span, by key name (`api_key`, `password`, …) and by shape (`sk-…`, JWTs, `Bearer …`, URL credentials). Personal data (emails, names, IDs) is anonymized only if you opt in with the `pii` extra: `init_tracer(redact=presidio_redactor(language="es"))` (`from memtrace.pii import presidio_redactor`; also `python -m spacy download <model>`), or with your own `init_tracer(redact=fn)`.
|
|
125
|
+
|
|
126
|
+
Self-signed TLS: `OTEL_EXPORTER_OTLP_CERTIFICATE` (standard OTel) with an `https://` endpoint.
|
|
127
|
+
|
|
128
|
+
## Architecture
|
|
129
|
+
|
|
130
|
+
Hexagonal (ADR-008): `domain` (pure) → `application` (`SpanPort` port + `TracingService`) ← `adapters/inbound` (decorators, LangChain, Pydantic AI) and `adapters/outbound` (OpenTelemetry, no-op). `dependency_container.py` is the composition root. The dependency rule is checked by `tests/test_architecture.py`. Auto-instrumentors receive MemTrace's tracer provider explicitly, never a global one, and every span passes through a redacting exporter before it leaves the process (ADR-021).
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install -e ".[dev]"
|
|
136
|
+
ruff check memtrace tests && mypy memtrace && pytest
|
|
137
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# memtrace-ai (Python SDK)
|
|
2
|
+
|
|
3
|
+
Instruments AI agents and exports traces over OTLP (OpenTelemetry GenAI conventions). It does not know the storage: it only talks to the OTel Collector.
|
|
4
|
+
|
|
5
|
+
> The package is named `memtrace-ai` (the Python import is `memtrace`). It is **not on PyPI yet**: install it from this repository.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e sdk/python # from the repository root; PyPI publication is pending
|
|
9
|
+
# extras (pip install -e "sdk/python[eval]"): grpc (OTLP/gRPC), langchain, eval, pydantic-ai, pii, dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from memtrace import init_tracer, trace_step, trace_llm_call, shutdown
|
|
14
|
+
|
|
15
|
+
init_tracer(service_name="my-agent") # or MEMTRACE_* environment variables
|
|
16
|
+
|
|
17
|
+
@trace_step(name="search", step_type="tool")
|
|
18
|
+
def search(q: str) -> str: ...
|
|
19
|
+
|
|
20
|
+
# LangChain / LangGraph - Option 1: manual callback handler
|
|
21
|
+
from memtrace.langchain import MemTraceCallbackHandler
|
|
22
|
+
chain.invoke(x, config={"callbacks": [MemTraceCallbackHandler()]})
|
|
23
|
+
|
|
24
|
+
# LangChain / LangGraph - Option 2: automatic (pip install 'memtrace-ai[langchain]')
|
|
25
|
+
# from memtrace.langchain import enable_langchain_instrumentation
|
|
26
|
+
# enable_langchain_instrumentation() # once, after init_tracer(); do not combine with option 1
|
|
27
|
+
|
|
28
|
+
# Pydantic AI - automatic (pip install 'memtrace-ai[pydantic-ai]')
|
|
29
|
+
# from memtrace.pydantic_ai import enable_pydantic_ai_instrumentation
|
|
30
|
+
# enable_pydantic_ai_instrumentation()
|
|
31
|
+
|
|
32
|
+
shutdown() # optional: the OTel SDK also flushes at exit; use flush() in short scripts / serverless
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Error policy**: the tracing calls never raise into your code. The only exceptions are the `enable_*_instrumentation()` functions, which raise `ImportError` if their extra is not installed.
|
|
36
|
+
|
|
37
|
+
**Conversations**: wrap each turn with `with memtrace.session("conversation-id"):` (or pass `thread_id` / `session_id` / `conversation_id` in LangChain's `metadata`) and the dashboard groups the traces into one conversation. The id must be stable across turns, unique per conversation and free of personal data (it shows up in URLs). Without it, every turn is a standalone trace. `session()` also applies to spans emitted by auto-instrumented libraries.
|
|
38
|
+
|
|
39
|
+
Spans created inside a `@trace_step` (or inside `with memtrace.session("id")`) hang from it, including those of the LangChain handler and of auto-instrumented libraries.
|
|
40
|
+
|
|
41
|
+
`trace_step`, `trace_step_context` and `trace_llm_call` accept `service=` (the value returned by `init_tracer`) to use an explicit tracer instead of the global one.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Variable | Default | |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `MEMTRACE_ENABLED` | `true` | `false` disables everything (no-op) |
|
|
48
|
+
| `MEMTRACE_SERVICE_NAME` | `default-agent` | `service.name` |
|
|
49
|
+
| `MEMTRACE_SERVICE_VERSION`, `MEMTRACE_ENVIRONMENT` | - | Resource attributes |
|
|
50
|
+
| `MEMTRACE_OTLP_ENDPOINT` | `http://localhost:4318` (http) / `:4317` (grpc) | any OTLP endpoint |
|
|
51
|
+
| `MEMTRACE_OTLP_PROTOCOL` | `http/protobuf` | or `grpc` (extra `grpc`) |
|
|
52
|
+
| `MEMTRACE_OTLP_HEADERS` | - | `k1=v1,k2=v2` |
|
|
53
|
+
| `MEMTRACE_CAPTURE_CONTENT` | `false` | store prompts/completions/arguments (ADR-004); also gates the auto-instrumentations |
|
|
54
|
+
| `MEMTRACE_MAX_CONTENT_LENGTH` | `16384` | truncation of captured content |
|
|
55
|
+
| `MEMTRACE_REDACT_KEYS` | - | extra key names to mask, on top of the built-in list (ADR-021) |
|
|
56
|
+
| `MEMTRACE_SPAN_TTL_SECONDS` | `3600` | closes orphaned spans |
|
|
57
|
+
| `MEMTRACE_MAX_ACTIVE_RUNS` | `10000` | cap on in-flight spans |
|
|
58
|
+
| `MEMTRACE_EXPORT_TIMEOUT_MS`, `MEMTRACE_BATCH_MAX_QUEUE_SIZE`, `MEMTRACE_BATCH_SCHEDULE_DELAY_MS`, `MEMTRACE_BATCH_MAX_EXPORT_SIZE` | 5000 / 2048 / 5000 / 512 | `BatchSpanProcessor` |
|
|
59
|
+
|
|
60
|
+
**Redaction**: secrets are masked in every exported span, by key name (`api_key`, `password`, …) and by shape (`sk-…`, JWTs, `Bearer …`, URL credentials). Personal data (emails, names, IDs) is anonymized only if you opt in with the `pii` extra: `init_tracer(redact=presidio_redactor(language="es"))` (`from memtrace.pii import presidio_redactor`; also `python -m spacy download <model>`), or with your own `init_tracer(redact=fn)`.
|
|
61
|
+
|
|
62
|
+
Self-signed TLS: `OTEL_EXPORTER_OTLP_CERTIFICATE` (standard OTel) with an `https://` endpoint.
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
Hexagonal (ADR-008): `domain` (pure) → `application` (`SpanPort` port + `TracingService`) ← `adapters/inbound` (decorators, LangChain, Pydantic AI) and `adapters/outbound` (OpenTelemetry, no-op). `dependency_container.py` is the composition root. The dependency rule is checked by `tests/test_architecture.py`. Auto-instrumentors receive MemTrace's tracer provider explicitly, never a global one, and every span passes through a redacting exporter before it leaves the process (ADR-021).
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
ruff check memtrace tests && mypy memtrace && pytest
|
|
73
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from memtrace import prompts
|
|
2
|
+
from memtrace._version import __version__
|
|
3
|
+
from memtrace.adapters.inbound.manual import record_retrieved_chunks, trace_llm_call, trace_step, trace_step_context
|
|
4
|
+
from memtrace.application.context import get_current_run_id, session, user
|
|
5
|
+
from memtrace.dependency_container import flush, init_tracer, shutdown
|
|
6
|
+
from memtrace.langchain import MemTraceCallbackHandler, enable_langchain_instrumentation
|
|
7
|
+
from memtrace.openai_agents import enable_openai_agents_instrumentation
|
|
8
|
+
from memtrace.pydantic_ai import enable_pydantic_ai_instrumentation
|
|
9
|
+
from memtrace.user_feedback import current_trace_id, feedback, retract_feedback
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"init_tracer",
|
|
14
|
+
"flush",
|
|
15
|
+
"shutdown",
|
|
16
|
+
"session",
|
|
17
|
+
"user",
|
|
18
|
+
"trace_step",
|
|
19
|
+
"trace_step_context",
|
|
20
|
+
"trace_llm_call",
|
|
21
|
+
"record_retrieved_chunks",
|
|
22
|
+
"get_current_run_id",
|
|
23
|
+
"current_trace_id",
|
|
24
|
+
"feedback",
|
|
25
|
+
"retract_feedback",
|
|
26
|
+
"prompts",
|
|
27
|
+
"MemTraceCallbackHandler",
|
|
28
|
+
"enable_langchain_instrumentation",
|
|
29
|
+
"enable_pydantic_ai_instrumentation",
|
|
30
|
+
"enable_openai_agents_instrumentation",
|
|
31
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ASGI middleware that lets MemTrace's playground test a prompt version in this agent (ADR-071).
|
|
2
|
+
|
|
3
|
+
MemTrace calls the agent's chat with the token of a short-lived override in the `x-memtrace-prompt-override` header. This
|
|
4
|
+
middleware reads it and, for the duration of that request only, makes `handle.compile()` ask MemTrace for the version the
|
|
5
|
+
token grants. It does nothing unless the agent opted in with `MEMTRACE_ALLOW_PROMPT_OVERRIDE=true`, and a token MemTrace
|
|
6
|
+
does not recognize changes nothing.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Any, Awaitable, Callable, MutableMapping
|
|
9
|
+
|
|
10
|
+
from memtrace.application.prompt_override import use_override
|
|
11
|
+
from memtrace.config import settings
|
|
12
|
+
from memtrace.domain.prompt import PROMPT_OVERRIDE_HEADER, is_override_token
|
|
13
|
+
|
|
14
|
+
Scope = MutableMapping[str, Any]
|
|
15
|
+
Receive = Callable[[], Awaitable[MutableMapping[str, Any]]]
|
|
16
|
+
Send = Callable[[MutableMapping[str, Any]], Awaitable[None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PromptOverrideMiddleware:
|
|
20
|
+
"""Pure ASGI (FastAPI, Starlette, Quart…): `app.add_middleware(PromptOverrideMiddleware)`."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, app: Callable[[Scope, Receive, Send], Awaitable[None]]) -> None:
|
|
23
|
+
self.app = app
|
|
24
|
+
self._header = PROMPT_OVERRIDE_HEADER.encode("latin-1")
|
|
25
|
+
|
|
26
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
27
|
+
token = self._token(scope) if scope.get("type") in ("http", "websocket") and settings.allow_prompt_override else None
|
|
28
|
+
if token is None:
|
|
29
|
+
await self.app(scope, receive, send)
|
|
30
|
+
return
|
|
31
|
+
with use_override(token):
|
|
32
|
+
await self.app(scope, receive, send)
|
|
33
|
+
|
|
34
|
+
def _token(self, scope: Scope) -> "str | None":
|
|
35
|
+
for name, value in scope.get("headers") or ():
|
|
36
|
+
if name.lower() == self._header:
|
|
37
|
+
candidate = value.decode("latin-1")
|
|
38
|
+
return candidate if is_override_token(candidate) else None
|
|
39
|
+
return None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Shared plumbing for "auto" instrumentors (LangChain, Pydantic AI…): each just points a
|
|
2
|
+
third-party library at the tracer provider configured by `init_tracer`, no callbacks needed.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from memtrace.application.tracing_service import TracingService
|
|
8
|
+
from memtrace.dependency_container import get_service
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("memtrace")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_tracer_provider(feature: str, service: Optional[TracingService] = None) -> Tuple[TracingService, Optional[Any]]:
|
|
14
|
+
"""The `(service, tracer_provider)` to instrument with, or `(service, None)` if tracing is off.
|
|
15
|
+
|
|
16
|
+
`feature`: human-readable name for the log line (e.g. "LangChain", "Pydantic AI").
|
|
17
|
+
"""
|
|
18
|
+
service = service or get_service()
|
|
19
|
+
provider = service.tracer_provider
|
|
20
|
+
if provider is None:
|
|
21
|
+
logger.info("[MemTrace] Tracing is off; %s instrumentation not enabled.", feature)
|
|
22
|
+
return service, provider
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from memtrace.adapters.inbound.langchain.auto import enable_langchain_instrumentation
|
|
2
|
+
from memtrace.adapters.inbound.langchain.callback import MemTraceCallbackHandler
|
|
3
|
+
from memtrace.adapters.inbound.langchain.prompt_middleware import prompt_middleware
|
|
4
|
+
|
|
5
|
+
__all__ = ["MemTraceCallbackHandler", "enable_langchain_instrumentation", "prompt_middleware"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Automatic LangChain / LangGraph instrumentation through OpenTelemetry's instrumentor."""
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from memtrace.adapters.inbound.auto_instrumentation import resolve_tracer_provider
|
|
7
|
+
from memtrace.application.tracing_service import TracingService
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("memtrace")
|
|
10
|
+
|
|
11
|
+
_INSTALL_HINT = "pip install 'memtrace-ai[langchain]'"
|
|
12
|
+
_CONTENT_ENV = "TRACELOOP_TRACE_CONTENT" # honored by opentelemetry-instrumentation-langchain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def enable_langchain_instrumentation(service: Optional[TracingService] = None) -> None:
|
|
16
|
+
"""Enables automatic LangChain / LangGraph instrumentation (no callbacks needed).
|
|
17
|
+
|
|
18
|
+
Requires: pip install 'memtrace-ai[langchain]'. Raises ImportError if it is missing.
|
|
19
|
+
|
|
20
|
+
Spans go to the tracer configured by `init_tracer`, and prompts/completions are only
|
|
21
|
+
recorded when MEMTRACE_CAPTURE_CONTENT=true. Safe to call more than once. It is an alternative to
|
|
22
|
+
MemTraceCallbackHandler; use one of them, not both, or every run is traced twice.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
26
|
+
except ImportError as exc:
|
|
27
|
+
raise ImportError(f"LangChain instrumentation requires: {_INSTALL_HINT}") from exc
|
|
28
|
+
|
|
29
|
+
# provider is process-wide, survives shutdown()/init_tracer()
|
|
30
|
+
service, provider = resolve_tracer_provider("LangChain", service)
|
|
31
|
+
if provider is None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Respect the privacy default of ADR-004 unless the user set the instrumentor's own switch
|
|
35
|
+
os.environ.setdefault(_CONTENT_ENV, "true" if service.captures_content else "false")
|
|
36
|
+
|
|
37
|
+
instrumentor = LangchainInstrumentor()
|
|
38
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
39
|
+
# Already bound to MemTrace's switchable provider, which follows init_tracer()/shutdown()
|
|
40
|
+
logger.debug("[MemTrace] LangChain auto-instrumentation was already enabled")
|
|
41
|
+
return
|
|
42
|
+
instrumentor.instrument(tracer_provider=provider)
|
|
43
|
+
logger.info("[MemTrace] LangChain auto-instrumentation enabled")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from memtrace.adapters.inbound.langchain.mapping import (
|
|
7
|
+
generation_dicts,
|
|
8
|
+
message_dicts,
|
|
9
|
+
request_to_call,
|
|
10
|
+
response_to_call,
|
|
11
|
+
step_name,
|
|
12
|
+
)
|
|
13
|
+
from memtrace.application.retrieval_capture import note_retrieved
|
|
14
|
+
from memtrace.application.tracing_service import TracingService
|
|
15
|
+
from memtrace.dependency_container import get_service
|
|
16
|
+
from memtrace.domain import semconv as sc
|
|
17
|
+
from memtrace.domain.attributes import llm_attributes, retrieved_chunks
|
|
18
|
+
from memtrace.domain.model import StepType
|
|
19
|
+
from memtrace.domain.serialization import to_json
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("memtrace")
|
|
22
|
+
|
|
23
|
+
_INSTALL_HINT = "pip install 'memtrace-ai[langchain]'"
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
27
|
+
except ImportError:
|
|
28
|
+
class BaseCallbackHandler: # type: ignore[no-redef]
|
|
29
|
+
"""Stand-in so importing this module never requires LangChain; instantiating does."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
32
|
+
raise ImportError(f"MemTraceCallbackHandler requires LangChain: {_INSTALL_HINT}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MemTraceCallbackHandler(BaseCallbackHandler):
|
|
36
|
+
"""LangChain / LangGraph callback handler.
|
|
37
|
+
|
|
38
|
+
Usage: `chain.invoke(x, config={"callbacks": [MemTraceCallbackHandler()]})`.
|
|
39
|
+
Inside a `@trace_step`, its spans hang from it.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, service: Optional[TracingService] = None) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
self._service = service or get_service()
|
|
45
|
+
|
|
46
|
+
# ----- helpers -----
|
|
47
|
+
|
|
48
|
+
def _context_attrs(self, tags: Optional[List[str]], metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
49
|
+
md = metadata or {}
|
|
50
|
+
return {
|
|
51
|
+
sc.MEMTRACE_TAGS: [str(t) for t in tags] if tags else None,
|
|
52
|
+
sc.MEMTRACE_METADATA: to_json(md, self._service.max_content_length) if md else None,
|
|
53
|
+
# `thread_id` is the LangGraph conversation identifier
|
|
54
|
+
sc.GEN_AI_CONVERSATION_ID: md.get("session_id") or md.get("conversation_id") or md.get("thread_id"),
|
|
55
|
+
sc.ENDUSER_ID: md.get("user_id"),
|
|
56
|
+
# LangGraph injects `langgraph_node`/`langgraph_step` into each run's metadata (ADR-011)
|
|
57
|
+
# the graph's own root run has no `langgraph_*` keys, only `ls_integration` (ADR-011)
|
|
58
|
+
sc.MEMTRACE_FRAMEWORK: "langgraph" if md.get("ls_integration") == "langgraph" or any(k.startswith("langgraph_") for k in md) else "langchain",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _start(self, name, step_type, run_id, parent_run_id, tags, metadata, extra=None) -> None:
|
|
62
|
+
attrs = {**self._context_attrs(tags, metadata), **(extra or {})}
|
|
63
|
+
self._service.start_run(name, step_type, run_id=run_id, parent_run_id=parent_run_id, attributes=attrs)
|
|
64
|
+
|
|
65
|
+
def _end(self, run_id, attrs=None, error=None) -> None:
|
|
66
|
+
self._service.end_run(run_id, error=error, attributes=attrs)
|
|
67
|
+
|
|
68
|
+
# ----- chains / agents -----
|
|
69
|
+
|
|
70
|
+
def on_chain_start(self, serialized, inputs, *, run_id, parent_run_id=None, tags=None, metadata=None, **kwargs) -> Any:
|
|
71
|
+
self._start(step_name(serialized, kwargs, "Chain"), StepType.CHAIN, run_id, parent_run_id, tags, metadata,
|
|
72
|
+
{sc.MEMTRACE_INPUT: self._service.capture(inputs)})
|
|
73
|
+
|
|
74
|
+
def on_chain_end(self, outputs, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
75
|
+
self._end(run_id, {sc.MEMTRACE_OUTPUT: self._service.capture(outputs)})
|
|
76
|
+
|
|
77
|
+
def on_chain_error(self, error, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
78
|
+
self._end(run_id, error=error)
|
|
79
|
+
|
|
80
|
+
# ----- tools -----
|
|
81
|
+
|
|
82
|
+
def on_tool_start(self, serialized, input_str, *, run_id, parent_run_id=None, tags=None, metadata=None, inputs=None, **kwargs) -> Any:
|
|
83
|
+
name = step_name(serialized, kwargs, "Tool")
|
|
84
|
+
self._start(name, StepType.TOOL, run_id, parent_run_id, tags, metadata, {
|
|
85
|
+
sc.GEN_AI_TOOL_NAME: name,
|
|
86
|
+
sc.GEN_AI_TOOL_CALL_ID: kwargs.get("tool_call_id"),
|
|
87
|
+
sc.GEN_AI_TOOL_CALL_ARGUMENTS: self._service.capture(inputs if inputs is not None else input_str),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
def on_tool_end(self, output, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
91
|
+
result = getattr(output, "content", output) # ToolMessage or plain value
|
|
92
|
+
self._end(run_id, {sc.GEN_AI_TOOL_CALL_RESULT: self._service.capture(result)})
|
|
93
|
+
|
|
94
|
+
def on_tool_error(self, error, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
95
|
+
self._end(run_id, error=error)
|
|
96
|
+
|
|
97
|
+
# ----- retrievers -----
|
|
98
|
+
|
|
99
|
+
def on_retriever_start(self, serialized, query, *, run_id, parent_run_id=None, tags=None, metadata=None, **kwargs) -> Any:
|
|
100
|
+
self._start(step_name(serialized, kwargs, "Retriever"), StepType.RETRIEVER, run_id, parent_run_id, tags, metadata,
|
|
101
|
+
{sc.MEMTRACE_INPUT: self._service.capture(query)})
|
|
102
|
+
|
|
103
|
+
def on_retriever_end(self, documents, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
104
|
+
chunks = retrieved_chunks(documents)
|
|
105
|
+
note_retrieved(chunks)
|
|
106
|
+
self._end(run_id, {
|
|
107
|
+
sc.MEMTRACE_RETRIEVER_DOCUMENTS: len(documents or []),
|
|
108
|
+
sc.MEMTRACE_RETRIEVER_CHUNKS: self._service.capture(chunks),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
def on_retriever_error(self, error, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
112
|
+
self._end(run_id, error=error)
|
|
113
|
+
|
|
114
|
+
# ----- LLMs -----
|
|
115
|
+
|
|
116
|
+
def _llm_start(self, serialized, run_id, parent_run_id, tags, metadata, invocation_params, kwargs, input_messages) -> None:
|
|
117
|
+
call = request_to_call(metadata, invocation_params or kwargs.get("invocation_params") or {})
|
|
118
|
+
base = step_name(serialized, {}, "llm")
|
|
119
|
+
self._start(
|
|
120
|
+
f"{base}: {call.model}" if call.model else base, StepType.LLM, run_id, parent_run_id, tags, metadata,
|
|
121
|
+
llm_attributes(call, self._service.capture(input_messages)),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def on_chat_model_start(self, serialized, messages, *, run_id, parent_run_id=None, tags=None, metadata=None, invocation_params=None, **kwargs) -> Any:
|
|
125
|
+
self._llm_start(serialized, run_id, parent_run_id, tags, metadata, invocation_params, kwargs,
|
|
126
|
+
message_dicts(messages) if self._service.captures_content else None)
|
|
127
|
+
|
|
128
|
+
def on_llm_start(self, serialized, prompts, *, run_id, parent_run_id=None, tags=None, metadata=None, invocation_params=None, **kwargs) -> Any:
|
|
129
|
+
self._llm_start(serialized, run_id, parent_run_id, tags, metadata, invocation_params, kwargs,
|
|
130
|
+
[{"role": "user", "content": p} for p in prompts])
|
|
131
|
+
|
|
132
|
+
def on_llm_end(self, response, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
133
|
+
try:
|
|
134
|
+
output = generation_dicts(response) if self._service.captures_content else None
|
|
135
|
+
attrs = llm_attributes(response_to_call(response), output_messages=self._service.capture(output))
|
|
136
|
+
except Exception as exc: # an unexpectedly shaped response must not lose the span
|
|
137
|
+
logger.warning("[MemTrace] Error extracting data from the LLM response: %s", exc)
|
|
138
|
+
attrs = None
|
|
139
|
+
self._end(run_id, attrs)
|
|
140
|
+
|
|
141
|
+
def on_llm_error(self, error, *, run_id, parent_run_id=None, **kwargs) -> Any:
|
|
142
|
+
self._end(run_id, error=error)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Pure translation of LangChain structures into domain concepts. No `TracingService`, no OTel."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from memtrace.domain.model import LlmCall
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get(obj: Any, key: str) -> Any:
|
|
10
|
+
return obj.get(key) if isinstance(obj, dict) else getattr(obj, key, None)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _as_int(val: Any) -> Optional[int]:
|
|
14
|
+
return val if isinstance(val, int) and not isinstance(val, bool) else None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _tokens(usage: Any) -> Tuple[Optional[int], Optional[int], Optional[int]]:
|
|
18
|
+
"""(input, output, total) from `usage_metadata` (LangChain), `token_usage` (OpenAI) or `usage` (Anthropic)."""
|
|
19
|
+
if not usage:
|
|
20
|
+
return None, None, None
|
|
21
|
+
inp = _as_int(_get(usage, "input_tokens"))
|
|
22
|
+
inp = inp if inp is not None else _as_int(_get(usage, "prompt_tokens"))
|
|
23
|
+
out = _as_int(_get(usage, "output_tokens"))
|
|
24
|
+
out = out if out is not None else _as_int(_get(usage, "completion_tokens"))
|
|
25
|
+
return inp, out, _as_int(_get(usage, "total_tokens"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def response_to_call(response: Any) -> LlmCall:
|
|
29
|
+
"""Response data of an `LLMResult` (duck-typed): messages first, `llm_output` as a fallback."""
|
|
30
|
+
inp = out = total = None
|
|
31
|
+
model = resp_id = None
|
|
32
|
+
finish: List[str] = []
|
|
33
|
+
|
|
34
|
+
for gens in getattr(response, "generations", None) or []:
|
|
35
|
+
for gen in gens or []:
|
|
36
|
+
msg = _get(gen, "message")
|
|
37
|
+
meta = _get(msg, "response_metadata") or {}
|
|
38
|
+
info = _get(gen, "generation_info") or {}
|
|
39
|
+
if inp is None and out is None:
|
|
40
|
+
inp, out, total = _tokens(_get(msg, "usage_metadata"))
|
|
41
|
+
model = model or _get(meta, "model_name") or _get(meta, "model")
|
|
42
|
+
resp_id = resp_id or _get(msg, "id") or _get(meta, "id")
|
|
43
|
+
reason = _get(info, "finish_reason") or _get(meta, "finish_reason") or _get(meta, "stop_reason")
|
|
44
|
+
if reason:
|
|
45
|
+
finish.append(str(reason))
|
|
46
|
+
|
|
47
|
+
llm_output = getattr(response, "llm_output", None) or {}
|
|
48
|
+
if inp is None and out is None:
|
|
49
|
+
inp, out, total = _tokens(_get(llm_output, "token_usage") or _get(llm_output, "usage"))
|
|
50
|
+
model = model or _get(llm_output, "model_name") or _get(llm_output, "model")
|
|
51
|
+
return LlmCall(
|
|
52
|
+
response_model=model, response_id=resp_id, finish_reasons=finish,
|
|
53
|
+
input_tokens=inp, output_tokens=out, total_tokens=total,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def request_to_call(metadata: Optional[Dict[str, Any]], params: Dict[str, Any]) -> LlmCall:
|
|
58
|
+
md = metadata or {}
|
|
59
|
+
return LlmCall(
|
|
60
|
+
provider=md.get("ls_provider") or params.get("_type"),
|
|
61
|
+
model=md.get("ls_model_name") or params.get("model_name") or params.get("model") or params.get("model_id"),
|
|
62
|
+
temperature=md.get("ls_temperature", params.get("temperature")),
|
|
63
|
+
max_tokens=md.get("ls_max_tokens", params.get("max_tokens") or params.get("max_tokens_to_sample")),
|
|
64
|
+
top_p=params.get("top_p"),
|
|
65
|
+
frequency_penalty=params.get("frequency_penalty"),
|
|
66
|
+
presence_penalty=params.get("presence_penalty"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def step_name(serialized: Optional[Dict[str, Any]], kwargs: Dict[str, Any], default: str) -> str:
|
|
71
|
+
"""`serialized` may be None (LangGraph, runnables); LangChain passes `name` as a kwarg."""
|
|
72
|
+
ser = serialized or {}
|
|
73
|
+
ident = ser.get("id")
|
|
74
|
+
return kwargs.get("name") or ser.get("name") or (ident[-1] if ident else None) or default
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _tool_calls_of(msg: Any) -> Any:
|
|
78
|
+
"""`tool_calls` of the model message (or of `additional_kwargs` for some providers)."""
|
|
79
|
+
calls = getattr(msg, "tool_calls", None)
|
|
80
|
+
if not calls:
|
|
81
|
+
calls = (getattr(msg, "additional_kwargs", None) or {}).get("tool_calls")
|
|
82
|
+
return calls or None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def message_dicts(messages: List[List[Any]]) -> List[Dict[str, Any]]:
|
|
86
|
+
out = []
|
|
87
|
+
for batch in messages:
|
|
88
|
+
for m in batch:
|
|
89
|
+
d: Dict[str, Any] = {
|
|
90
|
+
"role": getattr(m, "type", None) or getattr(m, "role", None) or "user",
|
|
91
|
+
"content": getattr(m, "content", str(m)),
|
|
92
|
+
}
|
|
93
|
+
calls = _tool_calls_of(m)
|
|
94
|
+
if calls:
|
|
95
|
+
d["tool_calls"] = calls
|
|
96
|
+
out.append(d)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generation_dicts(response: Any) -> List[Dict[str, Any]]:
|
|
101
|
+
out = []
|
|
102
|
+
for gens in getattr(response, "generations", None) or []:
|
|
103
|
+
for g in gens:
|
|
104
|
+
msg = getattr(g, "message", None)
|
|
105
|
+
d: Dict[str, Any] = {"role": getattr(msg, "type", "assistant"), "content": getattr(g, "text", "")}
|
|
106
|
+
calls = _tool_calls_of(msg)
|
|
107
|
+
if calls:
|
|
108
|
+
d["tool_calls"] = calls
|
|
109
|
+
out.append(d)
|
|
110
|
+
return out
|