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:
idcreated_atprompt_tokenstool_callsdenieddenied_tool_callsexplain()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)
| File | Size | Uploaded | |
|---|---|---|---|
| strahl-0.1.1.tar.gz | 22.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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