Skip to content

Appendix E: HR Leave Agent — Code Reference

Version: 1.0 Last updated: February 2026

Navigation: ← Appendix D | Appendix Index | Appendix F →

This appendix provides key code excerpts from the HR Leave Agent implementation. Use these as templates for your own agent development.

📦 Full Implementation: The complete runnable code is available at:
github.com/sunilsivadas/agentic-ai-handbook-code

📚 Related sections: - State machine scaffold → Chapter 4, §4.2.2 - Tool patterns → Chapter 9, §9.2 - Evaluation harness → Chapter 7, §7.2 - Prompts and rubrics → Appendix C - Synthetic datasets → Appendix D


E.1 Repository Structure

agentic-ai-handbook-code/
├── README.md
├── requirements.txt
├── pyproject.toml
│
├── hr_leave_agent/
│   ├── __init__.py
│   ├── agent.py          # State machine scaffold (~600 lines)
│   ├── tools.py          # Mock HR system tools (~740 lines)
│   ├── prompts.py        # All prompts by state (~440 lines)
│   └── run.py            # Interactive demo (~270 lines)
│
├── eval/
│   ├── __init__.py
│   ├── evaluate.py       # Evaluation harness (~680 lines)
│   ├── judges/
│   │   ├── eligibility_judge.py
│   │   ├── safety_judge.py
│   │   ├── citation_judge.py
│   │   ├── quality_judge.py
│   │   └── composite_judge.py
│   └── datasets/
│       └── hr_leave/
│           ├── manifest.yaml
│           ├── golden/
│           ├── adversarial/
│           └── regression/
│
├── config/
│   ├── config.yaml
│   └── eval_config.yaml
│
└── docs/
    ├── prompts-rubrics.md
    └── synthetic-datasets.md

E.2 State Machine Scaffold

The core pattern: states with specific responsibilities, allowed tools, and transition rules.

E.2.1 State Definitions

from enum import Enum
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

class State(Enum):
    """Agent states in the scaffold."""
    INTAKE = "intake"      # Parse user intent
    RETRIEVE = "retrieve"  # Gather context via tools
    ASSESS = "assess"      # Evaluate eligibility
    RESPOND = "respond"    # Generate user response
    PREVIEW = "preview"    # Show submission preview
    SUBMIT = "submit"      # Execute write operation
    END = "end"            # Terminal state


@dataclass
class AgentContext:
    """Holds all context accumulated during agent execution."""
    employee_id: str
    conversation_history: List[Dict] = field(default_factory=list)

    # Parsed from intake
    parsed_intent: Optional[Dict] = None

    # Retrieved data
    employee_profile: Optional[Dict] = None
    leave_balances: Optional[Dict] = None
    policy_excerpts: List[Dict] = field(default_factory=list)
    calendar_data: Optional[Dict] = None

    # Assessment
    assessment: Optional[Dict] = None

    # Submission
    request_preview: Optional[Dict] = None
    submitted_request: Optional[Dict] = None
    idempotency_key: Optional[str] = None

    # Tracking
    current_state: State = State.INTAKE
    step_count: int = 0
    tool_calls: List[Dict] = field(default_factory=list)

E.2.2 State Machine Loop

class HRLeaveAgent:
    """
    HR Leave Agent using state machine scaffold.

    State flow:
        INTAKE → RETRIEVE → ASSESS → RESPOND → (END or PREVIEW)
                                         ↓
                                      PREVIEW → SUBMIT → RESPOND → END
    """

    MAX_STEPS = 10

    def __init__(self, model: str = "claude-sonnet-4-20250514"):
        self.client = anthropic.Anthropic()
        self.model = model

    async def run(self, user_message: str, employee_id: str) -> Dict[str, Any]:
        """Run the agent for a single user message."""
        context = AgentContext(employee_id=employee_id)
        context.conversation_history.append({
            "role": "user",
            "content": user_message
        })
        return await self._run_state_machine(context)

    async def _run_state_machine(self, context: AgentContext) -> Dict[str, Any]:
        """Execute the state machine until terminal state or max steps."""

        while context.current_state != State.END and context.step_count < self.MAX_STEPS:
            context.step_count += 1

            # Execute current state
            next_state, output = await self._execute_state(context)

            # Transition
            context.current_state = next_state

            # If we have a response to return, break
            if output.get("response"):
                context.conversation_history.append({
                    "role": "assistant",
                    "content": output["response"]
                })
                return {
                    "response": output["response"],
                    "context": context,
                    "state": context.current_state.value,
                    "tool_calls": context.tool_calls,
                }

        # Fallback if we hit max steps
        return {
            "response": "I apologize, but I'm having trouble processing your request. "
                       "Please try rephrasing or contact HR directly.",
            "context": context,
            "state": context.current_state.value,
            "error": "max_steps_exceeded" if context.step_count >= self.MAX_STEPS else None,
        }

    async def _execute_state(self, context: AgentContext) -> tuple[State, Dict]:
        """Execute a single state and return next state + output."""

        state = context.current_state

        if state == State.INTAKE:
            return await self._execute_intake(context)
        elif state == State.RETRIEVE:
            return await self._execute_retrieve(context)
        elif state == State.ASSESS:
            return await self._execute_assess(context)
        elif state == State.RESPOND:
            return await self._execute_respond(context)
        elif state == State.PREVIEW:
            return await self._execute_preview(context)
        elif state == State.SUBMIT:
            return await self._execute_submit(context)
        else:
            return State.END, {"response": "Unknown state"}

E.2.3 State-Specific Tool Restrictions

def get_tools_for_state(state: str) -> List[Dict]:
    """Get tool definitions allowed for a given state."""
    state_tools = {
        "intake": [],  # No tools - pure parsing
        "retrieve": [
            "get_employee_profile",
            "check_leave_balance",
            "get_policy_sections",
            "check_calendar"
        ],
        "assess": [],  # No tools - pure reasoning
        "respond": [],  # No tools - pure generation
        "preview": ["prepare_leave_request"],
        "submit": ["submit_leave_request"],
    }

    allowed = state_tools.get(state, [])
    return [t for t in TOOL_DEFINITIONS if t["name"] in allowed]

E.3 Tool Patterns

Tools follow strict contracts with typed inputs/outputs and side-effect classification.

E.3.1 Tool Definitions

TOOL_DEFINITIONS = [
    {
        "name": "get_employee_profile",
        "description": "Get employee profile including name, department, grade, and dependents.",
        "input_schema": {
            "type": "object",
            "properties": {
                "employee_id": {
                    "type": "string",
                    "description": "The employee ID to look up"
                }
            },
            "required": ["employee_id"]
        }
    },
    {
        "name": "check_leave_balance",
        "description": "Check leave balance for an employee.",
        "input_schema": {
            "type": "object",
            "properties": {
                "employee_id": {"type": "string"},
                "leave_type": {
                    "type": "string",
                    "enum": ["annual", "sick", "childcare", "maternity", "paternity"]
                }
            },
            "required": ["employee_id"]
        }
    },
    {
        "name": "prepare_leave_request",
        "description": "Prepare a leave request for preview. Does NOT submit.",
        "input_schema": {
            "type": "object",
            "properties": {
                "employee_id": {"type": "string"},
                "leave_type": {"type": "string"},
                "start_date": {"type": "string", "description": "YYYY-MM-DD format"},
                "end_date": {"type": "string", "description": "YYYY-MM-DD format"},
                "reason": {"type": "string"}
            },
            "required": ["employee_id", "leave_type", "start_date", "end_date"]
        }
    },
    {
        "name": "submit_leave_request",
        "description": "Submit a prepared leave request. Requires idempotency_key.",
        "input_schema": {
            "type": "object",
            "properties": {
                "request_id": {"type": "string"},
                "idempotency_key": {
                    "type": "string",
                    "description": "Unique key to prevent duplicate submissions"
                }
            },
            "required": ["request_id", "idempotency_key"]
        }
    }
]

E.3.2 Idempotent Write Operation

def submit_leave_request(
    self,
    request_id: str,
    idempotency_key: str
) -> Dict[str, Any]:
    """
    Submit a prepared leave request.

    Uses idempotency_key to ensure exactly-once semantics:
    - If the same key is used twice, returns the original result
    - Prevents duplicate submissions on retry
    """
    # Check idempotency
    if idempotency_key in self.idempotency_keys:
        existing = self.submitted_requests.get(request_id)
        if existing:
            return {
                "request_id": existing.request_id,
                "status": existing.status,
                "submitted_at": existing.created_at,
                "note": "Duplicate submission detected, returning original result"
            }

    # Mark idempotency key as used
    self.idempotency_keys.add(idempotency_key)

    # Create the submission
    final_request_id = request_id.replace("DRAFT", "2025")
    submitted_at = datetime.now().isoformat()

    request = LeaveRequest(
        request_id=final_request_id,
        status="pending",
        created_at=submitted_at,
        # ... other fields
    )

    self.submitted_requests[final_request_id] = request

    return {
        "request_id": final_request_id,
        "status": "pending",
        "submitted_at": submitted_at,
        "message": "Leave request submitted. Awaiting manager approval."
    }

E.4 Evaluation Harness

The harness runs test cases through the agent and evaluates with multiple judges.

E.4.1 Test Case Structure

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

@dataclass
class TestCase:
    """A single evaluation test case."""
    id: str
    name: str
    category: str
    complexity: str
    tags: List[str]
    input: Dict[str, Any]
    expected: Dict[str, Any]


@dataclass
class JudgeResult:
    """Result from a single judge."""
    judge_name: str
    scores: Dict[str, float]
    passed: bool
    justifications: Dict[str, str] = field(default_factory=dict)
    issues: List[str] = field(default_factory=list)


@dataclass
class EvalResult:
    """Result for a single test case."""
    case_id: str
    case_name: str
    passed: bool
    overall_score: float
    judge_results: Dict[str, JudgeResult]
    agent_response: str
    tool_calls: List[Dict]
    error: Optional[str] = None
    critical_failures: List[str] = field(default_factory=list)

E.4.2 Evaluation Harness

class EvaluationHarness:
    """Runs evaluation suites against an agent."""

    def __init__(self, agent, judges: List[LLMJudge] = None):
        self.agent = agent
        self.judges = judges or [
            EligibilityJudge(),
            SafetyJudge(),
            QualityJudge(),
        ]
        self.rule_checker = RuleBasedChecker()

    def load_cases(self, path: str) -> List[TestCase]:
        """Load test cases from YAML file."""
        with open(path) as f:
            data = yaml.safe_load(f)

        return [
            TestCase(
                id=case["id"],
                name=case["name"],
                category=case["category"],
                complexity=case["complexity"],
                tags=case.get("tags", []),
                input=case["input"],
                expected=case["expected"],
            )
            for case in data.get("cases", [])
        ]

    def run_evaluation(
        self,
        cases: List[TestCase],
        filter_tags: List[str] = None,
        verbose: bool = True
    ) -> EvalReport:
        """Run evaluation on all cases."""

        # Filter if needed
        if filter_tags:
            cases = [c for c in cases if any(t in c.tags for t in filter_tags)]

        results = []
        passed = 0
        failed = 0
        non_negotiable_failures = []

        for case in cases:
            if verbose:
                print(f"Evaluating: {case.id} - {case.name}")

            result = self._evaluate_case(case)
            results.append(result)

            if result.passed:
                passed += 1
            else:
                failed += 1
                if result.critical_failures:
                    non_negotiable_failures.extend([
                        f"{case.id}: {f}" for f in result.critical_failures
                    ])

        return EvalReport(
            timestamp=datetime.now().isoformat(),
            total_cases=len(cases),
            passed=passed,
            failed=failed,
            pass_rate=passed / len(cases) if cases else 0,
            results=results,
            non_negotiable_failures=non_negotiable_failures,
            metrics=self._aggregate_metrics(results),
        )

    def _evaluate_case(self, case: TestCase) -> EvalResult:
        """Evaluate a single test case."""

        # Run agent
        try:
            result = self.agent.run(
                case.input["user_query"],
                case.input["employee_id"]
            )
            agent_response = result.get("response", "")
            tool_calls = result.get("tool_calls", [])
        except Exception as e:
            return EvalResult(
                case_id=case.id,
                case_name=case.name,
                passed=False,
                overall_score=0,
                judge_results={},
                agent_response="",
                tool_calls=[],
                error=str(e),
                critical_failures=[f"Agent error: {str(e)}"]
            )

        # Run rule-based checks (fast)
        critical_failures = []

        if "must_mention" in case.expected:
            missing = self.rule_checker.check_must_mention(
                agent_response, case.expected["must_mention"]
            )
            if missing:
                critical_failures.append(f"Missing required terms: {missing}")

        if "must_not_mention" in case.expected:
            violations = self.rule_checker.check_must_not_mention(
                agent_response, case.expected["must_not_mention"]
            )
            if violations:
                critical_failures.append(f"Contains forbidden terms: {violations}")

        # Run LLM judges
        judge_results = {}
        for judge in self.judges:
            try:
                judge_result = judge.evaluate(case, agent_response, tool_calls)
                judge_results[judge.name] = judge_result

                if not judge_result.passed and judge.name in ["eligibility", "safety"]:
                    critical_failures.extend(judge_result.issues)
            except Exception as e:
                judge_results[judge.name] = JudgeResult(
                    judge_name=judge.name,
                    scores={},
                    passed=False,
                    issues=[f"Judge error: {str(e)}"]
                )

        # Calculate overall score and pass/fail
        overall_score = self._calculate_score(judge_results)
        passed = (
            len(critical_failures) == 0 and
            all(jr.passed for name, jr in judge_results.items() if name != "quality")
        )

        return EvalResult(
            case_id=case.id,
            case_name=case.name,
            passed=passed,
            overall_score=overall_score,
            judge_results=judge_results,
            agent_response=agent_response,
            tool_calls=tool_calls,
            critical_failures=critical_failures,
        )

E.5 Interactive Demo

The companion repository includes an interactive demo. Run it with:

python -m hr_leave_agent.run

The demo lets you role-play as one of four sample employees (varying balances, dependents, and edge cases) and test queries against the agent.


E.6 Running the Code

Installation

# Clone the repository
git clone https://github.com/sunilsivadas/agentic-ai-handbook-code.git
cd agentic-ai-handbook-code

# Create virtual environment
python -m venv venv
source venv/bin/activate  # or `venv\Scripts\activate` on Windows

# Install dependencies
pip install -r requirements.txt

# Set API key
export ANTHROPIC_API_KEY='your-key-here'

Interactive Demo

# Run interactive demo
python -m hr_leave_agent.run

# Or run a single query
python -m hr_leave_agent.run --query "Can I take 3 days off next week?" --employee EMP-2024-001

Run Evaluation

# Run against sample cases
python -m eval.evaluate --cases eval/datasets/sample_cases.yaml --output eval_report.json

# Run with tag filter
python -m eval.evaluate --cases eval/datasets/sample_cases.yaml --tags childcare --output childcare_report.json

# Run adversarial tests
python -m eval.evaluate --cases eval/datasets/hr_leave/adversarial --output adversarial_report.json

E.7 Requirements

# requirements.txt
anthropic>=0.18.0
pyyaml>=6.0
python-dateutil>=2.8.2

📦 Full Implementation: Clone the repository to explore the complete code, run the demo, and adapt for your own use cases. Design rationale (why state machine over ReAct, why idempotency keys, why separate judges) is discussed in Chapter 4 §4.2, Chapter 9 §9.2, and Chapter 7 §7.1 respectively.


Navigation: ← Appendix D | Appendix Index | Appendix F →