Metadata-Version: 2.4
Name: mvk-sdk-py
Version: 1.3.13
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: 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: genai
Requires-Dist: openai>=1.0.0; extra == "genai"
Requires-Dist: google-generativeai>=0.3.0; extra == "genai"
Requires-Dist: anthropic>=0.15.0; extra == "genai"
Requires-Dist: boto3>=1.26.0; extra == "genai"
Requires-Dist: google-cloud-aiplatform>=1.38.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: 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>=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: 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: 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. Reference](#5-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)
- [6. Support](#6-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

# Vector databases
pip install "mvk-sdk-py[vectordb]"     # All vector DBs
pip install "mvk-sdk-py[pinecone]"     # Pinecone 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 |

<details>
<summary>Full <code>instrument()</code> call with every group</summary>

```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"},
)
```

</details>

### 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)
    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, 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,
    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,
)
```

Inherits `user_id`, `session_id`, `customer_id`, `region`, `use_case`, 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. 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
```

---

## 6. Support

- Agent Onboarding](https://help.mavvrik.ai/content/agent-onboarding-1)
- [Quick Start Guide](https://help.mavvrik.ai/content/quick-start-guide)
- Email: support@mavvrik.ai
