Skip to main content

Strahl Prism Python SDK

Prism analyzes tool-call transcripts before tools execute. You label message roles and registered tools, then call analyze(messages) on a transcript ending in the assistant tool-call response.

The SDK is pre-release and intentionally small. It supports Python 3.11+ and uses httpx and pydantic.

Install

pip install strahl

With uv:

uv add strahl

Quick Start

Set STRAHL_API_KEY in your environment, then use the module-level helpers for simple applications:

import strahl
from strahl import Label

strahl.set_role_labels({
    "user": Label(source={"user"}, visibility={"user"}),
    "assistant": Label(source={"assistant"}, visibility={"user"}),
})


@strahl.tool(
    requires=Label(source={"user"}, visibility={"user"}),
    produces=Label(source={"email-tool"}, visibility={"user"}),
)
def send_email(to: str, subject: str, body: str) -> str:
    ...


messages = [
    {"role": "user", "content": "Send a note to alice@example.com."},
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [
            {
                "id": "call_1",
                "function": {
                    "name": "send_email",
                    "arguments": (
                        '{"to": "alice@example.com", '
                        '"subject": "Note", '
                        '"body": "Hello"}'
                    ),
                },
            }
        ],
    },
]

analysis = strahl.analyze(messages)
analysis.raise_if_denied()

raise_if_denied() raises StrahlDenied if any final tool call is denied. HTTP errors remain httpx.HTTPStatusError.

Labels

Labels have two tag sets:

  • source: where the information came from, used for integrity checks.
  • visibility: where the information may flow, used for confidentiality checks.
Label(source={"user"}, visibility={"user", "support"})

Labels may be dynamic when attached to tools:

Label(source={"support-agent"}, visibility=lambda customer_id: {f"customer:{customer_id}"})

Callable label parameters must match declared tool parameters. Role labels must be static.

Tools

Register Python callables with a decorator:

@strahl.tool(
    requires=Label(source={"support-agent"}, visibility={"support-agent"}),
    produces=Label(source={"crm"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
)
def lookup_customer(customer_id: str) -> str:
    ...

Or imperatively:

strahl.add_tool(
    fn=lookup_customer,
    requires=Label(source={"support-agent"}, visibility={"support-agent"}),
    produces=Label(source={"crm"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
)

Use params when individual arguments have different requirements:

@strahl.tool(
    requires=Label(source={"assistant"}, visibility={"user"}),
    params={
        "to": Label(source={"user"}, visibility={"user"}),
        "subject": Label(source={"user"}, visibility={"user"}),
        "body": Label(source={"user"}, visibility={"user"}),
    },
    produces=Label(source={"email-tool"}, visibility={"user"}),
)
def send_email(to: str, subject: str, body: str) -> str:
    ...

If params is omitted, every actual tool-call argument inherits requires. If params is provided, every declared top-level parameter must be listed.

You can also register OpenAI function tool schemas:

openai_tool = {
    "type": "function",
    "function": {
        "name": "lookup_customer",
        "description": "Look up a customer record.",
        "parameters": {
            "type": "object",
            "properties": {"customer_id": {"type": "string"}},
            "required": ["customer_id"],
        },
    },
}

strahl.add_tool(
    fn=openai_tool,
    requires=Label(source={"support-agent"}, visibility={"support-agent"}),
    produces=Label(source={"crm"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
)

Isolated Runtimes

The top-level helpers use a process-global default Prism runtime. Use Prism directly when you need isolated state for different agents, tenants, or test cases:

import os
from strahl import Label, Prism

prism = Prism(api_key=os.environ["STRAHL_API_KEY"])
prism.set_role_labels({
    "user": Label(source={"user:alice"}, visibility={"user:alice"}),
    "assistant": Label(source={"assistant"}, visibility={"user:alice"}),
    "system": Label(source={"system"}, visibility={"internal"}),
})

prism.add_tool(
    name="send_email",
    fn=send_email,
    requires=Label(source={"user:alice"}, visibility={"user:alice"}),
    produces=Label(source={"email-tool"}, visibility={"user:alice"}),
)

analysis = prism.analyze(messages)

Use Prism.from_default() when modules register tools through top-level decorators and you want an isolated runtime seeded with those registrations:

import strahl
from strahl import Label, Prism

import my_tools

prism = Prism.from_default()
prism.set_role_labels({
    "user": Label(source={"user:alice"}, visibility={"user:alice"}),
    "assistant": Label(source={"assistant"}, visibility={"user:alice"}),
})

OpenAI Message Format

Call analyze() after the assistant response that requests tool calls, before executing those tools:

messages = [
    {"role": "user", "content": "Find my order."},
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [
            {
                "id": "call_lookup",
                "function": {
                    "name": "lookup_order",
                    "arguments": '{"order_id": "ord_123"}',
                },
            }
        ],
    },
]

Tool result messages from earlier turns may be present in the transcript:

{"role": "tool", "tool_call_id": "call_lookup", "content": "Order found: ..."}

The final trace item must be a pending tool call.

Low-Level Client

PrismClient exposes the raw Prism API when you already have wire models:

from strahl.client import PrismClient

client = PrismClient()
response = client.analyses.create(request)

Most applications should use Prism or the module-level helpers instead.

Reading Results

analysis = strahl.analyze(messages)

for tool_call in analysis:
    print(tool_call.name, "denied" if tool_call.denied else "permitted")
    for violation in tool_call.violations:
        print(violation.explain())
        print(violation.evidence_spans)
        print(violation.evidence)

analysis.raise_if_denied()

AnalysisResult exposes:

  • id
  • created_at
  • prompt_tokens
  • tool_calls
  • denied
  • denied_tool_calls
  • explain()
  • raise_if_denied()

Each high-level Violation exposes the server-provided evidence_spans as the canonical evidence representation. Its evidence property is display-only: it slices the matching source text and joins disjoint spans with .... Confidentiality and integrity explanations format the reason tags returned by the server; the SDK does not re-evaluate policy labels.

For now, the SDK trims surrounding source whitespace to mirror the server's canonical trace normalization. Therefore the spans do not yet address the byte-for-byte request text. Original-trace coordinate mapping is tracked in strahl-labs/strahl#51.

Development

uv run pytest -q tests/test_result.py tests/test_client.py tests/test_tools.py
uv run python -m compileall -q strahl

Metadata

Release files for strahl 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for strahl 0.1.1
File Size Uploaded Provenance
strahl-0.1.1.tar.gz 22.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for strahl 0.1.1
File Interpreter ABI Platform
strahl-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 45.6 kB

Release files / strahl-0.1.1.tar.gz

Download URL strahl-0.1.1.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
603d8d101dd0134f703a497060ecece51e7f27ab59de206a91b36a1a5fe9ed1f
BLAKE2b-256 checksum
How to use checksums
4726525731ee0e11c3314802fcee8cc915d9fd8a50e45821b2d4627cb94ebb23
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Release files / strahl-0.1.1-py3-none-any.whl

Download URL strahl-0.1.1-py3-none-any.whl
Size 23.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
de08c7f87c44631381853a7faf1c766fec1938e13aec1cb90cfcb4edfc5674a3
BLAKE2b-256 checksum
How to use checksums
570d28fd6f559db034e25346cf8531234e744529e9ed1be42ac8f5366dd86023
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Security for strahl 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for strahl-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page