Skip to content
Merged
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
32 changes: 32 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,38 @@ asyncio.run(main())

**That's it!** Your existing OpenAI code now includes automatic guardrail validation based on your pipeline configuration. Just use `response.llm_response` instead of `response`.

## Multi-Turn Conversations

When maintaining conversation history across multiple turns, **only append messages after guardrails pass**. This prevents blocked input messages from polluting your conversation context.

```python
messages: list[dict] = []

while True:
user_input = input("You: ")

try:
# ✅ Pass user input inline (don't mutate messages first)
response = await client.chat.completions.create(
messages=messages + [{"role": "user", "content": user_input}],
model="gpt-4o"
)

response_content = response.llm_response.choices[0].message.content
print(f"Assistant: {response_content}")

# ✅ Only append AFTER guardrails pass
messages.append({"role": "user", "content": user_input})
messages.append({"role": "assistant", "content": response_content})

except GuardrailTripwireTriggered:
# ❌ Guardrail blocked - message NOT added to history
print("Message blocked by guardrails")
continue
```

**Why this matters**: If you append the user message before the guardrail check, blocked messages remain in your conversation history and get sent on every subsequent turn, even though they violated your safety policies.

## Guardrail Execution Error Handling

Guardrails supports two error handling modes for guardrail execution failures:
Expand Down
28 changes: 23 additions & 5 deletions examples/basic/azure_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,24 @@
}


async def process_input(guardrails_client: GuardrailsAsyncAzureOpenAI, user_input: str) -> None:
"""Process user input with complete response validation using GuardrailsClient."""
async def process_input(
guardrails_client: GuardrailsAsyncAzureOpenAI,
user_input: str,
messages: list[dict],
) -> None:
"""Process user input with complete response validation using GuardrailsClient.

Args:
guardrails_client: GuardrailsAsyncAzureOpenAI instance.
user_input: User's input text.
messages: Conversation history (modified in place after guardrails pass).
"""
try:
# Use GuardrailsClient to handle all guardrail checks and LLM calls
# Pass user input inline WITHOUT mutating messages first
# Only add to messages AFTER guardrails pass and LLM call succeeds
response = await guardrails_client.chat.completions.create(
model=AZURE_DEPLOYMENT,
messages=[{"role": "user", "content": user_input}],
messages=messages + [{"role": "user", "content": user_input}],
)

# Extract the response content from the GuardrailsResponse
Expand All @@ -69,11 +80,16 @@ async def process_input(guardrails_client: GuardrailsAsyncAzureOpenAI, user_inpu
# Only show output if all guardrails pass
print(f"\nAssistant: {response_text}")

# Guardrails passed - now safe to add to conversation history
messages.append({"role": "user", "content": user_input})
messages.append({"role": "assistant", "content": response_text})

except GuardrailTripwireTriggered as e:
# Extract information from the triggered guardrail
triggered_result = e.guardrail_result
print(" Input blocked. Please try a different message.")
print(f" Full result: {triggered_result}")
# Guardrail blocked - user message NOT added to history
raise
except BadRequestError as e:
# Handle Azure's built-in content filter errors
Expand All @@ -97,6 +113,8 @@ async def main():
api_version="2025-01-01-preview",
)

messages: list[dict] = []

while True:
try:
prompt = input("\nEnter a message: ")
Expand All @@ -105,7 +123,7 @@ async def main():
print("Goodbye!")
break

await process_input(guardrails_client, prompt)
await process_input(guardrails_client, prompt, messages)
except (EOFError, KeyboardInterrupt):
break
except (GuardrailTripwireTriggered, BadRequestError):
Expand Down
70 changes: 42 additions & 28 deletions examples/basic/multiturn_chat_with_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,21 @@ async def main(malicious: bool = False) -> None:
if not user_input:
continue

messages.append({"role": "user", "content": user_input})

# Pass user input inline WITHOUT mutating messages first
# Only add to messages AFTER guardrails pass and LLM call succeeds
try:
resp = await client.chat.completions.create(model="gpt-4.1-nano", messages=messages, tools=tools)
resp = await client.chat.completions.create(
model="gpt-4.1-nano",
messages=messages + [{"role": "user", "content": user_input}],
tools=tools,
)
print_guardrail_results("initial", resp)
choice = resp.llm_response.choices[0]
message = choice.message
tool_calls = getattr(message, "tool_calls", []) or []

# Guardrails passed - now safe to add user message to conversation history
messages.append({"role": "user", "content": user_input})
except GuardrailTripwireTriggered as e:
info = getattr(e, "guardrail_result", None)
info = info.info if info else {}
Expand All @@ -252,29 +259,29 @@ async def main(malicious: bool = False) -> None:
border_style="red",
)
)
# Guardrail blocked - user message NOT added to history
continue

if tool_calls:
# Add assistant message with tool calls to conversation
messages.append(
{
"role": "assistant",
"content": message.content,
"tool_calls": [
{
"id": call.id,
"type": "function",
"function": {
"name": call.function.name,
"arguments": call.function.arguments or "{}",
},
}
for call in tool_calls
],
}
)

# Execute tool calls
# Prepare assistant message with tool calls (don't append yet)
assistant_message = {
"role": "assistant",
"content": message.content,
"tool_calls": [
{
"id": call.id,
"type": "function",
"function": {
"name": call.function.name,
"arguments": call.function.arguments or "{}",
},
}
for call in tool_calls
],
}

# Execute tool calls and collect results (don't append yet)
tool_messages = []
for call in tool_calls:
fname = call.function.name
fargs = json.loads(call.function.arguments or "{}")
Expand All @@ -293,7 +300,7 @@ async def main(malicious: bool = False) -> None:
"ssn": "123-45-6789",
"credit_card": "4111-1111-1111-1111",
}
messages.append(
tool_messages.append(
{
"role": "tool",
"tool_call_id": call.id,
Expand All @@ -302,7 +309,7 @@ async def main(malicious: bool = False) -> None:
}
)
else:
messages.append(
tool_messages.append(
{
"role": "tool",
"tool_call_id": call.id,
Expand All @@ -311,9 +318,13 @@ async def main(malicious: bool = False) -> None:
}
)

# Final call
# Final call with tool results (pass inline without mutating messages)
try:
resp = await client.chat.completions.create(model="gpt-4.1-nano", messages=messages, tools=tools)
resp = await client.chat.completions.create(
model="gpt-4.1-nano",
messages=messages + [assistant_message] + tool_messages,
tools=tools,
)

print_guardrail_results("final", resp)
final_message = resp.llm_response.choices[0].message
Expand All @@ -325,7 +336,9 @@ async def main(malicious: bool = False) -> None:
)
)

# Add final assistant response to conversation
# Guardrails passed - now safe to add all messages to conversation history
messages.append(assistant_message)
messages.extend(tool_messages)
messages.append({"role": "assistant", "content": final_message.content})
except GuardrailTripwireTriggered as e:
info = getattr(e, "guardrail_result", None)
Expand All @@ -345,6 +358,7 @@ async def main(malicious: bool = False) -> None:
border_style="red",
)
)
# Guardrail blocked - tool results NOT added to history
continue
else:
# No tool calls; just print assistant content and add to conversation
Expand Down
27 changes: 18 additions & 9 deletions examples/basic/pii_mask_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,20 @@
async def process_input(
guardrails_client: GuardrailsAsyncOpenAI,
user_input: str,
messages: list[dict],
) -> None:
"""Process user input using GuardrailsClient with automatic PII masking.

Args:
guardrails_client: GuardrailsClient instance with PII masking configuration.
user_input: User's input text.
messages: Conversation history (modified in place after guardrails pass).
"""
try:
# Use GuardrailsClient - it handles all PII masking automatically
# Pass user input inline WITHOUT mutating messages first
# Only add to messages AFTER guardrails pass and LLM call succeeds
response = await guardrails_client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant. Comply with the user's request.",
},
{"role": "user", "content": user_input},
],
messages=messages + [{"role": "user", "content": user_input}],
model="gpt-4",
)

Expand Down Expand Up @@ -125,11 +122,16 @@ async def process_input(
)
)

# Guardrails passed - now safe to add to conversation history
messages.append({"role": "user", "content": user_input})
messages.append({"role": "assistant", "content": content})

except GuardrailTripwireTriggered as exc:
stage_name = exc.guardrail_result.info.get("stage_name", "unknown")
guardrail_name = exc.guardrail_result.info.get("guardrail_name", "unknown")
console.print(f"[bold red]Guardrail '{guardrail_name}' triggered in stage '{stage_name}'![/bold red]")
console.print(Panel(str(exc.guardrail_result), title="Guardrail Result", border_style="red"))
# Guardrail blocked - user message NOT added to history
raise


Expand All @@ -138,14 +140,21 @@ async def main() -> None:
# Initialize GuardrailsAsyncOpenAI with PII masking configuration
guardrails_client = GuardrailsAsyncOpenAI(config=PIPELINE_CONFIG)

messages: list[dict] = [
{
"role": "system",
"content": "You are a helpful assistant. Comply with the user's request.",
}
]

with suppress(KeyboardInterrupt, asyncio.CancelledError):
while True:
try:
user_input = input("\nEnter a message: ").strip()
if user_input.lower() == "exit":
break

await process_input(guardrails_client, user_input)
await process_input(guardrails_client, user_input, messages)

except EOFError:
break
Expand Down
42 changes: 34 additions & 8 deletions examples/basic/structured_outputs_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,64 @@ class UserInfo(BaseModel):
"version": 1,
"guardrails": [
{"name": "Moderation", "config": {"categories": ["hate", "violence"]}},
{
"name": "Custom Prompt Check",
"config": {
"model": "gpt-4.1-nano",
"confidence_threshold": 0.7,
"system_prompt_details": "Check if the text contains any math problems.",
},
},
],
},
}


async def extract_user_info(guardrails_client: GuardrailsAsyncOpenAI, text: str) -> UserInfo:
"""Extract user information using responses_parse with structured output."""
async def extract_user_info(
guardrails_client: GuardrailsAsyncOpenAI,
text: str,
previous_response_id: str | None = None,
) -> tuple[UserInfo, str]:
"""Extract user information using responses.parse with structured output."""
try:
# Use responses.parse() for structured outputs with guardrails
# Note: responses.parse() requires input as a list of message dicts
response = await guardrails_client.responses.parse(
input=[{"role": "system", "content": "Extract user information from the provided text."}, {"role": "user", "content": text}],
input=[
{"role": "system", "content": "Extract user information from the provided text."},
{"role": "user", "content": text},
],
model="gpt-4.1-nano",
text_format=UserInfo,
previous_response_id=previous_response_id,
)

# Access the parsed structured output
user_info = response.llm_response.output_parsed
print(f"✅ Successfully extracted: {user_info.name}, {user_info.age}, {user_info.email}")

return user_info
# Return user info and response ID (only returned if guardrails pass)
return user_info, response.llm_response.id

except GuardrailTripwireTriggered as exc:
print(f"❌ Guardrail triggered: {exc}")
except GuardrailTripwireTriggered:
# Guardrail blocked - no response ID returned, conversation history unchanged
raise


async def main() -> None:
"""Interactive loop demonstrating structured outputs."""
"""Interactive loop demonstrating structured outputs with conversation history."""
# Initialize GuardrailsAsyncOpenAI
guardrails_client = GuardrailsAsyncOpenAI(config=PIPELINE_CONFIG)

# Use previous_response_id to maintain conversation history with responses API
response_id: str | None = None

while True:
try:
text = input("Enter text to extract user info. Include name, age, and email: ")
user_info = await extract_user_info(guardrails_client, text)

# Extract user info - only updates response_id if guardrails pass
user_info, response_id = await extract_user_info(guardrails_client, text, response_id)

# Demonstrate structured output clearly
print("\n✅ Parsed structured output:")
Expand All @@ -66,6 +91,7 @@ async def main() -> None:
print("\nExiting.")
break
except GuardrailTripwireTriggered as exc:
# Guardrail blocked - response_id unchanged, so blocked message not in history
print(f"🛑 Guardrail triggered: {exc}")
continue
except Exception as e:
Expand Down
Loading
Loading