Custom AI Models

Yeriko Vargas

AI Engineer · AI Systems Architect · Detroit, MI

Prototypes

CascForward: build it, automate it, preserve what works.

A Python-first system for turning ideas and AI-assisted workflows into structured, reusable software. AI does the parts that need reasoning; everything repeatable moves into Python that runs the same way every time.

01Builders

create and improve one component each, inside their own folder

02Automations

connect proven components into larger workflows

03Preserve

validated systems are kept and reused, never rebuilt from zero

The rule underneath

The model reasons. Python repeats.

  idea ──▶ AI reasons ──▶ Python builds it ──▶ gates check it ──▶ it runs on a schedule
                                  │                                        │
                                  └──────── kept, versioned, reused ◀──────┘

Validation, reproducibility, cost control and failure recovery are built in, not added after.

Prototype 01 · The local cascade

A URL in. A website and a chatbot out. The model runs on my machine.

Python crawls, a local Llama (Ollama) reads, Python checks. The model only turns clean text into structured JSON; design is my own components, never a model. Tested on a real page: valid JSON in under 10 seconds at $0, and the validator caught the facts the small model got wrong.

01crawl

sitemap · ~20 pages · Playwright + Trafilatura

02facts

Ollama · JSON schema only · temperature 0

03design

my components · no model

04RAG

chunks · local embeddings · FAISS

05validate

no source string, no fact → export

cai_cascademy diagram · verbatim49 lines
                   URL
                    │
                    ▼
            ① PYTHON CRAWLER
                    │
        sitemap + navigation
        max ~20 useful pages
                    │
           ┌────────┴────────┐
           ▼                 ▼
      PLAYWRIGHT         TRAFILATURA
      DOM/rendering       clean text
      screenshots         content
           │                 │
           └────────┬────────┘
                    ▼
             ② FACT ENGINE
                    │
              LOCAL LLAMA
                    │
        STRUCTURED JSON ONLY
                    │
    ┌───────────────┼────────────────┐
    ▼               ▼                ▼
 SERVICES          FAQ            PRICING
 CONTACTS          LOCATIONS       PRODUCTS
    │
    └───────────────┬────────────────┘
                    ▼
             COMPANY_MODEL.JSON
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
   ③ CAI DESIGN ENGINE     ④ RAG ENGINE
          │                   │
   YOUR components        chunk facts
   YOUR spacing           embeddings
   YOUR palettes          FAISS
   YOUR typography        metadata
          │                   │
          ▼                   ▼
     WEBSITE BUILD          CHATBOT
          │                   │
          └─────────┬─────────┘
                    ▼
              ⑤ VALIDATOR
                    │
                    ▼
                  EXPORT

Status: testing. The fact step has run on a live page; the rest is being wired.

Prototype 02 · The scaffold and the function system

File = stage. Function = action. Pipeline = story. Print = report.

One command lays out a whole project: a menu that finds every builder, a collector that feeds them, numbered stages inside each one. Every function is named so it reads like a sentence, checks what comes in, and reports what it did. This is how I keep code readable after years of changes, and how I find a failure in one look.

00auto.py

loop menu · finds every run.py

01collector

new · append · filter → _df_collect.csv

02run.py

each builder's backbone

03src_N

_0 get · _1 clean · _2 features · _3 model · _4 eval · _5 utils

04data

raw → processed → exports

$ /scaffold <project_name> <domain>

Read a function name like a sentence

_2stage
proctype
audiokeyword
splitaction
GETreturns
chunksdfoutput
  _1_build_audiofolders_create_GET_paths
      ↓
  _2_proc_audio_split_GET_chunksdf
      ↓
  _3_ext_audiofreq_detect_GET_freqdf
      ↓
  _4_ana_audioquality_score_GET_statsdf

The name tells you the stage, what it does and what it gives back, before you open the file.

Every function reports

-----######----- FUNCTION SUMMARY -----######-----
FUNC   : {function_name}
STAGE  : {stage}
INPUT  : {input_count}
OUTPUT : {output_count}
ERRORS : {n_errors}
TIME   : {runtime}s
STATUS : SUCCESS / FAIL

The source, as I wrote it

project treewhat /scaffold creates26 lines
<project_name>/
├── auto.py          ← loop menu; discovers run.py files; [C] pinned for collector
├── collector.py     ← smart collector with interactive menu; outputs _df_collect.csv
├── README.md
├── requirements.txt
├── .gitignore
│
├── builders_<domain>/
│   ├── builder_1/
│   │   ├── run.py          ← builder backbone (called by auto.py)
│   │   ├── src_1/          ← all stage files live here
│   │   │   ├── _0_fn_get_data.py
│   │   │   ├── _1_fn_clean_data.py
│   │   │   ├── _2_fn_features.py
│   │   │   ├── _3_fn_model.py
│   │   │   ├── _4_fn_eval.py
│   │   │   └── _5_fn_utils.py
│   │   └── data/
│   │       ├── raw/
│   │       ├── processed/
│   │       └── exports/
│   ├── builder_2/          (run.py + src_2/ + data/)
│   └── builder_3/          (run.py + src_3/ + data/)
│
└── config/
    └── config.py
auto.pythe loop menu · full implementation82 lines
import os
import sys
import subprocess
 
ROOT = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = os.path.join(ROOT, "_df_collect.csv")
 
 
def clear():
    os.system("cls" if os.name == "nt" else "clear")
 
 
def df_status():
    if os.path.exists(CSV_PATH):
        try:
            import pandas as pd
            df = pd.read_csv(CSV_PATH)
            return f"df available  {len(df)} rows"
        except Exception:
            return "df exists (unreadable)"
    return "no df"
 
 
def find_runners():
    runners = []
    for entry in sorted(os.listdir(ROOT)):
        if entry.startswith("builders_"):
            domain_path = os.path.join(ROOT, entry)
            for builder in sorted(os.listdir(domain_path)):
                run_py = os.path.join(domain_path, builder, "run.py")
                if os.path.exists(run_py):
                    runners.append((builder, run_py))
    return runners
 
 
def show_menu(runners):
    print("\n" + "=" * 44)
    print("   MAIN MENU")
    print("=" * 44)
    for i, (name, _) in enumerate(runners, 1):
        print(f"   [{i}]  {name}")
    print()
    print(f"   [C]  Collector  ({df_status()})")
    print("   [Q]  Quit")
    print("=" * 44)
 
 
def main():
    while True:
        clear()
        runners = find_runners()
        show_menu(runners)
        choice = input("\n   Select: ").strip().lower()
 
        if choice == "q":
            clear()
            break
 
        elif choice == "c":
            sys.path.insert(0, ROOT)
            import collector
            import importlib
            importlib.reload(collector)
            collector.menu()
 
        elif choice.isdigit():
            idx = int(choice) - 1
            if 0 <= idx < len(runners):
                name, run_path = runners[idx]
                clear()
                print(f"\n   Running {name}...\n")
                subprocess.run([sys.executable, run_path])
                input("\n   Press Enter to return to menu...")
            else:
                input("   Invalid selection — Press Enter...")
 
        else:
            input("   Invalid selection — Press Enter...")
 
 
if __name__ == "__main__":
    main()
core functionthe template every stage function follows43 lines
-----######----- CORE FUNCTION -----######-----
def _2_proc_audio_split_GET_chunksdf(df, output_dir, ...):
 
import os
from tqdm import tqdm
import time
 
function_name = "_2_proc_audio_split_GET_chunksdf"
stage = "PROC"
t0 = time.time()
 
# 1. VALIDATION
assert 'Path' in df.columns, "Missing 'Path' column"
 
# 2. SETUP
results = []
n_errors = 0
 
# 3. TQDM LOOP
for path in tqdm(df['Path'], desc='[PROC] Splitting Audio'):
 
    try:
        # 4. CORE LOGIC
        # (your processing here)
 
        # 5. STORE
        results.append(...)
 
    except Exception:
        n_errors += 1
 
# 6. OUTPUT
df_chunks = ...
 
# 7. SUMMARY PRINT
runtime = round(time.time() - t0, 2)
 
print(f"""
 
-----######----- FUNCTION SUMMARY -----######----- FUNC : {function_name} STAGE : {stage} INPUT : {len(df)} OUTPUT : {len(df_chunks)} ERRORS : {n_errors} TIME : {runtime}s STATUS : SUCCESS
""")
 
return df_chunks
/scaffoldthe command · verbatim172 lines
# /scaffold — Create a new app project with standard structure
 
Scaffold a new project using the user's standard layout.
 
## Usage
```
/scaffold <project_name> <domain>
```
Examples:
- `/scaffold music_code_structure music`
- `/scaffold finance_pipeline finance`
 
## Directory structure
 
```
<project_name>/
├── auto.py          ← loop menu; discovers run.py files; [C] pinned for collector
├── collector.py     ← smart collector with interactive menu; outputs _df_collect.csv
├── README.md
├── requirements.txt
├── .gitignore
│
├── builders_<domain>/
│   ├── builder_1/
│   │   ├── run.py          ← builder backbone (called by auto.py)
│   │   ├── src_1/          ← all stage files live here
│   │   │   ├── _0_fn_get_data.py
│   │   │   ├── _1_fn_clean_data.py
│   │   │   ├── _2_fn_features.py
│   │   │   ├── _3_fn_model.py
│   │   │   ├── _4_fn_eval.py
│   │   │   └── _5_fn_utils.py
│   │   └── data/
│   │       ├── raw/
│   │       ├── processed/
│   │       └── exports/
│   ├── builder_2/          (run.py + src_2/ + data/)
│   └── builder_3/          (run.py + src_3/ + data/)
│
└── config/
    └── config.py
```
 
Data folders get a `.gitkeep`. Stage files inside `src_N/` are empty skeletons (`pass`).
 
## auto.py — full implementation
 
```python
import os
import sys
import subprocess
 
ROOT = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = os.path.join(ROOT, "_df_collect.csv")
 
 
def clear():
    os.system("cls" if os.name == "nt" else "clear")
 
 
def df_status():
    if os.path.exists(CSV_PATH):
        try:
            import pandas as pd
            df = pd.read_csv(CSV_PATH)
            return f"df available  {len(df)} rows"
        except Exception:
            return "df exists (unreadable)"
    return "no df"
 
 
def find_runners():
    runners = []
    for entry in sorted(os.listdir(ROOT)):
        if entry.startswith("builders_"):
            domain_path = os.path.join(ROOT, entry)
            for builder in sorted(os.listdir(domain_path)):
                run_py = os.path.join(domain_path, builder, "run.py")
                if os.path.exists(run_py):
                    runners.append((builder, run_py))
    return runners
 
 
def show_menu(runners):
    print("\n" + "=" * 44)
    print("   MAIN MENU")
    print("=" * 44)
    for i, (name, _) in enumerate(runners, 1):
        print(f"   [{i}]  {name}")
    print()
    print(f"   [C]  Collector  ({df_status()})")
    print("   [Q]  Quit")
    print("=" * 44)
 
 
def main():
    while True:
        clear()
        runners = find_runners()
        show_menu(runners)
        choice = input("\n   Select: ").strip().lower()
 
        if choice == "q":
            clear()
            break
 
        elif choice == "c":
            sys.path.insert(0, ROOT)
            import collector
            import importlib
            importlib.reload(collector)
            collector.menu()
 
        elif choice.isdigit():
            idx = int(choice) - 1
            if 0 <= idx < len(runners):
                name, run_path = runners[idx]
                clear()
                print(f"\n   Running {name}...\n")
                subprocess.run([sys.executable, run_path])
                input("\n   Press Enter to return to menu...")
            else:
                input("   Invalid selection — Press Enter...")
 
        else:
            input("   Invalid selection — Press Enter...")
 
 
if __name__ == "__main__":
    main()
```
 
## collector.py — full implementation
 
Copy the full collector.py from `music_code_structure/collector.py` as the canonical template.
It implements: `_new`, `_append`, `_replace`, `_view`, `_filter`, `_clear`, `menu()`, `main()`.
Menu options:
- **[N] New** — fresh collection, replaces df
- **[A] Append** — adds rows, dedupes by file_path
- **[R] Replace** — preview current df then swap with new input
- **[V] View** — shape, columns, extension breakdown with bar, total size MB, head(5)
- **[F] Filter** — filter by extension(s), optionally save filtered result
- **[X] Clear** — delete _df_collect.csv with confirmation
- **[B] Back** — return to auto.py menu
Status line always shows: row count + top extensions with counts.
 
## Other file rules
 
### run.py (each builder)
- Empty skeleton: `pass`
 
### config/config.py
- Empty skeleton: `pass`
 
### requirements.txt
```
pandas
```
 
### .gitignore
```
__pycache__/
*.pyc
_df_collect.csv
data/raw/*
data/processed/*
data/exports/*
!**/.gitkeep
```
 
## After scaffolding
Print a short tree confirming the created structure.
FUNCTION SYSTEMnaming · flow · print · hard rules · checklist146 lines
# 🧱 FUNCTION SYSTEM — FULL STRUCTURE (SCALABLE)
 
---
 
## 1. FILE STRUCTURE (Stage-Based)
 
### Rule: **One stage per file**
 
src/
_1_build_audio.py _2_proc_audio.py _3_ext_audio.py _4_ana_audio.py _5_util_audio.py
 
 
- `_1` → build (setup, folders, paths)
- `_2` → process (transformations)
- `_3` → extract (features, metadata)
- `_4` → analyze (scoring, insights)
- `_5` → utilities (helpers)
 
---
 
## 2. FUNCTION NAMING (LOCK THIS)
 
### Format:
 
_#stage##_type_keyword##_action_GET_output
 
 
### Examples:
 
_1_build_audiofolders_create_GET_paths _2_proc_audio_split_GET_chunksdf _3_ext_audiofreq_detect_GET_freqdf _4_ana_audioquality_score_GET_statsdf
 
 
---
 
## 3. PIPELINE FLOW (Readable Story)
 
_1_build → _2_proc → _3_ext → _4_ana
 
 
### Example:
 
_1_build_audiofolders_create_GET_paths ↓ _2_proc_audio_split_GET_chunksdf ↓ _3_ext_audiofreq_detect_GET_freqdf ↓ _4_ana_audioquality_score_GET_statsdf
 
 
---
 
## 4. CORE FUNCTION TEMPLATE
 
-----######----- CORE FUNCTION -----######-----
def _2_proc_audio_split_GET_chunksdf(df, output_dir, ...):
 
import os
from tqdm import tqdm
import time
 
function_name = "_2_proc_audio_split_GET_chunksdf"
stage = "PROC"
t0 = time.time()
 
# 1. VALIDATION
assert 'Path' in df.columns, "Missing 'Path' column"
 
# 2. SETUP
results = []
n_errors = 0
 
# 3. TQDM LOOP
for path in tqdm(df['Path'], desc='[PROC] Splitting Audio'):
 
    try:
        # 4. CORE LOGIC
        # (your processing here)
 
        # 5. STORE
        results.append(...)
 
    except Exception:
        n_errors += 1
 
# 6. OUTPUT
df_chunks = ...
 
# 7. SUMMARY PRINT
runtime = round(time.time() - t0, 2)
 
print(f"""
 
-----######----- FUNCTION SUMMARY -----######----- FUNC : {function_name} STAGE : {stage} INPUT : {len(df)} OUTPUT : {len(df_chunks)} ERRORS : {n_errors} TIME : {runtime}s STATUS : SUCCESS
""")
 
return df_chunks
 
 
 
---
 
## 5. STANDARD PRINT FORMAT
 
### Always use:
 
-----######----- FUNCTION SUMMARY -----######----- FUNC : {function_name} STAGE : {stage} INPUT : {input_count} OUTPUT : {output_count} ERRORS : {n_errors} TIME : {runtime}s STATUS : SUCCESS / FAIL
 
 
---
 
## 6. RUNNING PIPELINE
 
#!#!#!#!#! RUNNING STATEMENTS #!#!#!#!#!
df_1 = _1_build_audiofolders_create_GET_paths(...) df_2 = _2_proc_audio_split_GET_chunksdf(df_1, ...) df_3 = _3_ext_audiofreq_detect_GET_freqdf(df_2, ...) df_4 = _4_ana_audioquality_score_GET_statsdf(df_3)
 
 
---
 
## 7. HARD RULES
 
- One function = one responsibility
- One file = one stage
- Always return usable output (df or structured object)
- Always include tqdm for loops
- Always include summary print
- Never mix stages in one function
- Naming must reflect:
  - stage
  - action
  - output
 
---
 
## 8. MENTAL MODEL
 
- File = **Stage**
- Function = **Action**
- Pipeline = **Story**
- Print = **Report**
 
---
 
## 9. QUICK CHECKLIST BEFORE CODING
 
- [ ] Input clearly defined
- [ ] Output clearly defined
- [ ] Stage assigned (_1, _2, _3...)
- [ ] Function name follows format
- [ ] tqdm included if looping
- [ ] Summary print included
- [ ] Returns structured output
validation layerinput + output checks · error bucket17 lines
🧪 3. VALIDATION LAYER (INPUT + OUTPUT CHECKS)
🔹 Why
* Prevent silent errors → critical for pipelines
🔹 Add pattern:
 
def _validate_df_has_cols(df, cols):
    for c in cols:
        if c not in df.columns:
            raise ValueError(f"Missing column: {c}")
 
🔹 Also validate output:
 
assert len(df_chunks) > 0, "No chunks created"
 
🔹 Benefit
* Fail fast instead of corrupting pipeline
⚡ BONUS (HIGH IMPACT) 🔹 6. ERROR BUCKET SYSTEM Instead of losing failures: errors = [] try: ... except Exception as e: errors.append((path, str(e))) Return: return df_chunks, errors

Written as a Claude Code command, so every project an agent scaffolds starts in the same shape.

Prototype 03 · /struct and its web twin

One shape for every project. Python or web: the same menu, the same stages, the same proof.

/struct lays out any data project so it runs on any machine without one edit. Its web twin keeps every law and swaps the parts: builders become layers, the dataframe becomes the site, and a landing page that asks for input in steps ships with its own run test.

/struct/webstruct
builders_<domain>/builder_Nlayers_<domain>/layer_N
_df_collect.csvthe site dict
fn(df, paths, cfg) → dffn(site, paths, cfg) → site
collector.pyschema.json per layer
auto.py · [C] collectorauto.py · P · K · T · A · E
export_2_desk.pyexport_2_desk.py
  layer 1 ──Next──▶ layer 2 ──Next──▶ layer 3 ──Send──▶ server checks it all again ──▶ stored
     │                 │                 │                                              │
     └── page checks ──┴── page checks ──┘          steps.jsonl ──▶ funnel per layer ◀──┘
$ python run.py --test
/structthe command · verbatim214 lines
# /struct — Create a new app project with standard structure
 
This is the general-purpose project structure used across all domains (music, finance, etc.).
 
## ⚠️ PORTABILITY RULE — ENFORCE ALWAYS
 
**This app must work on any machine, in any folder, without modification.**
 
- **ZERO hardcoded external paths** anywhere in the project — not in functions, not in run.py, not in config files
- All internal paths are computed at runtime using `os.path.dirname(os.path.abspath(__file__))` as the anchor
- `_df_collect.csv` lives at project root and is always located relative to `__file__`
- The **only** file that ever touches external/desktop paths is `export_2_desk.py` — that is its entire purpose
- If you ever see an absolute path like `/Users/...` or `C:\...` inside any file except `export_2_desk.py`, it is a bug
 
## ROLE OF EACH ROOT FILE
 
| File | Role |
|------|------|
| `auto.py` | Loop menu — orchestrates builders and collector |
| `collector.py` | Builds `_df_collect.csv` — the df we work on |
| `export_2_desk.py` | **Only file with external/desktop paths** — highly customizable per project; copies builder exports to desktop with appropriate naming/format |
| `_df_collect.csv` | The working dataframe — input to all builders |
 
## Collector note
`collector.py` is the one file that may vary by domain. When the user invokes `/struct` for the first time on a new project, ask: **"Does this project need a custom collector, or use the standard file/folder collector?"**
- **Standard**: file/folder walker → `_df_collect.csv` (copy as-is from template)
- **Custom**: user will describe the data source (API, database, spreadsheet, etc.) — adapt `collector.py` for that source while keeping the same menu structure (`menu()`) and CSV output contract (`_df_collect.csv`)
 
## Usage
```
/struct <project_name> <domain>
```
Examples:
- `/struct music_code_structure music`
- `/struct finance_pipeline finance`
 
## Directory structure
 
```
<project_name>/
├── auto.py              ← loop menu; discovers run.py files; [C] pinned for collector
├── collector.py         ← smart collector; outputs _df_collect.csv
├── export_2_desk.py     ← ONLY file with external paths; copies exports → Desktop
├── _df_collect.csv      ← working df (gitignored; created by collector)
├── README.md
├── requirements.txt
├── .gitignore
│
├── builders_<domain>/
│   ├── builder_1/
│   │   ├── run.py          ← builder backbone (called by auto.py)
│   │   ├── src_1/          ← stage files (named by /pystr convention)
│   │   │   ├── _1_build_<domain>.py
│   │   │   ├── _2_proc_<domain>.py
│   │   │   ├── _3_ext_<domain>.py
│   │   │   ├── _4_ana_<domain>.py
│   │   │   └── _5_util_<domain>.py
│   │   └── data/
│   │       ├── raw/
│   │       ├── processed/
│   │       └── exports/
│   ├── builder_2/          (run.py + src_2/ + data/)
│   └── builder_3/          (run.py + src_3/ + data/)
│
└── config/
    ├── config.py        ← global app config
    ├── config_u1.py     ← user-editable config for builder_1
    ├── config_u2.py     ← user-editable config for builder_2
    └── config_u3.py     ← user-editable config for builder_3
```
 
Data folders get a `.gitkeep`. Stage files inside `src_N/` are empty skeletons (`pass`).
Each `config_uN.py` is the first import in its `run.py` — see /pystr Section 11 for the full pattern.
 
## auto.py — full implementation
 
```python
import os
import sys
import subprocess
 
ROOT = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = os.path.join(ROOT, "_df_collect.csv")
 
 
def clear():
    os.system("cls" if os.name == "nt" else "clear")
 
 
def df_status():
    if os.path.exists(CSV_PATH):
        try:
            import pandas as pd
            df = pd.read_csv(CSV_PATH)
            return f"df available  {len(df)} rows"
        except Exception:
            return "df exists (unreadable)"
    return "no df"
 
 
def find_runners():
    runners = []
    for entry in sorted(os.listdir(ROOT)):
        if entry.startswith("builders_"):
            domain_path = os.path.join(ROOT, entry)
            for builder in sorted(os.listdir(domain_path)):
                run_py = os.path.join(domain_path, builder, "run.py")
                if os.path.exists(run_py):
                    runners.append((builder, run_py))
    return runners
 
 
def show_menu(runners):
    print("\n" + "=" * 44)
    print("   MAIN MENU")
    print("=" * 44)
    for i, (name, _) in enumerate(runners, 0):
        print(f"   [{i}]  {name}")
    print()
    print(f"   [C]  Collector  ({df_status()})")
    print("   [Q]  Quit")
    print("=" * 44)
 
 
def main():
    while True:
        clear()
        runners = find_runners()
        show_menu(runners)
        choice = input("\n   Select: ").strip().lower()
 
        if choice == "q":
            clear()
            break
 
        elif choice == "c":
            sys.path.insert(0, ROOT)
            import collector
            import importlib
            importlib.reload(collector)
            collector.menu()
 
        elif choice.isdigit():
            idx = int(choice)
            if 0 <= idx < len(runners):
                name, run_path = runners[idx]
                clear()
                print(f"\n   Running {name}...\n")
                subprocess.run([sys.executable, run_path])
                input("\n   Press Enter to return to menu...")
            else:
                input("   Invalid selection — Press Enter...")
 
        else:
            input("   Invalid selection — Press Enter...")
 
 
if __name__ == "__main__":
    main()
```
 
## collector.py — full implementation
 
Copy the full collector.py from `music_code_structure/collector.py` as the canonical template.
It implements: `_new`, `_append`, `_replace`, `_view`, `_filter`, `_clear`, `menu()`, `main()`.
Menu options:
- **[N] New** — fresh collection, replaces df
- **[A] Append** — adds rows, dedupes by file_path
- **[R] Replace** — preview current df then swap with new input
- **[V] View** — shape, columns, extension breakdown with bar, total size MB, head(5)
- **[F] Filter** — filter by extension(s), optionally save filtered result
- **[X] Clear** — delete _df_collect.csv with confirmation
- **[B] Back** — return to auto.py menu
Status line always shows: row count + top extensions with counts.
 
## Other file rules
 
### run.py (each builder)
- Empty skeleton: `pass`
- First import must be its `config_uN.py` (e.g. `from config.config_u1 import *`)
 
### config/config.py
- Empty skeleton: `pass`
 
### config/config_uN.py (one per builder)
- Empty skeleton with comment: `# USER CONFIG — builder_N`
- Contains ONLY functional attributes: numbers, thresholds, formats, flags, extension lists, seeds
- NEVER contains paths — paths are always computed at runtime via `__file__`
 
### export_2_desk.py
- The one and only file allowed to use desktop/external paths (`os.path.expanduser("~/Desktop")` etc.)
- Reads from builder `data/exports/` folders (project-relative)
- Copies/exports to Desktop with appropriate naming
- Highly customizable per project: zip, rename, format conversion, selective export, etc.
- Skeleton only at scaffold time — user/AI fills it in based on project needs
 
### requirements.txt
```
pandas
```
 
### .gitignore
```
__pycache__/
*.pyc
_df_collect.csv
data/raw/*
data/processed/*
data/exports/*
!**/.gitkeep
```
 
## After creating
Print a short tree confirming the created structure.
struct treewhat /struct creates30 lines
<project_name>/
├── auto.py              ← loop menu; discovers run.py files; [C] pinned for collector
├── collector.py         ← smart collector; outputs _df_collect.csv
├── export_2_desk.py     ← ONLY file with external paths; copies exports → Desktop
├── _df_collect.csv      ← working df (gitignored; created by collector)
├── README.md
├── requirements.txt
├── .gitignore
│
├── builders_<domain>/
│   ├── builder_1/
│   │   ├── run.py          ← builder backbone (called by auto.py)
│   │   ├── src_1/          ← stage files (named by /pystr convention)
│   │   │   ├── _1_build_<domain>.py
│   │   │   ├── _2_proc_<domain>.py
│   │   │   ├── _3_ext_<domain>.py
│   │   │   ├── _4_ana_<domain>.py
│   │   │   └── _5_util_<domain>.py
│   │   └── data/
│   │       ├── raw/
│   │       ├── processed/
│   │       └── exports/
│   ├── builder_2/          (run.py + src_2/ + data/)
│   └── builder_3/          (run.py + src_3/ + data/)
│
└── config/
    ├── config.py        ← global app config
    ├── config_u1.py     ← user-editable config for builder_1
    ├── config_u2.py     ← user-editable config for builder_2
    └── config_u3.py     ← user-editable config for builder_3
webstruct treethe web twin37 lines
<site_name>/
├── auto.py              ← loop menu; lists layers; P · K · T · A · E · Q
├── run.py               ← build → preview · --check · --test · --analyze
├── export_2_desk.py     ← ONLY file with external paths
├── README.md
├── requirements.txt     ← stdlib; playwright optional (browser test)
├── .gitignore
│
├── layers_<domain>/     ← one folder per input layer, shown one at a time
│   ├── layer_1_<step>/
│   │   ├── layer.html      ← the fields, markup only (no form, no buttons)
│   │   ├── schema.json     ← title + fields: name · type · label · required · options
│   │   └── layer.css       ← optional, this layer only
│   ├── layer_2_<step>/
│   └── layer_3_<step>/
│
├── src_web/             ← stage files, /pystr names, contract fn(site, paths, cfg) → site
│   ├── _1_build_web.py     ← find layers in number order, load parts
│   ├── _2_proc_web.py      ← markup ⇄ schema must agree · unique names · known types · fail fast
│   ├── _3_ext_web.py       ← stitch shell + layers → dist/ (index · css · js · schema.json)
│   ├── _4_ana_web.py       ← submissions → csv · steps → funnel per layer
│   ├── _5_util_web.py      ← print · files · validate_payload (the only cross-import)
│   └── shell/              ← index.html · site.css · site.js (the stepper)
│
├── tests/
│   ├── test_site.py        ← the run test: build · portability · worst-case submits · funnel
│   └── test_browser.py     ← optional: headless Chrome walks every layer at phone width
│
├── config/
│   ├── config.py        ← global: name · port · limits · field types (no paths)
│   └── config_u1.py     ← user-editable: the words on the page (no paths)
│
├── data/
│   ├── raw/             ← submissions.jsonl · steps.jsonl (append only)
│   ├── processed/       ← submissions.csv
│   └── exports/         ← funnel.csv
└── dist/                ← built site (gitignored)
run.pybuild · preview · --check · --test116 lines
"""The site's front door. Python only, stdlib only.
  python run.py            build dist/ and preview it at 127.0.0.1:PORT (POST /submit · POST /step)
  python run.py --check    build only: exit 0 when every layer is sound, 1 when not
  python run.py --test     the run test: every check in tests/ (schema · build · portability · submit flow)
  python run.py --analyze  submissions → data/processed/submissions.csv · funnel → data/exports/funnel.csv"""
import json
import os
import sys
from datetime import datetime, timezone
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
 
from config import config, config_u1      # USER CONFIG first, always
 
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
 
from src_web._1_build_web import build      # noqa: E402
from src_web._2_proc_web import process     # noqa: E402
from src_web._3_ext_web import extract      # noqa: E402
from src_web._4_ana_web import analyze      # noqa: E402
from src_web._5_util_web import validate_payload  # noqa: E402
 
CFG = {k: v for m in (config, config_u1) for k, v in vars(m).items() if k.isupper()}
 
PATHS = {
    "layers": os.path.join(_HERE, "layers_web"),
    "src": os.path.join(_HERE, "src_web"),
    "dist": os.path.join(_HERE, "dist"),
    "raw": os.path.join(_HERE, "data", "raw"),
    "processed": os.path.join(_HERE, "data", "processed"),
    "exports": os.path.join(_HERE, "data", "exports"),
}
 
 
def run(paths=PATHS, cfg=CFG):
    print(f"\n  ┌─ {cfg['SITE_NAME']} {'─' * (40 - len(cfg['SITE_NAME']))}┐")
    site = {}
    site = build(site, paths, cfg)
    site = process(site, paths, cfg)
    site = extract(site, paths, cfg)
    print("  └──────────────────────────────────────────┘\n")
    return site
 
 
def _append(path, row):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(row, ensure_ascii=False) + "\n")
 
 
def make_server(schema, paths=PATHS, cfg=CFG, port=None):
    class Handler(SimpleHTTPRequestHandler):
        def __init__(self, *a, **k):
            super().__init__(*a, directory=paths["dist"], **k)
 
        def log_message(self, *a):
            pass
 
        def _send(self, code, body):
            data = json.dumps(body).encode()
            self.send_response(code)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
 
        def do_POST(self):
            n = int(self.headers.get("Content-Length") or 0)
            if n > cfg["MAX_BODY_BYTES"]:
                return self._send(413, {"ok": False, "errors": ["too big"]})
            try:
                body = json.loads(self.rfile.read(n) or b"null")
            except ValueError:
                return self._send(400, {"ok": False, "errors": ["not json"]})
            now = datetime.now(timezone.utc).isoformat(timespec="seconds")
            if self.path == "/submit":
                errors = validate_payload(schema, body, cfg)
                if errors:
                    return self._send(400, {"ok": False, "errors": errors})
                _append(os.path.join(paths["raw"], "submissions.jsonl"), {"at": now, **body})
                return self._send(200, {"ok": True})
            if self.path == "/step" and isinstance(body, dict) and str(body.get("layer", "")).isdigit():
                _append(os.path.join(paths["raw"], "steps.jsonl"),
                        {"at": now, "visit": str(body.get("visit", ""))[:16], "layer": int(body["layer"])})
                return self._send(200, {"ok": True})
            return self._send(404, {"ok": False, "errors": ["no such route"]})
 
    return ThreadingHTTPServer(("127.0.0.1", cfg["PORT"] if port is None else port), Handler)
 
 
def main():
    arg = sys.argv[1] if len(sys.argv) > 1 else ""
    if arg == "--test":
        import unittest
        suite = unittest.defaultTestLoader.discover(os.path.join(_HERE, "tests"), top_level_dir=_HERE)
        raise SystemExit(0 if unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() else 1)
    try:
        site = run()
    except ValueError as e:
        print(f"  ✗ {e}")
        raise SystemExit(1)
    if arg == "--check":
        raise SystemExit(0)
    if arg == "--analyze":
        analyze(site, PATHS, CFG)
        raise SystemExit(0)
    srv = make_server(site["schema"])
    print(f"  preview  http://127.0.0.1:{CFG['PORT']}/   (ctrl-c stops it)")
    try:
        srv.serve_forever()
    except KeyboardInterrupt:
        print("\n  stopped")
 
 
if __name__ == "__main__":
    main()
_2_proc_web.pymarkup and schema must agree41 lines
"""_2 process — check every layer before anything is written. Markup and schema must agree. Fails fast."""
import re
import time
 
from src_web._5_util_web import hr, summary
 
 
def process(site, paths, cfg):
    t0 = time.time()
    hr("PROCESS")
    errors, seen = [], set()
    if not site["layers"]:
        errors.append("no layers found")
    for layer in site["layers"]:
        name, sch = layer["name"], layer["schema"]
        if not sch.get("title"):
            errors.append(f"{name}: schema has no title")
        if not sch.get("fields"):
            errors.append(f"{name}: schema has no fields")
        for f in sch.get("fields", []):
            fn = f.get("name", "")
            if not re.fullmatch(r"[a-z][a-z0-9_]*", fn):
                errors.append(f"{name}: bad field name {fn!r}")
            if fn in seen:
                errors.append(f"{name}: field {fn} used twice")
            seen.add(fn)
            if f.get("type") not in cfg["FIELD_TYPES"]:
                errors.append(f"{name}: {fn} has unknown type {f.get('type')!r}")
            if f.get("type") == "choice" and not f.get("options"):
                errors.append(f"{name}: choice {fn} has no options")
            if f'name="{fn}"' not in layer["html"]:
                errors.append(f"{name}: {fn} is in schema.json but not in layer.html")
        for fn in re.findall(r'name="([^"]+)"', layer["html"]):
            if fn not in {f.get("name") for f in sch.get("fields", [])}:
                errors.append(f"{name}: {fn} is in layer.html but not in schema.json")
    for e in errors:
        print(f"   ✗ {e}")
    summary("_2_proc_webschema_check_GET_site", "PROC", len(site["layers"]), len(site["layers"]), len(errors), t0)
    if errors:
        raise ValueError(f"{len(errors)} layer error(s): fix them before building")
    return site
tests/test_site.pythe run test144 lines
"""The run test. No network beyond 127.0.0.1, nothing written outside the project; test files are removed after."""
import json
import os
import re
import threading
import unittest
import urllib.error
import urllib.request
 
import run
 
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEST_RAW = os.path.join(ROOT, "data", "raw", "_test")
 
 
class Site(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.site = run.run()
        cls.paths = {**run.PATHS, "raw": TEST_RAW}
        cls.srv = run.make_server(cls.site["schema"], cls.paths, run.CFG, port=0)
        cls.url = f"http://127.0.0.1:{cls.srv.server_address[1]}"
        threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
 
    @classmethod
    def tearDownClass(cls):
        cls.srv.shutdown()
        for f in ("submissions.jsonl", "steps.jsonl"):
            p = os.path.join(TEST_RAW, f)
            if os.path.exists(p):
                os.remove(p)
        if os.path.isdir(TEST_RAW):
            os.rmdir(TEST_RAW)
 
    def post(self, route, body):
        req = urllib.request.Request(self.url + route, json.dumps(body).encode(), {"Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req) as r:
                return r.status, json.loads(r.read())
        except urllib.error.HTTPError as e:
            return e.code, json.loads(e.read())
 
    def good(self):
        return {"need": "A chatbot", "need_words": "Ñandú · 名前 · 🚀 " + "x" * 1500,
                "company": "O'Brien & Søn, Ltd.", "team_size": "12",
                "name": "Maximiliano Fernández-Oyarzún de la Torre", "email": "a.very.long.name@sub.example-company.co",
                "consent": True}
 
    # ── the page ──────────────────────────────────────────────────────────────────────────────
    def test_every_layer_in_order(self):
        page = open(os.path.join(ROOT, "dist", "index.html"), encoding="utf-8").read()
        found = [int(n) for n in re.findall(r'data-layer="(\d+)"', page)]
        self.assertEqual(found, list(range(len(self.site["layers"]))))
        self.assertEqual(page.count(" hidden>"), len(found) - 1 + 1)  # all layers but the first, plus the done panel
 
    def test_every_field_on_the_page(self):
        page = open(os.path.join(ROOT, "dist", "index.html"), encoding="utf-8").read()
        for layer in self.site["schema"]["layers"]:
            for f in layer["fields"]:
                self.assertIn(f'name="{f["name"]}"', page, f["name"])
 
    def test_no_placeholder_left(self):
        page = open(os.path.join(ROOT, "dist", "index.html"), encoding="utf-8").read()
        self.assertNotIn("{{", page)
 
    # ── portability: only export_2_desk.py may name a place outside the project ───────────────────
    def test_no_outside_paths(self):
        bad = re.compile("/" + "Users/|[A-Z]:" + r"\\\\" + "|expand" + "user|/" + "home/")
        for d, _, files in os.walk(ROOT):
            if "dist" in d.split(os.sep) or "__pycache__" in d:
                continue
            for f in files:
                if f.endswith((".py", ".html", ".css", ".js", ".json")) and f != "export_2_desk.py":
                    text = open(os.path.join(d, f), encoding="utf-8").read()
                    self.assertIsNone(bad.search(text), os.path.join(d, f))
 
    # ── the layers, broken on purpose ───────────────────────────────────────────────────────────
    def test_schema_and_markup_must_agree(self):
        broken = {"layers": [{"name": "layer_1_x", "html": '<input name="a">', "css": "",
                              "schema": {"title": "x", "fields": [{"name": "b", "type": "text"}]}}]}
        with self.assertRaises(ValueError):
            run.process(broken, run.PATHS, run.CFG)
 
    def test_duplicate_field_refused(self):
        lay = lambda n: {"name": n, "html": '<input name="a">', "css": "",
                         "schema": {"title": "x", "fields": [{"name": "a", "type": "text"}]}}
        with self.assertRaises(ValueError):
            run.process({"layers": [lay("layer_1_a"), lay("layer_2_b")]}, run.PATHS, run.CFG)
 
    # ── the submit flow, worst cases first ─────────────────────────────────────────────────────
    def test_empty_refused(self):
        code, out = self.post("/submit", {})
        self.assertEqual(code, 400)
        self.assertTrue(any("required" in e for e in out["errors"]))
 
    def test_bad_email_refused(self):
        code, out = self.post("/submit", {**self.good(), "email": "not-an-email"})
        self.assertEqual(code, 400)
        self.assertIn("email: not an email", out["errors"])
 
    def test_consent_must_be_ticked(self):
        code, out = self.post("/submit", {**self.good(), "consent": "true"})
        self.assertEqual(code, 400)
 
    def test_unknown_field_refused(self):
        code, out = self.post("/submit", {**self.good(), "admin": "yes"})
        self.assertEqual(code, 400)
 
    def test_choice_outside_options_refused(self):
        code, _ = self.post("/submit", {**self.good(), "need": "Free money"})
        self.assertEqual(code, 400)
 
    def test_too_long_refused(self):
        code, _ = self.post("/submit", {**self.good(), "need_words": "x" * 5000})
        self.assertEqual(code, 400)
 
    def test_not_json_refused(self):
        req = urllib.request.Request(self.url + "/submit", b"{nope", {"Content-Type": "application/json"})
        with self.assertRaises(urllib.error.HTTPError) as e:
            urllib.request.urlopen(req)
        self.assertEqual(e.exception.code, 400)
 
    def test_worst_case_good_payload_stored(self):
        code, out = self.post("/submit", self.good())
        self.assertEqual((code, out), (200, {"ok": True}))
        rows = [json.loads(l) for l in open(os.path.join(TEST_RAW, "submissions.jsonl"), encoding="utf-8")]
        self.assertEqual(rows[-1]["name"], self.good()["name"])
 
    def test_steps_feed_the_funnel(self):
        for layer in range(3):
            self.assertEqual(self.post("/step", {"visit": "t1", "layer": layer})[0], 200)
        self.assertEqual(self.post("/step", {"visit": "t2", "layer": 0})[0], 200)
        self.assertEqual(self.post("/step", {"visit": "t3", "layer": "drop table"})[0], 404)
        paths = {**run.PATHS, "raw": TEST_RAW, "processed": TEST_RAW, "exports": TEST_RAW}
        run.analyze(self.site, paths, run.CFG)
        funnel = open(os.path.join(TEST_RAW, "funnel.csv"), encoding="utf-8").read().splitlines()
        os.remove(os.path.join(TEST_RAW, "funnel.csv"))
        os.remove(os.path.join(TEST_RAW, "submissions.csv"))
        self.assertTrue(funnel[1].endswith(",2"), funnel)   # two visits passed layer 1
        self.assertTrue(funnel[3].endswith(",1"), funnel)   # one visit passed layer 3
 
 
if __name__ == "__main__":
    unittest.main()

Stdlib only. Playwright is optional, for the browser walk.

Prototype 04 · CAI Hub beta

Custom AI Models presents

R-CAI · Remote Control for Aggregated Integrations

One door for clients: sign in like anywhere else, then scroll a landing page that is also the menu. Every step up the ladder hands the client more control over their own AI.

R-CAI

Remote Control for Aggregated Integrations

RCS-CAI

Remote Control Source for Custom Aggregated Integrations

SRC-CAI

Source Remote Control for Custom Aggregated Integrations

CTRAIL

Custom Training for Remote Aggregated Integrations Liaisons · trains people who find people

The ladder inside the hub

  sign in ──▶ scroll ──▶ 01 ──▶ 02 ──▶ 03 ──▶ 04 ──▶ 05
                         scope  clone  +chat  your AI  agentic
                         ─────────── more control for the client ──────────▶
01Add or edit a web app

on top of the app you already have · custom, scoped as a project

scoped
02Clone the website

custom adds · 6 months support · tool costs included · then from $15 with the URL · no surprise costs

$888
03Clone + chatbot

a year of support · tool costs included · then subscription from $35

$2,499
04SRC-CAI

full AI development: an optimised web solution · a chatbot agent built from how customers really ask · your own editable AI + devops tool for the site · standard / silver / gold

tiers
05ARC-CAI

agentic remote control for your custom AI models

2027

Status: beta. Prices and names are a working draft, not a published offer.

In my words

CascForward, as I wrote it
I’m also building CascForward, a Python-first system for turning ideas and AI-assisted workflows into structured, reusable software.

CascForward: build it, automate it, preserve what works.

The idea is simple. Use AI for the parts that require reasoning, then move repeatable work into reliable Python systems. Builders create and improve components, automations connect them into larger workflows, and validated systems can be preserved and reused instead of rebuilt every time.
CAI Hub beta, as I wrote it
CAI - HUB - BETA 
Custom AI Models presents 

=———

R-CAI
Remote Control for Aggregated Integrations 


RCS-CAI
Remote Control Source for Custom Aggregated Integrations 


SRC-CAI
Source Remote Control for Custom Aggregated Integrations 

=———
CTRAIL 
Custom training for Remote Aggregated Integrations Liaisons 
(Recruits ppl to help you find ppl ) - trainings 

=———

Design Page . Click “R-CAI” goes in -> 
Whats inside designs wise :

Log in / Sign up (this has to be very similar as other standard entry points )
Is basecally also a landing page , scroll down you will see    1) add/edit a web app to existing web app (custom side - scope a project )  2) like the package 888 for cloning the website , custom adds will be there -> 6 months support added , tool cost included , after 6 months starts at 15 with URL - no surprise costs
3. Clone website and add chatbot - 2499 -> yearly support included , tool cost included , after a year subscription starts 35 
4. SRCCAI - full AI development w source remote control your own custom AI model for you business  a) optimized web solution b) included Chat bot agent -> dev from how customers interact c) your own AI editable + dev ops tool for your website (standard /silver/gold) 
5. ARCCAI - AGENTIC REMOTE control for your Custom AI models - coming up 2027


Bersecally this is what we are selling .