Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions examples/python/anthropic_quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 SecureAgentics
#
# Licensed under the Apache Licence, Version 2.0 (the "Licence").
# You may not use this file except in compliance with the Licence.
# A copy of the Licence is included at LICENSE in the repository root.
"""Minimal quickstart: monitor Anthropic API calls with Adrian.

Run::

export ANTHROPIC_API_KEY="sk-ant-..."
python examples/python/anthropic_quickstart.py
"""

from __future__ import annotations

import asyncio
import os

import anthropic
import adrian

# ------------------------------------------------------------------
# 1. Initialise Adrian. This auto-instruments Anthropic by default.
# ------------------------------------------------------------------
adrian.init(
api_key=os.environ.get("ADRIAN_API_KEY", ""),
session_id="anthropic-quickstart-session",
)

# ------------------------------------------------------------------
# 2. Create an Anthropic client as normal.
# ------------------------------------------------------------------
client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])


async def main() -> None:
print("Sending first request...")

# ------------------------------------------------------------------
# 3. Wrap related calls in an invocation context so Adrian groups them.
# ------------------------------------------------------------------
async with adrian.anthropic_invocation():
response = await client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system="You are a concise assistant.",
messages=[{"role": "user", "content": "What is 2 + 2? Answer in one sentence."}],
)

text = next(
(block.text for block in response.content if hasattr(block, "text")),
"",
)
print(f"Model says: {text}")

# A second call in the same invocation -- same invocation_id in Adrian.
follow_up = await client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system="You are a concise assistant.",
messages=[
{"role": "user", "content": "What is 2 + 2? Answer in one sentence."},
{"role": "assistant", "content": text},
{"role": "user", "content": "Now multiply that result by 10."},
],
)

follow_text = next(
(block.text for block in follow_up.content if hasattr(block, "text")),
"",
)
print(f"Follow-up: {follow_text}")

# ------------------------------------------------------------------
# 4. Always shut down Adrian cleanly to flush any pending events.
# ------------------------------------------------------------------
adrian.shutdown()
print("Done. Check your Adrian dashboard for the captured events.")


if __name__ == "__main__":
asyncio.run(main())
74 changes: 74 additions & 0 deletions examples/python/anthropic_streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 SecureAgentics
#
# Licensed under the Apache Licence, Version 2.0 (the "Licence").
# You may not use this file except in compliance with the Licence.
# A copy of the Licence is included at LICENSE in the repository root.
"""Streaming quickstart: monitor Anthropic streamed calls with Adrian.

The streaming counterpart to ``anthropic_quickstart.py``. Text deltas arrive as
usual; the Adrian event is emitted -- and, under BLOCK / HITL, the verdict gate
runs -- when the final message is requested.

Run::

export ANTHROPIC_API_KEY="sk-ant-..."
python examples/python/anthropic_streaming.py
"""

from __future__ import annotations

import asyncio
import os

import anthropic
import adrian

# ------------------------------------------------------------------
# 1. Initialise Adrian. This auto-instruments Anthropic by default.
# ------------------------------------------------------------------
adrian.init(
api_key=os.environ.get("ADRIAN_API_KEY", ""),
session_id="anthropic-streaming-session",
)

client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])


async def main() -> None:
print("Streaming response...\n")

# ------------------------------------------------------------------
# 2. Wrap the call so the event carries a real invocation_id. Without
# this it is emitted as "no_invocation" -- a raw Anthropic call has no
# unit of work for Adrian to scope an invocation to.
# ------------------------------------------------------------------
async with adrian.anthropic_invocation():
async with client.messages.stream(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system="You are a concise assistant.",
messages=[{"role": "user", "content": "Count to five, one word per line."}],
) as stream:
# Text deltas stream through untouched.
async for text in stream.text_stream:
print(text, end="", flush=True)

# ------------------------------------------------------------------
# 3. The Adrian event is emitted here. Under BLOCK / HITL this also
# holds for the classifier verdict and rewrites any halted
# tool_use block to "[BLOCKED by security policy]".
# ------------------------------------------------------------------
message = await stream.get_final_message()

print(f"\n\nStop reason: {message.stop_reason}")

# ------------------------------------------------------------------
# 4. Always shut down Adrian cleanly to flush any pending events.
# ------------------------------------------------------------------
adrian.shutdown()
print("Done. Check your Adrian dashboard for the captured event.")


if __name__ == "__main__":
asyncio.run(main())
36 changes: 36 additions & 0 deletions sdk/python/adrian/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pathlib import Path
from typing import Any

from adrian.anthropic_handler import anthropic_invocation, anthropic_invocation_sync
from adrian.config import (
AdrianConfig,
OnAuditCallback,
Expand Down Expand Up @@ -106,6 +107,9 @@
"__version__",
"mcp_servers",
"redact_text",
"patch_anthropic",
"anthropic_invocation",
"anthropic_invocation_sync",
]

logger = logging.getLogger("adrian")
Expand Down Expand Up @@ -346,6 +350,7 @@ def init(

if auto_instrument:
_auto_instrument_langchain()
_auto_instrument_anthropic()

# MCP server tracking is independent of LangChain auto-instrumentation,
# it observes a different library (langchain-mcp-adapters) and is the
Expand Down Expand Up @@ -379,6 +384,28 @@ def shutdown() -> None:
set_config(None)


def patch_anthropic() -> None:
"""Apply Anthropic SDK instrumentation.

Monkey-patches ``anthropic.Anthropic`` and ``anthropic.AsyncAnthropic`` so
that every ``messages.create`` call is captured as an Adrian ``PairedEvent``.
Called automatically by :func:`init` when ``auto_instrument=True``.

Call explicitly only when ``auto_instrument=False``::

adrian.init(api_key="...", auto_instrument=False)
adrian.patch_anthropic()
"""
from adrian.anthropic_handler import patch_anthropic as _patch

_patch(
hooks_getter=lambda: _hooks,
config_getter=lambda: get_config() if is_initialized() else None,
ws_getter=lambda: _ws_client,
handler_getter=lambda: _handler,
)


def get_handler() -> AdrianCallbackHandler | None:
"""Return the SDK's callback handler, or ``None`` if uninitialised.

Expand Down Expand Up @@ -501,6 +528,15 @@ def patch_langchain() -> None:
# ------------------------------------------------------------------


def _auto_instrument_anthropic() -> None:
"""Apply Anthropic SDK monkey-patches if the package is installed."""
try:
patch_anthropic()
logger.debug("Anthropic auto-instrumentation applied")
except Exception:
logger.exception("Anthropic auto-instrumentation failed")


def _auto_instrument_langchain() -> None:
"""Apply LangChain / LangGraph monkey-patches if the libraries are present."""
try:
Expand Down
Loading