Metadata-Version: 2.4
Name: mvk-sdk-py
Version: 1.3.12
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

# MVK SDK

[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/pkorrakuti-mvrk/ac02cf5feef9f1029fb980a1bf8148c5/raw/agentic-python-sdk-coverage.json)](https://github.com/cloudwizio/agentic-python-sdk)

**Pure Python • Serverless Ready • Zero Breaking Guarantee • OTEL-style Smart Auto-instrumentation • W3C TraceContext**

## Table of Contents

- [Installation](#installation)
- [Key Features](#key-features)
- [Initialization](#initialization)
- [Enriching Spans with Context](#enriching-spans-with-mvkcontext)
- [Manual Instrumentation](#manual-instrumentation)
- [Context Inheritance](#context-inheritance-nearest-wins)
- [Tag Validation](#tag-validation)
- [Architecture](#architecture)
- [Supported Integrations](#supported-integrations)
- [Performance Characteristics](#performance-characteristics)
- [Serverless Deployment](#serverless-deployment)
- [Support](#support)

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.

## Installation

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

### Optional Dependencies

Install specific providers as needed:

> **Note:** 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]"
```

## Key Features

- **Non-Breaking Guarantee**: SDK will NEVER break client code under any circumstances
- **Serverless Ready**: Auto-detects Lambda, Cloud Functions, Azure Functions with optimizations
- **Simplified Configuration**: MVK-only environment variables, no OTEL complexity
- **DIRECT vs COLLECTOR**: Clear deployment modes for different topologies
- **Memory-First Architecture**: Optimized for performance with failed batch recovery
- **Multiple Exporters**: OTLP/HTTP, OTLP/gRPC, Console, and File exporters
- **OTEL-style AI Provider Focus**: Smart auto-instruments OpenAI, Anthropic, Gemini, Bedrock, Azure OpenAI, Vertex AI using wrapt
- **Vector DB Support**: Smart auto-instruments Pinecone, Weaviate, ChromaDB, Qdrant with immediate/lazy loading
- **Memory Protection**: Bounded 10MB queue prevents OOM issues
- **W3C TraceContext**: Standard distributed tracing across services
- **Metered Usage**: Automatic token tracking with structured metrics
- **Error Isolation**: OTEL-compliant error handling patterns

## Initialization

`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**;
there is no `config={...}` wrapper. Grouped settings (`exporter`, `batching`,
`failed_batch_disk`, `logging`, `serverless`, `wrappers`) are nested dicts.

### Minimal Setup

```python
import mvk_sdk as mvk

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

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

### DIRECT Mode — Export to MVK Backend

DIRECT is the default mode; the endpoint is auto-set if omitted.

```python
# Configuration via environment
export MVK_MODE=DIRECT
export MVK_TENANT_ID=tenant-123  # Required in DIRECT mode
export MVK_API_KEY=mvk_...
export MVK_ENDPOINT=https://ingest.mavvrik.ai/v1/traces

# Or in code
mvk.instrument(
    agent_id="agent-123",
    api_key="mvk_...",
    tenant_id="tenant-123",  # Required in DIRECT mode
    exporter={
        "mode": "DIRECT",
        "type": "otlp_http",
        "endpoint": "https://ingest.mavvrik.ai/v1/traces",
    },
)
```

### COLLECTOR Mode — Export to OTEL Collector

```python
# Configuration via environment
export MVK_MODE=COLLECTOR
export MVK_ENDPOINT=localhost:4317

# Or in code
mvk.instrument(
    agent_id="agent-123",
    exporter={
        "mode": "COLLECTOR",
        "type": "otlp_grpc",
        "endpoint": "localhost:4317",
    },
)
```

### All Parameters

```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 (to MVK) or COLLECTOR (to OTEL collector)
        "type": "otlp_http",  # otlp_http (default), otlp_grpc, console, file
        "endpoint": "https://ingest.mavvrik.ai/v1/traces",  # auto-set if omitted
        "timeout": 10,       # Export timeout in seconds (default: 10)
        "max_retries": 6,    # Max retry attempts (default: 6)
        "compression": "gzip",  # "gzip" (default) or "none"
    },
    batching={
        "max_items": 2000,    # Spans per batch (default: 2000)
        "max_bytes": 2097152,  # 2 MiB (default)
        "max_interval_ms": 3000,  # 3 seconds (default)
    },
    failed_batch_disk={
        "enabled": False,     # Save failed batches to disk (default: False)
        "path": "/tmp/mvk/failed_batches",  # required when enabled
        "max_size_mb": 1000,  # Max disk usage for failed batches (default: 1000)
        "retry_interval": 60,  # Retry every 60 seconds (default: 60)
    },
    serverless={"force": False},  # force serverless optimizations (default: auto-detect)
    strict_validation=False,      # Raise on schema violations (default: False)
    logging={
        "level": "INFO",          # "OFF" (default), "INFO", or "DEBUG"
        "prompts_responses": False,  # Log LLM prompts/responses (default: False) ⚠️
    },
    wrappers={"include": ["genai", "vectordb"]},  # Auto-instrumentation targets
    tags={"env": "prod"},  # Global context tags
)
```

### Logging

Logging is disabled by default (`level: "OFF"`) for security and performance.
When enabled, the SDK automatically configures Python logging — no manual setup
required. Enable all components with a single `logging.level`:

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

Or via environment variable:

```bash
export MVK_LOG_LEVEL=DEBUG
```

`wrapper_level` overrides the log level for the AI provider wrappers only, while
`level` controls the rest of the SDK:

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

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

**Levels**: `OFF` (default) · `INFO` (start, end, metrics, errors) · `DEBUG`
(detailed; enabling DEBUG also enables INFO). Example output:

```
2024-01-15 10:30:15 - mvk.wrappers.openai - INFO - Request started | operation=chat_completion model=gpt-4 request_id=req-123
2024-01-15 10:30:16 - mvk.exporters.otlp_http - INFO - Exporting batch | spans=5 endpoint=http://localhost:4318
2024-01-15 10:30:16 - mvk.wrappers.openai - INFO - Request completed | operation=chat_completion duration_ms=1200 success=True
```

## Enriching Spans with `@mvk.context()`

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

### 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 in the dashboard.
- **Business grouping** — `use_case` groups related operations under a named
  business process (e.g. `"fraud_detection_v2"`, `"customer_onboarding"`) for
  per-workflow cost and latency reporting.
- **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)),
  so per-request context cleanly overlays global tags from `mvk.instrument()`.

### Syntax

`mvk.context()` works as **both** a context manager (`with` block) and a
decorator, with the same 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

**As a 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(...)
```

**As a decorator — function-scoped enrichment:**

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

**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 context — 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=""):
        # user_id is removed from spans created in this inner block
        run_background_job()
```

### 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 format rules.
- **Async / generators:** as a **decorator**, `mvk.context()` supports sync
  functions and `async def` coroutines (context is preserved across `await`
  points within the same task). To scope an **async generator**, use the
  `with`/`async with` context-manager form rather than the decorator.
- **Per-request headers override everything:** incoming `x-mvk-*` headers on a
  single call take precedence over decorator and context-manager values for
  that one call only — useful when a gateway needs to override identity for an
  individual request.
- **Unknown kwargs are non-fatal:** passing a misspelled parameter (e.g.
  `userid=...`) is logged as a warning 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` in the OTLP
  span. The dashboard and BigQuery views use these prefixed names.

## Manual Instrumentation

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 Python 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` (anything not covered by auto-instrumentation) | Context manager     |
| `mvk.add_metered_usage()` | Attach billable quantity metrics (pages, API calls, storage bytes, custom tokens) to the current span                 | Plain function call |

For end-user identity, session, customer, and tag propagation see
[Enriching Spans with `@mvk.context()`](#enriching-spans-with-mvkcontext).

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

Wraps the decorated function call in a 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 (as
`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"`. If you
> need explicit manual span classification such as `operation` or
> `operation_subtype`, use
> [`mvk.create_signal()`](#mvkcreate_signal--manual-spans-with-explicit-step_type).

#### Syntax

```python
@mvk.signal(
    name: str | None = None,                 # Span name (default: function 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,          # Multi-tenant customer ID
    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)
    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.

#### Examples

```python
# Group 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


# Async function — context propagates across await
@mvk.signal(name="summarize_document")
async def summarize(doc_id: str):
    doc = await fetch(doc_id)
    return await openai.AsyncOpenAI().chat.completions.create(...)


# 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 you are making —
a custom in-house LLM service, a paid third-party API, a parsing step, a file
upload to cloud storage, etc. Unlike `@mvk.signal()`, this is a **context
manager** and lets you set `step_type` / `operation` explicitly so the backend
classifies the cost correctly.

#### Syntax

```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 label for per-tool attribution
    tags: dict[str, str] | None = None,          # Custom tags
)
```

Returns a span usable as a context manager. Inherits `user_id`, `session_id`,
`customer_id`, `region`, `use_case`, and tags from the surrounding
`mvk.context()` / `@mvk.signal()` automatically. Pass `tags={}` to opt out of
context tag inheritance.

#### Example

```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 to a span

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 to attach a billable
quantity. The Mavvrik backend uses `metric_kind`, `quantity`, `uom`, and the
optional `rate_per_unit` in `metadata` to compute the line-item cost and roll
it up by `customer_id` / `tags` / `use_case`.

#### Syntax

```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": {          # Optional — drives backend cost computation
            "rate_per_unit": float,   # Cost per UOM unit (USD by default)
            "provider": str,          # External vendor name (e.g. "metadata-service")
            # ...any other free-form metadata
        },
    },
    # ...additional metrics in the same call
])
```

Also accepts a list of `Metric` instances (`from mvk_sdk.metrics import Metric`)
for the three core fields only (`metric_kind`, `quantity`, `uom`); use the dict
form above 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

The pattern below combines all three manual APIs with auto-instrumentation:
`mvk.context()` propagates the user/session/customer identity to every child
span, `create_signal()` wraps each manual cost step with a `TOOL` span and an
explicit `rate_per_unit`, and the OpenAI call at the end is auto-traced with
token-level `metered_usage` populated by the wrapper.

```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,
                "uom": "page",
                "metadata": {"rate_per_unit": 0.0015},
            }])

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

In the Mavvrik dashboard 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 can be 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.
- **`rate_per_unit` is optional** — if omitted, the backend uses the configured
  rate template for that `metric_kind`. Include it inline to override or for
  metric kinds without a template.
- **Multiple metrics per span are supported** — pass them as 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`.

## Context Inheritance (Nearest Wins)

The SDK layers context from four sources, applied in order (outermost → innermost):
**Global** (`mvk.instrument(tags=...)`) → **Decorator** (`@mvk.signal` / `@mvk.context`)
→ **Context Manager** (`with mvk.context(...)`) → **Per-request headers** (`x-mvk-*`).
A concrete nesting example is shown in
[Enriching Spans with `@mvk.context()`](#enriching-spans-with-mvkcontext).

**Inheritance Rules**:

- Scalars (`user_id`, `session_id`, `customer_id`, `request_id`, etc.): 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

Tags must follow strict rules:

- 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
# Validate tags before use
valid, issues = mvk.validate_tags({
    "user.id": "123",  # Dots now allowed
    "tier": "premium",
    "INVALID!": "will-be-dropped"  # Invalid characters
})
```

## Architecture

### Memory-First Architecture

**Best for**: Development, QA, production, all environments

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

- Memory-first performance with 10MB bounded queue
- Exponential backoff retry (1s to 5min)
- Failed batches saved to disk for retry
- Non-blocking producers

### Step Types

**Active** (`MVKStepType`):

- `LLM`: Language model completions (auto-populates token metrics)
- `EMBEDDING`: Embedding generation (auto-populates embedding metrics)
- `RETRIEVER`: Vector/search operations (vector DB instrumentors)
- `TOOL`: Tool / HTTP client operations and manual tool spans
- `AGENT_CALL`: Agent orchestration
- `BATCH`: Batch operations (batch instrumentors)

**Reserved for future use**:

- `MEMORY`: Cache/store operations

### Smart Auto-instrumentation

The SDK uses OpenTelemetry's proven instrumentation strategy:

- **If library already imported** → Immediate instrumentation
- **If library not imported** → Lazy hook via `wrapt.when_imported()`

Fork-safe (Gunicorn, uWSGI, prefork servers) and shuts down gracefully on
SIGTERM/SIGINT, flushing pending spans.

## Supported Integrations

**AI Providers** (enabled with `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** (enabled with `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**:
| Framework       | Integration      | What's traced              |
| --------------- | ---------------- | -------------------------- |
| Semantic Kernel | Direct wrapping  | Kernel functions, plugins  |
| LangChain       | Callback handler | Chain tracing, agent steps |
| LangGraph       | Direct wrapping  | Graph node tracing         |
| Agno            | Direct wrapping  | Agent tracing              |
| CrewAI          | Direct wrapping  | Crew/task tracing          |
| OpenAI Agents   | Direct wrapping  | Agent run tracing          |

**Routers & Proxies**:
| Router         | Operations        | Features                |
| -------------- | ----------------- | ----------------------- |
| OpenRouter     | chat, completions | Model routing, fallback |
| LiteLLM        | completion, embedding | Routing, load balancing |

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

**HTTP Clients** (disabled by default, enable with `wrappers={"include": ["http"]}`):
| Library | Versions  | Step Type |
| ------- | --------- | --------- |
| HTTPX   | 0.25-0.27 | TOOL      |

## Performance Characteristics

### Memory-First Mode (All Environments)

- **Throughput**: 500-2000 spans/sec
- **Memory Usage**: ~10MB (bounded queue)
- **Producer Latency**: <100 µs (non-blocking)
- **Export Latency p99**: <100ms (network dependent)
- **Reliability**: Failed batches saved to disk for retry

### Batching Defaults (All Modes)

- **Items**: 2000 spans max
- **Size**: 2 MiB max
- **Time**: 3000 ms max
- **Compression**: gzip (default) or none

## Serverless Deployment

### AWS Lambda

```python
from mvk_sdk.serverless import lambda_handler

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

### Google Cloud Functions

```python
import mvk_sdk as mvk

# Auto-detected via K_SERVICE or FUNCTION_NAME env vars
mvk.instrument(agent_id="gcf-function")

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

### Force Serverless Mode

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

## Support

- GitHub Issues: [github.com/cloudwizio/agentic-python-sdk](https://github.com/cloudwizio/agentic-python-sdk/issues)
- Documentation: [docs.mavvrik.ai](https://docs.mavvrik.ai)
- Email: support@mavvrik.ai
