CrewAI
Protect CrewAI crews with OGuardAI so PII is tokenized before the model and restored outside the crew
The OGuardAI Python SDK ships an optional CrewAI integration built around GuardedLLMHooks, a paired
before/after hook in CrewAI's native LLMCallHookContext shape. The before hook tokenizes every
message content in place, so every LLM call the crew makes sees only the tokenized safe_text. It
threads one sealed session across every message and every iteration, so token ids stay stable for the
whole crew. It fails closed: if a transform returns no session_state, before raises and the LLM
call is blocked. The after hook restores the tokens in the model response.
The single sealed session is held on the GuardedLLMHooks instance, never inside CrewAI. CrewAI, its
memory, and any telemetry only ever handle tokens and safe metadata during LLM calls.
Install
The integration is an optional extra, so CrewAI is only pulled in when you use it.
pip install "oguardai-sdk[crewai]"CrewAI is imported lazily (only when you register the hooks), so importing guardai_sdk never
requires CrewAI.
Two ways to run it
Both keep raw PII out of the model: before tokenizes every LLM call in place either way. They differ
in where restoration happens relative to the crew.
- Boundary-safe (recommended). Wire only
before. Every LLM call the crew makes is tokenized, the crew's own outputs stay tokenized, and you restore the single final crew output once withclient.rehydrateafterkickoffreturns, outside CrewAI. No CrewAI memory, downstream agent, or tracer ever observes a restored value. - In-framework (
register).register()wiresbeforeandafter. Theafterhook restores the tokens incontext.responseand returns the restored string, which CrewAI then uses as the LLM response. This is convenient, but the restored (real) values re-enter the crew: any subsequent agent step, CrewAI memory, or tracer attached at or below the LLM-call hook can observe them. Use it only when CrewAI's own telemetry and memory are not exported to a PII-sensitive sink.
Boundary-safe path
from guardai_sdk import OGuardAIClient
from guardai_sdk.integrations.crewai import GuardedLLMHooks
from crewai.hooks import before_llm_call
client = OGuardAIClient(base_url="http://localhost:3000")
hooks = GuardedLLMHooks(client, policy="german-support", language="de")
# Wire ONLY the inbound tokenizer. Every LLM call the crew makes is tokenized in
# place and one sealed session is threaded across all of them. The crew's own
# outputs stay tokenized; nothing is restored inside CrewAI.
before_llm_call(hooks.before)
result = crew.kickoff(inputs={"lead": raw_lead_record})
# Restore once, outside the crew, on the sealed session the before-hook threaded.
final = client.rehydrate(
str(result),
session_state=hooks.session_state,
output_channel="customer_email",
restore_mode="full",
).restored_texthooks.session_state is the sealed session the before hook threaded across the crew's LLM calls.
Read it through that public property, or drive client.transform / client.rehydrate around the
crew yourself if you prefer not to. If the crew made no PII-bearing call, session_state stays
None; guard for that before calling rehydrate, which requires a non-empty session.
In-framework path
from guardai_sdk import OGuardAIClient
from guardai_sdk.integrations.crewai import GuardedLLMHooks
client = OGuardAIClient(base_url="http://localhost:3000")
# Wires `before` + `after` onto CrewAI's global hooks. `rehydrate_in_hook=True`
# opts into in-place restoration, so each LLM response is restored before it
# re-enters the crew and the framework (and any tracer) sees the restored output.
GuardedLLMHooks(
client, policy="german-support", language="de", rehydrate_in_hook=True
).register()
result = crew.kickoff(inputs={"lead": raw_lead_record})The constructor's output_channel (default "user_output") and restore_mode (default "full")
control the after hook's restore in this path. In the boundary-safe path they are ignored, because
you pass the channel and mode straight to client.rehydrate.
What the model and tracer see
During every LLM call, the model and CrewAI's telemetry only ever observe tokenized text like
Kontakt {{person:p_001:ad4f97591c16}} bei {{company:c_001:dfe4fe5cf129}}, {{email:e_001:1bcaef1a4aff}} plus the safe metadata the policy
permits (gender, formality, language). In the boundary-safe setup, restoration happens with
client.rehydrate after kickoff returns, entirely outside CrewAI, so no CrewAI memory, no
subsequent agent, and no tracer ever sees a raw or a restored value. In the in-framework setup,
after replaces context.response with the restored text, so from that point on the restored value
is visible to anything attached at or below the LLM-call hook.
How it works
beforereadscontext.messages, and for each string-bearingcontentcalls/v1/transform, replacing PII with semantic tokens and writing thesafe_textback into the message in place. It chains the sealedsession_statefrom one message to the next, and persists it on the hooks instance so it carries across every LLM call the crew makes.- The crew runs on the tokenized messages. The model drafts on tokens and safe metadata only.
- Restoration calls
/v1/rehydratewith the sealed session. In the boundary-safe path you call it once on the final crew output, outside CrewAI; in the in-framework pathaftercalls it on each response and returns the restored string to CrewAI.
If a transform returns no session_state, before raises ValidationError and the LLM call is
blocked (fail closed). Raw PII is never sent to the model on a failure.