Interactive Lesson

Architecture

Note

This document explains how the project is structured, how its components relate to each other, and the key design decisions behind it.

Directory Overview

agents/
├── main.py               # Entry point; LMStudio (OpenAI-compatible) agent loop
├── config.py             # Shared configuration (e.g. MAX_ITERS)
├── prompts.py            # System prompt definition
├── call_functions.py     # Dispatches tool calls to the correct function
├── list_functions.py     # Builds the tool schema lists for each provider
├── providers/
│   └── gemini.py         # Optional Gemini agent loop
├── functions/
│   ├── get_dir_info.py   # Tool: list files and directories
│   ├── get_file_content.py # Tool: read file content
│   ├── write_file.py     # Tool: write or overwrite a file
│   ├── run_python_file.py  # Tool: run a Python file
│   └── search_files.py   # Tool: search text across files
├── calculator/           # Sandbox working directory for the agent
└── tests.py              # Local harness for testing tool functions

The Agent Loop

Both main.py (LMStudio) and providers/gemini.py (Gemini) implement the same conceptual loop:

  1. Build messages list: [system prompt, user prompt]
  2. Send messages + tool schemas to the LLM.
  3. If the model returns tool calls:
    • Execute all tool calls concurrently via ThreadPoolExecutor.
    • Append tool results to messages.
    • Go to step 2.
  4. If the model returns a text response: return it.
  5. If MAX_ITERS is reached: return the last partial response.
Tip

The loop lives entirely in the provider file. The functions/ layer is stateless and has no knowledge of the loop — keeping concerns cleanly separated.


Provider Abstraction

The project supports two LLM backends, each isolated in its own module.

Columns

LMStudio (main.py)

Uses the OpenAI-compatible openai SDK. Configure with:

env
LLM_PROVIDER=lmstudio
LMSTUDIO_BASE_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=your-model
LMSTUDIO_TEMPERATURE=0
LMSTUDIO_MAX_TOKENS=800

Gemini (providers/gemini.py)

Uses Google's google-genai SDK. Configure with:

env
LLM_PROVIDER=gemini
GEMINI_API_KEY=your_api_key
GEMINI_MODEL=gemini-2.5-flash

The active provider is selected via the LLM_PROVIDER environment variable. This keeps provider-specific code isolated and makes it straightforward to add new providers without modifying the shared tool layer.


Tool Schema Design

Every tool in functions/ exports two things:

Export Purpose
Python function The actual implementation — reads files, runs code, etc.
Schema dictionary A structured description of the tool for the LLM (name, description, parameter types).

Keeping descriptions short is intentional: shorter schemas consume fewer tokens per iteration, which extends the effective context budget across a multi-step agent run.

list_functions.py assembles the full list of schemas into the format expected by each provider (OpenAI vs. Gemini have slightly different schema conventions).


Concurrent Tool Execution

When the LLM returns multiple tool calls in a single step, there is no reason to execute them sequentially. Both main.py and providers/gemini.py use ThreadPoolExecutor to run all tool calls for a given step in parallel:

python
with ThreadPoolExecutor(max_workers=len(tool_calls)) as executor:
    tool_results = list(executor.map(call_function, tool_calls))

Results are collected in order and appended to the message history before the next LLM call.

Tip

This is especially impactful when the model reads multiple files in one step. Instead of sequential disk reads, they all happen simultaneously.


The Sandbox

The agent's tools are scoped to a single working directory: calculator/. This directory acts as a controlled sandbox.

Note

The tool functions receive the working directory as an injected argument and validate that all file paths stay within it. The LLM never sees or controls the working directory itself — it only provides relative paths.

This design limits the blast radius of mistakes and makes the agent's file access fully auditable.


Configuration

config.py exposes a small set of shared constants:

  • MAX_ITERS — maximum number of reasoning iterations before the loop stops.

LMStudio-specific runtime options (LMSTUDIO_TEMPERATURE, LMSTUDIO_MAX_TOKENS) are read from environment variables at runtime in main.py.