Faaz commited on
Commit Β·
553fbf7
0
Parent(s):
feat: initial project scaffold for MINDI 1.5 Vision-Coder
Browse files- 21 directory structure (src, api, configs, scripts, data, etc.)
- 4 YAML configs (model, training, data, search)
- Core modules: model, vision encoder, agents, sandbox, search
- FastAPI server with auth middleware
- EnvLoader + ConfigLoader utilities with typed dataclasses
- Training pipeline, dataset loader, inference pipeline, evaluator
- Comprehensive validation script (validate_setup.py)
- .gitignore properly excludes .env, venv/, checkpoints/, etc.
This view is limited to 50 files because it contains too many changes. Β See raw diff
- .env.example +54 -0
- .gitattributes +1 -0
- .gitignore +0 -0
- README.md +41 -0
- api/__init__.py +1 -0
- api/main.py +48 -0
- api/middleware/__init__.py +1 -0
- api/middleware/auth.py +31 -0
- api/routes/__init__.py +1 -0
- api/routes/generate.py +46 -0
- api/routes/health.py +21 -0
- configs/data_config.yaml +62 -0
- configs/model_config.yaml +53 -0
- configs/search_config.yaml +45 -0
- configs/training_config.yaml +57 -0
- data/knowledge_base/.gitkeep +0 -0
- data/processed/.gitkeep +0 -0
- data/tokenizer/.gitkeep +0 -0
- docs/.gitkeep +0 -0
- frontend/.gitkeep +0 -0
- requirements.txt +0 -0
- scripts/health_check.py +123 -0
- scripts/train.py +40 -0
- scripts/validate_setup.py +228 -0
- scripts/verify_install.py +115 -0
- setup.py +28 -0
- src/__init__.py +9 -0
- src/agents/__init__.py +1 -0
- src/agents/error_fixer.py +56 -0
- src/agents/orchestrator.py +149 -0
- src/agents/ui_critic.py +73 -0
- src/evaluation/__init__.py +1 -0
- src/evaluation/evaluator.py +68 -0
- src/inference/__init__.py +1 -0
- src/inference/pipeline.py +79 -0
- src/model/__init__.py +1 -0
- src/model/code_model.py +121 -0
- src/model/vision_encoder.py +91 -0
- src/sandbox/__init__.py +1 -0
- src/sandbox/sandbox_runner.py +89 -0
- src/search/__init__.py +1 -0
- src/search/search_agent.py +103 -0
- src/tokenizer/__init__.py +1 -0
- src/tokenizer/tokenizer.py +75 -0
- src/training/__init__.py +1 -0
- src/training/dataset.py +84 -0
- src/training/trainer.py +75 -0
- src/utils/__init__.py +6 -0
- src/utils/config_loader.py +369 -0
- src/utils/env_loader.py +215 -0
.env.example
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# MINDI 1.5 Vision-Coder β Environment Variables
|
| 3 |
+
# ==========================================
|
| 4 |
+
# Copy this file to .env and fill in your keys
|
| 5 |
+
# NEVER commit .env to git!
|
| 6 |
+
|
| 7 |
+
# ββ Project Identity ββ
|
| 8 |
+
PROJECT_NAME=MINDI-1.5-Vision-Coder
|
| 9 |
+
STARTUP_NAME=MINDIGENOUS.AI
|
| 10 |
+
HF_USERNAME=Mindigenous
|
| 11 |
+
|
| 12 |
+
# ββ HuggingFace ββ
|
| 13 |
+
HUGGINGFACE_TOKEN=hf_your_token_here
|
| 14 |
+
HUGGINGFACE_REPO=Mindigenous/MINDI-1.5-Vision-Coder
|
| 15 |
+
|
| 16 |
+
# ββ Tavily (Web Search) ββ
|
| 17 |
+
TAVILY_API_KEY=tvly-your_key_here
|
| 18 |
+
MAX_SEARCH_RESULTS=5
|
| 19 |
+
SEARCH_TIMEOUT=30
|
| 20 |
+
|
| 21 |
+
# ββ Weights & Biases (Training Monitor) ββ
|
| 22 |
+
WANDB_API_KEY=your_wandb_key_here
|
| 23 |
+
WANDB_PROJECT=mindi-1.5-vision-coder
|
| 24 |
+
WANDB_ENTITY=mindigenous
|
| 25 |
+
|
| 26 |
+
# ββ E2B (Cloud Sandbox) ββ
|
| 27 |
+
E2B_API_KEY=e2b_your_key_here
|
| 28 |
+
SANDBOX_TYPE=e2b
|
| 29 |
+
|
| 30 |
+
# ββ Model Settings ββ
|
| 31 |
+
MODEL_NAME=deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct
|
| 32 |
+
BASE_MODEL_PATH=./checkpoints/base
|
| 33 |
+
FINETUNED_MODEL_PATH=./checkpoints/finetuned
|
| 34 |
+
|
| 35 |
+
# ββ API Settings ββ
|
| 36 |
+
API_HOST=0.0.0.0
|
| 37 |
+
API_PORT=8000
|
| 38 |
+
API_WORKERS=4
|
| 39 |
+
|
| 40 |
+
# ββ Training Settings ββ
|
| 41 |
+
DEVICE=cuda
|
| 42 |
+
MIXED_PRECISION=bf16
|
| 43 |
+
MAX_SEQ_LENGTH=8192
|
| 44 |
+
TRAINING_OUTPUT_DIR=./checkpoints
|
| 45 |
+
|
| 46 |
+
# ββ Directories ββ
|
| 47 |
+
LOG_DIR=./logs
|
| 48 |
+
DATA_DIR=./data
|
| 49 |
+
CHECKPOINT_DIR=./checkpoints
|
| 50 |
+
|
| 51 |
+
# ββ Cloud GPU (AMD MI300X on DigitalOcean) ββ
|
| 52 |
+
CLOUD_GPU_HOST=your_cloud_gpu_ip
|
| 53 |
+
CLOUD_GPU_USER=root
|
| 54 |
+
CLOUD_GPU_SSH_KEY=~/.ssh/id_rsa
|
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
data/tokenizer/*/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
Binary file (821 Bytes). View file
|
|
|
README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MINDI 1.5 Vision-Coder
|
| 2 |
+
|
| 3 |
+
**Built by [MINDIGENOUS.AI](https://mindigenous.ai)**
|
| 4 |
+
|
| 5 |
+
**Builder:** Faaz ([@Mindigenous](https://huggingface.co/Mindigenous) on HuggingFace)
|
| 6 |
+
|
| 7 |
+
**Started:** April 14, 2026
|
| 8 |
+
|
| 9 |
+
**Target Launch:** May 5, 2026
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## What is MINDI 1.5?
|
| 14 |
+
|
| 15 |
+
MINDI 1.5 Vision-Coder is a multimodal agentic AI coding model that:
|
| 16 |
+
|
| 17 |
+
- Generates production-ready Next.js 14 + Tailwind CSS + TypeScript code
|
| 18 |
+
- Sees its own output via vision capabilities (CLIP ViT-L/14)
|
| 19 |
+
- Critiques its own UI/UX design and iterates
|
| 20 |
+
- Searches the internet for latest packages and documentation
|
| 21 |
+
- Tests code in an isolated sandbox environment
|
| 22 |
+
- Fixes its own errors automatically
|
| 23 |
+
- Suggests improvements to the user
|
| 24 |
+
|
| 25 |
+
## Architecture
|
| 26 |
+
|
| 27 |
+
- **Base Model:** Open-source coding model (3B-7B parameters, Apache 2.0 / MIT)
|
| 28 |
+
- **Fine-tuning:** LoRA on AMD MI300X 192GB VRAM
|
| 29 |
+
- **Vision Encoder:** CLIP ViT-L/14
|
| 30 |
+
- **Agents:** Search + Sandbox + UI Critic + Code Generation
|
| 31 |
+
- **Training Data:** 500,000+ curated examples
|
| 32 |
+
- **Backend:** FastAPI
|
| 33 |
+
- **Output Format:** Next.js 14 + Tailwind CSS + TypeScript
|
| 34 |
+
|
| 35 |
+
## HuggingFace
|
| 36 |
+
|
| 37 |
+
Final model will be published at: `Mindigenous/MINDI-1.5-Vision-Coder`
|
| 38 |
+
|
| 39 |
+
## License
|
| 40 |
+
|
| 41 |
+
Apache 2.0
|
api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β FastAPI backend package."""
|
api/main.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β FastAPI Application
|
| 3 |
+
|
| 4 |
+
Main entry point for the MINDI API server.
|
| 5 |
+
Serves code generation, vision critique, and agent orchestration endpoints.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from fastapi import FastAPI
|
| 13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
+
|
| 15 |
+
from api.routes.generate import router as generate_router
|
| 16 |
+
from api.routes.health import router as health_router
|
| 17 |
+
|
| 18 |
+
app = FastAPI(
|
| 19 |
+
title="MINDI 1.5 Vision-Coder API",
|
| 20 |
+
description="Multimodal agentic AI code generator by MINDIGENOUS.AI",
|
| 21 |
+
version="1.5.0",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# CORS β allow the frontend to call the API
|
| 25 |
+
app.add_middleware(
|
| 26 |
+
CORSMiddleware,
|
| 27 |
+
allow_origins=["http://localhost:3000"], # Next.js dev server
|
| 28 |
+
allow_credentials=True,
|
| 29 |
+
allow_methods=["*"],
|
| 30 |
+
allow_headers=["*"],
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Register route modules
|
| 34 |
+
app.include_router(health_router, prefix="/api", tags=["Health"])
|
| 35 |
+
app.include_router(generate_router, prefix="/api", tags=["Generation"])
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@app.on_event("startup")
|
| 39 |
+
async def startup_event() -> None:
|
| 40 |
+
"""Load models and initialize agents on server start."""
|
| 41 |
+
# Models and agents will be initialized here in later phases
|
| 42 |
+
print("[MINDI API] Server starting up...")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@app.on_event("shutdown")
|
| 46 |
+
async def shutdown_event() -> None:
|
| 47 |
+
"""Cleanup on server shutdown."""
|
| 48 |
+
print("[MINDI API] Server shutting down...")
|
api/middleware/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β API middleware (auth, rate limiting, CORS)."""
|
api/middleware/auth.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Auth Middleware
|
| 3 |
+
|
| 4 |
+
API key validation for production deployment.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
from fastapi import HTTPException, Security
|
| 13 |
+
from fastapi.security import APIKeyHeader
|
| 14 |
+
|
| 15 |
+
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
async def verify_api_key(
|
| 19 |
+
api_key: Optional[str] = Security(API_KEY_HEADER),
|
| 20 |
+
) -> str:
|
| 21 |
+
"""Validate the API key from request headers."""
|
| 22 |
+
expected_key = os.environ.get("MINDI_API_KEY", "")
|
| 23 |
+
|
| 24 |
+
# In development, skip auth if no key is configured
|
| 25 |
+
if not expected_key:
|
| 26 |
+
return "dev-mode"
|
| 27 |
+
|
| 28 |
+
if not api_key or api_key != expected_key:
|
| 29 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 30 |
+
|
| 31 |
+
return api_key
|
api/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β API route handlers."""
|
api/routes/generate.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Code Generation Route
|
| 3 |
+
|
| 4 |
+
Accepts user prompts and returns generated Next.js + Tailwind + TypeScript code
|
| 5 |
+
via the agent orchestration pipeline.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, HTTPException
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
|
| 15 |
+
router = APIRouter()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class GenerateRequest(BaseModel):
|
| 19 |
+
"""Request body for code generation."""
|
| 20 |
+
prompt: str = Field(..., min_length=1, max_length=10000, description="User's code generation prompt")
|
| 21 |
+
temperature: float = Field(0.7, ge=0.0, le=2.0)
|
| 22 |
+
max_tokens: int = Field(4096, ge=1, le=8192)
|
| 23 |
+
use_search: bool = Field(True, description="Enable web search for context")
|
| 24 |
+
use_sandbox: bool = Field(True, description="Enable sandbox testing")
|
| 25 |
+
use_vision: bool = Field(True, description="Enable vision-based UI critique")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class GenerateResponse(BaseModel):
|
| 29 |
+
"""Response body for code generation."""
|
| 30 |
+
code: str
|
| 31 |
+
language: str = "typescript"
|
| 32 |
+
file_path: str = "page.tsx"
|
| 33 |
+
critique: Optional[str] = None
|
| 34 |
+
search_sources: list[str] = []
|
| 35 |
+
iterations: int = 1
|
| 36 |
+
success: bool = True
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@router.post("/generate", response_model=GenerateResponse)
|
| 40 |
+
async def generate_code(request: GenerateRequest) -> GenerateResponse:
|
| 41 |
+
"""Generate code from a user prompt using the MINDI agent pipeline."""
|
| 42 |
+
# Will be wired to AgentOrchestrator in later phases
|
| 43 |
+
raise HTTPException(
|
| 44 |
+
status_code=503,
|
| 45 |
+
detail="Model not loaded yet. Complete training pipeline first.",
|
| 46 |
+
)
|
api/routes/health.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Health Check Route
|
| 3 |
+
|
| 4 |
+
Simple health/readiness endpoint for monitoring.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from fastapi import APIRouter
|
| 10 |
+
|
| 11 |
+
router = APIRouter()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@router.get("/health")
|
| 15 |
+
async def health_check() -> dict[str, str]:
|
| 16 |
+
"""Return server health status."""
|
| 17 |
+
return {
|
| 18 |
+
"status": "healthy",
|
| 19 |
+
"model": "MINDI-1.5-Vision-Coder",
|
| 20 |
+
"version": "1.5.0",
|
| 21 |
+
}
|
configs/data_config.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# MINDI 1.5 Vision-Coder β Data Configuration
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
dataset:
|
| 6 |
+
name: "mindi-1.5-training-data"
|
| 7 |
+
target_size: 500000
|
| 8 |
+
format: "jsonl"
|
| 9 |
+
|
| 10 |
+
# Data sources for fine-tuning
|
| 11 |
+
sources:
|
| 12 |
+
- name: "code_generation"
|
| 13 |
+
description: "Prompt β Next.js + Tailwind + TypeScript code pairs"
|
| 14 |
+
path: "./data/raw/code_generation/"
|
| 15 |
+
weight: 0.40
|
| 16 |
+
|
| 17 |
+
- name: "ui_critique"
|
| 18 |
+
description: "Screenshot + code β critique + improved code pairs"
|
| 19 |
+
path: "./data/raw/ui_critique/"
|
| 20 |
+
weight: 0.20
|
| 21 |
+
|
| 22 |
+
- name: "error_correction"
|
| 23 |
+
description: "Broken code β fixed code pairs with explanations"
|
| 24 |
+
path: "./data/raw/error_correction/"
|
| 25 |
+
weight: 0.15
|
| 26 |
+
|
| 27 |
+
- name: "documentation_qa"
|
| 28 |
+
description: "Documentation context β code answer pairs"
|
| 29 |
+
path: "./data/raw/documentation_qa/"
|
| 30 |
+
weight: 0.10
|
| 31 |
+
|
| 32 |
+
- name: "multi_turn"
|
| 33 |
+
description: "Multi-turn conversation with iterative refinement"
|
| 34 |
+
path: "./data/raw/multi_turn/"
|
| 35 |
+
weight: 0.15
|
| 36 |
+
|
| 37 |
+
# Processing
|
| 38 |
+
processing:
|
| 39 |
+
tokenizer: "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
|
| 40 |
+
max_length: 8192
|
| 41 |
+
min_length: 64
|
| 42 |
+
dedup_strategy: "minhash"
|
| 43 |
+
quality_filter: true
|
| 44 |
+
output_dir: "./data/processed/"
|
| 45 |
+
|
| 46 |
+
# Train / validation split
|
| 47 |
+
splits:
|
| 48 |
+
train: 0.95
|
| 49 |
+
validation: 0.05
|
| 50 |
+
|
| 51 |
+
# Knowledge base for RAG
|
| 52 |
+
knowledge_base:
|
| 53 |
+
path: "./data/knowledge_base/"
|
| 54 |
+
sources:
|
| 55 |
+
- "nextjs-14-docs"
|
| 56 |
+
- "tailwindcss-docs"
|
| 57 |
+
- "typescript-docs"
|
| 58 |
+
- "react-docs"
|
| 59 |
+
- "shadcn-ui-docs"
|
| 60 |
+
embedding_model: "BAAI/bge-small-en-v1.5"
|
| 61 |
+
chunk_size: 512
|
| 62 |
+
chunk_overlap: 64
|
configs/model_config.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# MINDI 1.5 Vision-Coder β Model Configuration
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
model:
|
| 6 |
+
name: "MINDI-1.5-Vision-Coder"
|
| 7 |
+
version: "1.5.0"
|
| 8 |
+
|
| 9 |
+
# Base coding model (Apache 2.0 licensed)
|
| 10 |
+
base:
|
| 11 |
+
name: "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
|
| 12 |
+
parameters: "16B"
|
| 13 |
+
license: "Apache-2.0"
|
| 14 |
+
context_length: 8192
|
| 15 |
+
dtype: "bfloat16"
|
| 16 |
+
|
| 17 |
+
# Vision encoder for UI screenshot understanding
|
| 18 |
+
vision:
|
| 19 |
+
name: "openai/clip-vit-large-patch14"
|
| 20 |
+
image_size: 224
|
| 21 |
+
patch_size: 14
|
| 22 |
+
hidden_size: 1024
|
| 23 |
+
projection_dim: 768
|
| 24 |
+
freeze_backbone: true
|
| 25 |
+
trainable_projection: true
|
| 26 |
+
|
| 27 |
+
# LoRA fine-tuning configuration
|
| 28 |
+
lora:
|
| 29 |
+
rank: 64
|
| 30 |
+
alpha: 128
|
| 31 |
+
dropout: 0.05
|
| 32 |
+
target_modules:
|
| 33 |
+
- "q_proj"
|
| 34 |
+
- "k_proj"
|
| 35 |
+
- "v_proj"
|
| 36 |
+
- "o_proj"
|
| 37 |
+
- "gate_proj"
|
| 38 |
+
- "up_proj"
|
| 39 |
+
- "down_proj"
|
| 40 |
+
bias: "none"
|
| 41 |
+
task_type: "CAUSAL_LM"
|
| 42 |
+
|
| 43 |
+
# Output format
|
| 44 |
+
output:
|
| 45 |
+
framework: "nextjs-14"
|
| 46 |
+
styling: "tailwindcss"
|
| 47 |
+
language: "typescript"
|
| 48 |
+
template_format: "markdown-codeblock"
|
| 49 |
+
|
| 50 |
+
huggingface:
|
| 51 |
+
repo_id: "Mindigenous/MINDI-1.5-Vision-Coder"
|
| 52 |
+
private: false
|
| 53 |
+
license: "apache-2.0"
|
configs/search_config.yaml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# MINDI 1.5 Vision-Coder β Search Agent Configuration
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
search:
|
| 6 |
+
# Primary search provider
|
| 7 |
+
provider: "tavily"
|
| 8 |
+
api_key_env: "TAVILY_API_KEY"
|
| 9 |
+
|
| 10 |
+
# Search behavior
|
| 11 |
+
max_results: 5
|
| 12 |
+
search_depth: "advanced"
|
| 13 |
+
include_domains:
|
| 14 |
+
- "nextjs.org"
|
| 15 |
+
- "tailwindcss.com"
|
| 16 |
+
- "typescriptlang.org"
|
| 17 |
+
- "react.dev"
|
| 18 |
+
- "ui.shadcn.com"
|
| 19 |
+
- "developer.mozilla.org"
|
| 20 |
+
- "npmjs.com"
|
| 21 |
+
- "github.com"
|
| 22 |
+
exclude_domains:
|
| 23 |
+
- "w3schools.com"
|
| 24 |
+
- "geeksforgeeks.org"
|
| 25 |
+
|
| 26 |
+
# Rate limiting
|
| 27 |
+
rate_limit:
|
| 28 |
+
requests_per_minute: 30
|
| 29 |
+
retry_attempts: 3
|
| 30 |
+
retry_delay_seconds: 2
|
| 31 |
+
|
| 32 |
+
# Caching
|
| 33 |
+
cache:
|
| 34 |
+
enabled: true
|
| 35 |
+
ttl_hours: 24
|
| 36 |
+
max_entries: 10000
|
| 37 |
+
storage_path: "./data/knowledge_base/search_cache.db"
|
| 38 |
+
|
| 39 |
+
# Documentation scraping
|
| 40 |
+
docs_scraper:
|
| 41 |
+
enabled: true
|
| 42 |
+
output_dir: "./docs/"
|
| 43 |
+
max_pages_per_site: 100
|
| 44 |
+
respect_robots_txt: true
|
| 45 |
+
request_delay_seconds: 1
|
configs/training_config.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# MINDI 1.5 Vision-Coder β Training Configuration
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
training:
|
| 6 |
+
# Hardware targets
|
| 7 |
+
local_device: "cuda" # RTX 4060 8GB β for dev/testing only
|
| 8 |
+
cloud_device: "cuda" # MI300X 192GB β for actual training
|
| 9 |
+
precision: "bf16"
|
| 10 |
+
|
| 11 |
+
# Hyperparameters
|
| 12 |
+
epochs: 3
|
| 13 |
+
batch_size: 4
|
| 14 |
+
gradient_accumulation_steps: 8
|
| 15 |
+
effective_batch_size: 32 # batch_size * grad_accum
|
| 16 |
+
learning_rate: 2.0e-4
|
| 17 |
+
weight_decay: 0.01
|
| 18 |
+
warmup_ratio: 0.03
|
| 19 |
+
lr_scheduler: "cosine"
|
| 20 |
+
max_grad_norm: 1.0
|
| 21 |
+
|
| 22 |
+
# Sequence settings
|
| 23 |
+
max_seq_length: 8192
|
| 24 |
+
packing: true # Pack short examples together
|
| 25 |
+
|
| 26 |
+
# Checkpointing
|
| 27 |
+
save_strategy: "steps"
|
| 28 |
+
save_steps: 500
|
| 29 |
+
save_total_limit: 5
|
| 30 |
+
checkpoint_dir: "./checkpoints"
|
| 31 |
+
resume_from_checkpoint: null
|
| 32 |
+
|
| 33 |
+
# Logging
|
| 34 |
+
logging_steps: 10
|
| 35 |
+
log_dir: "./logs/training"
|
| 36 |
+
report_to: "wandb"
|
| 37 |
+
|
| 38 |
+
# Evaluation
|
| 39 |
+
eval_strategy: "steps"
|
| 40 |
+
eval_steps: 250
|
| 41 |
+
eval_samples: 1000
|
| 42 |
+
|
| 43 |
+
# Memory optimization (for RTX 4060 local testing)
|
| 44 |
+
local_overrides:
|
| 45 |
+
batch_size: 1
|
| 46 |
+
gradient_accumulation_steps: 16
|
| 47 |
+
max_seq_length: 2048
|
| 48 |
+
gradient_checkpointing: true
|
| 49 |
+
optim: "adamw_8bit"
|
| 50 |
+
|
| 51 |
+
wandb:
|
| 52 |
+
project: "mindi-1.5-vision-coder"
|
| 53 |
+
entity: "mindigenous"
|
| 54 |
+
tags:
|
| 55 |
+
- "mindi-1.5"
|
| 56 |
+
- "lora"
|
| 57 |
+
- "vision-coder"
|
data/knowledge_base/.gitkeep
ADDED
|
File without changes
|
data/processed/.gitkeep
ADDED
|
File without changes
|
data/tokenizer/.gitkeep
ADDED
|
File without changes
|
docs/.gitkeep
ADDED
|
File without changes
|
frontend/.gitkeep
ADDED
|
File without changes
|
requirements.txt
ADDED
|
Binary file (4.88 kB). View file
|
|
|
scripts/health_check.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β System Health Check Script
|
| 3 |
+
|
| 4 |
+
Verifies that all dependencies, configs, and environment variables
|
| 5 |
+
are correctly set up before starting development or training.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def check_python() -> bool:
|
| 16 |
+
"""Verify Python version."""
|
| 17 |
+
v = sys.version_info
|
| 18 |
+
ok = v.major == 3 and v.minor >= 10
|
| 19 |
+
status = "OK" if ok else "FAIL"
|
| 20 |
+
print(f" [{status}] Python {v.major}.{v.minor}.{v.micro}")
|
| 21 |
+
return ok
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def check_env_vars() -> bool:
|
| 25 |
+
"""Check that required environment variables are set."""
|
| 26 |
+
required = ["HUGGINGFACE_TOKEN", "TAVILY_API_KEY", "WANDB_API_KEY", "E2B_API_KEY"]
|
| 27 |
+
all_ok = True
|
| 28 |
+
for var in required:
|
| 29 |
+
value = os.environ.get(var, "")
|
| 30 |
+
if value:
|
| 31 |
+
print(f" [OK] {var} = {value[:8]}...")
|
| 32 |
+
else:
|
| 33 |
+
print(f" [MISSING] {var}")
|
| 34 |
+
all_ok = False
|
| 35 |
+
return all_ok
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def check_directories() -> bool:
|
| 39 |
+
"""Verify project directory structure exists."""
|
| 40 |
+
project_root = Path(__file__).resolve().parent.parent
|
| 41 |
+
required_dirs = [
|
| 42 |
+
"configs", "data/raw", "data/processed", "data/tokenizer",
|
| 43 |
+
"data/knowledge_base", "src/model", "src/agents", "src/search",
|
| 44 |
+
"src/sandbox", "src/training", "src/inference", "src/evaluation",
|
| 45 |
+
"api/routes", "api/middleware", "scripts", "tests",
|
| 46 |
+
"checkpoints", "logs", "docs",
|
| 47 |
+
]
|
| 48 |
+
all_ok = True
|
| 49 |
+
for d in required_dirs:
|
| 50 |
+
path = project_root / d
|
| 51 |
+
if path.exists():
|
| 52 |
+
print(f" [OK] {d}/")
|
| 53 |
+
else:
|
| 54 |
+
print(f" [MISSING] {d}/")
|
| 55 |
+
all_ok = False
|
| 56 |
+
return all_ok
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def check_configs() -> bool:
|
| 60 |
+
"""Verify config files exist."""
|
| 61 |
+
project_root = Path(__file__).resolve().parent.parent
|
| 62 |
+
configs = [
|
| 63 |
+
"configs/model_config.yaml",
|
| 64 |
+
"configs/training_config.yaml",
|
| 65 |
+
"configs/data_config.yaml",
|
| 66 |
+
"configs/search_config.yaml",
|
| 67 |
+
]
|
| 68 |
+
all_ok = True
|
| 69 |
+
for c in configs:
|
| 70 |
+
path = project_root / c
|
| 71 |
+
if path.exists():
|
| 72 |
+
print(f" [OK] {c}")
|
| 73 |
+
else:
|
| 74 |
+
print(f" [MISSING] {c}")
|
| 75 |
+
all_ok = False
|
| 76 |
+
return all_ok
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def check_gpu() -> bool:
|
| 80 |
+
"""Check CUDA GPU availability."""
|
| 81 |
+
try:
|
| 82 |
+
import torch
|
| 83 |
+
if torch.cuda.is_available():
|
| 84 |
+
name = torch.cuda.get_device_name(0)
|
| 85 |
+
vram = torch.cuda.get_device_properties(0).total_mem / (1024**3)
|
| 86 |
+
print(f" [OK] GPU: {name} ({vram:.1f} GB VRAM)")
|
| 87 |
+
return True
|
| 88 |
+
else:
|
| 89 |
+
print(" [WARN] No CUDA GPU detected (CPU mode)")
|
| 90 |
+
return False
|
| 91 |
+
except ImportError:
|
| 92 |
+
print(" [WARN] PyTorch not installed yet")
|
| 93 |
+
return False
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> None:
|
| 97 |
+
"""Run all health checks."""
|
| 98 |
+
print("=" * 55)
|
| 99 |
+
print(" MINDI 1.5 Vision-Coder β System Health Check")
|
| 100 |
+
print("=" * 55)
|
| 101 |
+
|
| 102 |
+
print("\n[1] Python Version:")
|
| 103 |
+
check_python()
|
| 104 |
+
|
| 105 |
+
print("\n[2] GPU:")
|
| 106 |
+
check_gpu()
|
| 107 |
+
|
| 108 |
+
print("\n[3] Environment Variables:")
|
| 109 |
+
check_env_vars()
|
| 110 |
+
|
| 111 |
+
print("\n[4] Directory Structure:")
|
| 112 |
+
check_directories()
|
| 113 |
+
|
| 114 |
+
print("\n[5] Config Files:")
|
| 115 |
+
check_configs()
|
| 116 |
+
|
| 117 |
+
print("\n" + "=" * 55)
|
| 118 |
+
print(" Health check complete.")
|
| 119 |
+
print("=" * 55)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Training Launch Script
|
| 3 |
+
|
| 4 |
+
Entry point for starting LoRA fine-tuning.
|
| 5 |
+
Loads config, initializes model + dataset, and runs training.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
"""Parse args and launch training."""
|
| 16 |
+
parser = argparse.ArgumentParser(description="MINDI 1.5 β Launch LoRA Training")
|
| 17 |
+
parser.add_argument(
|
| 18 |
+
"--config", type=str, default="./configs/training_config.yaml",
|
| 19 |
+
help="Path to training config YAML",
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--local", action="store_true", default=True,
|
| 23 |
+
help="Use local GPU overrides (RTX 4060 mode)",
|
| 24 |
+
)
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--cloud", action="store_true",
|
| 27 |
+
help="Use cloud GPU settings (MI300X mode)",
|
| 28 |
+
)
|
| 29 |
+
args = parser.parse_args()
|
| 30 |
+
|
| 31 |
+
local_mode = not args.cloud
|
| 32 |
+
config_path = Path(args.config)
|
| 33 |
+
|
| 34 |
+
print(f"[MINDI Training] Config: {config_path}")
|
| 35 |
+
print(f"[MINDI Training] Mode: {'local (RTX 4060)' if local_mode else 'cloud (MI300X)'}")
|
| 36 |
+
print("[MINDI Training] Pipeline will be wired after Phase 3 setup.")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
main()
|
scripts/validate_setup.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Setup Validation Script
|
| 3 |
+
|
| 4 |
+
Comprehensive readiness check: environment, configs, directories,
|
| 5 |
+
API keys, GPU, and package imports.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import importlib
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
# Ensure project root is on sys.path
|
| 16 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def header(title: str) -> None:
|
| 21 |
+
print(f"\n{'='*50}")
|
| 22 |
+
print(f" {title}")
|
| 23 |
+
print(f"{'='*50}")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def check(label: str, passed: bool, detail: str = "") -> bool:
|
| 27 |
+
icon = "β
" if passed else "β"
|
| 28 |
+
msg = f" {icon} {label}"
|
| 29 |
+
if detail:
|
| 30 |
+
msg += f" β {detail}"
|
| 31 |
+
print(msg)
|
| 32 |
+
return passed
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def validate_directories() -> int:
|
| 36 |
+
header("1. Directory Structure")
|
| 37 |
+
required_dirs = [
|
| 38 |
+
"configs", "src", "src/model", "src/agents", "src/search",
|
| 39 |
+
"src/sandbox", "src/training", "src/inference", "src/evaluation",
|
| 40 |
+
"src/tokenizer", "src/utils", "api", "api/routes", "api/middleware",
|
| 41 |
+
"scripts", "data", "data/raw", "data/processed", "data/knowledge_base",
|
| 42 |
+
"checkpoints", "logs", "tests", "docs", "frontend",
|
| 43 |
+
]
|
| 44 |
+
failures = 0
|
| 45 |
+
for d in required_dirs:
|
| 46 |
+
path = PROJECT_ROOT / d
|
| 47 |
+
if not check(d, path.is_dir()):
|
| 48 |
+
failures += 1
|
| 49 |
+
return failures
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def validate_files() -> int:
|
| 53 |
+
header("2. Key Files")
|
| 54 |
+
required_files = [
|
| 55 |
+
".env", ".env.example", ".gitignore", "README.md",
|
| 56 |
+
"requirements.txt", "setup.py",
|
| 57 |
+
"configs/model_config.yaml", "configs/training_config.yaml",
|
| 58 |
+
"configs/data_config.yaml", "configs/search_config.yaml",
|
| 59 |
+
"src/__init__.py", "src/utils/__init__.py",
|
| 60 |
+
"src/utils/env_loader.py", "src/utils/config_loader.py",
|
| 61 |
+
"src/model/vision_encoder.py", "src/model/code_model.py",
|
| 62 |
+
"src/agents/orchestrator.py", "src/agents/ui_critic.py",
|
| 63 |
+
"src/agents/error_fixer.py", "src/search/search_agent.py",
|
| 64 |
+
"src/sandbox/sandbox_runner.py", "src/training/trainer.py",
|
| 65 |
+
"src/training/dataset.py", "src/inference/pipeline.py",
|
| 66 |
+
"src/evaluation/evaluator.py",
|
| 67 |
+
"api/main.py", "api/routes/generate.py", "api/middleware/auth.py",
|
| 68 |
+
"scripts/health_check.py", "scripts/train.py",
|
| 69 |
+
]
|
| 70 |
+
failures = 0
|
| 71 |
+
for f in required_files:
|
| 72 |
+
path = PROJECT_ROOT / f
|
| 73 |
+
if not check(f, path.is_file()):
|
| 74 |
+
failures += 1
|
| 75 |
+
return failures
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def validate_env() -> int:
|
| 79 |
+
header("3. Environment Variables")
|
| 80 |
+
from src.utils.env_loader import EnvLoader
|
| 81 |
+
|
| 82 |
+
env = EnvLoader()
|
| 83 |
+
env.load()
|
| 84 |
+
result = env.validate()
|
| 85 |
+
|
| 86 |
+
failures = 0
|
| 87 |
+
required = ["HUGGINGFACE_TOKEN", "TAVILY_API_KEY", "WANDB_API_KEY", "E2B_API_KEY"]
|
| 88 |
+
for key in required:
|
| 89 |
+
value = os.environ.get(key, "")
|
| 90 |
+
if value:
|
| 91 |
+
masked = value[:8] + "..." + value[-4:]
|
| 92 |
+
check(key, True, masked)
|
| 93 |
+
else:
|
| 94 |
+
check(key, False, "NOT SET")
|
| 95 |
+
failures += 1
|
| 96 |
+
|
| 97 |
+
for w in result.warnings:
|
| 98 |
+
print(f" β οΈ {w}")
|
| 99 |
+
|
| 100 |
+
return failures
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def validate_configs() -> int:
|
| 104 |
+
header("4. YAML Configurations")
|
| 105 |
+
from src.utils.config_loader import ConfigLoader
|
| 106 |
+
|
| 107 |
+
loader = ConfigLoader()
|
| 108 |
+
failures = 0
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
m = loader.model
|
| 112 |
+
check("model_config.yaml", True, f"{m.name} v{m.version}")
|
| 113 |
+
except Exception as e:
|
| 114 |
+
check("model_config.yaml", False, str(e))
|
| 115 |
+
failures += 1
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
t = loader.training
|
| 119 |
+
check("training_config.yaml", True, f"{t.epochs} epochs, lr={t.learning_rate}")
|
| 120 |
+
except Exception as e:
|
| 121 |
+
check("training_config.yaml", False, str(e))
|
| 122 |
+
failures += 1
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
d = loader.data
|
| 126 |
+
check("data_config.yaml", True, f"{d.target_size:,} target samples")
|
| 127 |
+
except Exception as e:
|
| 128 |
+
check("data_config.yaml", False, str(e))
|
| 129 |
+
failures += 1
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
s = loader.search
|
| 133 |
+
check("search_config.yaml", True, f"provider={s.provider}")
|
| 134 |
+
except Exception as e:
|
| 135 |
+
check("search_config.yaml", False, str(e))
|
| 136 |
+
failures += 1
|
| 137 |
+
|
| 138 |
+
return failures
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def validate_packages() -> int:
|
| 142 |
+
header("5. Critical Package Imports")
|
| 143 |
+
packages = [
|
| 144 |
+
("torch", "PyTorch"),
|
| 145 |
+
("transformers", "HuggingFace Transformers"),
|
| 146 |
+
("peft", "PEFT (LoRA)"),
|
| 147 |
+
("datasets", "HuggingFace Datasets"),
|
| 148 |
+
("wandb", "Weights & Biases"),
|
| 149 |
+
("fastapi", "FastAPI"),
|
| 150 |
+
("httpx", "HTTPX"),
|
| 151 |
+
("PIL", "Pillow"),
|
| 152 |
+
("yaml", "PyYAML"),
|
| 153 |
+
("dotenv", "python-dotenv"),
|
| 154 |
+
("pydantic", "Pydantic"),
|
| 155 |
+
("playwright", "Playwright"),
|
| 156 |
+
]
|
| 157 |
+
failures = 0
|
| 158 |
+
for module, label in packages:
|
| 159 |
+
try:
|
| 160 |
+
importlib.import_module(module)
|
| 161 |
+
check(label, True)
|
| 162 |
+
except ImportError:
|
| 163 |
+
check(label, False, "not installed")
|
| 164 |
+
failures += 1
|
| 165 |
+
return failures
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def validate_gpu() -> int:
|
| 169 |
+
header("6. GPU / CUDA")
|
| 170 |
+
failures = 0
|
| 171 |
+
try:
|
| 172 |
+
import torch
|
| 173 |
+
cuda_available = torch.cuda.is_available()
|
| 174 |
+
check("CUDA available", cuda_available)
|
| 175 |
+
if cuda_available:
|
| 176 |
+
gpu_name = torch.cuda.get_device_name(0)
|
| 177 |
+
vram = torch.cuda.get_device_properties(0).total_memory / (1024**3)
|
| 178 |
+
check("GPU", True, f"{gpu_name} ({vram:.1f} GB)")
|
| 179 |
+
check("PyTorch CUDA version", True, torch.version.cuda or "N/A")
|
| 180 |
+
else:
|
| 181 |
+
failures += 1
|
| 182 |
+
except Exception as e:
|
| 183 |
+
check("GPU check", False, str(e))
|
| 184 |
+
failures += 1
|
| 185 |
+
return failures
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def validate_gitignore() -> int:
|
| 189 |
+
header("7. Security Check")
|
| 190 |
+
gitignore = PROJECT_ROOT / ".gitignore"
|
| 191 |
+
failures = 0
|
| 192 |
+
if gitignore.is_file():
|
| 193 |
+
content = gitignore.read_text(encoding="utf-8")
|
| 194 |
+
check(".env in .gitignore", ".env" in content)
|
| 195 |
+
check("venv/ in .gitignore", "venv" in content)
|
| 196 |
+
if ".env" not in content:
|
| 197 |
+
failures += 1
|
| 198 |
+
else:
|
| 199 |
+
check(".gitignore exists", False)
|
| 200 |
+
failures += 1
|
| 201 |
+
return failures
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def main() -> None:
|
| 205 |
+
print("\nββββββββββββββββββββββββββββββββββββββββββββββββββββ")
|
| 206 |
+
print("β MINDI 1.5 Vision-Coder β Full Setup Validation β")
|
| 207 |
+
print("ββββββββββββββββββββββββββββββββββββββββββββββββββββ")
|
| 208 |
+
|
| 209 |
+
total_failures = 0
|
| 210 |
+
total_failures += validate_directories()
|
| 211 |
+
total_failures += validate_files()
|
| 212 |
+
total_failures += validate_env()
|
| 213 |
+
total_failures += validate_configs()
|
| 214 |
+
total_failures += validate_packages()
|
| 215 |
+
total_failures += validate_gpu()
|
| 216 |
+
total_failures += validate_gitignore()
|
| 217 |
+
|
| 218 |
+
header("RESULT")
|
| 219 |
+
if total_failures == 0:
|
| 220 |
+
print(" β
ALL CHECKS PASSED β MINDI 1.5 is ready!")
|
| 221 |
+
else:
|
| 222 |
+
print(f" β {total_failures} check(s) failed β review above")
|
| 223 |
+
|
| 224 |
+
sys.exit(0 if total_failures == 0 else 1)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
if __name__ == "__main__":
|
| 228 |
+
main()
|
scripts/verify_install.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Installation Verification Script
|
| 3 |
+
|
| 4 |
+
Checks that every required package is importable and reports
|
| 5 |
+
versions + GPU status. Run after Phase 3 setup.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
from importlib.metadata import version as pkg_version
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def check(package_name: str, import_name: str | None = None) -> bool:
|
| 15 |
+
"""Try to import a package and report status."""
|
| 16 |
+
mod = import_name or package_name
|
| 17 |
+
try:
|
| 18 |
+
__import__(mod)
|
| 19 |
+
v = pkg_version(package_name)
|
| 20 |
+
print(f" \u2705 {package_name} {v}")
|
| 21 |
+
return True
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f" \u274c {package_name} β FAILED β {e}")
|
| 24 |
+
return False
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def check_cuda() -> bool:
|
| 28 |
+
"""Verify PyTorch CUDA availability."""
|
| 29 |
+
try:
|
| 30 |
+
import torch
|
| 31 |
+
v = torch.__version__
|
| 32 |
+
if torch.cuda.is_available():
|
| 33 |
+
gpu = torch.cuda.get_device_name(0)
|
| 34 |
+
vram = round(torch.cuda.get_device_properties(0).total_memory / 1e9, 2)
|
| 35 |
+
print(f" \u2705 torch {v} β CUDA available β {gpu} ({vram} GB)")
|
| 36 |
+
return True
|
| 37 |
+
else:
|
| 38 |
+
print(f" \u26a0\ufe0f torch {v} β NO CUDA (CPU only)")
|
| 39 |
+
return False
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f" \u274c torch β FAILED β {e}")
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main() -> None:
|
| 46 |
+
print("=" * 60)
|
| 47 |
+
print(" MINDI 1.5 Vision-Coder β Package Verification")
|
| 48 |
+
print("=" * 60)
|
| 49 |
+
print(f"\n Python: {sys.version}")
|
| 50 |
+
print(f" Executable: {sys.executable}\n")
|
| 51 |
+
|
| 52 |
+
results: list[bool] = []
|
| 53 |
+
|
| 54 |
+
print("[PyTorch + CUDA]")
|
| 55 |
+
results.append(check_cuda())
|
| 56 |
+
check("torchvision")
|
| 57 |
+
check("torchaudio")
|
| 58 |
+
|
| 59 |
+
print("\n[Group A β Core Transformers]")
|
| 60 |
+
for pkg in ["transformers", "datasets", "tokenizers", "accelerate", "peft", "huggingface-hub"]:
|
| 61 |
+
imp = pkg.replace("-", "_")
|
| 62 |
+
results.append(check(pkg, imp))
|
| 63 |
+
|
| 64 |
+
print("\n[Group B β Vision]")
|
| 65 |
+
results.append(check("pillow", "PIL"))
|
| 66 |
+
results.append(check("opencv-python", "cv2"))
|
| 67 |
+
results.append(check("open-clip-torch", "open_clip"))
|
| 68 |
+
|
| 69 |
+
print("\n[Group C β Search]")
|
| 70 |
+
for pkg, imp in [("tavily-python", "tavily"), ("duckduckgo-search", "duckduckgo_search"),
|
| 71 |
+
("beautifulsoup4", "bs4"), ("playwright", "playwright"),
|
| 72 |
+
("requests", "requests"), ("httpx", "httpx"), ("lxml", "lxml")]:
|
| 73 |
+
results.append(check(pkg, imp))
|
| 74 |
+
|
| 75 |
+
print("\n[Group D β Sandbox]")
|
| 76 |
+
results.append(check("e2b"))
|
| 77 |
+
results.append(check("docker"))
|
| 78 |
+
|
| 79 |
+
print("\n[Group E β Web Framework]")
|
| 80 |
+
for pkg, imp in [("fastapi", "fastapi"), ("uvicorn", "uvicorn"), ("websockets", "websockets"),
|
| 81 |
+
("python-multipart", "multipart"), ("python-jose", "jose"), ("passlib", "passlib")]:
|
| 82 |
+
results.append(check(pkg, imp))
|
| 83 |
+
|
| 84 |
+
print("\n[Group F β Training Utilities]")
|
| 85 |
+
for pkg, imp in [("wandb", "wandb"), ("bitsandbytes", "bitsandbytes"), ("scipy", "scipy"),
|
| 86 |
+
("scikit-learn", "sklearn"), ("einops", "einops")]:
|
| 87 |
+
results.append(check(pkg, imp))
|
| 88 |
+
|
| 89 |
+
print("\n[Group G β Vector Store / RAG]")
|
| 90 |
+
results.append(check("faiss-cpu", "faiss"))
|
| 91 |
+
results.append(check("sentence-transformers", "sentence_transformers"))
|
| 92 |
+
|
| 93 |
+
print("\n[Group H β Utilities]")
|
| 94 |
+
for pkg, imp in [("rich", "rich"), ("tqdm", "tqdm"), ("python-dotenv", "dotenv"),
|
| 95 |
+
("pyyaml", "yaml"), ("numpy", "numpy"), ("pandas", "pandas"),
|
| 96 |
+
("matplotlib", "matplotlib")]:
|
| 97 |
+
results.append(check(pkg, imp))
|
| 98 |
+
|
| 99 |
+
print("\n[Group I β Code Quality]")
|
| 100 |
+
for pkg in ["black", "isort", "mypy"]:
|
| 101 |
+
results.append(check(pkg))
|
| 102 |
+
|
| 103 |
+
# Summary
|
| 104 |
+
passed = sum(results)
|
| 105 |
+
total = len(results)
|
| 106 |
+
print("\n" + "=" * 60)
|
| 107 |
+
if passed == total:
|
| 108 |
+
print(f" \u2705 ALL {total} PACKAGES VERIFIED β READY TO BUILD!")
|
| 109 |
+
else:
|
| 110 |
+
print(f" \u26a0\ufe0f {passed}/{total} passed β {total - passed} need fixing")
|
| 111 |
+
print("=" * 60)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|
setup.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Package Setup
|
| 3 |
+
|
| 4 |
+
Allows installing the project as a Python package:
|
| 5 |
+
pip install -e .
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from setuptools import setup, find_packages
|
| 9 |
+
|
| 10 |
+
setup(
|
| 11 |
+
name="mindi-vision-coder",
|
| 12 |
+
version="1.5.0",
|
| 13 |
+
author="Faaz",
|
| 14 |
+
author_email="faaz@mindigenous.ai",
|
| 15 |
+
description="Multimodal agentic AI code generator by MINDIGENOUS.AI",
|
| 16 |
+
long_description=open("README.md", encoding="utf-8").read(),
|
| 17 |
+
long_description_content_type="text/markdown",
|
| 18 |
+
url="https://huggingface.co/Mindigenous/MINDI-1.5-Vision-Coder",
|
| 19 |
+
packages=find_packages(),
|
| 20 |
+
python_requires=">=3.10",
|
| 21 |
+
classifiers=[
|
| 22 |
+
"Development Status :: 3 - Alpha",
|
| 23 |
+
"Intended Audience :: Developers",
|
| 24 |
+
"License :: OSI Approved :: Apache Software License",
|
| 25 |
+
"Programming Language :: Python :: 3.11",
|
| 26 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 27 |
+
],
|
| 28 |
+
)
|
src/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Source Package
|
| 3 |
+
|
| 4 |
+
Multimodal agentic AI coding model by MINDIGENOUS.AI
|
| 5 |
+
Generates Next.js 14 + Tailwind CSS + TypeScript code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
__version__ = "1.5.0"
|
| 9 |
+
__author__ = "Faaz @ MINDIGENOUS.AI"
|
src/agents/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Agent system: orchestrator, UI critic, code generator."""
|
src/agents/error_fixer.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Error Fixer Agent
|
| 3 |
+
|
| 4 |
+
Automatically diagnoses and fixes errors from sandbox execution,
|
| 5 |
+
lint failures, and type errors in generated code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class ErrorDiagnosis:
|
| 17 |
+
"""Structured error information for the fixer agent."""
|
| 18 |
+
error_type: str # "runtime", "compile", "lint", "type"
|
| 19 |
+
message: str # Raw error message
|
| 20 |
+
file_path: Optional[str] = None
|
| 21 |
+
line_number: Optional[int] = None
|
| 22 |
+
suggested_fix: Optional[str] = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class FixResult:
|
| 27 |
+
"""Output from an error fix attempt."""
|
| 28 |
+
original_code: str
|
| 29 |
+
fixed_code: str
|
| 30 |
+
errors_found: list[ErrorDiagnosis] = field(default_factory=list)
|
| 31 |
+
errors_fixed: int = 0
|
| 32 |
+
success: bool = False
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ErrorFixer:
|
| 36 |
+
"""Agent that diagnoses and fixes code errors automatically."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, log_dir: Optional[Path] = None) -> None:
|
| 39 |
+
self.log_dir = log_dir or Path("./logs/error_fixer")
|
| 40 |
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
|
| 42 |
+
async def diagnose(self, code: str, error_output: str) -> list[ErrorDiagnosis]:
|
| 43 |
+
"""Parse error output and classify errors."""
|
| 44 |
+
# Will be implemented with LLM-based error parsing
|
| 45 |
+
return []
|
| 46 |
+
|
| 47 |
+
async def fix(self, code: str, errors: list[ErrorDiagnosis]) -> FixResult:
|
| 48 |
+
"""Attempt to fix all diagnosed errors in the code."""
|
| 49 |
+
# Will be implemented with the fine-tuned model
|
| 50 |
+
return FixResult(
|
| 51 |
+
original_code=code,
|
| 52 |
+
fixed_code=code,
|
| 53 |
+
errors_found=errors,
|
| 54 |
+
errors_fixed=0,
|
| 55 |
+
success=False,
|
| 56 |
+
)
|
src/agents/orchestrator.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Agent Orchestrator
|
| 3 |
+
|
| 4 |
+
Coordinates multiple AI agents (Code Gen, Vision Critic, Search, Sandbox)
|
| 5 |
+
to produce, evaluate, and refine generated code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from enum import Enum
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any, Optional
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AgentRole(str, Enum):
|
| 17 |
+
"""Roles for MINDI's agent system."""
|
| 18 |
+
CODE_GENERATOR = "code_generator"
|
| 19 |
+
UI_CRITIC = "ui_critic"
|
| 20 |
+
SEARCH_AGENT = "search_agent"
|
| 21 |
+
SANDBOX_RUNNER = "sandbox_runner"
|
| 22 |
+
ERROR_FIXER = "error_fixer"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class AgentMessage:
|
| 27 |
+
"""A message passed between agents in the orchestration pipeline."""
|
| 28 |
+
role: AgentRole
|
| 29 |
+
content: str
|
| 30 |
+
metadata: dict[str, Any] = field(default_factory=dict)
|
| 31 |
+
artifacts: list[Path] = field(default_factory=list)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class GenerationResult:
|
| 36 |
+
"""Final output from the agent pipeline."""
|
| 37 |
+
code: str
|
| 38 |
+
language: str
|
| 39 |
+
file_path: str
|
| 40 |
+
critique: Optional[str] = None
|
| 41 |
+
search_context: Optional[str] = None
|
| 42 |
+
sandbox_output: Optional[str] = None
|
| 43 |
+
iterations: int = 1
|
| 44 |
+
success: bool = True
|
| 45 |
+
errors: list[str] = field(default_factory=list)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class AgentOrchestrator:
|
| 49 |
+
"""
|
| 50 |
+
Orchestrates the MINDI agent pipeline:
|
| 51 |
+
|
| 52 |
+
1. User prompt arrives
|
| 53 |
+
2. Search Agent gathers relevant docs/packages
|
| 54 |
+
3. Code Generator produces Next.js + Tailwind + TS code
|
| 55 |
+
4. Sandbox Runner tests the code in isolation
|
| 56 |
+
5. Vision Critic screenshots the output and evaluates UI/UX
|
| 57 |
+
6. Error Fixer resolves any issues
|
| 58 |
+
7. Loop until quality threshold or max iterations
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
max_iterations: int = 3,
|
| 64 |
+
quality_threshold: float = 0.85,
|
| 65 |
+
log_dir: Optional[Path] = None,
|
| 66 |
+
) -> None:
|
| 67 |
+
self.max_iterations = max_iterations
|
| 68 |
+
self.quality_threshold = quality_threshold
|
| 69 |
+
self.log_dir = log_dir or Path("./logs/agents")
|
| 70 |
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
self.history: list[AgentMessage] = []
|
| 72 |
+
|
| 73 |
+
async def run_pipeline(
|
| 74 |
+
self,
|
| 75 |
+
user_prompt: str,
|
| 76 |
+
context: Optional[dict[str, Any]] = None,
|
| 77 |
+
) -> GenerationResult:
|
| 78 |
+
"""
|
| 79 |
+
Execute the full agent pipeline for a user request.
|
| 80 |
+
|
| 81 |
+
This is the main entry point β called by the FastAPI backend.
|
| 82 |
+
Each step will be implemented as we build each agent module.
|
| 83 |
+
"""
|
| 84 |
+
self.history.clear()
|
| 85 |
+
context = context or {}
|
| 86 |
+
|
| 87 |
+
# Step 1: Search for relevant documentation
|
| 88 |
+
search_result = await self._run_search(user_prompt)
|
| 89 |
+
|
| 90 |
+
# Step 2: Generate code
|
| 91 |
+
code_result = await self._generate_code(user_prompt, search_result)
|
| 92 |
+
|
| 93 |
+
# Step 3: Test in sandbox
|
| 94 |
+
sandbox_result = await self._run_sandbox(code_result)
|
| 95 |
+
|
| 96 |
+
# Step 4: Vision critique (if sandbox produced a screenshot)
|
| 97 |
+
critique_result = await self._run_critique(code_result, sandbox_result)
|
| 98 |
+
|
| 99 |
+
# Step 5: Fix errors if any
|
| 100 |
+
final_code = code_result
|
| 101 |
+
iterations = 1
|
| 102 |
+
|
| 103 |
+
while iterations < self.max_iterations:
|
| 104 |
+
if sandbox_result.get("success") and critique_result.get("score", 0) >= self.quality_threshold:
|
| 105 |
+
break
|
| 106 |
+
final_code = await self._fix_errors(
|
| 107 |
+
final_code, sandbox_result, critique_result
|
| 108 |
+
)
|
| 109 |
+
sandbox_result = await self._run_sandbox(final_code)
|
| 110 |
+
critique_result = await self._run_critique(final_code, sandbox_result)
|
| 111 |
+
iterations += 1
|
| 112 |
+
|
| 113 |
+
return GenerationResult(
|
| 114 |
+
code=final_code,
|
| 115 |
+
language="typescript",
|
| 116 |
+
file_path="page.tsx",
|
| 117 |
+
critique=critique_result.get("feedback"),
|
| 118 |
+
search_context=search_result.get("context"),
|
| 119 |
+
sandbox_output=sandbox_result.get("output"),
|
| 120 |
+
iterations=iterations,
|
| 121 |
+
success=sandbox_result.get("success", False),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
async def _run_search(self, prompt: str) -> dict[str, Any]:
|
| 125 |
+
"""Search for relevant docs and packages. Implemented in src/search/."""
|
| 126 |
+
# Placeholder β will be wired to SearchAgent
|
| 127 |
+
return {"context": "", "sources": []}
|
| 128 |
+
|
| 129 |
+
async def _generate_code(self, prompt: str, search_ctx: dict[str, Any]) -> str:
|
| 130 |
+
"""Generate code using the fine-tuned model. Implemented in src/inference/."""
|
| 131 |
+
# Placeholder β will be wired to inference pipeline
|
| 132 |
+
return ""
|
| 133 |
+
|
| 134 |
+
async def _run_sandbox(self, code: str) -> dict[str, Any]:
|
| 135 |
+
"""Run code in sandbox. Implemented in src/sandbox/."""
|
| 136 |
+
# Placeholder β will be wired to SandboxRunner
|
| 137 |
+
return {"success": False, "output": "", "screenshot": None}
|
| 138 |
+
|
| 139 |
+
async def _run_critique(self, code: str, sandbox: dict[str, Any]) -> dict[str, Any]:
|
| 140 |
+
"""Critique UI via vision. Implemented in src/agents/ui_critic.py."""
|
| 141 |
+
# Placeholder β will be wired to VisionCritic
|
| 142 |
+
return {"score": 0.0, "feedback": ""}
|
| 143 |
+
|
| 144 |
+
async def _fix_errors(
|
| 145 |
+
self, code: str, sandbox: dict[str, Any], critique: dict[str, Any]
|
| 146 |
+
) -> str:
|
| 147 |
+
"""Fix errors in code. Implemented in src/agents/error_fixer.py."""
|
| 148 |
+
# Placeholder β will be wired to ErrorFixer
|
| 149 |
+
return code
|
src/agents/ui_critic.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β UI Critic Agent
|
| 3 |
+
|
| 4 |
+
Uses the vision encoder to evaluate screenshots of generated UI
|
| 5 |
+
and provide structured feedback for iterative improvement.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class CritiqueResult:
|
| 19 |
+
"""Structured critique of a UI screenshot."""
|
| 20 |
+
score: float # 0.0 to 1.0 overall quality
|
| 21 |
+
layout_score: float # Layout and spacing quality
|
| 22 |
+
typography_score: float # Text hierarchy and readability
|
| 23 |
+
color_score: float # Color contrast and consistency
|
| 24 |
+
responsiveness_score: float # Mobile-readiness estimation
|
| 25 |
+
feedback: str # Natural language critique
|
| 26 |
+
suggestions: list[str] # Actionable improvement items
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class UICritic:
|
| 30 |
+
"""Vision-powered UI/UX critic for evaluating generated web pages."""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
vision_encoder: Optional[object] = None,
|
| 35 |
+
device: Optional[str] = None,
|
| 36 |
+
) -> None:
|
| 37 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 38 |
+
self.vision_encoder = vision_encoder # VisionEncoder instance
|
| 39 |
+
|
| 40 |
+
async def critique_screenshot(
|
| 41 |
+
self,
|
| 42 |
+
screenshot_path: Path,
|
| 43 |
+
generated_code: str,
|
| 44 |
+
) -> CritiqueResult:
|
| 45 |
+
"""
|
| 46 |
+
Analyze a screenshot of the generated UI and produce a critique.
|
| 47 |
+
|
| 48 |
+
The critique is used by the orchestrator to decide whether to
|
| 49 |
+
iterate on the code or accept it as final output.
|
| 50 |
+
"""
|
| 51 |
+
if not screenshot_path.exists():
|
| 52 |
+
return CritiqueResult(
|
| 53 |
+
score=0.0,
|
| 54 |
+
layout_score=0.0,
|
| 55 |
+
typography_score=0.0,
|
| 56 |
+
color_score=0.0,
|
| 57 |
+
responsiveness_score=0.0,
|
| 58 |
+
feedback="Screenshot not found β cannot critique.",
|
| 59 |
+
suggestions=["Ensure sandbox produces a screenshot."],
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Encode the screenshot using vision encoder
|
| 63 |
+
# (Full implementation will use the VisionEncoder + LLM to generate critique)
|
| 64 |
+
# For now, return a placeholder that signals "needs implementation"
|
| 65 |
+
return CritiqueResult(
|
| 66 |
+
score=0.0,
|
| 67 |
+
layout_score=0.0,
|
| 68 |
+
typography_score=0.0,
|
| 69 |
+
color_score=0.0,
|
| 70 |
+
responsiveness_score=0.0,
|
| 71 |
+
feedback="Vision critique pipeline not yet connected.",
|
| 72 |
+
suggestions=["Wire VisionEncoder to critique pipeline."],
|
| 73 |
+
)
|
src/evaluation/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Evaluation system for model quality and benchmark testing."""
|
src/evaluation/evaluator.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Evaluation System
|
| 3 |
+
|
| 4 |
+
Evaluates model quality on code generation benchmarks,
|
| 5 |
+
UI quality metrics, and end-to-end task completion.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Optional
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class EvalMetrics:
|
| 17 |
+
"""Aggregated evaluation metrics."""
|
| 18 |
+
pass_at_1: float = 0.0 # Code correctness (passes tests)
|
| 19 |
+
pass_at_5: float = 0.0 # Code correctness with 5 samples
|
| 20 |
+
ui_quality_score: float = 0.0 # Average vision critic score
|
| 21 |
+
syntax_error_rate: float = 0.0 # Fraction with syntax errors
|
| 22 |
+
type_error_rate: float = 0.0 # Fraction with TypeScript errors
|
| 23 |
+
avg_iterations: float = 0.0 # Average fix iterations needed
|
| 24 |
+
total_examples: int = 0
|
| 25 |
+
details: list[dict[str, Any]] = field(default_factory=list)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Evaluator:
|
| 29 |
+
"""Evaluates MINDI 1.5 model across multiple quality dimensions."""
|
| 30 |
+
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
eval_data_dir: Optional[Path] = None,
|
| 34 |
+
results_dir: Optional[Path] = None,
|
| 35 |
+
) -> None:
|
| 36 |
+
self.eval_data_dir = eval_data_dir or Path("./data/processed/eval")
|
| 37 |
+
self.results_dir = results_dir or Path("./logs/evaluation")
|
| 38 |
+
self.results_dir.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
|
| 40 |
+
async def run_evaluation(
|
| 41 |
+
self,
|
| 42 |
+
pipeline: Any,
|
| 43 |
+
num_samples: int = 100,
|
| 44 |
+
) -> EvalMetrics:
|
| 45 |
+
"""Run full evaluation suite against the inference pipeline."""
|
| 46 |
+
# Will be implemented with actual eval logic
|
| 47 |
+
return EvalMetrics(total_examples=num_samples)
|
| 48 |
+
|
| 49 |
+
def save_results(self, metrics: EvalMetrics, run_name: str = "eval") -> Path:
|
| 50 |
+
"""Save evaluation results to disk."""
|
| 51 |
+
import json
|
| 52 |
+
|
| 53 |
+
output_path = self.results_dir / f"{run_name}_results.json"
|
| 54 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 55 |
+
json.dump(
|
| 56 |
+
{
|
| 57 |
+
"pass_at_1": metrics.pass_at_1,
|
| 58 |
+
"pass_at_5": metrics.pass_at_5,
|
| 59 |
+
"ui_quality_score": metrics.ui_quality_score,
|
| 60 |
+
"syntax_error_rate": metrics.syntax_error_rate,
|
| 61 |
+
"type_error_rate": metrics.type_error_rate,
|
| 62 |
+
"avg_iterations": metrics.avg_iterations,
|
| 63 |
+
"total_examples": metrics.total_examples,
|
| 64 |
+
},
|
| 65 |
+
f,
|
| 66 |
+
indent=2,
|
| 67 |
+
)
|
| 68 |
+
return output_path
|
src/inference/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Inference pipeline for code generation and vision."""
|
src/inference/pipeline.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Inference Pipeline
|
| 3 |
+
|
| 4 |
+
End-to-end inference: takes a user prompt, runs through the agent
|
| 5 |
+
pipeline, and returns generated Next.js + Tailwind + TypeScript code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from transformers import AutoTokenizer
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class InferencePipeline:
|
| 18 |
+
"""Inference pipeline for MINDI 1.5 code generation."""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
model: Optional[object] = None,
|
| 23 |
+
tokenizer: Optional[AutoTokenizer] = None,
|
| 24 |
+
device: Optional[str] = None,
|
| 25 |
+
max_new_tokens: int = 4096,
|
| 26 |
+
) -> None:
|
| 27 |
+
self.model = model
|
| 28 |
+
self.tokenizer = tokenizer
|
| 29 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 30 |
+
self.max_new_tokens = max_new_tokens
|
| 31 |
+
|
| 32 |
+
def generate(
|
| 33 |
+
self,
|
| 34 |
+
prompt: str,
|
| 35 |
+
temperature: float = 0.7,
|
| 36 |
+
top_p: float = 0.95,
|
| 37 |
+
top_k: int = 50,
|
| 38 |
+
) -> str:
|
| 39 |
+
"""Generate code from a user prompt."""
|
| 40 |
+
if self.model is None or self.tokenizer is None:
|
| 41 |
+
raise RuntimeError("Model and tokenizer must be loaded before inference.")
|
| 42 |
+
|
| 43 |
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
| 44 |
+
|
| 45 |
+
with torch.inference_mode():
|
| 46 |
+
outputs = self.model.generate(
|
| 47 |
+
**inputs,
|
| 48 |
+
max_new_tokens=self.max_new_tokens,
|
| 49 |
+
temperature=temperature,
|
| 50 |
+
top_p=top_p,
|
| 51 |
+
top_k=top_k,
|
| 52 |
+
do_sample=True,
|
| 53 |
+
pad_token_id=self.tokenizer.eos_token_id,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
generated = outputs[0][inputs["input_ids"].shape[1]:]
|
| 57 |
+
return self.tokenizer.decode(generated, skip_special_tokens=False)
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
def from_checkpoint(
|
| 61 |
+
cls,
|
| 62 |
+
checkpoint_dir: Path,
|
| 63 |
+
base_model_name: str = "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
| 64 |
+
) -> "InferencePipeline":
|
| 65 |
+
"""Load an inference pipeline from a saved checkpoint."""
|
| 66 |
+
from src.model.code_model import MindiCodeModel
|
| 67 |
+
|
| 68 |
+
model_wrapper = MindiCodeModel(model_name=base_model_name)
|
| 69 |
+
model_wrapper.load_base_model()
|
| 70 |
+
model_wrapper.load_adapter(checkpoint_dir)
|
| 71 |
+
|
| 72 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 73 |
+
base_model_name, trust_remote_code=True
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
return cls(
|
| 77 |
+
model=model_wrapper.peft_model,
|
| 78 |
+
tokenizer=tokenizer,
|
| 79 |
+
)
|
src/model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Model architecture, vision encoder, and LoRA configuration."""
|
src/model/code_model.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Code Generation Model
|
| 3 |
+
|
| 4 |
+
Loads the base coding model with LoRA adapters for fine-tuning
|
| 5 |
+
on Next.js + Tailwind + TypeScript code generation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from peft import LoraConfig, PeftModel, get_peft_model, TaskType
|
| 15 |
+
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MindiCodeModel:
|
| 19 |
+
"""Base coding model with LoRA for MINDI 1.5 fine-tuning."""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
model_name: str = "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct",
|
| 24 |
+
device: Optional[str] = None,
|
| 25 |
+
cache_dir: Optional[Path] = None,
|
| 26 |
+
load_in_4bit: bool = False,
|
| 27 |
+
) -> None:
|
| 28 |
+
self.model_name = model_name
|
| 29 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 30 |
+
self.cache_dir = cache_dir or Path("./checkpoints/base")
|
| 31 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 32 |
+
self.load_in_4bit = load_in_4bit
|
| 33 |
+
self.model: Optional[AutoModelForCausalLM] = None
|
| 34 |
+
self.peft_model: Optional[PeftModel] = None
|
| 35 |
+
|
| 36 |
+
def load_base_model(self) -> AutoModelForCausalLM:
|
| 37 |
+
"""Load the base model with optional 4-bit quantization."""
|
| 38 |
+
quantization_config = None
|
| 39 |
+
if self.load_in_4bit:
|
| 40 |
+
quantization_config = BitsAndBytesConfig(
|
| 41 |
+
load_in_4bit=True,
|
| 42 |
+
bnb_4bit_quant_type="nf4",
|
| 43 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 44 |
+
bnb_4bit_use_double_quant=True,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 48 |
+
self.model_name,
|
| 49 |
+
cache_dir=str(self.cache_dir),
|
| 50 |
+
torch_dtype=torch.bfloat16,
|
| 51 |
+
device_map="auto" if self.device == "cuda" else None,
|
| 52 |
+
quantization_config=quantization_config,
|
| 53 |
+
trust_remote_code=True,
|
| 54 |
+
)
|
| 55 |
+
return self.model
|
| 56 |
+
|
| 57 |
+
def apply_lora(
|
| 58 |
+
self,
|
| 59 |
+
rank: int = 64,
|
| 60 |
+
alpha: int = 128,
|
| 61 |
+
dropout: float = 0.05,
|
| 62 |
+
target_modules: Optional[list[str]] = None,
|
| 63 |
+
) -> PeftModel:
|
| 64 |
+
"""Apply LoRA adapters to the base model for efficient fine-tuning."""
|
| 65 |
+
if self.model is None:
|
| 66 |
+
raise RuntimeError("Base model not loaded. Call load_base_model() first.")
|
| 67 |
+
|
| 68 |
+
if target_modules is None:
|
| 69 |
+
target_modules = [
|
| 70 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 71 |
+
"gate_proj", "up_proj", "down_proj",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
lora_config = LoraConfig(
|
| 75 |
+
r=rank,
|
| 76 |
+
lora_alpha=alpha,
|
| 77 |
+
lora_dropout=dropout,
|
| 78 |
+
target_modules=target_modules,
|
| 79 |
+
bias="none",
|
| 80 |
+
task_type=TaskType.CAUSAL_LM,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
self.peft_model = get_peft_model(self.model, lora_config)
|
| 84 |
+
trainable, total = self._count_parameters()
|
| 85 |
+
print(f"[MindiCodeModel] LoRA applied β trainable: {trainable:,} / {total:,} "
|
| 86 |
+
f"({100 * trainable / total:.2f}%)")
|
| 87 |
+
return self.peft_model
|
| 88 |
+
|
| 89 |
+
def _count_parameters(self) -> tuple[int, int]:
|
| 90 |
+
"""Count trainable and total parameters."""
|
| 91 |
+
model = self.peft_model or self.model
|
| 92 |
+
if model is None:
|
| 93 |
+
return 0, 0
|
| 94 |
+
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 95 |
+
total = sum(p.numel() for p in model.parameters())
|
| 96 |
+
return trainable, total
|
| 97 |
+
|
| 98 |
+
def save_adapter(self, output_dir: Optional[Path] = None) -> Path:
|
| 99 |
+
"""Save the LoRA adapter weights."""
|
| 100 |
+
if self.peft_model is None:
|
| 101 |
+
raise RuntimeError("No LoRA adapter to save. Call apply_lora() first.")
|
| 102 |
+
save_path = output_dir or Path("./checkpoints/finetuned")
|
| 103 |
+
save_path.mkdir(parents=True, exist_ok=True)
|
| 104 |
+
self.peft_model.save_pretrained(str(save_path))
|
| 105 |
+
return save_path
|
| 106 |
+
|
| 107 |
+
def load_adapter(self, adapter_dir: Path) -> PeftModel:
|
| 108 |
+
"""Load a saved LoRA adapter onto the base model."""
|
| 109 |
+
if self.model is None:
|
| 110 |
+
self.load_base_model()
|
| 111 |
+
self.peft_model = PeftModel.from_pretrained(
|
| 112 |
+
self.model, str(adapter_dir)
|
| 113 |
+
)
|
| 114 |
+
return self.peft_model
|
| 115 |
+
|
| 116 |
+
def resize_embeddings(self, new_vocab_size: int) -> None:
|
| 117 |
+
"""Resize model embeddings to accommodate new special tokens."""
|
| 118 |
+
model = self.peft_model or self.model
|
| 119 |
+
if model is None:
|
| 120 |
+
raise RuntimeError("No model loaded.")
|
| 121 |
+
model.resize_token_embeddings(new_vocab_size)
|
src/model/vision_encoder.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Vision Encoder
|
| 3 |
+
|
| 4 |
+
Uses CLIP ViT-L/14 to encode UI screenshots into embeddings
|
| 5 |
+
that the coding model can understand and critique.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
from PIL import Image
|
| 16 |
+
from transformers import CLIPModel, CLIPProcessor
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class VisionEncoder(nn.Module):
|
| 20 |
+
"""CLIP-based vision encoder for UI screenshot understanding."""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
model_name: str = "openai/clip-vit-large-patch14",
|
| 25 |
+
projection_dim: int = 768,
|
| 26 |
+
device: Optional[str] = None,
|
| 27 |
+
cache_dir: Optional[Path] = None,
|
| 28 |
+
) -> None:
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 31 |
+
self.cache_dir = cache_dir or Path("./checkpoints/vision")
|
| 32 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
|
| 34 |
+
# Load CLIP model and processor
|
| 35 |
+
self.clip: CLIPModel = CLIPModel.from_pretrained(
|
| 36 |
+
model_name, cache_dir=str(self.cache_dir)
|
| 37 |
+
)
|
| 38 |
+
self.processor: CLIPProcessor = CLIPProcessor.from_pretrained(
|
| 39 |
+
model_name, cache_dir=str(self.cache_dir)
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Freeze CLIP backbone β we only train the projection layer
|
| 43 |
+
for param in self.clip.parameters():
|
| 44 |
+
param.requires_grad = False
|
| 45 |
+
|
| 46 |
+
# Trainable projection: CLIP hidden β LLM embedding space
|
| 47 |
+
clip_hidden_size: int = self.clip.config.vision_config.hidden_size # 1024
|
| 48 |
+
self.projection = nn.Sequential(
|
| 49 |
+
nn.Linear(clip_hidden_size, projection_dim),
|
| 50 |
+
nn.GELU(),
|
| 51 |
+
nn.Linear(projection_dim, projection_dim),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
self.to(self.device)
|
| 55 |
+
|
| 56 |
+
def encode_image(self, image: Image.Image) -> torch.Tensor:
|
| 57 |
+
"""Encode a PIL image into a projected embedding tensor."""
|
| 58 |
+
inputs = self.processor(images=image, return_tensors="pt")
|
| 59 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 60 |
+
|
| 61 |
+
with torch.no_grad():
|
| 62 |
+
vision_outputs = self.clip.vision_model(**inputs)
|
| 63 |
+
# Use [CLS] token embedding
|
| 64 |
+
cls_embedding = vision_outputs.last_hidden_state[:, 0, :]
|
| 65 |
+
|
| 66 |
+
# Project into LLM embedding space (this part IS trainable)
|
| 67 |
+
projected = self.projection(cls_embedding)
|
| 68 |
+
return projected
|
| 69 |
+
|
| 70 |
+
def encode_screenshot(self, screenshot_path: Path) -> torch.Tensor:
|
| 71 |
+
"""Load a screenshot from disk and encode it."""
|
| 72 |
+
if not screenshot_path.exists():
|
| 73 |
+
raise FileNotFoundError(f"Screenshot not found: {screenshot_path}")
|
| 74 |
+
|
| 75 |
+
image = Image.open(screenshot_path).convert("RGB")
|
| 76 |
+
return self.encode_image(image)
|
| 77 |
+
|
| 78 |
+
def save_projection(self, save_dir: Optional[Path] = None) -> Path:
|
| 79 |
+
"""Save only the trainable projection weights."""
|
| 80 |
+
save_path = save_dir or self.cache_dir / "projection"
|
| 81 |
+
save_path.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
torch.save(self.projection.state_dict(), save_path / "projection.pt")
|
| 83 |
+
return save_path
|
| 84 |
+
|
| 85 |
+
def load_projection(self, load_dir: Path) -> None:
|
| 86 |
+
"""Load projection weights from disk."""
|
| 87 |
+
weights_path = load_dir / "projection.pt"
|
| 88 |
+
if not weights_path.exists():
|
| 89 |
+
raise FileNotFoundError(f"Projection weights not found: {weights_path}")
|
| 90 |
+
state_dict = torch.load(weights_path, map_location=self.device, weights_only=True)
|
| 91 |
+
self.projection.load_state_dict(state_dict)
|
src/sandbox/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Sandbox execution environment for safe code testing."""
|
src/sandbox/sandbox_runner.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Sandbox Runner
|
| 3 |
+
|
| 4 |
+
Executes generated code in an isolated environment (E2B cloud sandbox
|
| 5 |
+
or local Docker container) to test for errors and capture screenshots.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Optional
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class SandboxResult:
|
| 18 |
+
"""Result from running code in the sandbox."""
|
| 19 |
+
success: bool
|
| 20 |
+
stdout: str = ""
|
| 21 |
+
stderr: str = ""
|
| 22 |
+
exit_code: int = -1
|
| 23 |
+
screenshot_path: Optional[Path] = None
|
| 24 |
+
execution_time_ms: float = 0.0
|
| 25 |
+
errors: list[str] = field(default_factory=list)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class SandboxRunner:
|
| 29 |
+
"""
|
| 30 |
+
Isolated code execution environment.
|
| 31 |
+
|
| 32 |
+
Supports two backends:
|
| 33 |
+
- E2B (cloud): For production β real browser rendering + screenshots
|
| 34 |
+
- Docker (local): For development/testing
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
backend: str = "e2b",
|
| 40 |
+
e2b_api_key: Optional[str] = None,
|
| 41 |
+
screenshot_dir: Optional[Path] = None,
|
| 42 |
+
) -> None:
|
| 43 |
+
self.backend = backend
|
| 44 |
+
self.e2b_api_key = e2b_api_key or os.environ.get("E2B_API_KEY", "")
|
| 45 |
+
self.screenshot_dir = screenshot_dir or Path("./logs/screenshots")
|
| 46 |
+
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
|
| 47 |
+
|
| 48 |
+
async def run_code(
|
| 49 |
+
self,
|
| 50 |
+
code: str,
|
| 51 |
+
filename: str = "page.tsx",
|
| 52 |
+
capture_screenshot: bool = True,
|
| 53 |
+
) -> SandboxResult:
|
| 54 |
+
"""
|
| 55 |
+
Execute code in the sandbox and optionally capture a screenshot.
|
| 56 |
+
|
| 57 |
+
The screenshot is used by the VisionCritic to evaluate UI quality.
|
| 58 |
+
"""
|
| 59 |
+
if self.backend == "e2b":
|
| 60 |
+
return await self._run_e2b(code, filename, capture_screenshot)
|
| 61 |
+
elif self.backend == "docker":
|
| 62 |
+
return await self._run_docker(code, filename, capture_screenshot)
|
| 63 |
+
else:
|
| 64 |
+
return SandboxResult(
|
| 65 |
+
success=False,
|
| 66 |
+
stderr=f"Unknown sandbox backend: {self.backend}",
|
| 67 |
+
errors=[f"Unknown backend: {self.backend}"],
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
async def _run_e2b(
|
| 71 |
+
self, code: str, filename: str, capture_screenshot: bool
|
| 72 |
+
) -> SandboxResult:
|
| 73 |
+
"""Execute in E2B cloud sandbox."""
|
| 74 |
+
if not self.e2b_api_key:
|
| 75 |
+
return SandboxResult(
|
| 76 |
+
success=False,
|
| 77 |
+
stderr="E2B_API_KEY not set",
|
| 78 |
+
errors=["E2B_API_KEY not configured"],
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Will be implemented with e2b-code-interpreter SDK
|
| 82 |
+
return SandboxResult(success=False, stderr="E2B integration pending")
|
| 83 |
+
|
| 84 |
+
async def _run_docker(
|
| 85 |
+
self, code: str, filename: str, capture_screenshot: bool
|
| 86 |
+
) -> SandboxResult:
|
| 87 |
+
"""Execute in local Docker container."""
|
| 88 |
+
# Will be implemented with Docker SDK
|
| 89 |
+
return SandboxResult(success=False, stderr="Docker integration pending")
|
src/search/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Web search agent for documentation and package lookup."""
|
src/search/search_agent.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Web Search Agent
|
| 3 |
+
|
| 4 |
+
Uses Tavily API to search for latest documentation, packages,
|
| 5 |
+
and code examples to ground the model's code generation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Optional
|
| 14 |
+
|
| 15 |
+
import httpx
|
| 16 |
+
import yaml
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class SearchResult:
|
| 21 |
+
"""A single search result from Tavily."""
|
| 22 |
+
title: str
|
| 23 |
+
url: str
|
| 24 |
+
content: str
|
| 25 |
+
score: float
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class SearchResponse:
|
| 30 |
+
"""Aggregated search response."""
|
| 31 |
+
query: str
|
| 32 |
+
results: list[SearchResult] = field(default_factory=list)
|
| 33 |
+
context: str = "" # Concatenated relevant content for the model
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SearchAgent:
|
| 37 |
+
"""Web search agent powered by Tavily for documentation lookup."""
|
| 38 |
+
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
config_path: Optional[Path] = None,
|
| 42 |
+
api_key: Optional[str] = None,
|
| 43 |
+
) -> None:
|
| 44 |
+
self.config_path = config_path or Path("./configs/search_config.yaml")
|
| 45 |
+
self.config = self._load_config()
|
| 46 |
+
|
| 47 |
+
self.api_key = api_key or os.environ.get("TAVILY_API_KEY", "")
|
| 48 |
+
if not self.api_key:
|
| 49 |
+
print("[SearchAgent] WARNING: TAVILY_API_KEY not set")
|
| 50 |
+
|
| 51 |
+
self.base_url = "https://api.tavily.com"
|
| 52 |
+
|
| 53 |
+
def _load_config(self) -> dict:
|
| 54 |
+
"""Load search configuration from YAML."""
|
| 55 |
+
if self.config_path.exists():
|
| 56 |
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
| 57 |
+
return yaml.safe_load(f).get("search", {})
|
| 58 |
+
return {}
|
| 59 |
+
|
| 60 |
+
async def search(self, query: str, max_results: int = 5) -> SearchResponse:
|
| 61 |
+
"""Execute a web search via Tavily API."""
|
| 62 |
+
if not self.api_key:
|
| 63 |
+
return SearchResponse(query=query, context="Search unavailable β no API key.")
|
| 64 |
+
|
| 65 |
+
payload = {
|
| 66 |
+
"api_key": self.api_key,
|
| 67 |
+
"query": query,
|
| 68 |
+
"search_depth": self.config.get("search_depth", "advanced"),
|
| 69 |
+
"max_results": max_results,
|
| 70 |
+
"include_domains": self.config.get("include_domains", []),
|
| 71 |
+
"exclude_domains": self.config.get("exclude_domains", []),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 75 |
+
response = await client.post(f"{self.base_url}/search", json=payload)
|
| 76 |
+
response.raise_for_status()
|
| 77 |
+
data = response.json()
|
| 78 |
+
|
| 79 |
+
results = [
|
| 80 |
+
SearchResult(
|
| 81 |
+
title=r.get("title", ""),
|
| 82 |
+
url=r.get("url", ""),
|
| 83 |
+
content=r.get("content", ""),
|
| 84 |
+
score=r.get("score", 0.0),
|
| 85 |
+
)
|
| 86 |
+
for r in data.get("results", [])
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
# Build concatenated context for the model
|
| 90 |
+
context_parts = [f"### {r.title}\n{r.content}" for r in results]
|
| 91 |
+
context = "\n\n".join(context_parts)
|
| 92 |
+
|
| 93 |
+
return SearchResponse(query=query, results=results, context=context)
|
| 94 |
+
|
| 95 |
+
async def search_docs(self, topic: str) -> SearchResponse:
|
| 96 |
+
"""Search specifically for framework documentation."""
|
| 97 |
+
query = f"{topic} documentation latest Next.js 14 Tailwind TypeScript"
|
| 98 |
+
return await self.search(query)
|
| 99 |
+
|
| 100 |
+
async def search_package(self, package_name: str) -> SearchResponse:
|
| 101 |
+
"""Search for an npm package's usage and API."""
|
| 102 |
+
query = f"npm {package_name} usage example TypeScript"
|
| 103 |
+
return await self.search(query)
|
src/tokenizer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Tokenizer module for data preprocessing and encoding."""
|
src/tokenizer/tokenizer.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Tokenizer Wrapper
|
| 3 |
+
|
| 4 |
+
Wraps the base model tokenizer with MINDI-specific special tokens
|
| 5 |
+
and encoding utilities for code generation tasks.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
from transformers import AutoTokenizer, PreTrainedTokenizerFast
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Special tokens for MINDI's structured output format
|
| 17 |
+
SPECIAL_TOKENS: dict[str, str] = {
|
| 18 |
+
"code_start": "<|code_start|>",
|
| 19 |
+
"code_end": "<|code_end|>",
|
| 20 |
+
"file_start": "<|file_start|>",
|
| 21 |
+
"file_end": "<|file_end|>",
|
| 22 |
+
"critique_start": "<|critique_start|>",
|
| 23 |
+
"critique_end": "<|critique_end|>",
|
| 24 |
+
"search_start": "<|search_start|>",
|
| 25 |
+
"search_end": "<|search_end|>",
|
| 26 |
+
"fix_start": "<|fix_start|>",
|
| 27 |
+
"fix_end": "<|fix_end|>",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class MindiTokenizer:
|
| 32 |
+
"""Tokenizer wrapper with MINDI-specific special tokens."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, model_name: str, cache_dir: Optional[Path] = None) -> None:
|
| 35 |
+
self.model_name = model_name
|
| 36 |
+
self.cache_dir = cache_dir or Path("./data/tokenizer")
|
| 37 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
self.tokenizer: PreTrainedTokenizerFast = AutoTokenizer.from_pretrained(
|
| 40 |
+
model_name,
|
| 41 |
+
cache_dir=str(self.cache_dir),
|
| 42 |
+
trust_remote_code=True,
|
| 43 |
+
)
|
| 44 |
+
self._add_special_tokens()
|
| 45 |
+
|
| 46 |
+
def _add_special_tokens(self) -> None:
|
| 47 |
+
"""Register MINDI special tokens with the tokenizer."""
|
| 48 |
+
new_tokens = list(SPECIAL_TOKENS.values())
|
| 49 |
+
num_added = self.tokenizer.add_special_tokens(
|
| 50 |
+
{"additional_special_tokens": new_tokens}
|
| 51 |
+
)
|
| 52 |
+
if num_added > 0:
|
| 53 |
+
print(f"[MindiTokenizer] Added {num_added} special tokens")
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def vocab_size(self) -> int:
|
| 57 |
+
"""Return the full vocabulary size including special tokens."""
|
| 58 |
+
return len(self.tokenizer)
|
| 59 |
+
|
| 60 |
+
def encode(self, text: str, max_length: int = 8192) -> list[int]:
|
| 61 |
+
"""Encode text to token IDs with truncation."""
|
| 62 |
+
return self.tokenizer.encode(
|
| 63 |
+
text, max_length=max_length, truncation=True
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def decode(self, token_ids: list[int]) -> str:
|
| 67 |
+
"""Decode token IDs back to text."""
|
| 68 |
+
return self.tokenizer.decode(token_ids, skip_special_tokens=False)
|
| 69 |
+
|
| 70 |
+
def save(self, output_dir: Optional[Path] = None) -> Path:
|
| 71 |
+
"""Save the tokenizer to disk."""
|
| 72 |
+
save_path = output_dir or self.cache_dir / "mindi_tokenizer"
|
| 73 |
+
save_path.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
self.tokenizer.save_pretrained(str(save_path))
|
| 75 |
+
return save_path
|
src/training/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 β Training pipeline: LoRA fine-tuning and data loading."""
|
src/training/dataset.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Dataset Loader
|
| 3 |
+
|
| 4 |
+
Loads and preprocesses training data from JSONL files into
|
| 5 |
+
tokenized format for LoRA fine-tuning.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Optional
|
| 13 |
+
|
| 14 |
+
import yaml
|
| 15 |
+
from torch.utils.data import Dataset
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MindiDataset(Dataset):
|
| 19 |
+
"""Dataset for MINDI 1.5 fine-tuning data (JSONL format)."""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
data_dir: Path,
|
| 24 |
+
tokenizer: Any,
|
| 25 |
+
max_length: int = 8192,
|
| 26 |
+
split: str = "train",
|
| 27 |
+
) -> None:
|
| 28 |
+
self.data_dir = Path(data_dir)
|
| 29 |
+
self.tokenizer = tokenizer
|
| 30 |
+
self.max_length = max_length
|
| 31 |
+
self.split = split
|
| 32 |
+
self.examples: list[dict[str, Any]] = []
|
| 33 |
+
self._load_data()
|
| 34 |
+
|
| 35 |
+
def _load_data(self) -> None:
|
| 36 |
+
"""Load all JSONL files from the data directory."""
|
| 37 |
+
data_path = self.data_dir / f"{self.split}.jsonl"
|
| 38 |
+
if not data_path.exists():
|
| 39 |
+
print(f"[MindiDataset] No data file at {data_path} β dataset is empty")
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
with open(data_path, "r", encoding="utf-8") as f:
|
| 43 |
+
for line in f:
|
| 44 |
+
line = line.strip()
|
| 45 |
+
if line:
|
| 46 |
+
self.examples.append(json.loads(line))
|
| 47 |
+
|
| 48 |
+
print(f"[MindiDataset] Loaded {len(self.examples)} examples ({self.split})")
|
| 49 |
+
|
| 50 |
+
def __len__(self) -> int:
|
| 51 |
+
return len(self.examples)
|
| 52 |
+
|
| 53 |
+
def __getitem__(self, idx: int) -> dict[str, Any]:
|
| 54 |
+
"""Tokenize and return a single training example."""
|
| 55 |
+
example = self.examples[idx]
|
| 56 |
+
|
| 57 |
+
# Expected format: {"prompt": "...", "completion": "..."}
|
| 58 |
+
prompt = example.get("prompt", "")
|
| 59 |
+
completion = example.get("completion", "")
|
| 60 |
+
full_text = f"{prompt}\n{completion}"
|
| 61 |
+
|
| 62 |
+
encoded = self.tokenizer(
|
| 63 |
+
full_text,
|
| 64 |
+
max_length=self.max_length,
|
| 65 |
+
truncation=True,
|
| 66 |
+
padding="max_length",
|
| 67 |
+
return_tensors="pt",
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"input_ids": encoded["input_ids"].squeeze(0),
|
| 72 |
+
"attention_mask": encoded["attention_mask"].squeeze(0),
|
| 73 |
+
"labels": encoded["input_ids"].squeeze(0),
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_data_config(config_path: Optional[Path] = None) -> dict:
|
| 78 |
+
"""Load data configuration from YAML."""
|
| 79 |
+
path = config_path or Path("./configs/data_config.yaml")
|
| 80 |
+
if not path.exists():
|
| 81 |
+
raise FileNotFoundError(f"Data config not found: {path}")
|
| 82 |
+
|
| 83 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 84 |
+
return yaml.safe_load(f).get("dataset", {})
|
src/training/trainer.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Training Pipeline
|
| 3 |
+
|
| 4 |
+
LoRA fine-tuning pipeline using Hugging Face Transformers + PEFT.
|
| 5 |
+
Designed to run on AMD MI300X (192GB) cloud GPU for full training,
|
| 6 |
+
with RTX 4060 (8GB) local overrides for development testing.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
import yaml
|
| 15 |
+
from transformers import TrainingArguments
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TrainingPipeline:
|
| 19 |
+
"""Manages the LoRA fine-tuning pipeline for MINDI 1.5."""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
config_path: Optional[Path] = None,
|
| 24 |
+
local_mode: bool = True,
|
| 25 |
+
) -> None:
|
| 26 |
+
self.config_path = config_path or Path("./configs/training_config.yaml")
|
| 27 |
+
self.local_mode = local_mode
|
| 28 |
+
self.config = self._load_config()
|
| 29 |
+
|
| 30 |
+
def _load_config(self) -> dict:
|
| 31 |
+
"""Load training configuration from YAML."""
|
| 32 |
+
if not self.config_path.exists():
|
| 33 |
+
raise FileNotFoundError(f"Training config not found: {self.config_path}")
|
| 34 |
+
|
| 35 |
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
| 36 |
+
full_config = yaml.safe_load(f)
|
| 37 |
+
|
| 38 |
+
config = full_config.get("training", {})
|
| 39 |
+
|
| 40 |
+
# Apply local overrides if running on RTX 4060
|
| 41 |
+
if self.local_mode and "local_overrides" in config:
|
| 42 |
+
overrides = config.pop("local_overrides")
|
| 43 |
+
config.update(overrides)
|
| 44 |
+
print("[TrainingPipeline] Applied local GPU overrides (RTX 4060 mode)")
|
| 45 |
+
|
| 46 |
+
return config
|
| 47 |
+
|
| 48 |
+
def build_training_args(self, output_dir: Optional[Path] = None) -> TrainingArguments:
|
| 49 |
+
"""Build HuggingFace TrainingArguments from config."""
|
| 50 |
+
output = output_dir or Path("./checkpoints/finetuned")
|
| 51 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
return TrainingArguments(
|
| 54 |
+
output_dir=str(output),
|
| 55 |
+
num_train_epochs=self.config.get("epochs", 3),
|
| 56 |
+
per_device_train_batch_size=self.config.get("batch_size", 1),
|
| 57 |
+
gradient_accumulation_steps=self.config.get("gradient_accumulation_steps", 16),
|
| 58 |
+
learning_rate=self.config.get("learning_rate", 2e-4),
|
| 59 |
+
weight_decay=self.config.get("weight_decay", 0.01),
|
| 60 |
+
warmup_ratio=self.config.get("warmup_ratio", 0.03),
|
| 61 |
+
lr_scheduler_type=self.config.get("lr_scheduler", "cosine"),
|
| 62 |
+
max_grad_norm=self.config.get("max_grad_norm", 1.0),
|
| 63 |
+
bf16=self.config.get("precision", "bf16") == "bf16",
|
| 64 |
+
logging_steps=self.config.get("logging_steps", 10),
|
| 65 |
+
save_strategy=self.config.get("save_strategy", "steps"),
|
| 66 |
+
save_steps=self.config.get("save_steps", 500),
|
| 67 |
+
save_total_limit=self.config.get("save_total_limit", 5),
|
| 68 |
+
eval_strategy=self.config.get("eval_strategy", "steps"),
|
| 69 |
+
eval_steps=self.config.get("eval_steps", 250),
|
| 70 |
+
report_to=self.config.get("report_to", "wandb"),
|
| 71 |
+
gradient_checkpointing=self.config.get("gradient_checkpointing", True),
|
| 72 |
+
optim=self.config.get("optim", "adamw_torch"),
|
| 73 |
+
dataloader_num_workers=2,
|
| 74 |
+
remove_unused_columns=False,
|
| 75 |
+
)
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MINDI 1.5 Vision-Coder β Utility modules."""
|
| 2 |
+
|
| 3 |
+
from src.utils.env_loader import EnvLoader
|
| 4 |
+
from src.utils.config_loader import ConfigLoader
|
| 5 |
+
|
| 6 |
+
__all__ = ["EnvLoader", "ConfigLoader"]
|
src/utils/config_loader.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Configuration Loader
|
| 3 |
+
|
| 4 |
+
Typed dataclasses for all YAML configuration files.
|
| 5 |
+
Provides validated, type-safe access to model, training, data, and search configs.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Optional
|
| 13 |
+
|
| 14 |
+
import yaml
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ββ Model Config Dataclasses ββ
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class BaseModelConfig:
|
| 21 |
+
name: str = "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
|
| 22 |
+
parameters: str = "16B"
|
| 23 |
+
license: str = "Apache-2.0"
|
| 24 |
+
context_length: int = 8192
|
| 25 |
+
dtype: str = "bfloat16"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class VisionConfig:
|
| 30 |
+
name: str = "openai/clip-vit-large-patch14"
|
| 31 |
+
image_size: int = 224
|
| 32 |
+
patch_size: int = 14
|
| 33 |
+
hidden_size: int = 1024
|
| 34 |
+
projection_dim: int = 768
|
| 35 |
+
freeze_backbone: bool = True
|
| 36 |
+
trainable_projection: bool = True
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class LoraConfig:
|
| 41 |
+
rank: int = 64
|
| 42 |
+
alpha: int = 128
|
| 43 |
+
dropout: float = 0.05
|
| 44 |
+
target_modules: list[str] = field(default_factory=lambda: [
|
| 45 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 46 |
+
"gate_proj", "up_proj", "down_proj",
|
| 47 |
+
])
|
| 48 |
+
bias: str = "none"
|
| 49 |
+
task_type: str = "CAUSAL_LM"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class OutputConfig:
|
| 54 |
+
framework: str = "nextjs-14"
|
| 55 |
+
styling: str = "tailwindcss"
|
| 56 |
+
language: str = "typescript"
|
| 57 |
+
template_format: str = "markdown-codeblock"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass
|
| 61 |
+
class HuggingFaceConfig:
|
| 62 |
+
repo_id: str = "Mindigenous/MINDI-1.5-Vision-Coder"
|
| 63 |
+
private: bool = False
|
| 64 |
+
license: str = "apache-2.0"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class ModelConfig:
|
| 69 |
+
name: str = "MINDI-1.5-Vision-Coder"
|
| 70 |
+
version: str = "1.5.0"
|
| 71 |
+
base: BaseModelConfig = field(default_factory=BaseModelConfig)
|
| 72 |
+
vision: VisionConfig = field(default_factory=VisionConfig)
|
| 73 |
+
lora: LoraConfig = field(default_factory=LoraConfig)
|
| 74 |
+
output: OutputConfig = field(default_factory=OutputConfig)
|
| 75 |
+
huggingface: HuggingFaceConfig = field(default_factory=HuggingFaceConfig)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ββ Training Config Dataclasses ββ
|
| 79 |
+
|
| 80 |
+
@dataclass
|
| 81 |
+
class LocalOverrides:
|
| 82 |
+
batch_size: int = 1
|
| 83 |
+
gradient_accumulation_steps: int = 16
|
| 84 |
+
max_seq_length: int = 2048
|
| 85 |
+
gradient_checkpointing: bool = True
|
| 86 |
+
optim: str = "adamw_8bit"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@dataclass
|
| 90 |
+
class WandbConfig:
|
| 91 |
+
project: str = "mindi-1.5-vision-coder"
|
| 92 |
+
entity: str = "mindigenous"
|
| 93 |
+
tags: list[str] = field(default_factory=lambda: ["mindi-1.5", "lora", "vision-coder"])
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@dataclass
|
| 97 |
+
class TrainingConfig:
|
| 98 |
+
local_device: str = "cuda"
|
| 99 |
+
cloud_device: str = "cuda"
|
| 100 |
+
precision: str = "bf16"
|
| 101 |
+
epochs: int = 3
|
| 102 |
+
batch_size: int = 4
|
| 103 |
+
gradient_accumulation_steps: int = 8
|
| 104 |
+
effective_batch_size: int = 32
|
| 105 |
+
learning_rate: float = 2.0e-4
|
| 106 |
+
weight_decay: float = 0.01
|
| 107 |
+
warmup_ratio: float = 0.03
|
| 108 |
+
lr_scheduler: str = "cosine"
|
| 109 |
+
max_grad_norm: float = 1.0
|
| 110 |
+
max_seq_length: int = 8192
|
| 111 |
+
packing: bool = True
|
| 112 |
+
save_strategy: str = "steps"
|
| 113 |
+
save_steps: int = 500
|
| 114 |
+
save_total_limit: int = 5
|
| 115 |
+
checkpoint_dir: str = "./checkpoints"
|
| 116 |
+
resume_from_checkpoint: Optional[str] = None
|
| 117 |
+
logging_steps: int = 10
|
| 118 |
+
log_dir: str = "./logs/training"
|
| 119 |
+
report_to: str = "wandb"
|
| 120 |
+
eval_strategy: str = "steps"
|
| 121 |
+
eval_steps: int = 250
|
| 122 |
+
eval_samples: int = 1000
|
| 123 |
+
local_overrides: LocalOverrides = field(default_factory=LocalOverrides)
|
| 124 |
+
wandb: WandbConfig = field(default_factory=WandbConfig)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# ββ Data Config Dataclasses ββ
|
| 128 |
+
|
| 129 |
+
@dataclass
|
| 130 |
+
class DataSource:
|
| 131 |
+
name: str = ""
|
| 132 |
+
description: str = ""
|
| 133 |
+
path: str = ""
|
| 134 |
+
weight: float = 0.0
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@dataclass
|
| 138 |
+
class DataProcessing:
|
| 139 |
+
tokenizer: str = "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
|
| 140 |
+
max_length: int = 8192
|
| 141 |
+
min_length: int = 64
|
| 142 |
+
dedup_strategy: str = "minhash"
|
| 143 |
+
quality_filter: bool = True
|
| 144 |
+
output_dir: str = "./data/processed/"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@dataclass
|
| 148 |
+
class DataSplits:
|
| 149 |
+
train: float = 0.95
|
| 150 |
+
validation: float = 0.05
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@dataclass
|
| 154 |
+
class KnowledgeBase:
|
| 155 |
+
path: str = "./data/knowledge_base/"
|
| 156 |
+
sources: list[str] = field(default_factory=lambda: [
|
| 157 |
+
"nextjs-14-docs", "tailwindcss-docs", "typescript-docs",
|
| 158 |
+
"react-docs", "shadcn-ui-docs",
|
| 159 |
+
])
|
| 160 |
+
embedding_model: str = "BAAI/bge-small-en-v1.5"
|
| 161 |
+
chunk_size: int = 512
|
| 162 |
+
chunk_overlap: int = 64
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@dataclass
|
| 166 |
+
class DataConfig:
|
| 167 |
+
name: str = "mindi-1.5-training-data"
|
| 168 |
+
target_size: int = 500000
|
| 169 |
+
format: str = "jsonl"
|
| 170 |
+
sources: list[DataSource] = field(default_factory=list)
|
| 171 |
+
processing: DataProcessing = field(default_factory=DataProcessing)
|
| 172 |
+
splits: DataSplits = field(default_factory=DataSplits)
|
| 173 |
+
knowledge_base: KnowledgeBase = field(default_factory=KnowledgeBase)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ββ Search Config Dataclasses ββ
|
| 177 |
+
|
| 178 |
+
@dataclass
|
| 179 |
+
class RateLimit:
|
| 180 |
+
requests_per_minute: int = 30
|
| 181 |
+
retry_attempts: int = 3
|
| 182 |
+
retry_delay_seconds: int = 2
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@dataclass
|
| 186 |
+
class SearchCache:
|
| 187 |
+
enabled: bool = True
|
| 188 |
+
ttl_hours: int = 24
|
| 189 |
+
max_entries: int = 10000
|
| 190 |
+
storage_path: str = "./data/knowledge_base/search_cache.db"
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@dataclass
|
| 194 |
+
class DocsScraper:
|
| 195 |
+
enabled: bool = True
|
| 196 |
+
output_dir: str = "./docs/"
|
| 197 |
+
max_pages_per_site: int = 100
|
| 198 |
+
respect_robots_txt: bool = True
|
| 199 |
+
request_delay_seconds: int = 1
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@dataclass
|
| 203 |
+
class SearchConfig:
|
| 204 |
+
provider: str = "tavily"
|
| 205 |
+
api_key_env: str = "TAVILY_API_KEY"
|
| 206 |
+
max_results: int = 5
|
| 207 |
+
search_depth: str = "advanced"
|
| 208 |
+
include_domains: list[str] = field(default_factory=list)
|
| 209 |
+
exclude_domains: list[str] = field(default_factory=list)
|
| 210 |
+
rate_limit: RateLimit = field(default_factory=RateLimit)
|
| 211 |
+
cache: SearchCache = field(default_factory=SearchCache)
|
| 212 |
+
docs_scraper: DocsScraper = field(default_factory=DocsScraper)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
# ββ Config Loader ββ
|
| 216 |
+
|
| 217 |
+
def _dict_to_dataclass(cls: type, data: dict[str, Any]) -> Any:
|
| 218 |
+
"""Recursively convert a dict to a dataclass, handling nested dataclasses and lists."""
|
| 219 |
+
if not isinstance(data, dict):
|
| 220 |
+
return data
|
| 221 |
+
|
| 222 |
+
field_types = {f.name: f.type for f in cls.__dataclass_fields__.values()}
|
| 223 |
+
kwargs: dict[str, Any] = {}
|
| 224 |
+
|
| 225 |
+
for key, value in data.items():
|
| 226 |
+
if key not in field_types:
|
| 227 |
+
continue
|
| 228 |
+
|
| 229 |
+
field_type = field_types[key]
|
| 230 |
+
|
| 231 |
+
# Handle list of dataclasses (e.g., list[DataSource])
|
| 232 |
+
if isinstance(value, list) and hasattr(field_type, "__origin__"):
|
| 233 |
+
# For list[DataSource] etc.
|
| 234 |
+
inner = getattr(field_type, "__args__", [None])[0]
|
| 235 |
+
if inner and hasattr(inner, "__dataclass_fields__"):
|
| 236 |
+
kwargs[key] = [_dict_to_dataclass(inner, item) for item in value]
|
| 237 |
+
else:
|
| 238 |
+
kwargs[key] = value
|
| 239 |
+
elif isinstance(value, dict):
|
| 240 |
+
# Try to match nested dataclass
|
| 241 |
+
field_cls = cls.__dataclass_fields__[key].default_factory if hasattr(cls.__dataclass_fields__[key], "default_factory") else None
|
| 242 |
+
# Get actual type from annotations
|
| 243 |
+
import typing
|
| 244 |
+
actual_type = typing.get_type_hints(cls).get(key)
|
| 245 |
+
if actual_type and hasattr(actual_type, "__dataclass_fields__"):
|
| 246 |
+
kwargs[key] = _dict_to_dataclass(actual_type, value)
|
| 247 |
+
else:
|
| 248 |
+
kwargs[key] = value
|
| 249 |
+
else:
|
| 250 |
+
kwargs[key] = value
|
| 251 |
+
|
| 252 |
+
return cls(**kwargs)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class ConfigLoader:
|
| 256 |
+
"""
|
| 257 |
+
Loads and provides typed access to all YAML configuration files.
|
| 258 |
+
|
| 259 |
+
Usage:
|
| 260 |
+
loader = ConfigLoader()
|
| 261 |
+
model_cfg = loader.model
|
| 262 |
+
training_cfg = loader.training
|
| 263 |
+
data_cfg = loader.data
|
| 264 |
+
search_cfg = loader.search
|
| 265 |
+
"""
|
| 266 |
+
|
| 267 |
+
def __init__(self, config_dir: Optional[Path] = None) -> None:
|
| 268 |
+
self.config_dir = config_dir or Path(__file__).resolve().parents[2] / "configs"
|
| 269 |
+
self._model: Optional[ModelConfig] = None
|
| 270 |
+
self._training: Optional[TrainingConfig] = None
|
| 271 |
+
self._data: Optional[DataConfig] = None
|
| 272 |
+
self._search: Optional[SearchConfig] = None
|
| 273 |
+
|
| 274 |
+
def _load_yaml(self, filename: str) -> dict[str, Any]:
|
| 275 |
+
"""Load a YAML file from the config directory."""
|
| 276 |
+
path = self.config_dir / filename
|
| 277 |
+
if not path.exists():
|
| 278 |
+
raise FileNotFoundError(f"Config file not found: {path}")
|
| 279 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 280 |
+
return yaml.safe_load(f)
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def model(self) -> ModelConfig:
|
| 284 |
+
"""Load and return typed model configuration."""
|
| 285 |
+
if self._model is None:
|
| 286 |
+
raw = self._load_yaml("model_config.yaml")
|
| 287 |
+
model_data = raw.get("model", {})
|
| 288 |
+
self._model = _dict_to_dataclass(ModelConfig, model_data)
|
| 289 |
+
# HuggingFace config is at root level
|
| 290 |
+
hf_data = raw.get("huggingface", {})
|
| 291 |
+
if hf_data:
|
| 292 |
+
self._model.huggingface = _dict_to_dataclass(HuggingFaceConfig, hf_data)
|
| 293 |
+
return self._model
|
| 294 |
+
|
| 295 |
+
@property
|
| 296 |
+
def training(self) -> TrainingConfig:
|
| 297 |
+
"""Load and return typed training configuration."""
|
| 298 |
+
if self._training is None:
|
| 299 |
+
raw = self._load_yaml("training_config.yaml")
|
| 300 |
+
training_data = raw.get("training", {})
|
| 301 |
+
self._training = _dict_to_dataclass(TrainingConfig, training_data)
|
| 302 |
+
# WandB config is at root level
|
| 303 |
+
wandb_data = raw.get("wandb", {})
|
| 304 |
+
if wandb_data:
|
| 305 |
+
self._training.wandb = _dict_to_dataclass(WandbConfig, wandb_data)
|
| 306 |
+
return self._training
|
| 307 |
+
|
| 308 |
+
@property
|
| 309 |
+
def data(self) -> DataConfig:
|
| 310 |
+
"""Load and return typed data configuration."""
|
| 311 |
+
if self._data is None:
|
| 312 |
+
raw = self._load_yaml("data_config.yaml")
|
| 313 |
+
dataset_data = raw.get("dataset", {})
|
| 314 |
+
self._data = _dict_to_dataclass(DataConfig, dataset_data)
|
| 315 |
+
return self._data
|
| 316 |
+
|
| 317 |
+
@property
|
| 318 |
+
def search(self) -> SearchConfig:
|
| 319 |
+
"""Load and return typed search configuration."""
|
| 320 |
+
if self._search is None:
|
| 321 |
+
raw = self._load_yaml("search_config.yaml")
|
| 322 |
+
search_data = raw.get("search", {})
|
| 323 |
+
self._search = _dict_to_dataclass(SearchConfig, search_data)
|
| 324 |
+
return self._search
|
| 325 |
+
|
| 326 |
+
def reload(self) -> None:
|
| 327 |
+
"""Force reload all configurations from disk."""
|
| 328 |
+
self._model = None
|
| 329 |
+
self._training = None
|
| 330 |
+
self._data = None
|
| 331 |
+
self._search = None
|
| 332 |
+
|
| 333 |
+
def print_summary(self) -> None:
|
| 334 |
+
"""Print a summary of all loaded configurations."""
|
| 335 |
+
print("\nββββββββββββββββββββββββββββββββββββββββββββ")
|
| 336 |
+
print("β MINDI 1.5 β Configuration Summary β")
|
| 337 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 338 |
+
|
| 339 |
+
m = self.model
|
| 340 |
+
print(f" Model: {m.name} v{m.version}")
|
| 341 |
+
print(f" Base: {m.base.name} ({m.base.parameters})")
|
| 342 |
+
print(f" Vision: {m.vision.name}")
|
| 343 |
+
print(f" LoRA: r={m.lora.rank}, alpha={m.lora.alpha}")
|
| 344 |
+
print(f" Output: {m.output.framework} + {m.output.styling} + {m.output.language}")
|
| 345 |
+
|
| 346 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 347 |
+
|
| 348 |
+
t = self.training
|
| 349 |
+
print(f" Epochs: {t.epochs}")
|
| 350 |
+
print(f" Batch: {t.batch_size} (effective: {t.effective_batch_size})")
|
| 351 |
+
print(f" LR: {t.learning_rate}")
|
| 352 |
+
print(f" Precision: {t.precision}")
|
| 353 |
+
print(f" Seq length: {t.max_seq_length}")
|
| 354 |
+
|
| 355 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 356 |
+
|
| 357 |
+
d = self.data
|
| 358 |
+
print(f" Dataset: {d.name} ({d.target_size:,} target)")
|
| 359 |
+
print(f" Sources: {len(d.sources)}")
|
| 360 |
+
print(f" Format: {d.format}")
|
| 361 |
+
|
| 362 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 363 |
+
|
| 364 |
+
s = self.search
|
| 365 |
+
print(f" Provider: {s.provider}")
|
| 366 |
+
print(f" Max results: {s.max_results}")
|
| 367 |
+
print(f" Domains: {len(s.include_domains)} included, {len(s.exclude_domains)} excluded")
|
| 368 |
+
|
| 369 |
+
print("ββββββββββββββββββββββββββββββββββββββββββββ\n")
|
src/utils/env_loader.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MINDI 1.5 Vision-Coder β Environment Variable Loader
|
| 3 |
+
|
| 4 |
+
Loads secrets from .env, validates required keys, and provides
|
| 5 |
+
typed access to environment configuration.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class EnvValidationResult:
|
| 21 |
+
"""Result of environment variable validation."""
|
| 22 |
+
valid: bool
|
| 23 |
+
missing: list[str]
|
| 24 |
+
warnings: list[str]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class EnvLoader:
|
| 28 |
+
"""
|
| 29 |
+
Loads and validates environment variables from .env files.
|
| 30 |
+
|
| 31 |
+
Usage:
|
| 32 |
+
env = EnvLoader()
|
| 33 |
+
env.load()
|
| 34 |
+
env.validate()
|
| 35 |
+
key = env.get("TAVILY_API_KEY")
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
REQUIRED_KEYS = [
|
| 39 |
+
"HUGGINGFACE_TOKEN",
|
| 40 |
+
"TAVILY_API_KEY",
|
| 41 |
+
"WANDB_API_KEY",
|
| 42 |
+
"E2B_API_KEY",
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
OPTIONAL_KEYS = [
|
| 46 |
+
"HUGGINGFACE_REPO",
|
| 47 |
+
"WANDB_PROJECT",
|
| 48 |
+
"WANDB_ENTITY",
|
| 49 |
+
"MODEL_NAME",
|
| 50 |
+
"BASE_MODEL_PATH",
|
| 51 |
+
"FINETUNED_MODEL_PATH",
|
| 52 |
+
"API_HOST",
|
| 53 |
+
"API_PORT",
|
| 54 |
+
"API_WORKERS",
|
| 55 |
+
"DEVICE",
|
| 56 |
+
"MIXED_PRECISION",
|
| 57 |
+
"MAX_SEQ_LENGTH",
|
| 58 |
+
"TRAINING_OUTPUT_DIR",
|
| 59 |
+
"LOG_DIR",
|
| 60 |
+
"DATA_DIR",
|
| 61 |
+
"CHECKPOINT_DIR",
|
| 62 |
+
"SANDBOX_TYPE",
|
| 63 |
+
"MAX_SEARCH_RESULTS",
|
| 64 |
+
"SEARCH_TIMEOUT",
|
| 65 |
+
"CLOUD_GPU_HOST",
|
| 66 |
+
"CLOUD_GPU_USER",
|
| 67 |
+
"CLOUD_GPU_SSH_KEY",
|
| 68 |
+
"PROJECT_NAME",
|
| 69 |
+
"STARTUP_NAME",
|
| 70 |
+
"HF_USERNAME",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
KEY_PREFIXES = {
|
| 74 |
+
"HUGGINGFACE_TOKEN": "hf_",
|
| 75 |
+
"TAVILY_API_KEY": "tvly-",
|
| 76 |
+
"E2B_API_KEY": "e2b_",
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
def __init__(self, env_path: Optional[Path] = None) -> None:
|
| 80 |
+
self.env_path = env_path or Path(__file__).resolve().parents[2] / ".env"
|
| 81 |
+
self._loaded = False
|
| 82 |
+
|
| 83 |
+
def load(self, override: bool = False) -> None:
|
| 84 |
+
"""Load environment variables from .env file."""
|
| 85 |
+
if not self.env_path.exists():
|
| 86 |
+
raise FileNotFoundError(
|
| 87 |
+
f".env file not found at {self.env_path}\n"
|
| 88 |
+
f"Copy .env.example to .env and fill in your API keys."
|
| 89 |
+
)
|
| 90 |
+
load_dotenv(self.env_path, override=override)
|
| 91 |
+
self._loaded = True
|
| 92 |
+
|
| 93 |
+
def validate(self) -> EnvValidationResult:
|
| 94 |
+
"""Validate that all required environment variables are set and well-formed."""
|
| 95 |
+
if not self._loaded:
|
| 96 |
+
self.load()
|
| 97 |
+
|
| 98 |
+
missing: list[str] = []
|
| 99 |
+
warnings: list[str] = []
|
| 100 |
+
|
| 101 |
+
for key in self.REQUIRED_KEYS:
|
| 102 |
+
value = os.environ.get(key, "").strip()
|
| 103 |
+
if not value:
|
| 104 |
+
missing.append(key)
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# Check prefix format
|
| 108 |
+
expected_prefix = self.KEY_PREFIXES.get(key)
|
| 109 |
+
if expected_prefix and not value.startswith(expected_prefix):
|
| 110 |
+
warnings.append(
|
| 111 |
+
f"{key} doesn't start with expected prefix '{expected_prefix}'"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
return EnvValidationResult(
|
| 115 |
+
valid=len(missing) == 0,
|
| 116 |
+
missing=missing,
|
| 117 |
+
warnings=warnings,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
def get(self, key: str, default: Optional[str] = None) -> str:
|
| 121 |
+
"""Get an environment variable with optional default."""
|
| 122 |
+
if not self._loaded:
|
| 123 |
+
self.load()
|
| 124 |
+
return os.environ.get(key, default or "")
|
| 125 |
+
|
| 126 |
+
def get_int(self, key: str, default: int = 0) -> int:
|
| 127 |
+
"""Get an environment variable as an integer."""
|
| 128 |
+
value = self.get(key)
|
| 129 |
+
if not value:
|
| 130 |
+
return default
|
| 131 |
+
return int(value)
|
| 132 |
+
|
| 133 |
+
def get_path(self, key: str, default: str = ".") -> Path:
|
| 134 |
+
"""Get an environment variable as a Path."""
|
| 135 |
+
return Path(self.get(key, default))
|
| 136 |
+
|
| 137 |
+
# ββ Convenience properties ββ
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def huggingface_token(self) -> str:
|
| 141 |
+
return self.get("HUGGINGFACE_TOKEN")
|
| 142 |
+
|
| 143 |
+
@property
|
| 144 |
+
def tavily_api_key(self) -> str:
|
| 145 |
+
return self.get("TAVILY_API_KEY")
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def wandb_api_key(self) -> str:
|
| 149 |
+
return self.get("WANDB_API_KEY")
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def e2b_api_key(self) -> str:
|
| 153 |
+
return self.get("E2B_API_KEY")
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def model_name(self) -> str:
|
| 157 |
+
return self.get("MODEL_NAME", "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct")
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def device(self) -> str:
|
| 161 |
+
return self.get("DEVICE", "cuda")
|
| 162 |
+
|
| 163 |
+
@property
|
| 164 |
+
def mixed_precision(self) -> str:
|
| 165 |
+
return self.get("MIXED_PRECISION", "bf16")
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def sandbox_type(self) -> str:
|
| 169 |
+
return self.get("SANDBOX_TYPE", "e2b")
|
| 170 |
+
|
| 171 |
+
def print_status(self) -> None:
|
| 172 |
+
"""Print a summary of environment variable status."""
|
| 173 |
+
result = self.validate()
|
| 174 |
+
|
| 175 |
+
print("\nββββββββββββββββββββββββββββββββββββββββββββ")
|
| 176 |
+
print("β MINDI 1.5 β Environment Status β")
|
| 177 |
+
print("οΏ½οΏ½οΏ½βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 178 |
+
|
| 179 |
+
for key in self.REQUIRED_KEYS:
|
| 180 |
+
value = os.environ.get(key, "")
|
| 181 |
+
if value:
|
| 182 |
+
masked = value[:8] + "..." + value[-4:]
|
| 183 |
+
print(f" β
{key:<25} = {masked}")
|
| 184 |
+
else:
|
| 185 |
+
print(f" β {key:<25} = NOT SET")
|
| 186 |
+
|
| 187 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 188 |
+
|
| 189 |
+
for key in self.OPTIONAL_KEYS:
|
| 190 |
+
value = os.environ.get(key, "")
|
| 191 |
+
if value:
|
| 192 |
+
display = value if len(value) <= 40 else value[:37] + "..."
|
| 193 |
+
print(f" β
{key:<25} = {display}")
|
| 194 |
+
else:
|
| 195 |
+
print(f" βͺ {key:<25} = (not set)")
|
| 196 |
+
|
| 197 |
+
print("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 198 |
+
|
| 199 |
+
if result.valid:
|
| 200 |
+
print(" β
All required keys are set!")
|
| 201 |
+
else:
|
| 202 |
+
print(f" β Missing {len(result.missing)} required key(s): {', '.join(result.missing)}")
|
| 203 |
+
|
| 204 |
+
for w in result.warnings:
|
| 205 |
+
print(f" β οΈ {w}")
|
| 206 |
+
|
| 207 |
+
print("ββββββββββββββββββββββββββββββββββββββββββββ\n")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
env = EnvLoader()
|
| 212 |
+
env.load()
|
| 213 |
+
env.print_status()
|
| 214 |
+
result = env.validate()
|
| 215 |
+
sys.exit(0 if result.valid else 1)
|