Metadata-Version: 2.4
Name: mvk-sdk-py
Version: 1.3.16
Summary: MVK SDK for Python - Ultralight OpenTelemetry-compatible SDK for AI Observability
Author-email: Mavvrik AI <support@mavvrik.ai>
Maintainer-email: Mavvrik AI <support@mavvrik.ai>
License-Expression: Apache-2.0
Project-URL: Homepage, https://mavvrik.ai
Project-URL: Documentation, https://mavvrik.ai/docs
Project-URL: Support, https://mavvrik.ai/support
Project-URL: Source, https://github.com/cloudwizio/agentic-python-sdk
Project-URL: Issues, https://github.com/cloudwizio/agentic-python-sdk/issues
Project-URL: Changelog, https://github.com/cloudwizio/agentic-python-sdk/blob/master/CHANGELOG.md
Keywords: llm,observability,tracing,ai,ml,opentelemetry-compatible
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: wrapt<2.0.0,>=1.15.0
Requires-Dist: pydantic<3.0.0,>=2.0.0
Provides-Extra: compression
Requires-Dist: zstandard>=0.21.0; python_version < "3.13" and extra == "compression"
Requires-Dist: lz4>=4.0.0; python_version >= "3.13" and extra == "compression"
Provides-Extra: grpc
Requires-Dist: grpcio>=1.50.0; extra == "grpc"
Requires-Dist: opentelemetry-proto>=1.20.0; extra == "grpc"
Requires-Dist: protobuf>=3.20.0; extra == "grpc"
Provides-Extra: openai
Requires-Dist: openai>=1.0.0; extra == "openai"
Provides-Extra: gemini
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.15.0; extra == "anthropic"
Provides-Extra: bedrock
Requires-Dist: boto3>=1.26.0; extra == "bedrock"
Provides-Extra: vertexai
Requires-Dist: google-cloud-aiplatform>=1.38.0; extra == "vertexai"
Provides-Extra: azure-openai
Requires-Dist: openai>=1.0.0; extra == "azure-openai"
Provides-Extra: azure-ai
Requires-Dist: azure-ai-inference>=1.0.0b1; extra == "azure-ai"
Provides-Extra: anthropic-vertex
Requires-Dist: anthropic[vertex]>=0.15.0; extra == "anthropic-vertex"
Provides-Extra: google-genai
Requires-Dist: google-genai>=0.1.0; extra == "google-genai"
Provides-Extra: generativelanguage
Requires-Dist: google-ai-generativelanguage>=0.4.0; extra == "generativelanguage"
Provides-Extra: perplexity
Requires-Dist: perplexityai>=0.3.0; extra == "perplexity"
Provides-Extra: oci
Requires-Dist: oci>=2.0.0; extra == "oci"
Provides-Extra: agentcore
Requires-Dist: boto3>=1.26.0; extra == "agentcore"
Provides-Extra: pinecone
Requires-Dist: pinecone-client>=2.2.0; extra == "pinecone"
Provides-Extra: weaviate
Requires-Dist: weaviate-client>=3.0.0; extra == "weaviate"
Provides-Extra: chromadb
Requires-Dist: chromadb>=0.4.0; extra == "chromadb"
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.0.0; extra == "qdrant"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.0.30; extra == "langgraph"
Provides-Extra: crewai
Requires-Dist: crewai>=0.30.0; extra == "crewai"
Provides-Extra: agno
Requires-Dist: agno>=0.1.0; extra == "agno"
Provides-Extra: semantic-kernel
Requires-Dist: semantic-kernel>=1.0.0; extra == "semantic-kernel"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.0.1; extra == "openai-agents"
Provides-Extra: agent-framework
Requires-Dist: agent-framework>=1.0.0; extra == "agent-framework"
Provides-Extra: litellm
Requires-Dist: litellm>=1.0.0; extra == "litellm"
Provides-Extra: openrouter
Requires-Dist: openai>=1.0.0; extra == "openrouter"
Provides-Extra: genai
Requires-Dist: openai>=1.0.0; extra == "genai"
Requires-Dist: google-generativeai>=0.3.0; extra == "genai"
Requires-Dist: anthropic[vertex]>=0.15.0; extra == "genai"
Requires-Dist: boto3>=1.26.0; extra == "genai"
Requires-Dist: google-cloud-aiplatform>=1.38.0; extra == "genai"
Requires-Dist: azure-ai-inference>=1.0.0b1; extra == "genai"
Requires-Dist: google-genai>=0.1.0; extra == "genai"
Requires-Dist: google-ai-generativelanguage>=0.4.0; extra == "genai"
Requires-Dist: perplexityai>=0.3.0; extra == "genai"
Requires-Dist: oci>=2.0.0; extra == "genai"
Provides-Extra: vectordb
Requires-Dist: pinecone-client>=2.2.0; extra == "vectordb"
Requires-Dist: weaviate-client>=3.0.0; extra == "vectordb"
Requires-Dist: chromadb>=0.4.0; extra == "vectordb"
Requires-Dist: qdrant-client>=1.0.0; extra == "vectordb"
Provides-Extra: frameworks
Requires-Dist: langchain-core>=0.1.0; extra == "frameworks"
Requires-Dist: langgraph>=0.0.30; extra == "frameworks"
Requires-Dist: crewai>=0.30.0; extra == "frameworks"
Requires-Dist: agno>=0.1.0; extra == "frameworks"
Requires-Dist: semantic-kernel>=1.0.0; extra == "frameworks"
Requires-Dist: openai-agents>=0.0.1; extra == "frameworks"
Requires-Dist: agent-framework>=1.0.0; extra == "frameworks"
Provides-Extra: all
Requires-Dist: zstandard>=0.21.0; python_version < "3.13" and extra == "all"
Requires-Dist: lz4>=4.0.0; python_version >= "3.13" and extra == "all"
Requires-Dist: grpcio>=1.50.0; extra == "all"
Requires-Dist: opentelemetry-proto>=1.20.0; extra == "all"
Requires-Dist: protobuf>=3.20.0; extra == "all"
Requires-Dist: openai>=1.0.0; extra == "all"
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
Requires-Dist: anthropic[vertex]>=0.15.0; extra == "all"
Requires-Dist: boto3>=1.26.0; extra == "all"
Requires-Dist: google-cloud-aiplatform>=1.38.0; extra == "all"
Requires-Dist: azure-ai-inference>=1.0.0b1; extra == "all"
Requires-Dist: google-genai>=0.1.0; extra == "all"
Requires-Dist: google-ai-generativelanguage>=0.4.0; extra == "all"
Requires-Dist: perplexityai>=0.3.0; extra == "all"
Requires-Dist: oci>=2.0.0; extra == "all"
Requires-Dist: pinecone-client>=2.2.0; extra == "all"
Requires-Dist: weaviate-client>=3.0.0; extra == "all"
Requires-Dist: chromadb>=0.4.0; extra == "all"
Requires-Dist: qdrant-client>=1.0.0; extra == "all"
Requires-Dist: langchain-core>=0.1.0; extra == "all"
Requires-Dist: langgraph>=0.0.30; extra == "all"
Requires-Dist: crewai>=0.30.0; extra == "all"
Requires-Dist: agno>=0.1.0; extra == "all"
Requires-Dist: semantic-kernel>=1.0.0; extra == "all"
Requires-Dist: openai-agents>=0.0.1; extra == "all"
Requires-Dist: agent-framework>=1.0.0; extra == "all"
Requires-Dist: litellm>=1.0.0; extra == "all"
Requires-Dist: httpx>=0.24.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
Requires-Dist: black==26.3.1; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Requires-Dist: pytest-xdist>=3.5.0; extra == "test"
Requires-Dist: responses>=0.23.0; extra == "test"
Requires-Dist: grpcio>=1.50.0; extra == "test"
Provides-Extra: stress
Requires-Dist: pytest-html>=4.0.0; extra == "stress"
Requires-Dist: fastapi>=0.104.0; extra == "stress"
Requires-Dist: uvicorn>=0.24.0; extra == "stress"
Requires-Dist: sse-starlette>=1.8.0; extra == "stress"
Requires-Dist: httpx>=0.24.0; extra == "stress"
Requires-Dist: psutil>=5.9.0; extra == "stress"
Requires-Dist: pyyaml>=6.0.0; extra == "stress"
Requires-Dist: plotly>=5.18.0; extra == "stress"
Requires-Dist: numpy>=1.24.0; extra == "stress"
Dynamic: license-file

<h1 align="center">MVK SDK</h1>

<p align="center">
  <a href="https://pypi.org/project/mvk-sdk-py/"><img src="https://img.shields.io/pypi/v/mvk-sdk-py?color=blue&label=PyPI" alt="PyPI version"></a>
  <a href="https://pypi.org/project/mvk-sdk-py/"><img src="https://img.shields.io/pypi/pyversions/mvk-sdk-py" alt="Python versions"></a>
  <a href="https://github.com/cloudwizio/agentic-python-sdk"><img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/pkorrakuti-mvrk/ac02cf5feef9f1029fb980a1bf8148c5/raw/agentic-python-sdk-coverage.json" alt="Coverage"></a>
</p>

<p align="center">
  <strong>Pure Python</strong> &nbsp;•&nbsp; <strong>Serverless Ready</strong> &nbsp;•&nbsp; <strong>Zero Breaking Guarantee</strong> &nbsp;•&nbsp; <strong>OTEL-style Smart Auto-instrumentation</strong> &nbsp;•&nbsp; <strong>W3C TraceContext</strong>
</p>

The `MVK SDK` is a production-grade telemetry SDK with serverless auto-detection, simplified MVK-only configuration, and a non-breaking guarantee. Deploy with confidence using DIRECT mode (straight to MVK) or COLLECTOR mode (via OTEL Collector). The SDK uses OTEL-style smart auto-instrumentation with wrapt for immediate and lazy loading of AI providers and vector databases with zero risk to your application.

> **❗ Important — Zero Breaking Guarantee.** Instrumentation never raises into your code. Any
> SDK-internal error is caught, logged, and swallowed — your application's
> execution flow is never interrupted.

## Table of Contents

- [Table of Contents](#table-of-contents)
- [1. Installation](#1-installation)
- [2. Configuration \& Instrumentation](#2-configuration--instrumentation)
  - [2.1 Configuration Reference — all values](#21-configuration-reference--all-values)
  - [2.2 Precedence — defaults \< parameters \< environment](#22-precedence--defaults--parameters--environment)
  - [2.3 Secrets \& environment (recommended)](#23-secrets--environment-recommended)
  - [2.4 Logging](#24-logging)
- [3. `@mvk.context()` — enriching spans (decorator vs. context manager)](#3-mvkcontext--enriching-spans-decorator-vs-context-manager)
  - [Dual purpose — when to use which](#dual-purpose--when-to-use-which)
  - [Why it matters](#why-it-matters)
  - [Signature](#signature)
  - [Examples](#examples)
  - [Notes \& limits](#notes--limits)
- [4. Cost signals — `@mvk.signal()`, `create_signal()` \& metered usage](#4-cost-signals--mvksignal-create_signal--metered-usage)
  - [`@mvk.signal()` — decorate a function as a named span](#mvksignal--decorate-a-function-as-a-named-span)
  - [`mvk.create_signal()` — manual spans with explicit `step_type`](#mvkcreate_signal--manual-spans-with-explicit-step_type)
  - [`mvk.add_metered_usage()` — attach billable quantities](#mvkadd_metered_usage--attach-billable-quantities)
  - [End-to-end example — tools, custom costs, and auto-traced LLM together](#end-to-end-example--tools-custom-costs-and-auto-traced-llm-together)
  - [Notes \& limits](#notes--limits-1)
- [5. Outcome signals — `mvk.record_outcome()`](#5-outcome-signals--mvkrecord_outcome)
  - [Signature](#signature-1)
  - [Examples](#examples-1)
  - [What it emits](#what-it-emits)
  - [Notes \& limits](#notes--limits-2)
- [6. Reference](#6-reference)
  - [Context inheritance (Nearest Wins)](#context-inheritance-nearest-wins)
  - [Tag validation](#tag-validation)
  - [Architecture](#architecture)
  - [Supported integrations](#supported-integrations)
  - [Performance characteristics](#performance-characteristics)
  - [Serverless deployment](#serverless-deployment)
- [7. Support](#7-support)

---

## 1. Installation

```bash
pip install mvk-sdk-py
```

Install providers and protocols as optional extras:

> **💡 Tip.** Quote the package name when installing extras. Shells like zsh treat
> unquoted square brackets as glob patterns and the install will fail.

```bash
# AI providers
pip install "mvk-sdk-py[genai]"             # All AI providers
pip install "mvk-sdk-py[openai]"            # OpenAI only
pip install "mvk-sdk-py[anthropic]"         # Anthropic only
pip install "mvk-sdk-py[gemini]"            # Google Gemini only
pip install "mvk-sdk-py[bedrock]"           # AWS Bedrock only
pip install "mvk-sdk-py[vertexai]"          # Vertex AI only
pip install "mvk-sdk-py[azure-openai]"      # Azure OpenAI (OpenAI SDK) only
pip install "mvk-sdk-py[azure-ai]"          # Azure native AI SDK only
pip install "mvk-sdk-py[anthropic-vertex]"  # Claude on Vertex AI only
pip install "mvk-sdk-py[google-genai]"      # google.genai SDK only
pip install "mvk-sdk-py[perplexity]"        # Perplexity only
pip install "mvk-sdk-py[oci]"               # OCI Generative AI only

# AI frameworks
pip install "mvk-sdk-py[frameworks]"        # All AI frameworks
pip install "mvk-sdk-py[langchain]"         # LangChain only
pip install "mvk-sdk-py[langgraph]"         # LangGraph only
pip install "mvk-sdk-py[crewai]"            # CrewAI only
pip install "mvk-sdk-py[agno]"              # Agno only
pip install "mvk-sdk-py[semantic-kernel]"   # Semantic Kernel only
pip install "mvk-sdk-py[openai-agents]"     # OpenAI Agents SDK only
pip install "mvk-sdk-py[agent-framework]"   # Microsoft Agent Framework only

# Proxies / routers
pip install "mvk-sdk-py[litellm]"           # LiteLLM only
pip install "mvk-sdk-py[openrouter]"        # OpenRouter only

# Vector databases
pip install "mvk-sdk-py[vectordb]"          # All vector DBs
pip install "mvk-sdk-py[pinecone]"          # Pinecone only
pip install "mvk-sdk-py[weaviate]"          # Weaviate only
pip install "mvk-sdk-py[chromadb]"          # ChromaDB only
pip install "mvk-sdk-py[qdrant]"            # Qdrant only

# Export protocols
pip install "mvk-sdk-py[grpc]"              # gRPC exporter
pip install "mvk-sdk-py[compression]"       # Compression support

# Everything
pip install "mvk-sdk-py[all]"
```

---

## 2. Configuration & Instrumentation

`mvk.instrument()` is the single entry point — call it **once per process**,
before your provider calls. All settings are passed as **top-level keyword
arguments** (no `config={...}` wrapper); grouped settings (`exporter`,
`batching`, `failed_batch_disk`, `logging`, `serverless`, `wrappers`) are
nested dicts. Every parameter also has a `MVK_*` environment-variable
equivalent.

**Minimal setup:**

```python
import mvk_sdk as mvk

mvk.instrument(
    agent_id="agent-123",    # Required (or MVK_AGENT_ID)
    api_key="mvk_...",       # Required for DIRECT mode (or MVK_API_KEY)
    tenant_id="tenant-123",  # Required for DIRECT mode (or MVK_TENANT_ID)
)

# That's it — all provider calls are now traced
import openai
response = openai.ChatCompletion.create(...)  # Automatically traced
```

**DIRECT mode** (default) sends traces straight to Mavvrik; the endpoint is
auto-set if omitted. **COLLECTOR mode** sends to your own OTEL Collector:

```python
# DIRECT — to the Mavvrik backend
mvk.instrument(
    agent_id="agent-123", api_key="mvk_...", tenant_id="tenant-123",
    exporter={"mode": "DIRECT", "type": "otlp_http"},  # endpoint auto-set
)

# COLLECTOR — to your OTEL Collector
mvk.instrument(
    agent_id="agent-123",
    exporter={"mode": "COLLECTOR", "type": "otlp_grpc", "endpoint": "localhost:4317"},
)
```

### 2.1 Configuration Reference — all values

Every setting below can be supplied as an `instrument()` parameter **or** the
matching environment variable. Nested `instrument()` keys are shown as
`group.key` (e.g. `exporter.timeout` → `exporter={"timeout": 30}`). 🔒 marks
sensitive values — see [§2.3](#23-secrets--environment-recommended).

**Core / identity**

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `agent_id`   | `MVK_AGENT_ID`   | — | **Required.** Agent identifier |
| `api_key` 🔒 | `MVK_API_KEY`    | — | **Required in DIRECT mode** |
| `tenant_id`  | `MVK_TENANT_ID`  | — | **Required in DIRECT mode**; sent as `X-Tenant-ID` |
| `enabled`    | `MVK_ENABLED`    | `true` | Master on/off switch |

**Exporter / transport** (`exporter={...}`)

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `exporter.mode`         | `MVK_MODE`                  | `DIRECT` | `DIRECT` or `COLLECTOR` |
| `exporter.type`         | `MVK_EXPORTER_TYPE`         | `otlp_http` | `otlp_http`, `otlp_grpc`, `console`, `file` |
| `exporter.endpoint`     | `MVK_ENDPOINT`              | auto | URL or `host:port` (auto-set in DIRECT) |
| `exporter.headers`      | `MVK_HEADERS`               | — | JSON string or `k1=v1,k2=v2` |
| `exporter.insecure`     | `MVK_EXPORTER_INSECURE`     | `false` | HTTP instead of HTTPS (local only) |
| `exporter.compression`  | `MVK_EXPORTER_COMPRESSION`  | `gzip` | `gzip` or `none` |
| `exporter.timeout`      | `MVK_EXPORTER_TIMEOUT`      | `10` | Seconds (1–300) |
| `exporter.max_retries`  | `MVK_EXPORTER_MAX_RETRIES`  | `6` | 0–20 |
| `exporter.retry_timeout`| `MVK_EXPORTER_RETRY_TIMEOUT`| `60` | Total retry seconds (1–600) |
| `exporter.file_path`    | `MVK_EXPORTER_FILE_PATH`    | — | Directory for `file` exporter |
| `exporter.format`       | `MVK_EXPORTER_FORMAT`       | `simple` | `simple` or `json` (console/file) |

**Batching** (`batching={...}`)

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `batching.max_items`       | `MVK_BATCH_MAX_ITEMS`       | `2000` | Spans/batch (1–10000) |
| `batching.max_bytes`       | `MVK_BATCH_MAX_BYTES`       | `2097152` | 2 MiB (1 KB–10 MB) |
| `batching.max_interval_ms` | `MVK_BATCH_MAX_INTERVAL_MS` | `3000` | ms (100–60000) |

**Failed-batch disk recovery** (`failed_batch_disk={...}`)

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `failed_batch_disk.enabled`        | `MVK_FAILED_BATCH_DISK_ENABLED`        | `false` | |
| `failed_batch_disk.path`           | `MVK_FAILED_BATCH_DISK_PATH`           | — | Required when enabled |
| `failed_batch_disk.max_size_mb`    | `MVK_FAILED_BATCH_DISK_MAX_SIZE_MB`    | `1000` | 10–100000 |
| `failed_batch_disk.retry_interval` | `MVK_FAILED_BATCH_DISK_RETRY_INTERVAL` | `60` | Seconds (1–3600) |

**Logging** (`logging={...}`) — see [§2.4](#24-logging)

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `logging.level`             | `MVK_LOG_LEVEL`          | `OFF` | `OFF`, `INFO`, `DEBUG` |
| `logging.wrapper_level`     | `MVK_WRAPPER_LOG_LEVEL`  | — | Provider wrappers only |
| `logging.prompts_responses` 🔒 | `MVK_LOG_PROMPTS_RESPONSES` | `false` | Logs raw prompts/responses |
| `logging.prompts_storage_mode` | `MVK_PROMPTS_STORAGE_MODE` | `truncate` | `truncate`, `compress`, `envelope` |
| `logging.prompts_max_length` | `MVK_PROMPTS_MAX_LENGTH` | `1000` | Truncation length (100–100000) |
| `logging.prompts_masking`   | `MVK_PROMPTS_MASKING`    | `true` | Mask PII/PHI/PCI (on by default) |

**Validation, tags & wrappers**

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `strict_validation`        | `MVK_STRICT_VALIDATION` | `false` | Raise on schema violations |
| `tag_limit`                | `MVK_TAG_LIMIT`         | `10` | Max tags/span (1–10) |
| `tags={"env": "prod"}`     | `MVK_TAG_ENV=prod`      | — | One env var **per tag**: `MVK_TAG_<KEY>` |
| `wrappers={"include": [...]}` | `MVK_WRAPPERS`       | `genai,vectordb` | Comma-separated in env |
| `wrappers.http.exclusions` | `MVK_HTTP_EXCLUSIONS`   | — | JSON array or comma-separated |

**Serverless & diagnostics**

| `instrument()` | Environment variable | Default | Notes |
| -------------- | -------------------- | ------- | ----- |
| `serverless.force`        | `MVK_SERVERLESS`              | `false` | Force serverless optimizations |
| `system_alerts_enabled`   | `MVK_SYSTEM_ALERTS_ENABLED`  | `true`  | SDK self-diagnostics telemetry |
| `collect_cloud_metadata`  | `MVK_COLLECT_CLOUD_METADATA` | `true`  | Auto-skipped in local/CI |
| `auto_middleware`         | `MVK_AUTO_MIDDLEWARE`        | `true`  | Auto W3C context for Flask/FastAPI/Django |

**Full `instrument()` call with every group:**

```python
mvk.instrument(
    agent_id="agent-123",
    api_key="mvk_...",        # Required in DIRECT mode
    tenant_id="tenant-123",   # Required in DIRECT mode
    exporter={
        "mode": "DIRECT",          # DIRECT or COLLECTOR
        "type": "otlp_http",       # otlp_http | otlp_grpc | console | file
        "endpoint": "https://ingest.mavvrik.ai/v1/traces",  # auto-set if omitted
        "timeout": 10,
        "max_retries": 6,
        "compression": "gzip",     # gzip | none
    },
    batching={"max_items": 2000, "max_bytes": 2097152, "max_interval_ms": 3000},
    failed_batch_disk={
        "enabled": False,
        "path": "/tmp/mvk/failed_batches",  # required when enabled
        "max_size_mb": 1000,
        "retry_interval": 60,
    },
    logging={"level": "INFO", "prompts_responses": False},  # ⚠️ see §2.4
    serverless={"force": False},
    strict_validation=False,
    wrappers={"include": ["genai", "vectordb"]},
    tags={"env": "prod"},
)
```

### 2.2 Precedence — defaults < parameters < environment

When the same setting is supplied in more than one place, the SDK resolves it
in this order (lowest → highest priority):

```
Schema default  <  instrument() parameter  <  MVK_* environment variable
```

**Environment variables always win.** This is deliberate: it lets ops/platform
teams override application code at deploy time without a code change. Example:

```python
mvk.instrument(agent_id="agent-123", logging={"level": "INFO"})
```

```bash
export MVK_LOG_LEVEL=DEBUG   # effective level is DEBUG, not INFO
```

Tags merge the same way — `MVK_TAG_<KEY>` overrides the same key passed in
`tags={...}`, while other keys are preserved.

### 2.3 Secrets & environment (recommended)

The only **secret** value is `api_key` (`MVK_API_KEY`); `tenant_id` is
sensitive-but-identifying. **Do not hard-code these in source.** Because
environment variables take precedence ([§2.2](#22-precedence--defaults--parameters--environment)),
the recommended production pattern is:

- Keep `agent_id` and non-secret config in code or env as convenient.
- Inject `MVK_API_KEY` (and `MVK_TENANT_ID`) from a **secrets manager** at
  runtime — AWS Secrets Manager / SSM, GCP Secret Manager, Azure Key Vault, or
  Vault — never from a committed `.env` or the image.

```python
import mvk_sdk as mvk

# api_key / tenant_id resolved from MVK_API_KEY / MVK_TENANT_ID,
# which your platform injects from the secret manager.
mvk.instrument(agent_id="agent-123")
```

```bash
# Example: hydrate env from a secret manager before the process starts
export MVK_API_KEY="$(aws secretsmanager get-secret-value \
  --secret-id mvk/api-key --query SecretString --output text)"
export MVK_TENANT_ID=tenant-123
```

> **⚠️ Warning.** Avoid passing `api_key=` as a literal in code or logging it. Keep
> `logging.prompts_responses` disabled in production — it logs raw LLM
> prompts/responses, which may contain regulated data.

### 2.4 Logging

Logging is **off by default** (`level: "OFF"`) for security and performance.
When enabled, the SDK auto-configures Python logging — no manual setup needed.

```python
mvk.instrument(agent_id="agent-123", api_key="mvk_...",
               logging={"level": "DEBUG"})           # controls all components
```

`wrapper_level` overrides the level for the AI provider wrappers only:

```python
mvk.instrument(agent_id="agent-123", api_key="mvk_...",
               logging={"level": "INFO", "wrapper_level": "DEBUG"})
```

```bash
export MVK_LOG_LEVEL=INFO
export MVK_WRAPPER_LOG_LEVEL=DEBUG
```

**Levels**: `OFF` (default) · `INFO` (lifecycle — config, batch flushes,
shutdown) · `DEBUG` (per-span detail — instrumentation, token extraction,
queueing; enabling DEBUG also enables INFO).

Log lines are formatted as `TIMESTAMP - LOGGER - LEVEL - MESSAGE`, where the
timestamp is `YYYY-MM-DD HH:MM:SS,mmm` and loggers are namespaced `mvk.*` (e.g.
`mvk.instrumentation.openai`, `mvk.processors.writer`). Example `DEBUG` output
for a single auto-traced OpenAI chat completion:

```
2026-06-29 18:37:28,807 - mvk.instrumentation.wrapper_base - DEBUG - Wrapper invoked for span_name=openai.chat.completion, method=create, instance=Completions
2026-06-29 18:37:29,044 - mvk.instrumentation.wrapper_base - DEBUG - Sync wrapper: Created span openai.chat.completion with trace_id=6eeb9c79..., operation_subtype=sync
2026-06-29 18:37:29,061 - mvk.instrumentation.openai - DEBUG - [OpenAI Token Extraction] Extracted tokens - prompt: 12, completion: 8, total: 20
2026-06-29 18:37:29,061 - mvk.processors.writer - DEBUG - Span queued for export: openai.chat.completion (queue size: 1)
2026-06-29 18:37:29,062 - mvk.processors.writer - INFO - Flushing batch of 1 span(s) to exporter
```

---

## 3. `@mvk.context()` — enriching spans (decorator vs. context manager)

`mvk.instrument()` only knows _process-wide_ defaults (agent, environment,
global tags). It cannot know **who** the current user is, **which** session a
request belongs to, or **what** business workflow is executing. `mvk.context()`
layers that runtime identity and business context onto every span produced
inside its scope — auto-traced LLM calls, vector DB queries, and manual
`@mvk.signal` / `create_signal()` operations all inherit it automatically.

### Dual purpose — when to use which

`mvk.context()` has the **same signature** in both forms; pick by scope:

| Form | Use when |
| ---- | -------- |
| **Context manager** — `with mvk.context(...):` | Scoping a **block** at runtime: an HTTP handler, message consumer, or job; per-request identity that varies per call; or wrapping an **async generator** (the decorator does not support async generators). |
| **Decorator** — `@mvk.context(...)` | A **whole function** should share one (usually static) context. Works on sync functions and `async def` coroutines (context is preserved across `await` within the same task). |

### Why it matters

- **Cost attribution** — `customer_id`, `application_id`, and `tags` let the
  Mavvrik backend split AI spend by tenant, product, team, or feature for
  chargeback / showback reporting.
- **Trace correlation** — `user_id`, `session_id`, and `request_id` stitch
  multi-step agent workflows back to a single end-user request.
- **Business grouping** — `use_case` groups related operations under a named
  business process (e.g. `"fraud_detection_v2"`, `"customer_onboarding"`).
- **Distributed tracing** — `traceparent` / `tracestate` accept incoming W3C
  Trace Context headers so spans link across service boundaries.
- **Nearest-wins inheritance** — context layered closer to the call overrides
  outer layers (see [Context inheritance](#context-inheritance-nearest-wins)).

### Signature

```python
mvk.context(
    name: str | None = None,                 # Logical context name
    user_id: str | None = None,              # End-user identity
    session_id: str | None = None,           # Session / conversation ID
    application_id: str | None = None,       # Calling application
    customer_id: str | None = None,          # Your customer (multi-tenant)
    request_id: str | None = None,           # External request correlation ID
    region: str | None = None,               # Geographic region
    cloud_provider_code: str | None = None,  # e.g. "aws", "gcp", "azure"
    use_case: str | None = None,             # Business workflow (snake_case)
    outcome_id: str | None = None,           # Stamps mvk.outcome.id on every child span
    tags: dict[str, str] | None = None,      # Up to 10 custom key/value tags
    traceparent: str | None = None,          # W3C traceparent header
    tracestate: str | None = None,           # W3C tracestate header
)
```

All parameters are optional — pass only what is meaningful at the call site.
Unknown keyword arguments are logged and ignored; the SDK never raises into
client code.

### Examples

**Context manager — per-request scope (HTTP handler, consumer, job):**

```python
import mvk_sdk as mvk
import openai

mvk.instrument(agent_id="agent-123", api_key="mvk_...", tenant_id="tenant-123")

def handle_chat(request):
    with mvk.context(
        user_id=request.user.id,
        session_id=request.session.id,
        customer_id=request.tenant.id,
        use_case="customer_support_chat",
        tags={"feature": "chat", "tier": "premium"},
    ):
        # Every span below inherits the identity, use_case, and tags above
        return openai.ChatCompletion.create(...)
```

**Decorator — function-scoped enrichment:**

```python
@mvk.context(region="us-east-1", use_case="document_search",
             tags={"service": "search-api"})
def search_handler(query: str):
    return run_pipeline(query)   # all LLM / vector DB calls inherit the context
```

**Continuing a distributed trace from an upstream service:**

```python
def grpc_handler(request, metadata):
    with mvk.context(
        traceparent=metadata.get("traceparent"),
        tracestate=metadata.get("tracestate"),
        request_id=metadata.get("x-request-id"),
    ):
        return process(request)
```

**Nesting — innermost wins for scalars, tags merge:**

```python
with mvk.context(customer_id="acme", tags={"env": "prod"}):
    with mvk.context(user_id="u-42", tags={"feature": "summarize"}):
        # Span sees: customer_id="acme", user_id="u-42",
        #            tags={"env": "prod", "feature": "summarize"}
        summarize(doc)
```

**Clearing an inherited attribute — pass an empty string:**

```python
with mvk.context(user_id="u-42"):
    with mvk.context(user_id=""):
        run_background_job()   # user_id removed from spans in this inner block
```

### Notes & limits

- **Tag cap:** maximum **10 tags per span** after all levels merge. Excess tags
  are dropped (or raise if `strict_validation=True`). See
  [Tag validation](#tag-validation) for key/value rules.
- **Per-request headers override everything:** incoming `x-mvk-*` headers on a
  single call take precedence over decorator and context-manager values for
  that call only.
- **Unknown kwargs are non-fatal:** a misspelled parameter (e.g. `userid=...`)
  is logged and ignored. The SDK never raises into client code.
- **All attributes are prefixed `mvk.*` on the wire:** e.g. `user_id` becomes
  `mvk.user_id`, and `tags={"team": "ml"}` becomes `mvk.tags.team`.

---

## 4. Cost signals — `@mvk.signal()`, `create_signal()` & metered usage

Auto-instrumentation covers LLM providers, vector DBs, and supported frameworks
out of the box. For **everything else** — wrapping a business workflow under a
single named span, tracing an in-house LLM gateway, attributing custom tool /
storage / API costs — the SDK exposes three manual APIs:

| API | Purpose | Use as |
| --- | ------- | ------ |
| `@mvk.signal()`           | Wrap a function so its entire execution becomes one parent span; child auto-traced calls nest under it | Decorator |
| `mvk.create_signal()`     | Create a span for a code block with explicit `step_type` / `operation` | Context manager |
| `mvk.add_metered_usage()` | Attach billable quantity metrics (pages, API calls, bytes, custom tokens) to the current span | Plain function call |

Identity, session, customer, `outcome_id`, and tags propagate to these spans
automatically from [`mvk.context()`](#3-mvkcontext--enriching-spans-decorator-vs-context-manager).

### `@mvk.signal()` — decorate a function as a named span

The function name (or `name=...`) becomes the span name; every auto-traced call
inside (LLM, vector DB, HTTP) and every `create_signal()` block becomes a child
span. A `run_id` is auto-generated at the root signal and propagated to all
descendants (`mvk.run_id` on the wire) for trace correlation.

> **📝 Note.** `step_type`, `operation`, `operation_subtype`, and `model` are **auto-set by
> wrappers** for child spans. `@mvk.signal()` also accepts `step_type` and
> `tool_name` when the decorated function itself represents a tool wrapper, but
> `tool_name` is only applied when `step_type="TOOL"`. For explicit manual span
> classification (`operation`, `operation_subtype`), use
> [`mvk.create_signal()`](#mvkcreate_signal--manual-spans-with-explicit-step_type).

```python
@mvk.signal(
    name: str | None = None,                 # Span name (default: function name)
    user_id: str | None = None,
    session_id: str | None = None,
    application_id: str | None = None,
    customer_id: str | None = None,
    request_id: str | None = None,
    region: str | None = None,
    cloud_provider_code: str | None = None,
    use_case: str | None = None,
    outcome_id: str | None = None,           # Stamps mvk.outcome.id on this span
    step_type: str | MVKStepType | None = None,  # Optional; use with TOOL spans
    tool_name: str | None = None,            # TOOL-only label for the wrapped function
    tags: dict[str, str] | None = None,      # Custom tags (max 10 per span)
)
```

Works on sync functions, `async def` coroutines, and async generators — the SDK
auto-detects the function type.

```python
# Group a multi-step workflow under one named span
@mvk.signal(name="answer_question", use_case="customer_support_chat",
            tags={"tier": "premium"})
def answer_question(query: str):
    embedding = openai.Embedding.create(...)     # child span: EMBEDDING
    hits = pinecone.query(...)                    # child span: RETRIEVER
    return openai.ChatCompletion.create(...)      # child span: LLM


# Custom tool wrapper — attach mvk.tool_name to the decorator span
@mvk.signal(name="search_documents", step_type="TOOL",
            tool_name="knowledge_base_search")
def search_documents(query: str):
    return internal_search(query)
```

### `mvk.create_signal()` — manual spans with explicit `step_type`

Use this when auto-instrumentation does **not** cover the call — a custom
in-house LLM service, a paid third-party API, a parsing step, a file upload.
Unlike `@mvk.signal()`, this is a **context manager** and lets you set
`step_type` / `operation` explicitly so the backend classifies the cost
correctly.

```python
from mvk_sdk.schema import MVKStepType  # optional; strings also accepted

mvk.create_signal(
    name: str,                                   # Span name (required)
    step_type: MVKStepType | str | None = None,  # LLM | TOOL | RETRIEVER |
                                                 # EMBEDDING | BATCH | AGENT_CALL
    operation: str | None = None,                # e.g. "parse", "api_call"
    operation_subtype: str | None = None,        # Free-form refinement
    tool_name: str | None = None,                # TOOL-only per-tool attribution
    tags: dict[str, str] | None = None,
    outcome_id: str | None = None,               # Stamps mvk.outcome.id on this span
)
```

Inherits `user_id`, `session_id`, `customer_id`, `region`, `use_case`,
`outcome_id`, and tags from the surrounding `mvk.context()` / `@mvk.signal()`.
Pass `tags={}` to opt out of context-tag inheritance.

```python
with mvk.create_signal(name="parse-document", step_type="TOOL",
                       operation="parse", tool_name="document_parser"):
    content = parse_pdf(file_path)
```

### `mvk.add_metered_usage()` — attach billable quantities

Auto-instrumented LLM calls already populate `mvk.metered_usage` with token
counts. For **everything else with a cost dimension** — pages processed,
external API calls, storage bytes, image generations, characters translated —
call `add_metered_usage()` inside the current signal.

```python
mvk.add_metered_usage([
    {
        "metric_kind": str,    # e.g. "file.pages_processed", "api.calls", "storage.bytes"
        "quantity": float,     # Runtime-measured quantity (never hardcoded)
        "uom": str,            # Unit of measure: "page", "request", "byte", "image", ...
        "metadata": {
            "rate_per_unit": float,   # Cost per UOM unit (USD by default)
            "provider": str,          # External vendor name
            # ...any other free-form metadata
        },
    },
])
```

> **❗ Important — `rate_per_unit` is what turns a quantity into a cost.** The backend computes
> a line item as `quantity × rate_per_unit` and rolls it up by `customer_id` /
> `tags` / `use_case`.
> - **Omit it** → the backend applies the configured rate template for that
>   `metric_kind`. Use this when a template exists and you want central control.
> - **Set it inline** → overrides the template, and is **required** for
>   `metric_kind`s that have no template. Always pair a numeric `rate_per_unit`
>   with the correct `uom` so `quantity` and rate are in the same units.

Also accepts `Metric` instances (`from mvk_sdk.metrics import Metric`) for the
three core fields only (`metric_kind`, `quantity`, `uom`); use the dict form
when you need `metadata` / `rate_per_unit`. If no active span exists, the call
is logged and skipped — it never raises.

### End-to-end example — tools, custom costs, and auto-traced LLM together

```python
import mvk_sdk as mvk
from openai import OpenAI

mvk.instrument(agent_id="doc-agent", api_key="mvk_...", tenant_id="tenant-123",
               wrappers={"include": ["genai"]})

client = OpenAI()


def process_document(file_path: str, user_id: str,
                     session_id: str, customer_id: str) -> str:
    with mvk.context(user_id=user_id, session_id=session_id, customer_id=customer_id):
        # 1. Manual TOOL span — custom parsing cost (per-page)
        with mvk.create_signal(name="parse-document", step_type="TOOL",
                               operation="parse", tool_name="document_parser"):
            content = parse_pdf(file_path)
            page_count = get_page_count(file_path)
            mvk.add_metered_usage([{
                "metric_kind": "file.pages_processed",
                "quantity": page_count,            # runtime value, never hardcoded
                "uom": "page",
                "metadata": {"rate_per_unit": 0.0015},   # $/page
            }])

        # 2. Manual TOOL span — paid third-party API
        with mvk.create_signal(name="extract-metadata", step_type="TOOL",
                               operation="api_call", tool_name="metadata_service"):
            metadata = call_metadata_api(content)
            mvk.add_metered_usage([{
                "metric_kind": "api.calls",
                "quantity": 1,
                "uom": "request",
                "metadata": {"rate_per_unit": 0.05, "provider": "metadata-service"},
            }])

        # 3. Auto-traced LLM call — wrapper populates token metered_usage
        summary = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"Summarize: {content[:2000]}"}],
        )
        return summary.choices[0].message.content
```

This single request produces one trace with three child spans
(`parse-document`, `extract-metadata`, the auto-traced `chat`), all stamped with
`user_id` / `session_id` / `customer_id`, each carrying its own `metered_usage`
line items so cost is attributed end-to-end.

### Notes & limits

- **Always capture `quantity` from runtime values** (e.g. `len(file_bytes)`,
  `page_count`, `1` per API call) — never hardcode.
- **Multiple metrics per span** — pass a list in one `add_metered_usage()`
  call, or call multiple times within the same span (entries are appended).
- **No active span = no-op** — calling outside a `@mvk.signal()` /
  `create_signal()` / auto-traced call logs a warning and skips silently.
- **Common `metric_kind` examples**: `token.prompt`, `token.completion`,
  `file.pages_processed`, `api.calls`, `storage.bytes`, `images.generated`,
  `characters.translated`, `email.sent`, `db.rows_scanned`.

---

## 5. Outcome signals — `mvk.record_outcome()`

Cost is captured automatically (§4). **Outcome signals report the _value_ side** — what a
run actually delivered — so the backend can compute ROI:
`ROI = (human_value − ai_cost) / ai_cost`.

Call `mvk.record_outcome()` **once, at the end of a run**. You report *what happened* — the
status and how many units of work were delivered. The *per-unit economics* (the dollars and
human minutes each unit is worth) are configured per use case in the Mavvrik UI, **not in
code** — the SDK carries no dollar figures.

```python
import uuid

import mvk_sdk as mvk
from mvk_sdk import MVKOutcomeStatus, MVKOutcomeUnit

# A floor-plan validation agent — one unit = one floor
mvk.record_outcome(
    outcome_id=str(uuid.uuid4()),      # required — stable identity; reuse it to attach feedback later
    use_case="floorplan_validation",   # required — backend join key
    session_id=session_id,             # required — backend join key
    status=MVKOutcomeStatus.SUCCESS,
    units=[MVKOutcomeUnit(type="floor", attempted=15, succeeded=15)],
)
# backend: 15 floors × configured value_per_floor  (price from the UI, not the code)
```

**Recommended: the scope form.** `mvk.outcome()` / `mvk.task()` (decorator or context
manager) do the id minting, propagation, timing, and end-of-run reporting for you — the
run above becomes:

```python
with mvk.outcome(use_case="floorplan_validation"):     # id + session generated, timer starts
    with mvk.task(task_name="validate_floors"):        # task id generated, rolls up to the outcome
        for floor in plan.floors:                      # LLM calls auto-captured & correlated
            if validate(floor):
                mvk.add_task_units(1)                  # count work as it completes
    mvk.set_outcome_units([MVKOutcomeUnit(type="floor", attempted=15, succeeded=15)])
# task exit → TASK span; outcome exit → OUTCOME span with status + measured duration
```

See [`docs/outcome_roi.md` §12](docs/outcome_roi.md#12-scope-apis--mvkoutcome--mvktask)
for the scope APIs and the `mvk.*` accessors (`get_outcome_id`, `set_outcome_status`, …).

### Signature

```python
mvk.record_outcome(
    outcome_id,             # required, non-blank — stable identity (recommend a UUID) → mvk.outcome.id
    use_case,               # required, non-blank — backend join key
    session_id,             # required, non-blank — backend join key
    status,                 # MVKOutcomeStatus (or its string): SUCCESS | PARTIAL | FAILED
    units=None,             # list[MVKOutcomeUnit] — delivered counts, by unit type
    completion_ratio=None,  # 0.0–1.0 — whole-run progress; used only when there are no units
    duration_minutes=None,  # actual end-to-end run time (min); backend uses it for time-saved
    remarks=None,           # free-form triage note, surfaced per session in the dashboard
    user_id=None, application_id=None, customer_id=None, tags=None,   # correlation (inherited from context if omitted)
)  # -> Span | None — never raises; returns None on invalid input / internal failure
```

`MVKOutcomeUnit` reports counts only — no economics:

```python
MVKOutcomeUnit(type="claim", attempted=20, succeeded=18)   # + optional tags={...}
```

### Examples

```python
oid = str(uuid.uuid4())   # one stable id per outcome; store it to attach feedback later

# Partial — quantified by real counts (17 of 20)
mvk.record_outcome(oid, "contract_review", session_id, MVKOutcomeStatus.PARTIAL,
                   units=[MVKOutcomeUnit(type="contract", attempted=20, succeeded=17)],
                   remarks="3 contracts deferred to a reviewer")

# Partial — no countable units; report a completion fraction instead
mvk.record_outcome(oid, "research_summary", session_id, MVKOutcomeStatus.PARTIAL,
                   completion_ratio=0.6)

# Failed — zero value; the remark explains why at a glance in the dashboard
mvk.record_outcome(oid, "claims_extraction", session_id, MVKOutcomeStatus.FAILED,
                   units=[MVKOutcomeUnit(type="claim", attempted=30, succeeded=0)],
                   remarks="upstream schema change broke extraction")
```

### What it emits

One span with `mvk.step_type = "OUTCOME"`, carrying `mvk.outcome.id`, `mvk.outcome.status`,
`mvk.outcome.units` (a JSON array of
`{type, attempted, succeeded, tags}`) with `mvk.outcome.units.attempted` / `.succeeded`
rollups, `mvk.outcome.completion_ratio` (only when there are no units),
`mvk.outcome.duration_minutes` (when supplied), `mvk.outcome.remarks`, and the correlation
attributes (`mvk.use_case`, `mvk.session_id`, …).

### Link every span to the outcome (`outcome_id` propagation)

`record_outcome()` emits **one** outcome span carrying `mvk.outcome.id`. To let the backend
join **every** span in the run — the auto-captured LLM / vector-DB cost spans, your
`@mvk.signal` and `create_signal` steps — directly to that outcome, pass the *same*
`outcome_id` to `mvk.context()`. It is stamped as `mvk.outcome.id` on all child spans:

```python
oid = str(uuid.uuid4())

with mvk.context(outcome_id=oid, use_case="floorplan_validation", session_id=session_id):
    result = validate_floors(plan)          # auto + manual spans all carry mvk.outcome.id

mvk.record_outcome(oid, "floorplan_validation", session_id, MVKOutcomeStatus.SUCCESS,
                   units=[MVKOutcomeUnit(type="floor", attempted=result.total, succeeded=result.passed)])
```

`outcome_id` is also accepted directly on `@mvk.signal(outcome_id=...)` and
`mvk.create_signal(..., outcome_id=...)` (overriding the context value for one span). It is
**optional and additive** — omit it and spans are unchanged — and the SDK never generates
one, so use the same string you pass to `record_outcome()`.

**An outcome needs both `outcome_id` and `use_case` to correlate and be priced.** If you set
`outcome_id` on a context but no `use_case` is in scope, the SDK logs a **warning** (visible
by default). `record_outcome()` likewise warns and returns `None` if either is blank. A
wrong-but-valid string (a typo, or an `outcome_id` that doesn't match `record_outcome()`)
can't be caught locally — it shows up in the dashboard as `unresolved`, never a silent `$0`.

### Notes & limits

- **Report counts, not dollars** — per-unit value/time is configured per use case in the
  Mavvrik UI; the SDK never carries economics.
- **`outcome_id`, `use_case`, and `session_id` are required join keys** — each must be a
  non-blank string, or the call logs a warning and emits nothing. `outcome_id` is a stable
  identity for the outcome (a UUID string like `str(uuid.uuid4())` is ideal); reuse it to
  attach end-user feedback to this specific outcome later.
- **Prefer counts; `completion_ratio` is the fallback** for work with no countable units —
  pass it explicitly as a float `0.0`–`1.0` (it is **not** derived from `status`). Out-of-range
  values are clamped, and it is ignored when `units` are present.
- **You set `status`; it is never auto-derived from counts** — auxiliary steps (audit writes,
  email/SMS, metadata) succeed without being the customer's outcome, so decide `SUCCESS` /
  `PARTIAL` / `FAILED` from actual work-completion in your workflow and use `remarks` to explain misses.
- **`units` must be `MVKOutcomeUnit` instances** — any other entry is skipped with a warning.
- **Never breaks your app** — invalid input or any internal error returns `None`; it never
  raises into your workflow.

> **Full guide:** [`docs/outcome_roi.md`](docs/outcome_roi.md) — a client-facing walkthrough of
> outcome measurement: core concepts, every usage mode (with / without / multiple units) and
> when to use each, plus deep worked examples (insurance claims processing, accounts-payable).

### End-user feedback — `mvk.record_feedback()`

When an end user later rates an outcome, call `mvk.record_feedback()`. Feedback arrives **late
and out of process**, so it correlates back to the outcome **only** by the `outcome_id` you set
on `record_outcome()` (emitted as `mvk.outcome.id`). It reports activity only — a sentiment, an
optional numeric `score`, and how many respondents the row represents. Pass `score_max` with
`score` for a **self-describing "N out of M" rating** (e.g. `score=4, score_max=5` → "4 out of
5", Amazon-style, the customer sets the max); omit it and the scale comes from the Mavvrik UI.
One call is either a single respondent or a consolidated group.

```python
from mvk_sdk import MVKSentiment

# 1. Single user, hours later, in a different service — only the id ties it back
mvk.record_feedback(oid, MVKSentiment.POSITIVE,
                    remarks="Resolved my claim correctly.", user_id="user_123")

# 2. Single user, self-describing "4 out of 5" star rating (customer sets the max)
mvk.record_feedback(oid, MVKSentiment.POSITIVE, score=4, score_max=5)

# 3. Consolidated GROUP — one run served 42 people, feedback aggregated into one event
#    breakdown must sum to respondent_count (35+5+2 == 42), else the row is dropped
mvk.record_feedback(oid, MVKSentiment.POSITIVE, score=4.2,
                    respondent_count=42, positive=35, neutral=5, negative=2,
                    remarks="Team review: mostly positive, 2 flagged tone.")
```

**Signature:** `record_feedback(outcome_id, sentiment, score=None, score_max=None, remarks=None,
user_id=None, feedback_time=None, respondent_count=None, positive=None, neutral=None,
negative=None, application_id=None, customer_id=None, tags=None) -> Span | None`.

**What it emits:** one span with `mvk.step_type = "FEEDBACK"`, carrying `mvk.feedback.outcome_id`
(the join key), `mvk.feedback.sentiment`, and — when supplied — `mvk.feedback.score`,
`.score_max`, `.remarks`, `.user_id`, `.time`, `.respondent_count`, and
`.breakdown.{positive,neutral,negative}`.

**Notes:**
- **`outcome_id` + `sentiment` are required** (blank/invalid → logged, returns `None`); the
  `outcome_id` must match the value passed to `record_outcome()`.
- **Single vs. group is the same call** — omit `respondent_count` for one respondent; set it
  (with an optional breakdown) for a consolidated group. A breakdown must sum to
  `respondent_count`, or the row is dropped.
- **`user_id` is the feedback author** (`mvk.feedback.user_id`), not the run's user — it is
  never inherited from context.
- **Never breaks your app** — invalid input or any internal error returns `None`.

---

## 6. Reference

### Context inheritance (Nearest Wins)

The SDK layers context from four sources, applied outermost → innermost:
**Global** (`mvk.instrument(tags=...)`) → **Decorator** (`@mvk.signal` /
`@mvk.context`) → **Context manager** (`with mvk.context(...)`) →
**Per-request headers** (`x-mvk-*`).

- Scalars (`user_id`, `session_id`, `customer_id`, `request_id`, …): nearest wins completely.
- Tags: merge all levels; nearest wins for duplicate keys.
- Empty values clear attributes: `mvk.user_id=""` removes user attribution from this scope down.

### Tag validation

- Maximum 10 tags per span after merging
- Keys: `^[a-z0-9._-]{1,64}$` (dots allowed)
- Values: UTF-8 strings ≤256 chars
- Validation errors are logged (or raised if `strict_validation=True`)

```python
valid, issues = mvk.validate_tags({
    "user.id": "123",               # dots allowed
    "tier": "premium",
    "INVALID!": "will-be-dropped",  # invalid characters
})
```

### Architecture

**Memory-First Architecture** — best for development, QA, and production (all environments):

```
Producers → Memory Queue (10MB) → Writer Thread → Exporter
                                        ↓
                            (exponential backoff retry)
                                        ↓
                          (on failure after 10 attempts)
                                        ↓
                                FailedBatchDisk
```

- Memory-first performance with a 10MB bounded queue, non-blocking producers
- Exponential backoff retry (1s to 5min); persistently failed batches saved to disk
- Fork-safe (Gunicorn, uWSGI, prefork servers); shuts down gracefully on SIGTERM/SIGINT, flushing pending spans

**Step types** (`MVKStepType`): `LLM` (token metrics), `EMBEDDING` (embedding
metrics), `RETRIEVER` (vector/search), `TOOL` (tool/HTTP + manual tool spans),
`AGENT_CALL` (orchestration), `BATCH` (batch ops). `MEMORY` is reserved for
future use.

**Smart auto-instrumentation** uses OpenTelemetry's proven strategy: if a
library is already imported → immediate instrumentation; if not → lazy hook via
`wrapt.when_imported()`.

### Supported integrations

**AI Providers** (`wrappers={"include": ["genai"]}`):

| Provider     | Library Versions                 | Step Type      | Token Tracking  |
| ------------ | -------------------------------- | -------------- | --------------- |
| OpenAI       | 0.x, 1.x                         | LLM, EMBEDDING | ✓ metered_usage |
| Anthropic    | 0.20-0.35                        | LLM            | ✓ metered_usage |
| Gemini       | google.generativeai              | LLM, EMBEDDING | ✓ metered_usage |
| AWS Bedrock  | boto3 bedrock-runtime            | LLM, EMBEDDING | ✓ metered_usage |
| Azure OpenAI | azure.ai.openai                  | LLM, EMBEDDING | ✓ metered_usage |
| Vertex AI    | vertexai/google.cloud.aiplatform | LLM, EMBEDDING | ✓ metered_usage |

**Vector Databases** (`wrappers={"include": ["vectordb"]}`):

| Provider | Library Versions | Step Type | Metrics                 |
| -------- | ---------------- | --------- | ----------------------- |
| Pinecone | 2.x-4.x          | RETRIEVER | vector_count, dimension |
| Weaviate | 3.x-4.x          | RETRIEVER | vector_count, dimension |
| ChromaDB | 0.4.x-0.5.x      | RETRIEVER | vector_count, dimension |
| Qdrant   | 1.x              | RETRIEVER | vector_count, dimension |

**Frameworks**: Semantic Kernel, LangChain, LangGraph, Agno, CrewAI, OpenAI Agents.

**Routers & Proxies**: OpenRouter, LiteLLM.

**Batch APIs**: OpenAI, Anthropic, Azure OpenAI, AWS Bedrock, Gemini, Vertex AI,
LangChain, Agno, CrewAI, Semantic Kernel, and ThreadPool concurrent execution.

**HTTP Clients** (`wrappers={"include": ["http"]}`, disabled by default): HTTPX 0.25-0.27 (TOOL).

### Performance characteristics

**Memory-First Mode (all environments)** — throughput 500–2000 spans/sec;
~10MB bounded queue; producer latency <100 µs (non-blocking); export latency
p99 <100ms (network dependent); failed batches saved to disk for retry.

**Batching defaults (all modes)** — 2000 spans max, 2 MiB max, 3000 ms max,
gzip compression (default) or none.

### Serverless deployment

```python
# AWS Lambda
from mvk_sdk.serverless import lambda_handler

@lambda_handler(flush_timeout_ms=1000)   # auto flush on completion
def handler(event, context):
    # Auto-detects Lambda; optimizes batch_size=1, flush=100ms, memory-first
    return process_request(event)
```

```python
# Google Cloud Functions — auto-detected via K_SERVICE / FUNCTION_NAME
import mvk_sdk as mvk

mvk.instrument(agent_id="gcf-function")

def main(request):
    result = process_request(request)
    mvk.force_flush()    # manual flush for Cloud Functions
    return result
```

```bash
export MVK_SERVERLESS=true   # force serverless optimizations
```

---

## 7. Support

- [Agent Onboarding](https://help.mavvrik.ai/content/agent-onboarding-1)
- [Quick Start Guide](https://help.mavvrik.ai/content/quick-start-guide)
- [Changelog](https://github.com/cloudwizio/agentic-python-sdk/blob/master/CHANGELOG.md) — what changed in each version (also published as [GitHub Release notes](https://github.com/cloudwizio/agentic-python-sdk/releases))
- Email: support@mavvrik.ai
