virgo-observe
Four Python operations for Virgo: register a client, trace an operation, attach feedback, and publish an individual product metric. Traces use OpenTelemetry; feedback and metrics use unsampled HTTP channels authorized by the same Observe key.
Version 0.6.1 reuses existing deployment settings and sends the same resolved
environment with traces, feedback, and metrics. Unconfigured runtimes use
unspecified instead of being labeled production.
It retains complete conversation capture and upstream truncation reporting.
Python 3.10+ is supported. This version is prepared in the checkout; after
publication, install it with the single extra matching the application's
highest-level framework:
pip install virgo-observe==0.6.1
pip install 'virgo-observe[litellm]==0.6.1'
pip install 'virgo-observe[fastmcp]==0.6.1'
For local development, build/install the checkout instead:
uv build packages/virgo-observe-python
pip install dist/virgo_observe-0.6.1-py3-none-any.whl
Four operations
Choose coverage in Virgo's “Trace with Virgo-Observe” setup and create one key. Configure it outside application code:
export VIRGO_API_KEY='<observe-key>'
The SDK defaults to hosted Virgo. For local/custom Virgo deployments, the
generated UI/MCP setup prompt supplies the non-secret register(endpoint=...)
address automatically. The copy block still contains only the API key.
VIRGO_ENDPOINT remains available for advanced manual configuration.
Reuse your application's existing configuration without adding an environment variable:
virgo = register(project_name="support-agent", environment=settings.environment)
# Or reuse your existing tracer provider's public resource:
virgo = register(project_name="support-agent", tracer_provider=provider)
# An SDK provider already installed globally is reused automatically:
virgo = register(project_name="support-agent")
Resolution uses the first configured label, in this order:
VIRGO_ENVIRONMENToverride.- Explicit
environment=...from application configuration. - The supplied or globally registered SDK tracer provider's resource.
OTEL_RESOURCE_ATTRIBUTES.APP_ENV,VERCEL_TARGET_ENV, thenVERCEL_ENV.NODE_ENVonly when it isdevelopmentortest.unspecifiedwhen no deployment label is available.
Resource lookup prefers deployment.environment.name, then
deployment.environment, then platform.environment. Custom labels and case
are preserved; resource attribute values in the environment variable are
percent-decoded. Blank variables fall through; the selected label must contain
1–80 Unicode characters without control/non-printable characters. An invalid
override fails validation instead of silently using a lower-priority label.
NODE_ENV=production describes build mode, so
it does not prove a production deployment. CI flags and cloud host markers do
not classify environments.
Set one override before starting the process, even when application code already specifies an environment:
export VIRGO_ENVIRONMENT=local
# Or VIRGO_ENVIRONMENT=prod for production, or VIRGO_ENVIRONMENT=evals for evaluation runs.
The label is resolved once at registration for traces, feedback, and metrics.
local, prod, and evals stay exactly as supplied; custom labels remain valid.
Evaluation runs can also use environment="evals" in their runner configuration.
CI or NODE_ENV=test alone does not imply an evaluation run. prod and
production remain distinct scopes, so keep your existing label when upgrading.
The same Observe key can send multiple environments to one workspace. The hosted Virgo endpoint does not identify where your agent runs. Use the Traces environment filter to inspect deployments separately, and Columns → Environment to show their labels in the table.
Upgrading: deployments that previously relied on the implicit production
default must set or supply a production label to retain that scope. Historical
traces are not relabeled. Keep delayed feedback and metric producers on the same
environment as the execution they refer to.
from decimal import Decimal
from virgo_observe import MetricSubject, VirgoSubject, VirgoVersions, register
virgo = register(project_name="support-agent", product=True, metrics=True, release="release-7")
with virgo.agent_run(
"support.resolve_ticket",
run_ref="run-opaque-123",
subject=VirgoSubject(account_ref="account-opaque-123"),
versions=VirgoVersions(agent="agent-v4", prompt="prompt-v7"),
):
# Run your agent here. Selected supported integrations emit child spans.
with virgo.span("retrieval.context_pack", kind="RETRIEVER"):
pass
saved_execution = virgo.current_trace_ref()
completed = virgo.metric(
"resolved_ticket_value",
observation_id="ticket-123-value",
value=Decimal("12.34"),
unit="USD",
definition_version="resolved-ticket-v1",
subject=MetricSubject("account", "account-opaque-123"),
lineage={"producer": "ticket-service"},
)
# Feedback may arrive later. Persist saved_execution with your own run record
# when the response/callback crosses a process boundary.
feedback = virgo.feedback(
feedback_id="ticket-123-rating",
trace=saved_execution,
rating="down",
kind="correction",
correction="The answer should use the selected workspace.",
)
report = virgo.flush_report(timeout_seconds=5)
# Inspect report.feedback, report.metrics, and each receipt.status.
virgo.shutdown(timeout_seconds=5)
A saved TraceRef contains an external nonzero 32-character lowercase W3C
trace ID and/or your opaque run reference, never a Virgo-internal ptr_… ID.
An explicit reference wins over the active execution. Feedback requires one;
metrics may remain deliberately unlinked. Subject references are opaque
application IDs, not pre-hashed Virgo pseudonyms.
Feedback metadata accepts only environment, workflow, release,
model_version, configuration_id, and provenance. Values must be non-null
strings, booleans, or finite numbers. Omit missing values and keep arbitrary
event fields in the application's own record. For feedback actually generated
by a simulated user, use metadata={"provenance": "synthetic_feedback"}.
span() returns a native OpenTelemetry Span. kind is an optional AI role
such as AGENT, LLM, or TOOL; otel_kind=SpanKind.CLIENT independently
preserves native transport semantics. Generic spans are not inferred to be
tools from parentage. agent_run(), get_tracer(), flush(), and
instrumented_frameworks remain supported. Typed identity, run, release, and
version arguments own their reserved attribute keys.
Point observations and real outcome windows
A metric without window is one observed event: it requires a non-null
boolean/numeric value, revision 1, and no supersession. Its occurrence is
captured once when called, or set explicitly with an aware occurred_at.
It does not fabricate a time window or interpret False as a mature failure.
Use an event KPI definition to aggregate these observations; precomputed custom
KPI definitions consume windowed measurements only.
For a completed or censored outcome window, supply MetricWindow(start, end, mature_at) using timezone-aware datetimes. The order is
start < end <= mature_at. An observed window must already be mature;
occurred_at, if supplied, must equal its end. Supported statuses are
observed, immature, proxy, right_censored, not_achieved, and
reversed. Immature/right-censored values are null.
value accepts bool, int, finite float, finite bounded Decimal, or None where
the status permits it. Decimal is serialized as exact decimal text, not a float.
Arbitrary categorical strings are rejected. Subjects have exactly one grain:
account, user, journey, conversation, session, or aggregate (without a ref).
Approved lineage keys are source, source_version, definition_hash, export_id,
and producer.
Corrections retain the logical observation_id, increment revision, and
set supersedes_observation_id. Every revision gets a distinct stable HTTP
idempotency key. Retrying an unchanged revision is a duplicate; changing its
semantic value, timestamp, lineage, subject, release, versions, or execution
claim is a conflict. New revisions never edit the previous record.
Privacy and integration ownership
Full content is the default: prompts, responses, tool inputs/outputs, and
retrieval content. Observe onboarding also configures the server source for
full content. Explicit capture_content=False remains available when an
application intentionally needs metadata-only export. When the argument is
omitted, VIRGO_CAPTURE_CONTENT=true|false can override the default. Explicit
arguments win; the server's trace-source policy remains authoritative.
In explicit metadata-only mode, the exporter filters its own representation: unapproved attributes, events (including exception content), status descriptions, link attributes, and resource/scope attributes are removed. It does not mutate spans delivered to another exporter. Operational names and approved opaque IDs remain; do not put prompts, personal data, secrets, or arbitrary content in those fields. This trace setting does not erase explicitly submitted feedback text, which follows the feedback source's policy (full content for Observe onboarding).
Explicit metadata-only export retains vcs.ref.head.revision and
platform.commit.sha only as strings of exactly 40 hexadecimal characters.
Set the emitter commit on the OpenTelemetry Resource and provide
platform.release.id (for example, via release=) for canonical trace source
provenance. Span-only commit attributes do not establish that provenance.
Other source-control fields and arbitrary evaluator or deployment metadata
remain filtered. This does not reconstruct historical provenance.
product=True and metrics=True independently enable publication using the
same key. Their default URLs use the trace endpoint's origin; the backend
resolves the workspace from the key. Disabled channels are not configured.
Ordinary legacy trace keys do not gain publication access: enable coverage
through Observe setup. Rotating or revoking its trace-source key affects all
selected channels. Existing explicit feedback/outcome tokens remain supported
for older integrations; a shared key is never forwarded to a different origin.
Register before importing functions by value (such as from litellm import completion) and before constructing instrumented clients. Existing local aliases
do not change when a framework module is instrumented. Install only the one extra
matching the application's highest-level framework, then select its explicit
name. Each extra pins Virgo's adapter. Some upstream adapters declare a minimum
framework version, so resolve the one chosen extra through the application's
existing lock and never force an unrelated framework upgrade.
FastMCP 3.x and 4.x use an instance middleware instead of the instrumentors
selection. Full request and response content is enabled by default; credential,
authorization, cookie, token, password, and _meta values are always excluded
from the captured copy:
from fastmcp import FastMCP
from virgo_observe import register
virgo = register(project_name="customer-tools")
mcp = FastMCP("Customer tools")
virgo.instrument_fastmcp(mcp)
The adapter traces every MCP request and classifies tools/call, prompts/get,
and resources/read as tool, prompt, and retrieval operations. Do not also pass
fastmcp in instrumentors; FastMCP needs the concrete server instance. The
older bundled mcp.server.fastmcp import is not part of this certified range.
Stable transport session IDs are preserved when FastMCP provides one. FastMCP
4's modern protocol is sessionless, so Virgo keeps request correlation without
fabricating a cross-request session.
| Application path | Adapter install for 0.6.1 | instrumentors selection |
|---|---|---|
| FastMCP 3.x/4.x | virgo-observe[fastmcp]==0.6.1 |
virgo.instrument_fastmcp(mcp) |
| AutoGen AgentChat | virgo-observe[autogen]==0.6.1 |
["autogen"] |
| CrewAI | virgo-observe[crewai]==0.6.1 |
["crewai"] |
| LiteLLM | virgo-observe[litellm]==0.6.1 |
["litellm"] |
| LlamaIndex | virgo-observe[llama-index]==0.6.1 |
["llama_index"] |
| Agno | virgo-observe[agno]==0.6.1 |
["agno"] |
| LangGraph | virgo-observe[langgraph]==0.6.1 |
["langgraph"] |
| DSPy | virgo-observe[dspy]==0.6.1 |
["dspy"] |
| OpenAI Agents SDK | virgo-observe[openai-agents]==0.6.1 |
["openai_agents"] |
| Pydantic AI | virgo-observe[pydantic-ai]==0.6.1 |
["pydantic_ai"] |
| LangChain | virgo-observe[langchain]==0.6.1 |
["langchain"] |
| OpenAI SDK | virgo-observe[openai]==0.6.1 |
["openai"] |
| Anthropic SDK | virgo-observe[anthropic]==0.6.1 |
["anthropic"] |
Semantic Kernel 1.44.1 uses its native OpenTelemetry spans, so it needs no
adapter extra. Set
SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true
before importing Semantic Kernel, install base virgo-observe==0.6.1, and use
instrumentors=["semantic_kernel"]. Semantic Kernel's native tracer resolves
through the global SDK TracerProvider; pass that same provider explicitly to
register(). Virgo bridges the opted-in prompt/response log events onto the
native model span so its trace exporter applies the same content policy.
Installing an arbitrary OpenInference entry point is not a support guarantee.
Use auto_instrument=False for manual tracing.
Automatic selection activates framework, then gateway, then provider layers.
An active higher-level layer prevents duplicate lower-level model spans. Overlapping
gateway or provider adapters are reported as unsupported with
overlapping_model_instrumentation; direct provider calls outside the selected
higher-level framework then need a provider-only selection or manual spans.
Existing external instrumentation is never reconfigured and is reported
already_active_unverified, not healthy by inference.
Virgo-owned providers retain all span attributes by default so long message and
tool histories do not evict the model identity or early prompts. Explicit
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT / OTEL_ATTRIBUTE_COUNT_LIMIT settings and
application-owned provider limits remain authoritative. Exported spans preserve
OpenTelemetry dropped-attribute, event, and link counters when upstream limits
truncate data; privacy filtering is not counted as upstream data loss.
Inspect virgo.registration_report for each integration's distribution version,
status, ownership, and safe reason code. Discovery/activation is not proof of
server delivery. Native automatic metadata-only instrumentation requires patched
Pydantic AI 2.27.1+; earlier versions are skipped because retry content can leak
through their native instrumentation.
Each known integration report now includes compatibility: the package whose
version gate is being checked (the OpenInference adapter for adapter gates), its
installed version, supported range, blocked capability, and exact candidate
commands for pip, uv, and Poetry. The standalone
assess_integration_compatibility(name, installed_version, capture_content=...)
also returns this information without importing or patching a framework. Missing
version evidence remains unknown. Unsupported integrations do not prevent
independent supported integrations from activating.
OpenInference reports also expose framework_dependencies: each upstream
instrumentor requirement beside the actual installed framework version (or
None when missing). On a dependency conflict, use these requirements to assess
the framework change; reinstalling an already-certified adapter is insufficient.
Candidate upgrades remain unknown and are never applied by the SDK. The coding
host must inspect the application's manifest, resolved lock, upstream framework
and adapter requirements, extras and companion pins, release/migration notes,
and affected call sites. It must run focused behavior checks in a disposable
environment before calling an exact application upgrade verified-compatible.
Known API/configuration changes are migration-required; installation success or
a supported version number is insufficient. For pip requirements projects, update
the existing requirements/constraints and regenerate the existing lock after
upgrade approval. Preserve dependency groups and coordinate pydantic-ai with
pydantic-ai-slim when both are present.
The reproducible integration matrix uses real SDKs and local fake responses, including model streaming and native tool/validator retries:
| Layer | Tested minimum | Tested selected current |
|---|---|---|
| OpenTelemetry SDK/exporter | 1.42.0 | 1.44.0 (CrewAI: 1.42.0) |
| Pydantic AI slim | 2.27.1 | 2.37.0 |
| OpenAI SDK (OI adapter 0.1.57) | 1.69.0 | 3.7.0 |
| Anthropic SDK (OI adapter 2.1.1) | 1.0.0 | 1.3.0 |
| LangChain Core (OI adapter 0.1.73) | 0.3.50 | 1.6.1 |
| LangChain OpenAI | 0.3.12 | 1.6.0 |
| AutoGen AgentChat (OI adapter 0.1.14) | — | 0.7.5 |
| CrewAI (OI adapter 1.1.15) | — | 1.15.18 |
| LiteLLM (OI adapter 0.1.40) | — | 1.99.0 |
| LlamaIndex Core (OI adapter 4.4.8) | — | 0.14.24 |
| Agno (OI adapter 1.0.6) | — | 3.0.5 |
| LangGraph (LangChain OI adapter 0.1.73) | — | 1.2.11 |
| DSPy (OI adapter 0.1.42) | — | 3.3.1 |
| OpenAI Agents SDK (OI adapter 2.2.0) | — | 0.22.0 |
| Semantic Kernel native OTel | — | 1.44.1 |
Other versions are not individually certified. Install optional integrations alongside the application's existing dependencies; do not upgrade an application merely to silence an unsupported registration report.
Providers, configuration, and lifetime
register() uses an explicitly supplied SDK provider, then an existing global
SDK provider, or otherwise a private provider. It never replaces the global
provider. Compatible registrations share one export stream and sender with
reference-counted lifetime; conflicting registrations on that provider fail.
Closing one handle does not shut down another handle or the customer's provider.
Final shutdown releases Virgo-owned framework wrappers only while their ownership
still matches; externally installed or replaced wrappers are left untouched.
In pre-fork servers, create the provider and call register() inside each worker
after it starts. Do not share a live Virgo handle or delivery receipt across a
fork. Inherited handles reject operations instead of using stale threads/locks.
Use explicit shutdown for short-lived jobs; process exit is not a delivery receipt.
trace_export="existing" requires an existing SDK provider, needs no Virgo
trace key, and attaches no Virgo exporter. The application's exporter and privacy
policy own that path; Virgo's export-local content filter does not apply to it.
The independent feedback/metric channels still work.
When those channels use an Observe key, including one supplied through explicit channel token arguments or environment variables, an existing exporter must carry the environment contract on every span's resource, including children exported before their root. Configure it before creating the provider:
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
provider = TracerProvider(resource=Resource.create({
"deployment.environment.name": "local",
"platform.observe.environment_contract": "observe-runtime-environment-v1",
}))
# Attach your existing exporter to this provider before registration.
virgo = register(
project_name="support-agent",
tracer_provider=provider,
trace_export="existing",
environment="local",
product=True,
metrics=True,
)
Registration rejects a missing contract or mismatched environment before starting delivery. It never mutates the existing provider or another exporter's resources. Trace-only existing-export registrations keep their previous behavior.
Explicit arguments take precedence over the channel environment variables.
Trace compatibility also accepts OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
OTEL_EXPORTER_OTLP_TRACES_HEADERS/OTEL_EXPORTER_OTLP_HEADERS, and
OTEL_SERVICE_NAME. VIRGO_ENDPOINT, VIRGO_ENVIRONMENT, and
VIRGO_RELEASE override hosted defaults. The project display name is never
used to guess a workspace publication URL. Missing channels are permitted at
registration, but publishing to one raises a configuration error.
Each record channel has its own finite in-memory queue and worker (default 512 pending events each). Payloads, occurrence times, IDs, and operational W3C headers are captured before enqueueing. Network calls suppress recursive HTTP instrumentation. At most three attempts retry transport errors, 408, 429, and 5xx responses with bounded jitter/backoff and Retry-After. Other HTTP failures are terminal; redirects never forward credentials.
Receipts expose queued, accepted, rejected, conflict, failed, or dropped states.
Queue overflow is visible as dropped, not silently successful. A 202 means
accepted at ingress, not normalized, linked, or successfully analyzed.
flush_report() reports cumulative failures and pending records;
flush() is its boolean compatibility view. Trace force-flush completion is
not proof of remote ingestion.
Flush and shutdown share a total caller deadline across channels. An exporter or in-flight HTTP call can finish after the caller's deadline; Python cannot safely cancel arbitrary third-party I/O. Shutdown marks undelivered receipts failed and closes owned resources. This queue is not crash-durable: persist important application events in your own outbox and reuse their IDs when retrying after a process restart. Call shutdown during orderly teardown.
Server contract and verification
Point metrics, explicit observation/release/version fields, and durable
privacy-aware correlations require the accompanying Platform server changes
and forward migration 0197. Upgrade via platform-migrate; never use an SDK
client-side fallback against an older window-only endpoint.
Exact links require the same tenant, workspace, environment, and permitted
pseudonym scope. Multiple candidates and contradictory references abstain.
The Observe key selects the workspace; environment identifies the deployment
within it and resolves as described above. Version 0.6.1 requires the accompanying
server environment support. Deploy that server before upgrading the SDK.
SDK 0.6.1 declares platform.observe.environment_contract=observe-runtime-environment-v1
on exported resources. Only Observe traces with that exact contract use their
runtime environment; legacy Observe exports and payloads without an environment
retain their shared source/workspace default. This keeps older clients' links
working during an API-first rollout. Generic OTLP sources retain their existing
resource-environment behavior.
The API accepts metadata.environment on feedback and top-level environment
on metrics. Observe keys permit these deployment labels within their workspace;
legacy channel credentials require the workspace environment. Labels must be
nonblank strings of at most 80 characters. Normal Unicode labels are preserved;
control, format, private-use, unassigned, and surrogate characters are rejected
before export or ingestion because they cannot identify a correlation scope.
Late links live in a separate projection, not edits to immutable outcome rows.
Pending claims receive bounded retries and a final expiry check; missing or
sampled-out traces do not prevent observation acceptance.
From the Platform repository root:
uv run ruff format packages/virgo-observe-python
uv run ruff check packages/virgo-observe-python
uv run mypy packages/virgo-observe-python/src
uv run pytest packages/virgo-observe-python/tests
uv run python packages/virgo-observe-python/scripts/check_framework_matrix.py
uv build packages/virgo-observe-python
uv run python scripts/verify_virgo_observe_distribution.py
See docs/virgo/instrumentation/python.md, docs/observability.md, and
docs/virgo/instrumentation/python-release.md for server inspection and the
separately authorized publication procedure.
License
The virgo-observe Python SDK is licensed under the
Apache License, Version 2.0.
The full license is included in this package's LICENSE file and its wheel
and source distributions.
This license applies only to packages/virgo-observe-python within the
Platform repository. It does not license the rest of the Platform repository
or Virgo's hosted services. Third-party dependencies retain their own licenses.
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 virgo_observe-0.6.1.tar.gz.
File metadata
- Download URL: virgo_observe-0.6.1.tar.gz
- Upload date:
- Size: 75.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fa4010f80acbf83696ae1dbedece5d76f7dbfd8bcbf319606f59ea4862b531d
|
|
| MD5 |
e2f0ea58ceb8ca6ae5e10cdeaa078242
|
|
| BLAKE2b-256 |
26823a6a17854a91b9feec5c66ae21aa5d886208b6afe8b273e61f6cf42fff24
|
File details
Details for the file virgo_observe-0.6.1-py3-none-any.whl.
File metadata
- Download URL: virgo_observe-0.6.1-py3-none-any.whl
- Upload date:
- Size: 53.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
724b4328eb735db7249fd41424c8f54504070e899bcd11b5871a2329831c1cfc
|
|
| MD5 |
757e7666a65570da99690e20781a06bd
|
|
| BLAKE2b-256 |
d4264cd269ba4d71cc6bad3d154c48399066007ce5e07372829ef3d054d591ee
|