MVK SDK for Python - Ultralight OpenTelemetry-compatible SDK for AI Observability
Project description
MVK SDK
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
- Table of Contents
- 1. Installation
- 2. Configuration & Instrumentation
- 3.
@mvk.context()— enriching spans (decorator vs. context manager) - 4. Cost signals —
@mvk.signal(),create_signal()& metered usage - 5. Outcome signals —
mvk.record_outcome() - 6. Reference
- 7. Support
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.timeout → exporter={"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_idand non-secret config in code or env as convenient. - Inject
MVK_API_KEY(andMVK_TENANT_ID) from a secrets manager at runtime — AWS Secrets Manager / SSM, GCP Secret Manager, Azure Key Vault, or Vault — never from a committed.envor 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. Keeplogging.prompts_responsesdisabled 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 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, andtagslet the Mavvrik backend split AI spend by tenant, product, team, or feature for chargeback / showback reporting. - Trace correlation —
user_id,session_id, andrequest_idstitch multi-step agent workflows back to a single end-user request. - Business grouping —
use_casegroups related operations under a named business process (e.g."fraud_detection_v2","customer_onboarding"). - Distributed tracing —
traceparent/tracestateaccept 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_idbecomesmvk.user_id, andtags={"team": "ml"}becomesmvk.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, andmodelare auto-set by wrappers for child spans.@mvk.signal()also acceptsstep_typeandtool_namewhen the decorated function itself represents a tool wrapper, buttool_nameis only applied whenstep_type="TOOL". For explicit manual span classification (operation,operation_subtype), usemvk.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_unitis what turns a quantity into a cost. The backend computes a line item asquantity × rate_per_unitand rolls it up bycustomer_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 numericrate_per_unitwith the correctuomsoquantityand 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
quantityfrom runtime values (e.g.len(file_bytes),page_count,1per 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_kindexamples: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)
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, andsession_idare required join keys — each must be a non-blank string, or the call logs a warning and emits nothing.outcome_idis a stable identity for the outcome (a UUID string likestr(uuid.uuid4())is ideal); reuse it to attach end-user feedback to this specific outcome later.- Prefer counts;
completion_ratiois the fallback for work with no countable units — pass it explicitly as a float0.0–1.0(it is not derived fromstatus). Out-of-range values are clamped, and it is ignored whenunitsare 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 decideSUCCESS/PARTIAL/FAILEDfrom actual work-completion in your workflow and useremarksto explain misses. unitsmust beMVKOutcomeUnitinstances — 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+sentimentare required (blank/invalid → logged, returnsNone); theoutcome_idmust match the value passed torecord_outcome().- Single vs. group is the same call — omit
respondent_countfor one respondent; set it (with an optional breakdown) for a consolidated group. A breakdown must sum torespondent_count, or the row is dropped. user_idis 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mvk_sdk_py-1.3.15.tar.gz.
File metadata
- Download URL: mvk_sdk_py-1.3.15.tar.gz
- Upload date:
- Size: 865.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
600691b8c9b4d1817be00c6ac940bc14a6b517c98f8b6afdcab435be60406d24
|
|
| MD5 |
0b1dc3086d58dc0524f099523b8b3c40
|
|
| BLAKE2b-256 |
72c01bcd8713f4c56cadb6ad029d010dc762d9996c6dc09a3a41273962ecb7eb
|
Provenance
The following attestation bundles were made for mvk_sdk_py-1.3.15.tar.gz:
Publisher:
release.yml on cloudwizio/agentic-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mvk_sdk_py-1.3.15.tar.gz -
Subject digest:
600691b8c9b4d1817be00c6ac940bc14a6b517c98f8b6afdcab435be60406d24 - Sigstore transparency entry: 2171568356
- Sigstore integration time:
-
Permalink:
cloudwizio/agentic-python-sdk@4b41b61c2b5fca8b4f7f3821ff9f739d2d0d2a15 -
Branch / Tag:
refs/heads/releases/prod-2026.07.01 - Owner: https://github.com/cloudwizio
-
Access:
internal
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b41b61c2b5fca8b4f7f3821ff9f739d2d0d2a15 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mvk_sdk_py-1.3.15-py3-none-any.whl.
File metadata
- Download URL: mvk_sdk_py-1.3.15-py3-none-any.whl
- Upload date:
- Size: 917.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31b67f0e7cee10feeac829dfd3c4f9339a02df8b812a887869e57244ff60f641
|
|
| MD5 |
bf880742baa8f6858b7d3387a0574de4
|
|
| BLAKE2b-256 |
223e852e11f9953d31c9cdf5fd910354d970f6654b3dbe573a6eed29ca21ea8d
|
Provenance
The following attestation bundles were made for mvk_sdk_py-1.3.15-py3-none-any.whl:
Publisher:
release.yml on cloudwizio/agentic-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mvk_sdk_py-1.3.15-py3-none-any.whl -
Subject digest:
31b67f0e7cee10feeac829dfd3c4f9339a02df8b812a887869e57244ff60f641 - Sigstore transparency entry: 2171568361
- Sigstore integration time:
-
Permalink:
cloudwizio/agentic-python-sdk@4b41b61c2b5fca8b4f7f3821ff9f739d2d0d2a15 -
Branch / Tag:
refs/heads/releases/prod-2026.07.01 - Owner: https://github.com/cloudwizio
-
Access:
internal
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b41b61c2b5fca8b4f7f3821ff9f739d2d0d2a15 -
Trigger Event:
push
-
Statement type: