git:20260317.53f05ea to git:20260407.50741a3

10 added, 11 removed. Audit A to A.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Generate the API client first (requires Node.js + Java; python/src/arthur_genai_client/ is gitignored)
./scripts/generate_openapi_client.sh generate python
- # Install (runs poetry install, which picks up the generated client)
+ # Install (runs uv sync, which picks up the generated client)
./scripts/generate_openapi_client.sh install python
# Test (run from arthur-observability-sdk/python/)
cd python
- poetry run pytest tests -v # all tests (includes install/wheel smoke tests)
- poetry run pytest tests/test_client.py::test_arthur_requires_task_or_service_name # single test
- poetry run pytest -k "telemetry" # match expression
+ uv run pytest tests -v # all tests (includes install/wheel smoke tests)
+ uv run pytest tests/test_client.py::test_arthur_requires_task_or_service_name # single test
+ uv run pytest -k "telemetry" # match expression
# Lint (all at once, run from arthur-observability-sdk/)
./scripts/lint.sh
# Individually (run from arthur-observability-sdk/python/)
cd python
- poetry run black src tests
- poetry run isort src tests --profile black
- poetry run autoflake --remove-all-unused-imports --in-place --recursive src tests
- poetry run mypy src/arthur_observability_sdk
+ uv run black src tests
+ uv run isort src tests --profile black
+ uv run autoflake --remove-all-unused-imports --in-place --recursive src tests
+ uv run mypy src/arthur_observability_sdk
# Build wheel (run from arthur-observability-sdk/)
./scripts/build_sdk_wheel.sh
# Regenerate arthur_genai_client from GenAI Engine OpenAPI spec
./scripts/generate_openapi_client.sh generate python
```
## Architecture
The SDK has three layers:
**`Arthur`** (`arthur.py`) — the user-facing entrypoint. Owns the OTel `TracerProvider`, the API client, and the 32 `instrument_*` methods. At least one of `task_id`, `task_name`, or `service_name` must be provided.
**`setup_telemetry`** (`telemetry.py`) — creates a `TracerProvider` with a `BatchSpanProcessor` backed by an OTLP HTTP exporter. Passes `Authorization: Bearer {api_key}` in the exporter headers. Called by `Arthur.__init__` when `enable_telemetry=True`. Registers the provider globally via `trace.set_tracer_provider()`.
**`ArthurAPIClient`** (`_client.py`) — thin wrapper around the generated `arthur_genai_client`. Exposes `get_prompt_by_version`, `get_prompt_by_tag`, `render_prompt`, and `resolve_task_id`. All `ApiException`s are converted to `ArthurAPIError(status_code, detail)`.
## Generated Client (`arthur_genai_client`)
This package is auto-generated from `genai-engine/staging.openapi.json` using OpenAPI Generator v7 and is **gitignored** — it is not committed to the repository. It must be generated before running tests or using the SDK locally. **Do not hand-edit it.** Regenerate whenever the GenAI Engine API changes (or after a fresh clone):
```bash
./scripts/generate_openapi_client.sh generate python
```
`ArthurAPIClient` uses only `PromptsApi` and `TasksApi`. Two calling styles are acceptable depending on the response shape:
**Use `*_with_http_info()` + `response.raw_data`** when the response contains fields that the generated Pydantic models cannot reliably serialise:
```python
response = self._prompts_api.some_method_with_http_info(...)
return json.loads(response.raw_data)
```
**Use the plain variant and access model attributes directly** when the response fields are simple scalars (strings, ints, bools) with no serialisation concerns:
```python
result = self._tasks_api.some_method(...)
return result.some_field
```
The generated Pydantic models have three known serialisation bugs — avoid `.model_dump()` or `.to_dict()` in either case, but direct attribute access on simple fields is safe:
1. **`anyOf` wrappers** (e.g. `Content`, which represents `str | List[OpenAIMessageItem]`) — `model_dump()` returns internal wrapper fields instead of the actual value.
2. **`datetime` fields** — `model_dump()` returns a `datetime` object, not a JSON-serialisable string.
3. **`Set` fields** — `model_dump()` returns a Python `set`, which `json.dumps` cannot serialise.
Use `*_with_http_info()` + `raw_data` whenever the response includes prompts, messages, or any `anyOf`/datetime/set fields. Use the plain variant only for responses that are purely simple scalars.
## Key Patterns
**Task ID resolution** is lazy: if `task_name` is given instead of `task_id`, the name is resolved to a UUID on the first prompt call via `_api_client.resolve_task_id()` and cached in `_resolved_task_id`.
**PROMPT spans** (`get_prompt`, `render_prompt`) are created manually, not via an instrumentor. Because of this they bypass the OpenInference span processor, so `_apply_openinference_context()` must be called explicitly to copy `session.id`, `user.id`, etc. from the OTel context onto the span.
**`render_prompt` span attributes** deliberately distinguish template from result:
- `llm.prompt_template` / `input.value` → original unrendered messages + variable values
- `output.value` → rendered messages with variables substituted
**Framework instrumentation** is all optional. Each `instrument_*` method calls `importlib.import_module()` and raises `ImportError` with the correct `pip install arthur-observability-sdk[{extra}]` hint if the optional dependency is absent.
**`enable_telemetry=False`** skips `TracerProvider` creation entirely — useful when only using prompt management without telemetry.
**Adding a new instrumentor** — four files must be updated together:
1. **`python/src/arthur_observability_sdk/arthur.py`** — add a method after the last `instrument_*` method:
```python
def instrument_my_framework(self) -> Any:
return self._instrument(
"openinference-instrumentation-my-framework", # PyPI package name
"my-framework", # extras key (used in pip install hint)
"openinference.instrumentation.my_framework", # importlib path
"MyFrameworkInstrumentor", # class name
)
```
- 2. **`python/pyproject.toml`** — three additions:
- - In `[tool.poetry.dependencies]`: `openinference-instrumentation-my-framework = { version = "*", optional = true }`
- - In `[tool.poetry.extras]`: `my-framework = ["openinference-instrumentation-my-framework"]`
+ 2. **`python/pyproject.toml`** — two additions:
+ - In `[project.optional-dependencies]`: `my-framework = ["openinference-instrumentation-my-framework"]`
- In the `all` extra list: `"openinference-instrumentation-my-framework"`
3. **`README.md`** — add a row to the "Supported instrumentors" table.
Verify the correct module path and class name from the package's PyPI page before adding.
## Testing
Unit tests mock at the `OTLPSpanExporter` boundary (patch `arthur_observability_sdk.telemetry.OTLPSpanExporter`) or use `InMemorySpanExporter` with a real `TracerProvider` for span-content assertions.
The helper pattern used throughout `test_prompt_management.py` and `test_context_helpers.py`:
```python
def _make_arthur_with_in_memory_spans(task_id=TASK_ID):
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
arthur = Arthur(task_id=task_id, enable_telemetry=False)
arthur._tracer_provider = provider # inject real provider
arthur._api_client._prompts_api = MagicMock()
arthur._api_client._tasks_api = MagicMock()
return arthur, exporter
```
Mock a generated-client response (set `raw_data` to JSON bytes, matching what `*_with_http_info()` returns):
```python
mock_resp = MagicMock()
mock_resp.raw_data = json.dumps(prompt_data).encode()
arthur._api_client._prompts_api.some_method_with_http_info.return_value = mock_resp
```
`test_install.py` builds the real wheel into a temp venv and runs subprocesses. It also starts an `HTTPServer` on port 0 (OS-assigned) that doubles as both an OTLP collector (`POST /v1/traces`) and a mock OpenAI API (`POST /v1/chat/completions`). These tests run as part of the standard `pytest tests` run.