---
title: "Building a Library Services Agent: Catalog Search, Hold Management, and Program Registration"
description: "Build an AI agent for public libraries that searches the catalog, places and manages holds, handles account inquiries, and helps patrons discover library programs and events."
canonical: https://callsphere.ai/blog/building-library-services-agent-catalog-search-hold-management
category: "Learn Agentic AI"
tags: ["Government AI", "Library Services", "Catalog Search", "Public Libraries", "Community Services"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-06T01:02:44.825Z
---

# Building a Library Services Agent: Catalog Search, Hold Management, and Program Registration

> Build an AI agent for public libraries that searches the catalog, places and manages holds, handles account inquiries, and helps patrons discover library programs and events.

## The Modern Library Agent

Public libraries are among the most-used government services. A mid-size library system handles thousands of patron interactions daily: catalog searches, hold requests, account questions, program registrations, and reference inquiries. Many of these are repetitive and well-suited to automation — "Do you have this book?" "When is my hold ready?" "What programs are happening this week for kids?"

An AI agent can handle these routine interactions, freeing librarians to focus on the work that requires human expertise: readers' advisory, research assistance, community programming, and helping patrons with complex information needs. The agent is not a replacement for the librarian — it is a force multiplier.

## Modeling the Library Catalog

Library systems use standardized formats like MARC (Machine-Readable Cataloging) and communicate through protocols like SIP2 and Z39.50. For our agent, we abstract these into a clean data model.

```mermaid
flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus
classify"]
    PLAN["Plan and tool
selection"]
    AGENT["Agent loop
LLM plus tools"]
    GUARD{"Guardrails
and policy"}
    EXEC["Execute and
verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus
next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```python
from dataclasses import dataclass, field
from enum import Enum
from datetime import date

class MaterialType(Enum):
    BOOK = "book"
    EBOOK = "ebook"
    AUDIOBOOK = "audiobook"
    DVD = "dvd"
    MAGAZINE = "magazine"
    MUSIC_CD = "music_cd"
    VIDEO_GAME = "video_game"

class ItemStatus(Enum):
    AVAILABLE = "available"
    CHECKED_OUT = "checked_out"
    ON_HOLD = "on_hold"
    IN_TRANSIT = "in_transit"
    PROCESSING = "processing"
    LOST = "lost"

@dataclass
class CatalogItem:
    item_id: str
    title: str
    author: str
    material_type: MaterialType
    isbn: str = ""
    publication_year: int = 0
    subjects: list[str] = field(default_factory=list)
    summary: str = ""
    page_count: int = 0
    language: str = "English"
    series: str | None = None
    series_number: int | None = None
    audience: str = "adult"  # adult, teen, juvenile, children

@dataclass
class ItemCopy:
    copy_id: str
    item_id: str
    branch: str
    status: ItemStatus
    due_date: date | None = None
    call_number: str = ""
    location: str = ""  # fiction, nonfiction, reference, etc.

@dataclass
class PatronAccount:
    patron_id: str
    name: str
    email: str
    phone: str = ""
    home_branch: str = ""
    items_checked_out: int = 0
    items_on_hold: int = 0
    fines_owed: float = 0.0
    card_expiration: date | None = None
```

## Catalog Search Engine

The search engine must handle natural language queries like "mystery novels set in Japan" or "picture books about dinosaurs" and translate them into structured catalog searches.

```python
from openai import OpenAI
import json

client = OpenAI()

CATALOG_SEARCH_PROMPT = """Extract search parameters from the patron's
catalog query.

Return JSON with any of these fields (omit if not mentioned):
- "title": string (exact or partial title)
- "author": string (author name)
- "subject": string (topic/genre)
- "material_type": "book" | "ebook" | "audiobook" | "dvd" | "magazine"
- "audience": "adult" | "teen" | "juvenile" | "children"
- "language": string
- "series": string (series name)
- "keyword": string (general search term)
- "available_only": boolean
"""

def parse_catalog_query(patron_query: str) -> dict:
    """Extract structured search filters from natural language."""
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": CATALOG_SEARCH_PROMPT},
            {"role": "user", "content": patron_query},
        ],
        response_format={"type": "json_object"},
        temperature=0.0,
    )
    return json.loads(response.choices[0].message.content)

def search_catalog(
    filters: dict,
    catalog: list[CatalogItem] = None,
    copies: list[ItemCopy] = None,
) -> list[dict]:
    """Search the catalog using extracted filters."""
    results = catalog or []

    if "title" in filters:
        q = filters["title"].lower()
        results = [i for i in results if q in i.title.lower()]

    if "author" in filters:
        q = filters["author"].lower()
        results = [i for i in results if q in i.author.lower()]

    if "subject" in filters:
        q = filters["subject"].lower()
        results = [
            i for i in results
            if any(q in s.lower() for s in i.subjects)
        ]

    if "material_type" in filters:
        mt = filters["material_type"].lower()
        results = [i for i in results if i.material_type.value == mt]

    if "audience" in filters:
        aud = filters["audience"].lower()
        results = [i for i in results if i.audience == aud]

    if "language" in filters:
        lang = filters["language"].lower()
        results = [i for i in results if i.language.lower() == lang]

    # Enrich with availability
    enriched = []
    for item in results[:20]:
        item_copies = [c for c in (copies or []) if c.item_id == item.item_id]
        available_copies = [c for c in item_copies if c.status == ItemStatus.AVAILABLE]

        if filters.get("available_only") and not available_copies:
            continue

        enriched.append({
            "title": item.title,
            "author": item.author,
            "type": item.material_type.value,
            "year": item.publication_year,
            "total_copies": len(item_copies),
            "available_copies": len(available_copies),
            "branches_available": list({c.branch for c in available_copies}),
            "earliest_return": min(
                (c.due_date for c in item_copies if c.due_date),
                default=None,
            ),
            "item_id": item.item_id,
        })

    return enriched
```

## Hold Management

Placing and managing holds is one of the most common patron requests. The agent needs to handle hold placement, position tracking, and suspension.

```python
from datetime import datetime, timedelta
import uuid

@dataclass
class Hold:
    hold_id: str
    patron_id: str
    item_id: str
    pickup_branch: str
    placed_date: datetime
    status: str = "waiting"  # waiting, ready, expired, cancelled
    queue_position: int = 0
    estimated_wait_days: int | None = None
    ready_date: datetime | None = None
    expiration_date: datetime | None = None
    suspended_until: date | None = None

class HoldManager:
    """Manage patron holds on catalog items."""

    def __init__(self, db):
        self.db = db

    async def place_hold(
        self, patron_id: str, item_id: str, pickup_branch: str
    ) -> Hold:
        """Place a hold on a catalog item."""
        # Check patron eligibility
        patron = await self.db.get_patron(patron_id)
        if patron.fines_owed > 10.00:
            raise ValueError(
                "Hold cannot be placed with fines over $10.00. "
                f"Current balance: ${patron.fines_owed:.2f}"
            )

        # Check existing holds limit
        if patron.items_on_hold >= 25:
            raise ValueError("Maximum of 25 holds reached.")

        # Get current hold queue length
        existing_holds = await self.db.get_holds_for_item(item_id)
        queue_position = len(existing_holds) + 1

        # Estimate wait time based on copies and queue position
        copies = await self.db.get_copies(item_id)
        total_copies = len(copies)
        avg_checkout_days = 21
        estimated_wait = (queue_position / max(total_copies, 1)) * avg_checkout_days

        hold = Hold(
            hold_id=str(uuid.uuid4())[:8],
            patron_id=patron_id,
            item_id=item_id,
            pickup_branch=pickup_branch,
            placed_date=datetime.utcnow(),
            queue_position=queue_position,
            estimated_wait_days=int(estimated_wait),
        )

        await self.db.save_hold(hold)
        return hold

    async def get_patron_holds(self, patron_id: str) -> list[dict]:
        """Get all active holds for a patron with status details."""
        holds = await self.db.get_holds_by_patron(patron_id)
        results = []

        for hold in holds:
            item = await self.db.get_catalog_item(hold.item_id)
            results.append({
                "hold_id": hold.hold_id,
                "title": item.title,
                "author": item.author,
                "status": hold.status,
                "queue_position": hold.queue_position,
                "estimated_wait_days": hold.estimated_wait_days,
                "pickup_branch": hold.pickup_branch,
                "ready_date": hold.ready_date.isoformat() if hold.ready_date else None,
                "expires": hold.expiration_date.isoformat() if hold.expiration_date else None,
            })

        return results
```

## Library Programs and Events

Libraries run extensive programming — storytimes, book clubs, author visits, maker space workshops, ESL classes, and digital literacy training. The agent helps patrons discover and register for events.

```python
@dataclass
class LibraryEvent:
    event_id: str
    title: str
    description: str
    branch: str
    event_date: datetime
    duration_minutes: int
    audience: str  # children, teen, adult, all_ages
    category: str  # storytime, book_club, workshop, author, technology
    registration_required: bool = False
    max_attendees: int | None = None
    current_registrations: int = 0
    cost: float = 0.0  # almost always free

def find_upcoming_events(
    branch: str | None = None,
    audience: str | None = None,
    category: str | None = None,
    days_ahead: int = 14,
    events: list[LibraryEvent] = None,
) -> list[dict]:
    """Find upcoming library events with optional filtering."""
    now = datetime.utcnow()
    cutoff = now + timedelta(days=days_ahead)

    results = events or []
    results = [e for e in results if now <= e.event_date <= cutoff]

    if branch:
        results = [e for e in results if e.branch.lower() == branch.lower()]
    if audience:
        results = [
            e for e in results
            if e.audience == audience or e.audience == "all_ages"
        ]
    if category:
        results = [e for e in results if e.category.lower() == category.lower()]

    results.sort(key=lambda e: e.event_date)

    return [
        {
            "title": e.title,
            "branch": e.branch,
            "date": e.event_date.strftime("%A, %B %d at %I:%M %p"),
            "duration": f"{e.duration_minutes} minutes",
            "audience": e.audience,
            "category": e.category,
            "registration_required": e.registration_required,
            "spots_available": (
                e.max_attendees - e.current_registrations
                if e.max_attendees else "Unlimited"
            ),
            "free": e.cost == 0,
        }
        for e in results[:15]
    ]
```

## FAQ

### How does the agent provide readers' advisory — suggesting what to read next?

The agent builds a reading profile from the patron's checkout history and hold patterns. If a patron has checked out five cozy mysteries in the past year, the agent can suggest similar titles, new releases in the genre, or adjacent genres like domestic suspense. It uses the same approach as recommendation systems: collaborative filtering (patrons who read X also read Y) combined with content-based filtering (same author, subject, or series). The agent presents recommendations with brief explanations: "Since you enjoyed The Thursday Murder Club, you might like these other mystery novels featuring older protagonists."

### How does the agent handle patrons with accessibility needs?

The agent proactively surfaces alternative formats. When a patron searches for a title, results include all available formats — print, large print, audiobook, e-book, and Braille if available. If a patron has previously checked out only audiobooks or large print editions, the agent defaults to showing those formats first. For library events, the agent includes accessibility information: wheelchair access, ASL interpretation availability, and whether assistive listening devices are provided.

### Can the agent help manage interlibrary loan requests?

Yes. When a patron searches for a title that is not in the local catalog, the agent checks regional consortium catalogs and offers to place an interlibrary loan (ILL) request. It explains the process: "This title is not in our collection, but it is available at County Library. I can request it for you — ILL requests typically take 5-10 business days. There is no charge." The agent tracks the ILL status and notifies the patron when the item arrives at their pickup branch.

---

#GovernmentAI #LibraryServices #CatalogSearch #PublicLibraries #CommunityServices #AgenticAI #LearnAI #AIEngineering

---

Source: https://callsphere.ai/blog/building-library-services-agent-catalog-search-hold-management
