Files
microsoft--agent-lightning/examples/apo/room_selector.py
T
wehub-resource-sync 85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

363 lines
12 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import traceback
from typing import List, Optional, Tuple, TypedDict, cast
from openai import OpenAI
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionMessageFunctionToolCallParam,
ChatCompletionMessageParam,
ChatCompletionToolMessageParam,
ChatCompletionToolParam,
)
from pydantic import BaseModel, Field
from rich.console import Console
from agentlightning.adapter import TraceToMessages
from agentlightning.litagent import rollout
from agentlightning.reward import find_final_reward
from agentlightning.runner import LitAgentRunner
from agentlightning.store import InMemoryLightningStore
from agentlightning.tracer.agentops import AgentOpsTracer
from agentlightning.types import Dataset, PromptTemplate
console = Console()
class JudgeResponse(BaseModel):
reason: str = Field(description="The reason for the score. No more than 100 characters.")
score: float = Field(description="The score for the match on a 0-1 scale. Be critical.")
class Room(TypedDict):
id: str
capacity: int
equipment: List[str]
accessible: bool
distance_m: int
booked: List[Tuple[str, str, int]]
class RoomStatus(Room):
free: bool
class AvailableRooms(TypedDict):
rooms: List[RoomStatus]
class RoomRequirement(TypedDict):
date: str
time: str
duration_min: int
attendees: int
needs: List[str]
accessible_required: bool
class RoomSelectionTask(TypedDict):
id: str
task_input: RoomRequirement
expected_choice: str
TOOL_DEFINITIONS: List[ChatCompletionToolParam] = [
{
"type": "function",
"function": {
"name": "get_rooms_and_availability",
"description": "Return meeting rooms with capacity, equipment, accessibility, distance, and booked time slots.",
"parameters": {
"type": "object",
"properties": {
"date": {"type": "string", "description": "YYYY-MM-DD"},
"time": {"type": "string", "description": "HH:MM 24h local"},
"duration_min": {"type": "integer", "description": "Meeting duration minutes"},
},
"required": ["date", "time", "duration_min"],
},
},
},
]
def prompt_template_baseline() -> PromptTemplate:
return PromptTemplate(
template="Find a room on {date} at {time} for {duration_min} minutes, {attendees} attendees. Needs: {needs}. Accessible required: {accessible_required}",
engine="f-string",
)
def room_selection_grader(client: OpenAI, final_message: Optional[str], expected_choice: str) -> float:
judge_prompt = (
f"You are a strict grader of exact room choice."
f"Task output:\n{final_message}\n\n"
f"Task expected answer:\n{expected_choice}\n\n"
f"Score the match on a 0-1 scale. Be critical.\n"
f"Bear in mind that the score can be partially correct (between 0 and 1)."
)
judge = client.chat.completions.parse(
model="gpt-4.1-mini",
messages=[
{"role": "user", "content": judge_prompt},
],
response_format=JudgeResponse,
temperature=0.0,
)
judge_result = judge.choices[0].message.content
console.print(f"[bold yellow]=== Judge ===[/bold yellow]")
console.print(judge_result)
judge_result_parsed = JudgeResponse.model_validate_json(judge_result) # type: ignore
console.print(f"[bold yellow]=== Judge Score ===[/bold yellow]")
console.print(judge_result_parsed.score)
return judge_result_parsed.score
@rollout
def room_selector(task: RoomSelectionTask, prompt_template: PromptTemplate) -> float:
"""An agent to select a room based on the given requirements.
Oracle System Prompt (works with 100% accuracy with gpt-5 mini low reasoning effort):
You are a scheduling assistant.
Hard constraints: free for slot, capacity >= attendees, includes all required equipment,
accessible==True if requested.
Tie-break scoring (lower is better):
1) capacity_slack = capacity - attendees (minimize)
2) extra_equipment = provided_equipment_count - required_equipment_count (minimize)
3) distance_m (minimize)
4) fewer total booked blocks that day (minimize)
Return No Room if no room is found that satisfies the constraints.
Return strictly:
final_choice: <ROOM_ID>
reason: <one line stating the decisive criteria>
Oracle User Prompt Template:
Find a room on {task_input['date']} at {task_input['time']} for {task_input['duration_min']} minutes,
{task_input['attendees']} attendees. Needs: {', '.join(task_input['needs']) or 'none'}.
Accessible required: {task_input['accessible_required']}
The current implementation greatly simply the oracle prompt and prompt template is provided by a parameter.
The prompt template should be tuned by Agent-lightning's APO algorithm.
It also should work with a very small model like gpt-4.1-nano.
"""
client = OpenAI()
model = "gpt-4.1-nano"
user_message = prompt_template.format(**task["task_input"])
messages: List[ChatCompletionMessageParam] = [
{"role": "system", "content": "You are a scheduling assistant."},
{
"role": "user",
"content": user_message,
},
]
console.print(f"[bold yellow]=== User Message ===[/bold yellow]")
console.print(user_message)
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
# Minimize the randomness
temperature=0.0,
# Uncomment for gpt-5
# reasoning_effort="low",
)
console.print(f"[bold yellow]=== Assistant Message ===[/bold yellow]")
console.print(resp.choices[0].message)
# Parse and process the tool calls
tool_calls = resp.choices[0].message.tool_calls
if tool_calls:
tool_call_params: List[ChatCompletionMessageFunctionToolCallParam] = []
tool_results: List[ChatCompletionToolMessageParam] = []
for tc in tool_calls:
if tc.type != "function":
raise ValueError(f"Tool call is not a function: {tc}")
if tc.function.name != "get_rooms_and_availability":
raise ValueError(f"Tool call is not get_rooms_and_availability: {tc}")
tool_call_params.append(
ChatCompletionMessageFunctionToolCallParam(
id=tc.id,
type="function",
function={"name": tc.function.name, "arguments": tc.function.arguments},
)
)
args = json.loads(tc.function.arguments)
try:
tool_output = get_rooms_and_availability(args["date"], args["time"], args["duration_min"])
except Exception as e:
tool_output = {
"error": str(e),
"traceback": traceback.format_exc(),
}
console.print(f"[bold yellow]=== Tool Message ===[/bold yellow]")
console.print(tool_output)
tool_results.append(
ChatCompletionToolMessageParam(
role="tool",
tool_call_id=tc.id,
content=json.dumps(tool_output),
)
)
# Update the messages for the next call
messages.append(
ChatCompletionAssistantMessageParam(
role="assistant",
content=resp.choices[0].message.content,
tool_calls=tool_call_params,
)
)
messages.extend(tool_results)
next_resp = client.chat.completions.create(
model=model,
messages=messages,
# Minimize the randomness
temperature=0.0,
)
console.print(f"[bold yellow]=== Final Assistant Message ===[/bold yellow]")
console.print(next_resp.choices[0].message.content)
final_message = next_resp.choices[0].message.content
else:
final_message = resp.choices[0].message.content
return room_selection_grader(client, final_message, task["expected_choice"])
# Local tool database (there might be multiple plausible fits)
ROOMS: List[Room] = [
{
"id": "Orion",
"capacity": 4,
"equipment": ["tv", "whiteboard"],
"accessible": True,
"distance_m": 12,
"booked": [("2025-10-13", "10:00", 60), ("2025-10-13", "15:00", 30)],
},
{
"id": "Lyra",
"capacity": 10,
"equipment": ["projector", "whiteboard", "confphone"],
"accessible": True,
"distance_m": 30,
"booked": [("2025-10-13", "09:30", 30), ("2025-10-13", "11:00", 60)],
},
{
"id": "Vega",
"capacity": 6,
"equipment": ["tv"],
"accessible": False,
"distance_m": 22,
"booked": [("2025-10-13", "14:00", 60)],
},
{
"id": "Nova",
"capacity": 12,
"equipment": ["ledwall", "whiteboard", "confphone"],
"accessible": True,
"distance_m": 45,
"booked": [],
},
{
"id": "Quark",
"capacity": 8,
"equipment": ["projector", "whiteboard"],
"accessible": False,
"distance_m": 18,
"booked": [("2025-10-13", "10:30", 30)],
},
# Two extra to create harder ties
{
"id": "Atlas",
"capacity": 6,
"equipment": ["projector", "whiteboard"],
"accessible": True,
"distance_m": 10,
"booked": [("2025-10-13", "09:00", 30), ("2025-10-13", "13:30", 30)],
},
{
"id": "Pulse",
"capacity": 8,
"equipment": ["tv", "whiteboard", "confphone"],
"accessible": True,
"distance_m": 8,
"booked": [("2025-10-13", "16:30", 30)],
},
]
def overlaps(start: str, dur: int, other_start: str, other_dur: int) -> bool:
def tmin(t: str):
return int(t[:2]) * 60 + int(t[3:])
a0, a1 = tmin(start), tmin(start) + dur
b0, b1 = tmin(other_start), tmin(other_start) + other_dur
return max(a0, b0) < min(a1, b1)
def get_rooms_and_availability(date: str, time_str: str, duration_min: int) -> AvailableRooms:
avail: List[RoomStatus] = []
for r in ROOMS:
free = all(
not (b_date == date and overlaps(time_str, duration_min, b_time, b_dur))
for (b_date, b_time, b_dur) in r["booked"]
)
item: RoomStatus = {
**r,
"free": free,
}
avail.append(item)
return {"rooms": avail}
def load_room_tasks() -> Dataset[RoomSelectionTask]:
tasks: List[RoomSelectionTask] = []
for line in open("room_tasks.jsonl"):
task = json.loads(line)
tasks.append(RoomSelectionTask(**task))
return cast(Dataset[RoomSelectionTask], tasks)
async def debug_room_selector(limit: int = 1):
# Prepare all the components to run the agent
runner = LitAgentRunner[RoomSelectionTask](AgentOpsTracer())
store = InMemoryLightningStore()
prompt_template = prompt_template_baseline()
tasks = load_room_tasks()
with runner.run_context(agent=room_selector, store=store):
for task in tasks:
console.print("[bold green]=== Task ===[/bold green]", task, sep="\n")
# Run the agent
rollout = await runner.step(task, resources={"main_prompt": prompt_template})
# Get the spans and convert them to messages
# Useful for debugging and analysis
spans = await store.query_spans(rollout.rollout_id)
adapter = TraceToMessages()
messages = adapter.adapt(spans)
for message_idx, message in enumerate(messages):
console.print(f"[bold purple]=== Postmortem Message #{message_idx} ===[/bold purple]")
console.print(json.dumps(message))
reward = find_final_reward(spans)
console.print("[bold purple]=== Postmortem Reward ===[/bold purple]", reward, sep="\n")
if __name__ == "__main__":
asyncio.run(debug_room_selector())