Automatización del hogar
Comanda una casa simulada de cinco cuartos en lenguaje llano: "turn off the
Showcase — Automatización del hogar
Comanda una casa simulada de cinco cuartos en lenguaje llano: "turn off the
lights in every unoccupied room", "cool anything warmer than 24 down to 21". Los
comandos condicionales fuerzan el patrón leer-luego-actuar — el modelo debe
llamar a get_state, razonar sobre lo que regresó, y solo entonces emitir las
llamadas a set_light y set_temperature que califiquen. La respuesta agrega
una lista de cambios calculada del diff de estado en Python — no el propio
recuento del modelo de lo que hizo — más el estado final de la casa, así que lo
que ves es lo que de verdad pasó.
Córrelo
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Abre http://localhost:3000.
Para correrlo contra Gemini:
PROVIDER=gemini docker compose up --build
Qué hay aquí
backend/ai_openai.py— el estado de la casa, tres tools (una lee, dos escriben), un closure de dispatch por petición y el loop acotadobackend/ai_gemini.py— la misma casa, en la forma de Geminibackend/main.py— loader de FastAPI idéntico; lee PROVIDER y despachafrontend/app/page.tsx— textarea + resultadodocker-compose.yml— dos servicios, secrets montados desde./secrets/
Cada petición recibe una copia fresca de la casa (copy.deepcopy), así que las
corridas son repetibles y dos usuarios no pueden pisarse las luces. La sección
"Changes applied" es un diff en Python del estado antes-vs-después — los modelos
a veces reportan mal sus propios efectos secundarios, así que el reporte nunca
depende de la memoria del modelo de lo que llamó.
Detenlo
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.es.mdREADME.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