A smart home controller built from scratch using the ReAct (Reasoning + Acting) pattern with Gemma 3 and Ollama — no framework, no magic, pure Python.
Standard LLMs can only predict the next token. They cannot turn on a light or read a temperature sensor by themselves.
The ReAct pattern bridges this gap by having the model emit structured text commands that a Python runtime intercepts, executes against a real system, and feeds back as observations — all within the same conversation history.
This project implements a complete ReAct loop from scratch, controlling a simulated smart home without any agent framework (no LangChain, no LlamaIndex).
┌─────────────────────────────────────────────────────────────┐
│ USER REQUEST │
└────────────────────────────┬────────────────────────────────┘
│
┌────────▼────────┐
│ 1. THOUGHT │ Gemma 3 analyses the request
│ (Gemma 3) │ and decides what to do next
└────────┬────────┘
│
┌────────▼────────┐
│ 2. ACTION │ Gemma 3 emits a structured
│ (Gemma 3) │ command: Action: light_on
└────────┬────────┘ Action Input: salon
│
┌────────▼────────┐
│ 3. EXECUTION │ Python parses the command
│ (Python) │ and calls home.light_on("salon")
└────────┬────────┘
│
┌────────▼────────┐
│ 4. OBSERVATION │ Result is injected back into
│ (→ history) │ the conversation history
└────────┬────────┘
│
┌────────▼────────┐
│ Final Answer │ Gemma 3 formulates a natural
│ (Gemma 3) │ language response to the user
└─────────────────┘
| Feature | Description |
|---|---|
| ReAct loop | Thought → Action → Observation cycle up to 5 iterations |
| Conversation history | Full multi-turn memory passed on every LLM call |
| Robust parser | Handles parenthesis-style calls, extra whitespace, quotes |
| Whitelisted tools | Only registered functions can be called — no arbitrary exec |
| Dynamic tool discovery | Agent auto-discovers SmartHome methods via dir() |
| Dynamic system prompt | Tool list auto-generated from docstrings — zero hardcoding |
| Color-coded terminal | Visual distinction between thoughts, actions, observations |
tp2-react-agent/
├── agent.py # ReAct loop, prompt builder, parser, tool registry
├── smart_home.py # Simulated smart home environment
└── README.md
Prerequisites: Ollama running with gemma3:4b installed (see TP n°1).
# Install dependencies
pip install ollama
# Start Ollama server (Terminal 1)
ollama serve
# Run the agent (Terminal 2)
python agent.pyThe system prompt is the contract between the LLM and the Python runtime. It must define:
- The agent's role (smart home assistant)
- The exact output format (Thought / Action / Action Input)
- The available tools with their signatures
- Examples so the model internalizes the pattern
A poorly written system prompt leads to unparseable responses and a broken agent loop.
The agent has no persistent memory across Python runs. Within a session, the full conversation is passed on every API call:
[system_prompt, user_msg_1, ai_msg_1, observation_1, user_msg_2, ...]
This is how the agent "remembers" that it already read the temperature before deciding to turn on the heater.
The agent can only call functions explicitly registered in the actions dict. If Gemma 3 were to hallucinate Action: os.system, the parser would find it, look it up in actions, not find it, and return an error observation — without ever executing anything dangerous.
for name in dir(home):
if not name.startswith("_") and callable(getattr(home, name)):
tools[name] = getattr(home, name)Any new public method added to SmartHome is automatically exposed to the agent with zero changes elsewhere.
Regex-based parser that handles:
- Standard format:
Action: light_on+Action Input: salon - Parenthesis style:
Action: light_on(salon)— extracts just the function name - Case-insensitive, strips quotes and extra whitespace
Reads the first line of each tool's docstring to auto-generate the tool list. Adding a new tool to SmartHome and registering it updates the prompt automatically.
Main loop with two nested levels:
- Outer loop — one iteration per user turn (until
exit) - Inner loop — up to
MAX_STEPSThought→Action→Observation cycles per turn
| Prompt | Expected behavior |
|---|---|
"Turn on the salon light." |
Single action: light_on(salon) |
"Is the kitchen light on?" |
Single action: get_light_state(cuisine) |
"Is it cold? If so, turn on the kitchen light." |
Two actions: get_temperature → conditional light_on |
"What rooms do you control?" |
Single action: get_rooms() |
The parser relies on Gemma 3 following the format strictly. Edge cases:
Action: light_on(salon)→ handled (parentheses stripped)Action : light_on(space before colon) → handled (regex)Action: light_on\nAction Input: le salon(articles) → may fail silently
Solution: MCP, which replaces this fragile text parsing with a standardized JSON protocol — the production-grade approach.
If the actions dict were populated with {"os.system": os.system}, the agent could execute arbitrary shell commands. The current implementation is safe because:
get_available_tools()is manually curatedget_available_tools_auto()only exposesSmartHomemethods — never OS-level functions
Never expose eval, exec, os.system, or subprocess as agent tools.
History resets on every Python run. A production system would use a vector database (see TP n°1) to store and retrieve past interactions.
Both reasoning and action formatting are handled by the same model. A more robust architecture would use a dedicated tool-calling model (e.g. with OpenAI function calling or Ollama's native tool format).
- ReAct: Synergizing Reasoning and Acting in Language Models — Yao et al., 2023
- Ollama — Local LLM runtime
- Gemma 3 — Google DeepMind
Academic project — Polytech Nantes, IDIA.