Home automation
Command a simulated five-room house in plain language: "turn off the lights in
Showcase — Home automation
Command a simulated five-room house in plain language: "turn off the lights in
every unoccupied room", "cool anything warmer than 24 down to 21". Conditional
commands force the read-then-act pattern — the model must call get_state,
reason over what came back, and only then issue the set_light and
set_temperature calls that qualify. The reply appends a change list computed
from the state diff in Python — not the model's own account of what it did —
plus the final house state, so what you see is what actually happened.
Run
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Open http://localhost:3000.
To run against Gemini instead:
PROVIDER=gemini docker compose up --build
What's where
backend/ai_openai.py— the house state, three tools (one read, two writes), a per-request dispatch closure, and the bounded loopbackend/ai_gemini.py— same house, Gemini spellingbackend/main.py— identical FastAPI loader; reads PROVIDER and dispatchesfrontend/app/page.tsx— textarea + resultdocker-compose.yml— two services, secrets mounted from./secrets/
Every request gets a fresh copy of the house (copy.deepcopy), so runs are
repeatable and two users can't trample each other's lights. The "Changes
applied" section is a Python diff of before-vs-after state — models sometimes
misreport their own side effects, so the report never relies on the model's
memory of what it called.
Stop
docker compose down
Ejecútalo en tu máquina
Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.
unzip home-automation.zip
cd home-automation
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.
Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.
backend/ai_openai.py
"""Week 6 - Showcase 2 (OpenAI): a simulated smart home, read-then-act.
Conditional commands force real chaining: "turn off lights in rooms
warmer than 22" can't be done blind. The model reads state, decides which
rooms qualify, then acts — and the tool results after each write confirm
what actually changed. Each request gets a fresh copy of the house, so
runs are repeatable.
"""
import copy
import json
from openai import OpenAI
_client = OpenAI()
_MODEL = "gpt-5.4-nano"
# The house resets per request — a session-scoped world, not shared state.
_INITIAL = {
"living room": {"light": "on", "temperature_c": 23.5, "occupied": True},
"kitchen": {"light": "on", "temperature_c": 21.0, "occupied": False},
"bedroom": {"light": "off", "temperature_c": 24.0, "occupied": False},
"office": {"light": "on", "temperature_c": 26.0, "occupied": True},
"bathroom": {"light": "on", "temperature_c": 22.0, "occupied": False},
}
_TOOLS = [
{
"type": "function",
"name": "get_state",
"description": "Current state of every room: light, temperature_c, occupied.",
"parameters": {"type": "object", "properties": {}},
},
{
"type": "function",
"name": "set_light",
"description": "Switch one room's light on or off.",
"parameters": {
"type": "object",
"properties": {
"room": {"type": "string",
"enum": ["living room", "kitchen", "bedroom", "office", "bathroom"]},
"state": {"type": "string", "enum": ["on", "off"]},
},
"required": ["room", "state"],
},
},
{
"type": "function",
"name": "set_temperature",
"description": "Set one room's thermostat target in Celsius (10-30).",
"parameters": {
"type": "object",
"properties": {
"room": {"type": "string",
"enum": ["living room", "kitchen", "bedroom", "office", "bathroom"]},
"celsius": {"type": "number"},
},
"required": ["room", "celsius"],
},
},
]
def _make_dispatch(house: dict) -> dict:
def get_state() -> dict:
return house
def set_light(room: str, state: str) -> dict:
house[room]["light"] = state
return {"room": room, "light": state}
def set_temperature(room: str, celsius: float) -> dict:
if not 10 <= celsius <= 30:
return {"error": "celsius must be between 10 and 30"}
house[room]["temperature_c"] = celsius
return {"room": room, "temperature_c": celsius}
return {"get_state": get_state, "set_light": set_light,
"set_temperature": set_temperature}
def run(command: str) -> str:
house = copy.deepcopy(_INITIAL)
dispatch = _make_dispatch(house)
input_list = [{"role": "user", "content": command.strip()}]
response = None
for _ in range(8): # bounded — never ship an open loop
response = _client.responses.create(
model=_MODEL,
instructions="You control a smart home. For conditional commands, "
"read the state first, then act only on rooms that "
"match. Your final summary must list every set_light "
"and set_temperature call you made in this conversation, "
"with the room and value — repeat them from the tool "
"results, never from memory.",
input=input_list,
tools=_TOOLS,
)
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
break
input_list += response.output
for call in calls:
args = json.loads(call.arguments)
fn = dispatch.get(call.name)
result = fn(**args) if fn else {"error": f"unknown tool {call.name}"}
input_list.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
})
# Report side effects from the state diff, not from the model's memory —
# the model narrates, but the source of truth is computed.
changes = []
for room, before in _INITIAL.items():
for key, old in before.items():
new = house[room][key]
if new != old:
changes.append(f"- {room}: {key} {old} -> {new}")
changed = "\n".join(changes) if changes else "(none)"
summary = response.output_text if response else ""
return (f"{summary}\n\nChanges applied (computed from state):\n{changed}"
f"\n\nFinal state:\n{json.dumps(house, indent=2)}")
backend/ai_gemini.py
"""Week 6 - Showcase 2 (Gemini): a simulated smart home, read-then-act."""
import copy
import json
import os
from google import genai
from google.genai import types
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-3.1-flash-lite"
_INITIAL = {
"living room": {"light": "on", "temperature_c": 23.5, "occupied": True},
"kitchen": {"light": "on", "temperature_c": 21.0, "occupied": False},
"bedroom": {"light": "off", "temperature_c": 24.0, "occupied": False},
"office": {"light": "on", "temperature_c": 26.0, "occupied": True},
"bathroom": {"light": "on", "temperature_c": 22.0, "occupied": False},
}
_ROOMS = ["living room", "kitchen", "bedroom", "office", "bathroom"]
_CONFIG = types.GenerateContentConfig(
system_instruction="You control a smart home. For conditional commands, "
"read the state first, then act only on rooms that "
"match. Your final summary must list every set_light "
"and set_temperature call you made in this conversation, "
"with the room and value — repeat them from the tool "
"results, never from memory.",
tools=[types.Tool(function_declarations=[
types.FunctionDeclaration(
name="get_state",
description="Current state of every room: light, temperature_c, occupied.",
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
),
types.FunctionDeclaration(
name="set_light",
description="Switch one room's light on or off.",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"room": types.Schema(type=types.Type.STRING, enum=_ROOMS),
"state": types.Schema(type=types.Type.STRING, enum=["on", "off"]),
},
required=["room", "state"],
),
),
types.FunctionDeclaration(
name="set_temperature",
description="Set one room's thermostat target in Celsius (10-30).",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"room": types.Schema(type=types.Type.STRING, enum=_ROOMS),
"celsius": types.Schema(type=types.Type.NUMBER),
},
required=["room", "celsius"],
),
),
])],
)
def _make_dispatch(house: dict) -> dict:
def get_state() -> dict:
return house
def set_light(room: str, state: str) -> dict:
house[room]["light"] = state
return {"room": room, "light": state}
def set_temperature(room: str, celsius: float) -> dict:
if not 10 <= celsius <= 30:
return {"error": "celsius must be between 10 and 30"}
house[room]["temperature_c"] = celsius
return {"room": room, "temperature_c": celsius}
return {"get_state": get_state, "set_light": set_light,
"set_temperature": set_temperature}
def run(command: str) -> str:
house = copy.deepcopy(_INITIAL)
dispatch = _make_dispatch(house)
contents = [types.Content(role="user", parts=[types.Part(text=command.strip())])]
response = None
for _ in range(8): # bounded — never ship an open loop
response = _client.models.generate_content(
model=_MODEL, contents=contents, config=_CONFIG,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for fc in response.function_calls:
fn = dispatch.get(fc.name)
result = fn(**fc.args) if fn else {"error": f"unknown tool {fc.name}"}
parts.append(types.Part.from_function_response(name=fc.name, response=result))
contents.append(types.Content(role="user", parts=parts))
# Report side effects from the state diff, not from the model's memory —
# the model narrates, but the source of truth is computed.
changes = []
for room, before in _INITIAL.items():
for key, old in before.items():
new = house[room][key]
if new != old:
changes.append(f"- {room}: {key} {old} -> {new}")
changed = "\n".join(changes) if changes else "(none)"
summary = (response.text or "") if response else ""
return (f"{summary}\n\nChanges applied (computed from state):\n{changed}"
f"\n\nFinal state:\n{json.dumps(house, indent=2)}")
Archivos del proyecto
.gitignoreREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json