Skip to main content

MVK SDK for Python - Ultralight OpenTelemetry-compatible SDK for AI Observability

Project description

MVK SDK

PyPI version Python versions Coverage

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

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


1. Installation

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.

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

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:

# 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.timeoutexporter={"timeout": 30}). 🔒 marks sensitive values — see §2.3.

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

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:

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:

mvk.instrument(agent_id="agent-123", logging={"level": "INFO"})
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), 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.
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")
# 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.

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:

mvk.instrument(agent_id="agent-123", api_key="mvk_...",
               logging={"level": "INFO", "wrapper_level": "DEBUG"})
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 managerwith 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 attributioncustomer_id, application_id, and tags let the Mavvrik backend split AI spend by tenant, product, team, or feature for chargeback / showback reporting.
  • Trace correlationuser_id, session_id, and request_id stitch multi-step agent workflows back to a single end-user request.
  • Business groupinguse_case groups related operations under a named business process (e.g. "fraud_detection_v2", "customer_onboarding").
  • Distributed tracingtraceparent / 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).

Signature

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):

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:

@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:

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:

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:

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 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().

@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().

@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.

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

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.

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.

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_kinds 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

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.

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:

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 for the scope APIs and the mvk.* accessors (get_outcome_id, set_outcome_status, …).

Signature

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:

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

Examples

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:

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.01.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 — 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.

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)
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

# 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)
# 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
export MVK_SERVERLESS=true   # force serverless optimizations

7. Support

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mvk_sdk_py-1.3.16.tar.gz (871.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mvk_sdk_py-1.3.16-py3-none-any.whl (924.6 kB view details)

Uploaded Python 3

File details

Details for the file mvk_sdk_py-1.3.16.tar.gz.

File metadata

  • Download URL: mvk_sdk_py-1.3.16.tar.gz
  • Upload date:
  • Size: 871.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for mvk_sdk_py-1.3.16.tar.gz
Algorithm Hash digest
SHA256 ecdfc23296f069a5b4b92928e0bcc7493590fc0b6c9740d0d0eb878d676f48e9
MD5 c7c844d4847651abbe13af4c17691fda
BLAKE2b-256 ac579f20d9fc9da583ead871b8e066658395cdf6fbc93f64b2cbf57e04e9c7a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for mvk_sdk_py-1.3.16.tar.gz:

Publisher: release.yml on cloudwizio/agentic-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mvk_sdk_py-1.3.16-py3-none-any.whl.

File metadata

  • Download URL: mvk_sdk_py-1.3.16-py3-none-any.whl
  • Upload date:
  • Size: 924.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for mvk_sdk_py-1.3.16-py3-none-any.whl
Algorithm Hash digest
SHA256 c51876d8fac841b51103f33d0987a41884c87bc18717b64b1c5bce88fccc52fc
MD5 e9595a9a3a4be3172903d738869856b5
BLAKE2b-256 27b1d484c3ca8d9b9e7994a9e365d119934f9c5e2fb2a78ec8e93d1dda729157

See more details on using hashes here.

Provenance

The following attestation bundles were made for mvk_sdk_py-1.3.16-py3-none-any.whl:

Publisher: release.yml on cloudwizio/agentic-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page