Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
4c122dd
remove JudgeGroup evaluation from hotel_receptionist example
u9g Jul 21, 2026
2633d5a
drop unused evals extra from hotel_receptionist requirements
u9g Jul 21, 2026
407e04b
add per-request LLM token usage logging to hotel_receptionist
u9g Jul 21, 2026
bf98100
fix(hotel_receptionist): pin the sim clock automatically under lk age…
u9g Jul 21, 2026
b339e9c
feat(hotel_receptionist): gate the goodbye behind a pre-hangup policy…
u9g Jul 21, 2026
a2b60c0
fix(hotel_receptionist): give the goodbye a flush grace before sessio…
u9g Jul 21, 2026
5e7efbe
fix(beta): let the final message flush before EndCallTool shuts the s…
u9g Jul 21, 2026
e9dbc3d
fix(hotel_receptionist): let the caller hang up first after the goodbye
u9g Jul 21, 2026
8691c52
fix(beta): let the user hang up first before EndCallTool closes a tex…
u9g Jul 21, 2026
8d6fa84
test(hotel_receptionist): 11-scenario smoke subset for goodbye-path i…
u9g Jul 21, 2026
f80f3f9
fix(hotel_receptionist): state the policy position before relaying a …
u9g Jul 21, 2026
77eda72
fix(hotel_receptionist): gate dispute waivers and credits on an offer…
u9g Jul 21, 2026
3c9f3ff
fix(hotel_receptionist): split order_flowers destination into locatio…
u9g Jul 21, 2026
4bd5fa7
fix(hotel_receptionist): neutralize the order_flowers location example
u9g Jul 21, 2026
f9e0f79
fix(hotel_receptionist): one farewell per call - no goodbye loop on r…
u9g Jul 21, 2026
4017e40
feat(hotel_receptionist): capture flight departure time and compute t…
u9g Jul 21, 2026
d7e5021
fix(hotel_receptionist): lookup_invoice forbids improvised resolution…
u9g Jul 21, 2026
0f82c15
feat(hotel_receptionist): structured florist destination and order am…
u9g Jul 22, 2026
909180c
feat(hotel_receptionist): validate florist destinations and add amend…
u9g Jul 22, 2026
0936baf
test(hotel_receptionist): florist scenarios assert canonical room ids
u9g Jul 22, 2026
b9a125a
fix(hotel_receptionist): route price-match asks straight to the avail…
u9g Jul 22, 2026
b74eac0
style(hotel_receptionist): ruff format and lint fixes for florist cha…
u9g Jul 22, 2026
ce3b86e
refactor(hotel_receptionist): typed Room union replaces the room-phra…
u9g Jul 22, 2026
af3d71a
chore(vscode): resolve hotel receptionist imports
u9g Jul 22, 2026
599baa0
feat(hotel_receptionist): type florist delivery instructions
u9g Jul 22, 2026
0e63500
fix(inference): preserve Gemma tool discriminators
u9g Jul 22, 2026
c898e59
fix(hotel_receptionist): serialize room variants as scalars
u9g Jul 22, 2026
a050b91
fix(llm): reject lossy discriminated schemas
u9g Jul 22, 2026
2e96feb
fix(examples): remove remaining discriminated union
u9g Jul 22, 2026
f63f1c8
fix(hotel-receptionist): complete meal comparison offers
u9g Jul 22, 2026
fa56132
fix flight and airport car coordination
u9g Jul 23, 2026
1ae1629
fix(hotel_receptionist): store flight and car times as naive wall-clock
u9g Jul 23, 2026
466f5a4
fix(hotel_receptionist): address remaining smoke-sim failures
u9g Jul 23, 2026
08c187f
add A/B harness for simulator repeat-message bug (SRJ_7AFgSjRdtbfJ)
u9g Jul 23, 2026
abb0940
fix(hotel_receptionist): generalize special-occasion offer instruction
u9g Jul 23, 2026
93ac03c
fix(hotel_receptionist): A/B-tested special-occasion instruction on g…
u9g Jul 23, 2026
f00106d
fix(hotel_receptionist): model dispute resolution states
u9g Jul 23, 2026
e3f0995
Handle restaurant phone refusal safely
u9g Jul 23, 2026
ec87bb2
fix(hotel_receptionist): align simulation outcome tags
u9g Jul 23, 2026
f6d0dbf
fix hotel price-parity scenario ending
u9g Jul 23, 2026
af3615a
Use structured 12-hour restaurant times
u9g Jul 23, 2026
67bff29
simplify hotel price-parity reactions
u9g Jul 23, 2026
25b9286
fix(hotel_receptionist): distinguish callback direction
u9g Jul 23, 2026
041b1aa
fix(hotel receptionist): prioritize requested manager transfers
u9g Jul 23, 2026
f9b9003
fix(hotel receptionist): cancel stale close watchdogs
u9g Jul 23, 2026
9bdcce8
fix(hotel receptionist): speak folio before disputes
u9g Jul 23, 2026
e17a568
fix(hotel receptionist): route unrecognized charges
u9g Jul 23, 2026
348e842
fix(hotel_receptionist): honor direct housekeeping transfers
u9g Jul 23, 2026
957996d
fix(hotel receptionist): require stay details before price check
u9g Jul 23, 2026
c58ae9a
fix(hotel receptionist): remove redundant transfer rule
u9g Jul 23, 2026
5fe7017
fix(hotel receptionist): require flight departure for airport car
u9g Jul 24, 2026
b51d6a0
fix(hotel receptionist): require departure time ask on flight reconfi…
u9g Jul 24, 2026
5040432
fix(hotel receptionist): collect stay dates on a price-match ask
u9g Jul 24, 2026
bc2d4bd
fix(hotel receptionist): answer florist timing asks with the delivery…
u9g Jul 24, 2026
0dfff9e
fix(hotel receptionist): hold the line when a table can't book withou…
u9g Jul 24, 2026
9982e0f
fix(hotel receptionist): store generated codes in the uppercase spoke…
u9g Jul 24, 2026
4006209
fix(hotel receptionist): recommend breakfast and a dinner table on me…
u9g Jul 24, 2026
136615f
fix(hotel receptionist): fix the flows behind six simulation failures
u9g Jul 24, 2026
6d9a581
docs(hotel receptionist): mark the two intentionally nonexistent scen…
u9g Jul 24, 2026
07c15cb
fix(hotel receptionist): use seeded rooms in scenarios, restore the d…
u9g Jul 24, 2026
d49bbb2
fix(hotel receptionist): resolve the agent and scenario problems from…
u9g Jul 24, 2026
c4d0882
fix(hotel receptionist): clarify ambiguous booking dates
u9g Jul 24, 2026
755eb8e
fix(hotel receptionist): state the florist delivery window once, wher…
u9g Jul 24, 2026
e1c13ff
fix(hotel receptionist): bind each room type's views to its own line
u9g Jul 24, 2026
2883b37
fix(hotel receptionist): stop offering views a room type doesn't have
u9g Jul 24, 2026
cae4f85
fix(hotel receptionist): state the outcome when a confirm tool refuses
u9g Jul 24, 2026
6e9b440
fix(workflows): confirm a value already in the conversation instead o…
u9g Jul 24, 2026
3caed2a
fix(hotel receptionist): drop the redundant multi-room dialog paragraph
u9g Jul 24, 2026
b88168d
chore(hotel receptionist): ignore generated simulation problems
u9g Jul 27, 2026
0a04221
test(hotel receptionist): align close watchdog coverage
u9g Jul 27, 2026
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
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"python.analysis.extraPaths": [
"./examples/hotel_receptionist"
]
}
2 changes: 1 addition & 1 deletion examples/drive-thru/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This restricts the LLM to known IDs at the schema layer before any runtime logic

### Order Types

`order.py` defines three Pydantic models: `OrderedCombo`, `OrderedHappy`, and `OrderedRegular` . A discriminated union `OrderedItem` is also defined. Each ordered item receives a random short `order_id` on creation via `order_uid()`.
`order.py` defines three Pydantic models: `OrderedCombo`, `OrderedHappy`, and `OrderedRegular`. A plain union `OrderedItem` groups the three concrete item classes. Each ordered item receives a random short `order_id` on creation via `order_uid()`.

`OrderState` stores the current cart as a `dict[str, OrderedItem]` keyed by `order_id`, which the `remove_order_item` and `list_order_items` tools use to look up or modify existing items.
https://github.com/livekit/agents/blob/8283a5a5c9863a07bcf030ee90e8ab780e1e569b/examples/drive-thru/order.py#L45-L56
Expand Down
4 changes: 2 additions & 2 deletions examples/drive-thru/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import string
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Annotated, Literal
from typing import Literal

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -43,7 +43,7 @@ class OrderedRegular(BaseModel):
size: Literal["S", "M", "L"] | None = None


OrderedItem = Annotated[OrderedCombo | OrderedHappy | OrderedRegular, Field(discriminator="type")]
OrderedItem = OrderedCombo | OrderedHappy | OrderedRegular


@dataclass
Expand Down
1 change: 1 addition & 0 deletions examples/hotel_receptionist/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ fake_data/hotel.db-shm
fake_data/hotel.db-wal
__pycache__/
.env.local
/problems/
187 changes: 187 additions & 0 deletions examples/hotel_receptionist/ab_occasion_offer_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""A/B test: the special-occasion instruction (run SR_VJtN4xDxPPVe, job SRJ_AYWWLkNJLYCQ).

The judge failed the anniversary scenario because the agent noticed the
occasion but never offered to SET ANYTHING UP - it pitched a suite and moved
on. This harness replays that transcript's first bad turn (the caller has just
volunteered the anniversary) against bare gemma-4-31b with the agent's real
system prompt and tool schemas, per the prompting-gemma skill method: baseline
must reproduce, one wording idea per variant, n>=10 per cell, client
concurrency 2 with paced request starts, flagged samples read manually.

Measured 2026-07-23 (offer-to-arrange rate, n=10 per cell):
no-bullet 0/10 reproduces production: verbatim suite pitch, no offer
overfit 10/10 but every reply is the bullet's scripted dinner-table line
general 7/10 nominal - manual read: all 10 are still suite upsells,
the abstract action ("arrange something that fits") only
bolted "shall I move you?" onto the same room pitch
menu 10/10 situation + concrete action menu; zero suite pitches,
also 10/10 on an adjacent ask (mom's 70th birthday
instead of the anniversary), correctly adapted
The menu wording shipped to instructions.py. Confirms the skill: the named
concrete action is load-bearing for gemma; abstract discipline is not.

Run:
uv run --no-sync python examples/hotel_receptionist/ab_occasion_offer_test.py
"""

from __future__ import annotations

import asyncio
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path.home() / ".claude/skills/prompting-gemma"))

from gemma_cluster_health import install

install() # lk default-project credentials + gemma cluster health watcher

from agent import HotelReceptionistAgent # noqa: E402
from instructions import build_instructions # noqa: E402

from livekit.agents import inference # noqa: E402
from livekit.agents.llm import ChatContext # noqa: E402

N_PER_CELL = 10
CONCURRENCY = 2
STAGGER_SECONDS = 0.5

# The shipped wording: situation + a menu of concrete bookable actions.
MENU_BULLET = (
"- Caller volunteers a special occasion in passing: after handling what they "
"called about, offer to set up ONE thing on this call that fits the occasion - "
"a dinner table at the on-site restaurant, flowers to the room, or a spa visit - "
"and book it when they take it. Offer to arrange it, not just describe it. "
"Pressure-free; drop it the moment they decline."
)

# The first fix attempt: scenario-specific (scripted dinner-table line,
# occasion list, upgrade prohibition). Worked 10/10 but overfitted to the eval.
OVERFIT_BULLET = (
"- Caller volunteers a special occasion in passing (an anniversary, birthday, "
"honeymoon, celebration): after handling what they actually called about, make "
"ONE warm offer of something concrete you can set up on this call that fits the "
'occasion - most naturally a dinner table at the on-site restaurant ("would you '
'like me to reserve a table for the two of you tonight?") - and offer to book it '
"right then. Offer to arrange, don't just suggest: naming a nice option without "
"offering to set it up is the miss. Keep it benefit-first and pressure-free, drop "
"it gracefully the moment they decline, and don't pitch room changes or upgrades "
"to a guest already staying here."
)

# The fully-generalized rewrite: same situation trigger, abstract action.
# Failed the manual read - gemma kept the suite pitch and only added "shall I?".
GENERAL_BULLET = (
"- Caller volunteers a special occasion in passing: after handling what they "
"actually called about, make ONE warm, pressure-free offer to arrange something "
"on this call that genuinely fits what they shared - and offer to set it up "
"right then, not just mention that it exists. The offer has to grow out of what "
"the caller said, not out of what you'd like to sell; drop it gracefully the "
"moment they decline."
)

_BASE = build_instructions()
assert MENU_BULLET in _BASE, "instructions.py no longer contains the menu bullet"

CELLS = [
("no-bullet", _BASE.replace(MENU_BULLET + "\n", "")),
("overfit", _BASE.replace(MENU_BULLET, OVERFIT_BULLET)),
("general", _BASE.replace(MENU_BULLET, GENERAL_BULLET)),
("menu", _BASE),
]

# Transcript prefix at the first bad turn, verbatim from the failing job. For
# the adjacent-ask check, swap the last user line for e.g. "It's my mom's
# seventieth birthday this weekend, that's the whole reason we came".
HISTORY: list[tuple[str, str]] = [
("assistant", "Hello, thanks for calling The LiveKit Hotel; how can I help you?"),
("user", "Hi, quick question — what time does breakfast run until?"),
("assistant", "Breakfast is served from six thirty to ten thirty in the morning."),
(
"user",
"Great, thank you — that helps. We’re actually here celebrating our wedding "
"anniversary, so I was just checking timing before we make plans for the morning.",
),
]

TOOLS = HotelReceptionistAgent().tools

# An "offer to arrange": first-person offer language attached to an action the
# agent can take on this call. Regex is a flag, not a verdict - flagged rows
# get read manually (the "general" cell scores 7/10 here but fails the read).
_OFFER_RE = re.compile(
r"shall i|would you like (?:me|us) to|want me to|like me to "
r"(?:book|reserve|arrange|set)|i can (?:book|reserve|arrange|set)|"
r"happy to (?:book|reserve|arrange|set)|can i (?:book|reserve|arrange|set)",
re.I,
)


def classify(text: str, tool_calls: list[str]) -> str:
if tool_calls:
return "TOOL"
if not text.strip():
return "EMPTY"
return "offer" if _OFFER_RE.search(text) else "NO-OFFER"


async def sample(llm: inference.LLM, instructions_text: str) -> tuple[str, list[str]]:
chat_ctx = ChatContext.empty()
chat_ctx.add_message(role="system", content=instructions_text)
for role, text in HISTORY:
chat_ctx.add_message(role=role, content=text)
reply, tool_calls = "", []
async with llm.chat(chat_ctx=chat_ctx, tools=TOOLS) as stream:
async for chunk in stream:
delta = chunk.delta
if delta is None:
continue
if delta.content:
reply += delta.content
for call in delta.tool_calls or []:
if call.name:
tool_calls.append(call.name)
return reply.strip(), tool_calls


async def run_cell(llm: inference.LLM, name: str, instructions_text: str) -> tuple[int, int]:
semaphore = asyncio.Semaphore(CONCURRENCY)
results: list[tuple[int, str, str]] = []

async def one(i: int) -> None:
await asyncio.sleep(i * STAGGER_SECONDS) # pace request starts
async with semaphore:
reply, tool_calls = await sample(llm, instructions_text)
verdict = classify(reply, tool_calls)
label = reply if not tool_calls else f"{reply!r} + tools {tool_calls}"
results.append((i, label, verdict))

await asyncio.gather(*(one(i) for i in range(N_PER_CELL)))
offers = 0
for i, label, verdict in sorted(results):
print(f" [{i}] {verdict:8s} {label!r}", flush=True)
offers += verdict == "offer"
return offers, len(results)


async def main() -> None:
llm = inference.LLM("google/gemma-4-31b-it")
summary = []
try:
for name, instructions_text in CELLS:
print(f"\n=== cell: {name} (n={N_PER_CELL}) ===", flush=True)
offers, n = await run_cell(llm, name, instructions_text)
summary.append((name, offers, n))
finally:
await llm.aclose()

print("\n=== summary (offer-to-arrange rate) ===")
for name, offers, n in summary:
print(f" {name:10s} {offers}/{n}")


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading