A command-line agent that turns natural-language goals into small, working Python projects. It uses Google Gemini (via LangChain) to generate code, writes files into a local workspace/, validates them with pytest, and can self-heal when tests fail. Successful outputs are cached so repeat requests skip LLM calls entirely.
The stack is goal-driven (CLI tools, APIs, tiny libraries, etc.)—not locked to CRUD or FastAPI.
- Overview
- Features
- How It Works
- Project Structure
- Prerequisites
- Installation
- Configuration
- Usage
- Generated Application
- Caching
- LLM Budget
- Configuration Reference
- Troubleshooting
- Security Notes
- Limitations
You describe what you want in plain English. The agent:
- Normalizes your command (lowercase, trimmed, collapsed whitespace).
- Checks the cache for a prior successful result for that normalized command.
- On a cache miss, calls Gemini to generate a full project as JSON (
files→ filename → source code). - Writes all files into
workspace/. - Runs
pytestinsideworkspace/to verify the project. - If validation fails, self-heals with a second LLM call that includes the pytest error output.
- On success, persists the result to cache and as last-known-good for fallback.
Each run produces whatever files fit the goal, as long as they follow the generation rules (flat layout, pytest, JSON output).
| Feature | Description |
|---|---|
| Natural-language input | Single CLI argument: your project goal |
| Structured LLM output | JSON with a files map; no markdown or prose in responses |
| Automatic validation | pytest run in workspace/ after every generation or heal |
| Self-healing | One retry with error context when tests fail |
| Disk cache | SHA-256 keyed cache under .cache/ |
| Last-known-good fallback | Restores the last passing build if generation and self-heal both fail |
| LLM call budget | Hard cap (default: 2 calls) to limit cost and runaway retries |
| Rich CLI output | Colored progress, rules, and status via rich |
flowchart TD
A[CLI: user goal] --> B[Normalize command]
B --> C{Cache hit?}
C -->|Yes| D[Write cached files to workspace]
D --> E[Done - 0 LLM calls]
C -->|No| F[LLM: generate_project]
F --> G[Write files to workspace]
G --> H[pytest in workspace]
H -->|Pass| I[Save cache + last-known-good]
H -->|Fail| J[LLM: self_heal with error]
J --> K[Write healed files]
K --> L[pytest again]
L -->|Pass| I
L -->|Fail| M[Restore last-known-good if exists]
The LLM is instructed to:
- Place all modules in one directory (flat layout, no packages).
- Use flat imports only (
import utils, notfrom . import utils). - Include at least one
test_*.pyfile; tests must pass whenpytestis run fromworkspace/. - Optionally include
requirements.txtwhen third-party packages are needed. - Return only JSON with a flexible
filesmap (filenames and contents depend on the goal):
{
"files": {
"app.py": "...",
"test_app.py": "...",
"requirements.txt": "..."
}
}agent/json_utils.py extracts the first { … } block from the model response. cli.py also requires a non-empty files dict and at least one test_*.py entry before writing.
On every write, workspace/ is cleared first so files from a previous project (e.g. old crud.py or .db files) are not left behind.
forgepilot/
├── cli.py # Entry point: orchestration, cache, validation, fallback
├── config.py # Paths, model name, LLM budget, .env loading
├── requirements.txt # Agent dependencies (LangChain, Gemini, rich, dotenv)
├── .env # GOOGLE_API_KEY (create locally; do not commit)
├── .cache/ # SHA-256 cache files + .good last-known-good copies
├── agent/
│ ├── llm.py # ChatGoogleGenerativeAI (Gemini)
│ ├── prompts.py # Shared generation / self-heal instructions
│ ├── generator.py # Initial project generation + invoke
│ ├── self_heal.py # Repair prompt + invoke on validation failure
│ ├── validator.py # Runs pytest in workspace/
│ ├── cache.py # load/save cached and last-known-good results
│ ├── normalize.py # Command normalization for cache keys
│ ├── budget.py # LLMBudget: consume() with hard limit
│ └── json_utils.py # Parse and validate LLM JSON output
└── workspace/ # Output only: cleared and rewritten on each successful write
└── (generated) # e.g. app.py, cli.py, test_*.py, requirements.txt, *.db
The agent code lives at the repo root. workspace/ is not a fixed template—it holds whatever the LLM generated for the latest goal. Any files committed there (from an earlier run) are examples only and will be removed on the next generation.
- Python 3.10+ (recommended)
- A Google AI API key with access to Gemini models
- Network access for LLM calls (cache hits do not need the network)
From the repository root:
cd /path/to/forgepilot
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtThe root requirements.txt covers the agent only. Generated code may need extra packages.
Always install pytest (the agent runs it for validation):
pip install pytestIf the project includes workspace/requirements.txt after a run:
pip install -r workspace/requirements.txtCommon stacks (install only what your goal produced):
| Goal type | Typical extras |
|---|---|
| HTTP API | fastapi, uvicorn, httpx |
| CLI | click or typer |
| Database API | sqlalchemy, pydantic |
Create a .env file in the project root (or export the variable in your shell):
GOOGLE_API_KEY=your_google_api_key_hereconfig.py loads this via python-dotenv. LangChain’s ChatGoogleGenerativeAI reads GOOGLE_API_KEY from the environment automatically.
Do not commit .env or real API keys to version control.
python cli.py "Build a CLI that converts Celsius and Fahrenheit"You can phrase goals differently; normalization affects cache keys, not the text sent to the LLM on a cache miss (the original goal is still passed to generation and self-heal).
Examples:
python cli.py "Create a minimal FastAPI app with a /health endpoint and tests"
python cli.py "Write a tiny library for slugifying strings with pytest"
python cli.py "build a cli that converts celsius and fahrenheit" # cache key matches normalized form- ForgePilot — goal, normalized command, remaining LLM budget
- Cache Lookup — hit (instant write + exit) or miss
- LLM Generation — raw model output (via
json_utils), then file writes - Validation (pytest) — pass or fail with stderr
- On failure: Self-Heal Attempt, re-validation, then optional Fallback to last-known-good
- Cache hit: writes files, prints success, no LLM calls
- Generation + validation pass: caches result, exits after success message
- Self-heal pass: caches healed result
- Total failure: attempts last-known-good restore; may leave workspace in a partial or restored state
After a successful run, workspace/ contains only the files for that goal. There is no required filename like main.py or crud.py—the set is entirely up to the LLM, subject to:
| Requirement | Why |
|---|---|
At least one test_*.py |
Agent validation runs pytest |
Flat filenames only (no src/pkg/mod.py) |
Enforced in prompts and write_files() |
Optional requirements.txt |
Documents deps for you to pip install |
Examples of what you might see:
| Goal | Possible files |
|---|---|
| Health-check API | main.py, test_main.py, requirements.txt |
| Temperature CLI | cli.py, converter.py, test_cli.py |
| Slugify library | slugify.py, test_slugify.py |
Depends on what was generated—for example:
cd workspace
pip install -r requirements.txt # if present
pytest # same check the agent uses
# If it's a FastAPI app with main:app:
uvicorn main:app --reload
# If it's a CLI module:
python cli.py --helpcd workspace
pytestThis is the same command the agent runs in agent/validator.py.
ForgePilot uses a two-level hybrid cache:
-
Exact cache
- SHA-256 hash of the normalized command.
- Zero embedding computation.
- Immediate reuse.
-
Semantic cache
- Uses a local sentence-transformer embedding.
- Finds sufficiently similar previous successful requests.
- Semantic matches are revalidated with pytest before reuse.
Only projects that pass validation are added to the semantic cache.
Last-known-good results are maintained separately for fallback after generation/self-healing failures.
| File | Purpose |
|---|---|
.cache/<hash> |
Full serialized result (str(dict)) after a successful validation |
.cache/<hash>.good |
Last-known-good copy for fallback |
Normalization (agent/normalize.py):
- Lowercase
- Strip leading/trailing whitespace
- Collapse internal whitespace to single spaces
So "Create a FastAPI App" and "create a fastapi app" share the same cache key.
On a cache hit, the agent uses eval() on cached content (trusted internal cache only—do not hand-edit cache files with untrusted input).
To force a fresh generation, delete the matching file in .cache/ or change the wording so the normalized form differs.
config.py sets MAX_LLM_CALLS = 2:
- First call:
generate_project() - Second call (optional):
self_heal()if pytest fails
LLMBudget.consume() raises if the budget is exhausted. The CLI prints remaining budget at key steps.
| Setting | File | Default | Description |
|---|---|---|---|
WORKSPACE_DIR |
config.py |
./workspace |
Output directory for generated files |
CACHE_DIR |
config.py |
./.cache |
Disk cache location |
GEMINI_MODEL |
config.py |
gemini-2.5-flash |
Model id for ChatGoogleGenerativeAI |
MAX_LLM_CALLS |
config.py |
2 |
Max LLM invocations per CLI run |
GOOGLE_API_KEY |
.env / env |
— | Required for generation and self-heal |
Pass the goal as the first argument:
python cli.py "your goal here"You already used two LLM calls in one run. Re-run the CLI (new budget) or fix the project manually under workspace/.
- Check
GOOGLE_API_KEYis set and valid. - Confirm network access to Google’s API.
- Inspect Raw LLM Output in the terminal; the model must return parseable JSON with a
filesobject.
- Read pytest stderr printed after Validation Failed.
- Run
cd workspace && pytest -vfor full tracebacks. - Delete
.cache/<hash>to avoid reusing a bad cached entry. - Simplify or clarify the goal string for the next generation attempt.
Install workspace dependencies (see Installation).
Remove .cache/ entries for that command’s normalized hash, or bump MAX_LLM_CALLS / change prompts in agent/generator.py and regenerate.
- API keys: Keep
GOOGLE_API_KEYin.envor a secret manager; add.envto.gitignoreif you use git. - Cache
eval(): Cached values are evaluated as Python literals. Only the agent should write cache files. - Generated code: Review LLM output before deploying to production; this tool is aimed at local development and experimentation.
- Generated artifacts: Databases, temp files, or secrets created under
workspace/during tests should not be deployed as-is.
- Small scale only: A single directory, a handful of modules; not for large apps or monorepos.
- Flat layout only: No packages,
src/trees, or nested project layouts. - pytest only: No automatic linting, typing, or non-Python stacks.
- No incremental edits: Each full miss regenerates (or heals) the whole
filesmap. - Budget of two: No multi-step repair loop beyond one self-heal pass.
- Model availability: Depends on Gemini API and the configured
GEMINI_MODELname. - Stale cache: Old cache entries may still contain a previous CRUD-shaped project until you invalidate the cache.
If a valid cache entry exists for your normalized command, a repeat run uses zero API calls:
python cli.py "create a fastapi crud app with sqlite"| Change | Where |
|---|---|
| Model or call limit | config.py |
| Generation / heal instructions | agent/prompts.py |
| Validation command | agent/validator.py |
| Cache key logic | agent/normalize.py, agent/cache.py |
No license file is included in this repository. Add one if you plan to distribute or open-source the project.