Session 4: Adding Validation and Guard Logic
Synopsis
Introduces practical safeguards such as input checks, output validation, rule-based filters, and fallback behavior. This session prepares learners to build applications that behave more predictably in real-world conditions.
Session Content
Session 4: Adding Validation and Guard Logic
Session Overview
In this session, learners will build safer GenAI applications by adding validation and guard logic around model inputs and outputs. The focus is practical: how to reduce malformed outputs, detect unsafe or irrelevant responses, enforce business rules, and fail gracefully when model behavior is uncertain.
By the end of this session, learners will be able to:
- Explain why validation and guard logic are essential in GenAI systems
- Distinguish between input validation, output validation, and behavioral guardrails
- Implement schema-based response validation
- Add lightweight rule-based guards for safety and business constraints
- Build retry-and-repair loops when model outputs do not pass validation
- Create a reusable Python validation pipeline using the OpenAI Responses API
Learning Objectives
After this session, you should be able to:
- Validate user input before sending it to an LLM
- Constrain and verify model output using structured formats
- Add rule-based checks for safety, relevance, and business rules
- Repair or retry invalid outputs automatically
- Design a simple guard pipeline for agentic applications
Agenda (~45 minutes)
- 0–8 min: Why validation and guards matter
- 8–18 min: Input validation and prompt-level constraints
- 18–30 min: Output validation with structured responses
- 30–40 min: Guard logic, retry loops, and fallback handling
- 40–45 min: Wrap-up and extension ideas
1. Why Validation and Guard Logic Matter
LLMs are powerful but probabilistic. That means they can:
- produce malformed JSON
- omit required fields
- violate business rules
- answer questions outside scope
- invent unsupported facts
- generate unsafe or disallowed content
In a real application, "mostly correct" is not enough.
Common Failure Modes
1. Invalid structure
You expect JSON with specific keys, but receive extra prose or missing fields.
2. Invalid values
The shape is correct, but the values are wrong: - negative price - unsupported category - invalid email - confidence score outside 0–1
3. Scope violations
The model answers questions it should refuse or escalate.
4. Unsafe responses
The application must detect harmful or disallowed outputs.
5. Hallucinated certainty
The model gives a confident answer even when the prompt does not provide enough evidence.
2. Types of Validation and Guard Logic
A robust GenAI app typically uses multiple layers.
A. Input Validation
Validate user input before sending it to the model.
Examples: - required fields present - string length limits - allowed categories only - no empty text - no unsupported file types
B. Prompt Constraints
Tell the model what shape and behavior are expected.
Examples:
- “Return only JSON”
- “Use one of these labels”
- “If uncertain, set needs_review: true”
C. Output Validation
Check the response programmatically after generation.
Examples: - parse JSON - validate schema - validate ranges and enums - reject missing fields
D. Guard Logic
Apply domain-specific rules after validation.
Examples: - refuse if category is “medical” but app is for retail only - flag if confidence < 0.7 - reject if response includes prohibited claims - route to human review if explanation is too short
E. Retry / Repair Logic
If output fails checks: - retry with a stricter prompt - ask model to repair invalid JSON - use a fallback message - escalate to human review
3. Theory: Validation Pipeline Design
A practical validation pipeline often looks like this:
User Input
↓
Input Validation
↓
LLM Call with Constraints
↓
Parse Response
↓
Schema Validation
↓
Rule-Based Guard Checks
↓
Accept / Retry / Repair / Escalate
Design Principles
- Fail early: Catch bad input before the API call
- Be explicit: Ask for structured responses and fixed labels
- Validate independently: Never trust model output blindly
- Separate concerns: schema validation is different from business rules
- Provide fallbacks: retries and human-review paths reduce brittleness
- Log decisions: for debugging and quality improvement
4. Hands-On Exercise 1: Input Validation Before Calling the Model
Goal
Build a simple classifier input validator for a support-ticket triage system.
The app expects:
- ticket_text: non-empty string, 10–500 characters
- priority_hint: optional, must be one of low, medium, high
What You Will Learn
- how to validate user inputs in Python
- how to reject bad requests early
- how to keep prompts cleaner and more reliable
Code
from typing import Optional
ALLOWED_PRIORITIES = {"low", "medium", "high"}
def validate_ticket_input(ticket_text: str, priority_hint: Optional[str] = None) -> dict:
"""
Validate user-provided support ticket input before sending it to the LLM.
Returns:
A normalized dictionary if valid.
Raises:
ValueError: if any validation rule fails.
"""
if not isinstance(ticket_text, str):
raise ValueError("ticket_text must be a string.")
ticket_text = ticket_text.strip()
if not ticket_text:
raise ValueError("ticket_text cannot be empty.")
if len(ticket_text) < 10:
raise ValueError("ticket_text must be at least 10 characters long.")
if len(ticket_text) > 500:
raise ValueError("ticket_text must be 500 characters or fewer.")
if priority_hint is not None:
if not isinstance(priority_hint, str):
raise ValueError("priority_hint must be a string if provided.")
priority_hint = priority_hint.strip().lower()
if priority_hint not in ALLOWED_PRIORITIES:
raise ValueError(f"priority_hint must be one of: {sorted(ALLOWED_PRIORITIES)}")
return {
"ticket_text": ticket_text,
"priority_hint": priority_hint,
}
if __name__ == "__main__":
examples = [
{
"ticket_text": "I was charged twice for my monthly subscription and need a refund.",
"priority_hint": "high",
},
{
"ticket_text": "Too short",
"priority_hint": "medium",
},
{
"ticket_text": "My account page fails to load after login and shows a 500 error.",
"priority_hint": "urgent",
},
]
for idx, example in enumerate(examples, start=1):
print(f"\nExample {idx}:")
try:
validated = validate_ticket_input(**example)
print("Validated input:", validated)
except ValueError as exc:
print("Validation error:", exc)
Example Output
Example 1:
Validated input: {'ticket_text': 'I was charged twice for my monthly subscription and need a refund.', 'priority_hint': 'high'}
Example 2:
Validation error: ticket_text must be at least 10 characters long.
Example 3:
Validation error: priority_hint must be one of: ['high', 'low', 'medium']
Exercise Tasks
- Add validation for a new field called
customer_tier - Allow only
free,pro, orenterprise - Normalize all accepted values to lowercase
- Test both valid and invalid examples
5. Hands-On Exercise 2: Structured Output with the Responses API
Goal
Call the OpenAI model and request a structured JSON response for ticket triage.
We want the model to return:
category: one ofbilling,technical,account,otherpriority: one oflow,medium,highneeds_human_review: booleanshort_reply: short message to the customer
What You Will Learn
- how to use the OpenAI Python SDK with the Responses API
- how to instruct the model to return strict JSON
- how to parse model output safely
Code
import json
import os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def triage_ticket(ticket_text: str, priority_hint: str | None = None) -> dict:
"""
Send a support ticket to the model and ask for a structured triage response.
Args:
ticket_text: The support request text.
priority_hint: Optional priority hint supplied by the UI.
Returns:
Parsed dictionary response from the model.
"""
instructions = (
"You are a support ticket triage assistant. "
"Return ONLY valid JSON with the following keys: "
"category, priority, needs_human_review, short_reply. "
"Rules: "
"- category must be one of: billing, technical, account, other "
"- priority must be one of: low, medium, high "
"- needs_human_review must be true or false "
"- short_reply must be a brief customer-facing response under 30 words "
"Do not include markdown fences or extra text."
)
user_payload = {
"ticket_text": ticket_text,
"priority_hint": priority_hint,
}
response = client.responses.create(
model="gpt-5.4-mini",
instructions=instructions,
input=json.dumps(user_payload),
)
# The SDK provides a convenience property for the aggregated text output.
raw_text = response.output_text.strip()
# Parse the JSON returned by the model.
return json.loads(raw_text)
if __name__ == "__main__":
result = triage_ticket(
ticket_text="I updated my payment method, but I was still billed on the old card. Please fix this.",
priority_hint="high",
)
print("Structured model response:")
print(json.dumps(result, indent=2))
Example Output
{
"category": "billing",
"priority": "high",
"needs_human_review": false,
"short_reply": "Thanks for reporting this. We’re reviewing your billing issue and will help resolve it shortly."
}
Discussion
Prompting for JSON helps, but it does not guarantee correctness. We still need validation after parsing.
6. Hands-On Exercise 3: Output Validation with a Python Schema
Goal
Validate the model output against required fields and business constraints.
What You Will Learn
- how to validate parsed JSON
- how to separate model generation from application acceptance
- how to surface useful errors for retries
Code
import json
from typing import Any
ALLOWED_CATEGORIES = {"billing", "technical", "account", "other"}
ALLOWED_PRIORITIES = {"low", "medium", "high"}
def validate_triage_output(data: dict[str, Any]) -> dict[str, Any]:
"""
Validate the model-generated triage output.
Args:
data: Parsed JSON response from the model.
Returns:
The validated data.
Raises:
ValueError: if validation fails.
"""
required_fields = {
"category": str,
"priority": str,
"needs_human_review": bool,
"short_reply": str,
}
if not isinstance(data, dict):
raise ValueError("Output must be a dictionary.")
for field_name, field_type in required_fields.items():
if field_name not in data:
raise ValueError(f"Missing required field: {field_name}")
if not isinstance(data[field_name], field_type):
raise ValueError(
f"Field '{field_name}' must be of type {field_type.__name__}."
)
if data["category"] not in ALLOWED_CATEGORIES:
raise ValueError(
f"Invalid category: {data['category']}. Allowed: {sorted(ALLOWED_CATEGORIES)}"
)
if data["priority"] not in ALLOWED_PRIORITIES:
raise ValueError(
f"Invalid priority: {data['priority']}. Allowed: {sorted(ALLOWED_PRIORITIES)}"
)
short_reply = data["short_reply"].strip()
if not short_reply:
raise ValueError("short_reply cannot be empty.")
if len(short_reply.split()) > 30:
raise ValueError("short_reply must be 30 words or fewer.")
data["short_reply"] = short_reply
return data
if __name__ == "__main__":
valid_output = {
"category": "billing",
"priority": "high",
"needs_human_review": False,
"short_reply": "We’re sorry about the billing issue and will investigate it right away.",
}
invalid_output = {
"category": "refunds",
"priority": "urgent",
"needs_human_review": "maybe",
"short_reply": "",
}
for label, sample in [("VALID", valid_output), ("INVALID", invalid_output)]:
print(f"\n{label} SAMPLE")
try:
validated = validate_triage_output(sample)
print(json.dumps(validated, indent=2))
except ValueError as exc:
print("Validation error:", exc)
Example Output
VALID SAMPLE
{
"category": "billing",
"priority": "high",
"needs_human_review": false,
"short_reply": "We’re sorry about the billing issue and will investigate it right away."
}
INVALID SAMPLE
Validation error: Field 'needs_human_review' must be of type bool.
Exercise Tasks
- Add a new optional field called
confidence - Require it to be a float between
0.0and1.0 - If
confidence < 0.6, forceneeds_human_review = True - Test the validator with multiple examples
7. Hands-On Exercise 4: Guard Logic for Business Rules
Goal
Add domain-specific checks beyond structural validation.
Suppose your support bot is not allowed to: - give refund guarantees - make legal claims - classify sensitive cases as fully automated when uncertain
What You Will Learn
- how to add rule-based guardrails
- how to combine validation with policy enforcement
- how to flag risky outputs
Code
from typing import Any
PROHIBITED_PHRASES = [
"we guarantee a refund",
"you are legally entitled",
"this issue is definitely caused by",
]
def apply_guard_rules(data: dict[str, Any]) -> dict[str, Any]:
"""
Apply business and policy guards to validated triage data.
This function assumes schema validation already passed.
Rules:
- Prohibited phrases are not allowed in short_reply
- High priority technical tickets should be reviewed by a human
"""
reply_lower = data["short_reply"].lower()
for phrase in PROHIBITED_PHRASES:
if phrase in reply_lower:
raise ValueError(f"Guard violation: prohibited phrase detected: '{phrase}'")
if data["category"] == "technical" and data["priority"] == "high":
data["needs_human_review"] = True
return data
if __name__ == "__main__":
sample_ok = {
"category": "billing",
"priority": "medium",
"needs_human_review": False,
"short_reply": "Thanks for reaching out. We’re reviewing your billing concern now.",
}
sample_bad = {
"category": "billing",
"priority": "medium",
"needs_human_review": False,
"short_reply": "We guarantee a refund and will process it today.",
}
sample_technical = {
"category": "technical",
"priority": "high",
"needs_human_review": False,
"short_reply": "We’re investigating the outage and will update you soon.",
}
for label, sample in [
("OK", sample_ok),
("BAD", sample_bad),
("TECHNICAL_HIGH", sample_technical),
]:
print(f"\n{label}")
try:
guarded = apply_guard_rules(sample)
print(guarded)
except ValueError as exc:
print("Guard error:", exc)
Example Output
OK
{'category': 'billing', 'priority': 'medium', 'needs_human_review': False, 'short_reply': 'Thanks for reaching out. We’re reviewing your billing concern now.'}
BAD
Guard error: Guard violation: prohibited phrase detected: 'we guarantee a refund'
TECHNICAL_HIGH
{'category': 'technical', 'priority': 'high', 'needs_human_review': True, 'short_reply': 'We’re investigating the outage and will update you soon.'}
8. Hands-On Exercise 5: Retry and Repair Loop
Goal
Build a small pipeline that: 1. validates input 2. calls the model 3. parses output 4. validates schema 5. applies guard logic 6. retries once if validation fails
What You Will Learn
- how to create a practical LLM safety pipeline
- how to recover from bad outputs
- how to design a reusable flow for agentic systems
Full Example
import json
import os
from typing import Any
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
ALLOWED_CATEGORIES = {"billing", "technical", "account", "other"}
ALLOWED_PRIORITIES = {"low", "medium", "high"}
PROHIBITED_PHRASES = [
"we guarantee a refund",
"you are legally entitled",
"this issue is definitely caused by",
]
def validate_ticket_input(ticket_text: str, priority_hint: str | None = None) -> dict[str, Any]:
"""
Validate incoming user input before calling the model.
"""
if not isinstance(ticket_text, str):
raise ValueError("ticket_text must be a string.")
ticket_text = ticket_text.strip()
if not ticket_text:
raise ValueError("ticket_text cannot be empty.")
if len(ticket_text) < 10 or len(ticket_text) > 500:
raise ValueError("ticket_text must be between 10 and 500 characters.")
if priority_hint is not None:
if not isinstance(priority_hint, str):
raise ValueError("priority_hint must be a string if provided.")
priority_hint = priority_hint.strip().lower()
if priority_hint not in ALLOWED_PRIORITIES:
raise ValueError(f"priority_hint must be one of: {sorted(ALLOWED_PRIORITIES)}")
return {
"ticket_text": ticket_text,
"priority_hint": priority_hint,
}
def request_triage(payload: dict[str, Any], strict_retry: bool = False) -> dict[str, Any]:
"""
Request triage JSON from the model.
If strict_retry is True, the prompt becomes more explicit about formatting.
"""
instructions = (
"You are a support ticket triage assistant. "
"Return ONLY valid JSON with keys: "
"category, priority, needs_human_review, short_reply. "
"Allowed category values: billing, technical, account, other. "
"Allowed priority values: low, medium, high. "
"needs_human_review must be a boolean. "
"short_reply must be under 30 words and must not promise refunds or legal outcomes. "
"No markdown. No backticks. No extra text."
)
if strict_retry:
instructions += (
" This is a retry because the previous answer failed validation. "
"Be extra careful to output exactly one JSON object and nothing else."
)
response = client.responses.create(
model="gpt-5.4-mini",
instructions=instructions,
input=json.dumps(payload),
)
return json.loads(response.output_text.strip())
def validate_triage_output(data: dict[str, Any]) -> dict[str, Any]:
"""
Validate response structure and value constraints.
"""
if not isinstance(data, dict):
raise ValueError("Output must be a dictionary.")
required_fields = {
"category": str,
"priority": str,
"needs_human_review": bool,
"short_reply": str,
}
for field_name, field_type in required_fields.items():
if field_name not in data:
raise ValueError(f"Missing field: {field_name}")
if not isinstance(data[field_name], field_type):
raise ValueError(f"Field '{field_name}' must be {field_type.__name__}")
if data["category"] not in ALLOWED_CATEGORIES:
raise ValueError(f"Invalid category: {data['category']}")
if data["priority"] not in ALLOWED_PRIORITIES:
raise ValueError(f"Invalid priority: {data['priority']}")
short_reply = data["short_reply"].strip()
if not short_reply:
raise ValueError("short_reply cannot be empty")
if len(short_reply.split()) > 30:
raise ValueError("short_reply must be 30 words or fewer")
data["short_reply"] = short_reply
return data
def apply_guard_rules(data: dict[str, Any]) -> dict[str, Any]:
"""
Enforce policy and business rules after schema validation.
"""
reply_lower = data["short_reply"].lower()
for phrase in PROHIBITED_PHRASES:
if phrase in reply_lower:
raise ValueError(f"Prohibited phrase found: {phrase}")
if data["category"] == "technical" and data["priority"] == "high":
data["needs_human_review"] = True
return data
def triage_with_validation(ticket_text: str, priority_hint: str | None = None) -> dict[str, Any]:
"""
End-to-end pipeline:
- validate input
- call model
- parse output
- validate structure
- apply business guards
- retry once if needed
"""
payload = validate_ticket_input(ticket_text, priority_hint)
errors = []
for attempt in range(2):
try:
raw_output = request_triage(payload, strict_retry=(attempt == 1))
validated = validate_triage_output(raw_output)
guarded = apply_guard_rules(validated)
return guarded
except (json.JSONDecodeError, ValueError) as exc:
errors.append(str(exc))
return {
"category": "other",
"priority": "medium",
"needs_human_review": True,
"short_reply": "Thanks for your message. A support specialist will review this request shortly.",
"fallback_reason": errors,
}
if __name__ == "__main__":
ticket = "Our team cannot log in after the latest deployment, and production access seems broken."
result = triage_with_validation(ticket_text=ticket, priority_hint="high")
print("Final triage result:")
print(json.dumps(result, indent=2))
Example Output
{
"category": "technical",
"priority": "high",
"needs_human_review": true,
"short_reply": "We’re sorry for the disruption. A support specialist is reviewing this urgent issue now."
}
Extension Tasks
- Add logging for each failed validation attempt
- Add a third path: escalate to a human if the ticket includes words like
outage,security, orbreach - Store the original raw model output for debugging
- Add confidence scoring and threshold-based escalation
9. Applying This to Agentic Systems
Validation and guard logic become even more important in agentic workflows.
Why?
Agents often: - decide what action to take - call tools - transform intermediate data - generate final user-facing responses
A single invalid step can cascade into bigger failures.
Example Guard Points in Agentic Flows
Before tool calls
Validate: - required tool arguments - allowed parameter ranges - user authorization
After tool results
Validate: - result schema - null or empty values - stale or inconsistent data
Before final answer
Validate: - no unsupported claims - no hidden internal data leakage - response matches business policy
Mini Pattern
def agent_step(user_input: str) -> dict:
# 1. Validate input
# 2. Ask model for next action
# 3. Validate chosen action
# 4. Execute tool
# 5. Validate tool output
# 6. Generate final response
# 7. Apply final guards
return {}
The key lesson: guard each boundary.
10. Best Practices Checklist
Use this checklist when building GenAI applications:
Input Safety
- [ ] check required fields
- [ ] enforce length and type constraints
- [ ] normalize values
- [ ] reject unsupported requests early
Prompt Design
- [ ] request structured output
- [ ] define allowed values explicitly
- [ ] tell the model what to do when uncertain
- [ ] prohibit extra commentary when machine parsing is required
Output Validation
- [ ] parse carefully
- [ ] validate schema
- [ ] validate enums and ranges
- [ ] validate text length and formatting
Guard Logic
- [ ] encode business rules separately from schema rules
- [ ] detect prohibited claims or phrases
- [ ] route risky cases to human review
- [ ] log violations for analysis
Reliability
- [ ] retry invalid generations with stricter instructions
- [ ] use safe fallback responses
- [ ] keep raw outputs for debugging
- [ ] monitor validation failure rates
11. Common Mistakes to Avoid
Mistake 1: Trusting JSON-shaped output without checking values
Correct shape does not mean correct content.
Mistake 2: Mixing prompt logic with business policy
Keep policy rules in code where they are testable and auditable.
Mistake 3: No fallback path
A production system must know what to do when validation fails.
Mistake 4: Overly complex guard logic inside one function
Split into: - input validation - schema validation - business guards - escalation logic
Mistake 5: Ignoring observability
Without logs, you cannot improve your pipeline.
12. Quick Recap
In this session, you learned how to:
- validate user input before calling a model
- request structured responses using the OpenAI Responses API
- parse and validate model output in Python
- apply business rule guards after schema checks
- build retry and fallback logic for reliability
This is a core skill for safe and production-ready GenAI systems.
13. Practice Challenge
Build a product review moderation assistant with the following requirements:
Input
review_text: 20–300 charactersproduct_category: one ofbooks,electronics,clothing
Model Output
sentiment:positive,neutral,negativecontains_abuse: booleanpublish: booleansummary: under 20 words
Guard Rules
- If
contains_abuse = true, thenpublish = false - Reject summaries that contain insults
- If output is invalid, retry once
- If still invalid, return a safe fallback
Try implementing: 1. input validation 2. model call 3. schema validation 4. guard rules 5. retry logic
Useful Resources
- OpenAI Responses API Guide
- OpenAI API Reference
- OpenAI Python SDK
- Python json module
- Python typing module
End-of-Session Summary
Validation and guard logic are essential for turning LLM demos into dependable applications. Prompting helps, but code-level enforcement is what makes systems safer and more predictable. As your applications become more agentic, these checks should be added at every step where data crosses a boundary: user input, model output, tool arguments, tool results, and final responses.
In the next session, these patterns can be extended into more advanced agent workflows with tool usage, state handling, and decision control.