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 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)