Metadata-Version: 2.4
Name: visceral-ai
Version: 0.2.1
Summary: Visceral Python SDK — runtime optimization layer for AI agents
Author-email: Visceral AI <visceral.devs@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://visceralai.dev
Project-URL: Repository, https://github.com/visceral-ai/visceral-py
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: cryptography>=42.0
Requires-Dist: wrapt>=1.16
Requires-Dist: opentelemetry-api<2,>=1.27
Requires-Dist: opentelemetry-sdk<2,>=1.27
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.27
Provides-Extra: openai
Requires-Dist: openai>=1.40; extra == "openai"
Requires-Dist: openinference-instrumentation-openai>=0.1.18; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.25; extra == "anthropic"
Requires-Dist: openinference-instrumentation-anthropic>=0.1.0; extra == "anthropic"
Provides-Extra: google
Requires-Dist: google-genai>=1.0; extra == "google"
Requires-Dist: openinference-instrumentation-google-genai>=0.1.0; extra == "google"
Provides-Extra: langchain
Requires-Dist: openinference-instrumentation-langchain>=0.1.0; extra == "langchain"
Provides-Extra: llama-index
Requires-Dist: openinference-instrumentation-llama-index>=0.1.0; extra == "llama-index"
Provides-Extra: crewai
Requires-Dist: openinference-instrumentation-crewai>=0.1.0; extra == "crewai"
Provides-Extra: all-providers
Requires-Dist: visceral-ai[openai]; extra == "all-providers"
Requires-Dist: visceral-ai[anthropic]; extra == "all-providers"
Requires-Dist: visceral-ai[google]; extra == "all-providers"
Provides-Extra: dev
Requires-Dist: visceral-ai[openai]; extra == "dev"
Requires-Dist: visceral-ai[langchain]; extra == "dev"
Requires-Dist: visceral-ai[llama-index]; extra == "dev"
Requires-Dist: visceral-ai[crewai]; extra == "dev"
Requires-Dist: langchain>=0.3; extra == "dev"
Requires-Dist: langgraph>=0.2; extra == "dev"
Requires-Dist: langchain-openai>=0.2; extra == "dev"
Requires-Dist: llama-index-core>=0.11; extra == "dev"
Requires-Dist: crewai>=1.10.1; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# Visceral Python SDK

`visceral` — runtime optimization layer for AI agents. Wrap your LLM client and
Visceral observes traffic, finds wasted work, and proves a cheaper call is safe
before making it. Fails open: if Visceral is unreachable, your agent runs exactly
as before.

## Install

```bash
pip install "visceral-ai[openai]"   # or [anthropic], [google], [all-providers]
```

On a framework, add its extra so `instrument()` can capture it:

```bash
pip install "visceral-ai[langchain]"   # or [llama-index], [crewai]
```

Distribution name is `visceral-ai`; the import package is `visceral`.
Requires Python 3.10+.

### Agent-assisted setup

The package ships a `/visceral` skill for AI coding assistants (Claude Code
today). Register it once:

```bash
python -m visceral install
```

then open your assistant in the repo you want instrumented and run
`/visceral .` — it detects your stack (provider clients vs. frameworks) and
wires `wrap()` / `instrument()` / `@node` / `@tool` in the right spots,
observe-only, then reports what it did. `--project` installs the skill into
the current repo instead of your user profile; `--stdout` prints it for
other assistants.

## Usage

```python
from openai import OpenAI
from visceral import wrap

client = wrap(OpenAI(), agent_id="my-agent")   # same client back, now instrumented
```

Annotate the steps of your agent so they show up as named spans:

```python
from visceral import node

@node("retrieve")
def retrieve(query: str) -> list[str]:
    ...
```

## Beyond observation

Observation is the default; three opt-in paths build on it, each fully
fail-open. Full detail in the [SDK guide](docs/sdk-guide.md).

- **`wrap(..., apply=True)`** — apply the optimizations the backend has already
  proven safe for your traffic: output-neutral cache-layout rewrites
  (Anthropic `cache_control` markers, OpenAI `prompt_cache_key`) and tool-call
  reuse. Never changes what the model reads or returns.

  ```python
  client = wrap(Anthropic(), agent_id="support-bot", apply=True)
  ```

- **`instrument(framework, agent_id)`** — the client-less counterpart to
  `wrap()`. Capture an agent framework's structure (LangChain / LangGraph,
  LlamaIndex, CrewAI) without wrapping a client. Framework capture wins over
  provider capture, so don't also `wrap()` the same traffic.

  ```python
  from visceral import instrument
  instrument("langgraph", agent_id="research-agent")
  ```

- **`@tool(...)`** — annotate a tool function so read-only calls can be
  deduplicated and, once proven and with `apply=True`, served from a stored
  result instead of re-executed. Serving needs `readonly=True` **and**
  `replay_safe=True`; a failure always runs the real tool.

  ```python
  from visceral import tool

  @tool("get_customer", readonly=True, replay_safe=True)
  def get_customer(customer_id: str) -> dict:
      return db.fetch_customer(customer_id)
  ```

## Authentication

You hold exactly one secret: the **workspace API key** (`vsc_...`), from your
workspace's Settings page in the dashboard (or from Visceral during
onboarding). Set it as `VISCERAL_API_KEY`, or pass `api_key=` to `wrap()`;
set `VISCERAL_BASE_URL` only if self-hosting. The key both authenticates the
SDK and selects the workspace your traffic lands in — there is no separate
user or org credential. One process serves one workspace: wrap every client
in a process with the same key.

Prompt and response text never leaves your process unencrypted: content is
stripped or AES-256-GCM-encrypted before export with a per-workspace
encryption key the SDK fetches automatically — you never handle it — and
spans that cannot be redacted are dropped, never sent.

## Develop

```bash
pip install -e ".[dev]"
ruff check src/ tests/
pytest
```

## License

Apache-2.0 — see [LICENSE](LICENSE).
