OGuardAI
Integrations

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 with client.rehydrate after kickoff returns, outside CrewAI. No CrewAI memory, downstream agent, or tracer ever observes a restored value.
  • In-framework (register). register() wires before and after. The after hook restores the tokens in context.response and 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_text

hooks.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

  1. before reads context.messages, and for each string-bearing content calls /v1/transform, replacing PII with semantic tokens and writing the safe_text back into the message in place. It chains the sealed session_state from one message to the next, and persists it on the hooks instance so it carries across every LLM call the crew makes.
  2. The crew runs on the tokenized messages. The model drafts on tokens and safe metadata only.
  3. Restoration calls /v1/rehydrate with the sealed session. In the boundary-safe path you call it once on the final crew output, outside CrewAI; in the in-framework path after calls 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.