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.
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.
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.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
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.
@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
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.