from __future__ import annotations import json import logging import os import re import time from pathlib import Path from typing import Any, Mapping, Sequence import httpx from dotenv import load_dotenv from .file_service import ( build_preview_file_tree, finalize_preview_files, ) load_dotenv() DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434" DEFAULT_OLLAMA_MODEL = "qwen2.5-coder" FAST_PLANNER_TIMEOUT_SECONDS = 22.0 DEEP_PREVIEW_TIMEOUT_SECONDS = 70.0 MIN_CUSTOM_PASS_SECONDS = 8.0 MAX_CUSTOM_FILES = 8 MAX_CUSTOM_FILE_LINES = 300 FAST_PLANNER_RESPONSE_BYTES_LIMIT = 48 * 1024 REQUIRED_PLANNER_KEYS = { "projectName", "projectType", "selectedStack", "modules", "packageRequirements", "installCommands", "runCommands", "requiredInputs", "envVariables", "customFiles", } logger = logging.getLogger(__name__) STACK_FIELDS = ( "language", "frontend", "backend", "database", "aiTools", "deployment", ) STACK_DEFAULTS = { "language": "Auto", "frontend": "Auto", "backend": "Auto", "database": "Auto", "aiTools": "Auto", "deployment": "Auto", } NONE_LIKE = {"", "Auto", "None"} YES_ANSWERS = {"yes", "y", "true", "1", "required", "needed"} NO_ANSWERS = {"no", "n", "false", "0", "none", "not needed"} BOOLEAN_QUESTION_IDS = {"authentication", "admin_dashboard", "email_smtp", "payment_system"} SUPPORTED_SCOPE_VALUES = {"full-stack", "backend-only", "frontend-only"} SUPPORTED_LANGUAGE_VALUES = {"Python", "JavaScript", "TypeScript", "Java"} SUPPORTED_FRONTEND_VALUES = {"React", "Next.js", "Vue", "HTML/CSS/JavaScript", "None"} SUPPORTED_BACKEND_VALUES = {"FastAPI", "Flask", "Express", "NestJS", "Spring Boot"} SUPPORTED_DATABASE_VALUES = {"SQLite", "PostgreSQL", "MySQL", "MongoDB", "None"} SUPPORTED_AI_VALUES = {"None", "Ollama", "OpenAI API", "LangChain"} SUPPORTED_DEPLOYMENT_VALUES = {"Render", "Railway", "Vercel", "Docker", "None"} SUPPORTED_COMPLEXITY_VALUES = {"Simple", "Standard", "Advanced"} SUPPORTED_ROLE_VALUES = { "Single user / no roles", "Basic roles (admin + user)", "Custom roles needed", } CANONICAL_ANSWER_ALIASES = { "project_scope": { "full stack": "full-stack", "full-stack": "full-stack", "fullstack": "full-stack", "frontend and backend": "full-stack", "backend only": "backend-only", "backend-only": "backend-only", "api only": "backend-only", "service only": "backend-only", "frontend only": "frontend-only", "frontend-only": "frontend-only", "ui only": "frontend-only", }, "language": { "python": "Python", "py": "Python", "javascript": "JavaScript", "js": "JavaScript", "typescript": "TypeScript", "ts": "TypeScript", "java": "Java", }, "frontend_framework": { "react": "React", "next": "Next.js", "nextjs": "Next.js", "next.js": "Next.js", "vue": "Vue", "vanilla": "HTML/CSS/JavaScript", "html": "HTML/CSS/JavaScript", "html/css/javascript": "HTML/CSS/JavaScript", "plain html": "HTML/CSS/JavaScript", "none": "None", "no frontend": "None", }, "backend_framework": { "fastapi": "FastAPI", "flask": "Flask", "express": "Express", "node": "Express", "node.js": "Express", "nestjs": "NestJS", "nest": "NestJS", "spring": "Spring Boot", "spring boot": "Spring Boot", "none": "None", "no backend": "None", }, "database": { "sqlite": "SQLite", "postgres": "PostgreSQL", "postgresql": "PostgreSQL", "postgre sql": "PostgreSQL", "mysql": "MySQL", "mongo": "MongoDB", "mongodb": "MongoDB", "none": "None", "no database": "None", }, "ai_integration": { "none": "None", "no": "None", "ollama": "Ollama", "openai": "OpenAI API", "openai api": "OpenAI API", "langchain": "LangChain", }, "deployment_target": { "render": "Render", "railway": "Railway", "vercel": "Vercel", "docker": "Docker", "container": "Docker", "none": "None", }, "complexity_level": { "simple": "Simple", "basic": "Simple", "beginner": "Simple", "standard": "Standard", "normal": "Standard", "default": "Standard", "advanced": "Advanced", "complex": "Advanced", }, "user_roles": { "single": "Single user / no roles", "single user": "Single user / no roles", "no roles": "Single user / no roles", "admin and user": "Basic roles (admin + user)", "admin + user": "Basic roles (admin + user)", "basic roles": "Basic roles (admin + user)", "custom roles": "Custom roles needed", "custom": "Custom roles needed", "manager roles": "Custom roles needed", }, } KEYWORD_MAP = { "language": { "Python": ("python", "fastapi", "flask", "django"), "JavaScript": ("javascript", "node", "express"), "TypeScript": ("typescript", "next.js", "nextjs", "nest", "nestjs"), "Java": ("java", "spring", "spring boot"), }, "frontend": { "React": ("react",), "Next.js": ("next.js", "nextjs"), "Vue": ("vue",), "HTML/CSS/JavaScript": ("html", "css", "javascript", "vanilla js", "frontend"), "None": ("api only", "backend only", "cli", "script"), }, "backend": { "FastAPI": ("fastapi",), "Flask": ("flask",), "Express": ("express", "node api"), "NestJS": ("nestjs", "nest js"), "Spring Boot": ("spring boot", "spring"), "None": ("static site", "frontend only"), }, "database": { "SQLite": ("sqlite",), "PostgreSQL": ("postgres", "postgresql"), "MySQL": ("mysql",), "MongoDB": ("mongodb", "mongo"), "None": ("no database", "without database", "in memory"), }, "aiTools": { "Ollama": ("ollama", "local llm", "qwen", "llama"), "OpenAI API": ("openai", "gpt"), "LangChain": ("langchain",), "None": ("no ai", "without ai"), }, "deployment": { "Render": ("render",), "Railway": ("railway",), "Vercel": ("vercel",), "Docker": ("docker", "container"), "None": ("no deployment",), }, } async def generate_project_preview( idea: str, selected_stack: dict[str, str] | None = None, generation_mode: str = "fast", final_requirements: str = "", ) -> dict[str, Any]: from .agent_controller import agent_controller return await agent_controller.generate_files( idea, selected_stack, generation_mode, final_requirements, ) def prepare_preview_for_output(preview: dict[str, Any]) -> dict[str, Any]: base_idea = ( str(preview.get("problemStatement") or "").strip() or str(preview.get("summary") or "").strip() or str(preview.get("projectName") or "").strip() or "Generated project" ) requested_stack = normalize_stack_selection(preview.get("selectedStack")) return normalize_preview(preview, base_idea, requested_stack, "fast") async def generate_project_plan( idea: str, requested_stack: dict[str, str], generation_mode: str, deadline: float, ) -> dict[str, Any]: timeout_seconds = remaining_time(deadline) if timeout_seconds <= 0: raise TimeoutError("Preview time budget was exhausted before generation started.") prompt = build_planning_prompt(idea, requested_stack, generation_mode) planner_payload = await call_ollama_json( prompt, timeout_seconds, enforce_compact_output=(generation_mode == "fast"), response_kind="planner", ) return validate_planner_payload(planner_payload) async def generate_deep_custom_files( idea: str, project_name: str, selected_stack: dict[str, str], custom_manifest: list[dict[str, str]], timeout_seconds: float, ) -> list[dict[str, str]]: prompt = build_custom_files_prompt(idea, project_name, selected_stack, custom_manifest) payload = await call_ollama_json( prompt, timeout_seconds, enforce_compact_output=False, response_kind="custom", ) files = normalize_files(payload.get("files")) allowed_paths = {item["path"] for item in custom_manifest} trimmed: list[dict[str, str]] = [] for file_entry in files: if file_entry["path"] not in allowed_paths: continue content = trim_content_lines(file_entry["content"]) trimmed.append({"path": file_entry["path"], "content": content}) return trimmed async def call_ollama_json( prompt: str, timeout_seconds: float, *, enforce_compact_output: bool, response_kind: str, ) -> dict[str, Any]: configured_base_url = str(os.getenv("OLLAMA_BASE_URL") or "").strip() if not configured_base_url: raise RuntimeError( "AI generation is unavailable because OLLAMA_BASE_URL is not configured." ) base_url = configured_base_url.rstrip("/") model = os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip() or DEFAULT_OLLAMA_MODEL safe_timeout = max(1.0, timeout_seconds) request_timeout = httpx.Timeout( connect=min(3.5, safe_timeout), read=safe_timeout, write=min(5.0, safe_timeout), pool=min(5.0, safe_timeout), ) try: async with httpx.AsyncClient(timeout=request_timeout) as client: response = await client.post( f"{base_url}/api/generate", json={"model": model, "prompt": prompt, "stream": False}, ) except httpx.HTTPError as exc: raise RuntimeError( f"AI generation is unavailable because Ollama could not be reached at {base_url}." ) from exc if response.status_code >= 400: detail = response.text.strip() or f"HTTP {response.status_code}" raise RuntimeError(f"Ollama returned an error: {detail}") payload = response.json() raw_text = str(payload.get("response") or "").strip() if not raw_text: raise RuntimeError("Ollama returned an empty response.") if enforce_compact_output and len(raw_text.encode("utf-8")) > FAST_PLANNER_RESPONSE_BYTES_LIMIT: raise ValueError( f"{response_kind.capitalize()} response exceeded the compact output limit for Fast Mode." ) return parse_model_json(raw_text) def build_planning_prompt( idea: str, selected_stack: dict[str, str], generation_mode: str, ) -> str: stack_summary = "\n".join( f"- {field}: {selected_stack.get(field, 'Auto')}" for field in STACK_FIELDS ) return f""" You are Project Agent. Return compact JSON only. No markdown. No commentary. Goal: - Plan a 100% runnable starter project. - Do not generate the entire project. - The backend will create standard files from templates. Rules: 1. Preserve user-specified language, frameworks, tools, and deployment choices. 2. Respect any non-Auto selectedStack values exactly. 3. Choose beginner-friendly defaults only for missing stack categories. 4. For full-stack output, plan both frontend and backend. 5. Do not include README, dependency manifests, setup scripts, run scripts, env files, config boilerplate, or installed libraries in customFiles. 6. customFiles must contain only project-specific business logic files, max {MAX_CUSTOM_FILES}. 7. Each custom file should be worth generating because it contains app-specific logic or UI, not generic boilerplate. 8. Detect required external inputs and return them in requiredInputs. 9. If the project uses auth, database, email, payments, AI providers, or OAuth, include the needed keys in requiredInputs. 10. Do not claim this is a production-finished application. It is a 100% runnable starter project. Return this exact top-level shape: {{ "projectName": "string", "projectType": "frontend-only | backend-only | full-stack", "selectedStack": {{ "language": "string", "frontend": "string", "backend": "string", "database": "string", "aiTools": "string", "deployment": "string" }}, "modules": [ {{ "name": "string", "purpose": "string", "keyFiles": ["string"] }} ], "packageRequirements": ["string"], "installCommands": ["string"], "runCommands": ["string"], "requiredInputs": [ {{ "name": "string", "required": true, "example": "string", "whereToAdd": ".env", "purpose": "string" }} ], "envVariables": [ {{ "name": "string", "value": "string", "description": "string" }} ], "customFiles": [ {{ "path": "relative/path", "purpose": "why this file is needed" }} ] }} Mode: - generationMode: {generation_mode} - Fast Mode should keep customFiles compact and rely on backend templates for standard files. - Deep Mode may propose richer custom business files, but still max {MAX_CUSTOM_FILES}. Selected stack: {stack_summary} Project idea: {idea} """.strip() def build_custom_files_prompt( idea: str, project_name: str, selected_stack: dict[str, str], custom_manifest: list[dict[str, str]], ) -> str: manifest_json = json.dumps(custom_manifest, indent=2) stack_json = json.dumps(selected_stack, indent=2) return f""" You are Project Agent. Generate only the custom business-logic files listed below. Return JSON only in this shape: {{ "files": [ {{ "path": "relative/path", "content": "file content" }} ] }} Rules: 1. Generate only the listed files. 2. Do not generate standard boilerplate, dependency manifests, docs, setup scripts, or env files. 3. Keep each file under {MAX_CUSTOM_FILE_LINES} lines. 4. Use the selected stack exactly. 5. Produce runnable starter logic, not empty placeholders. Project name: {project_name} Project idea: {idea} Selected stack: {stack_json} Requested custom files: {manifest_json} """.strip() def parse_model_json(raw_text: str) -> dict[str, Any]: text = strip_markdown_fences(raw_text) candidate = extract_json_object(text) if not candidate: raise ValueError("Could not recover a JSON object from the AI response.") parsed = json.loads(candidate) if not isinstance(parsed, dict): raise ValueError("AI response JSON was not an object.") return parsed def strip_markdown_fences(text: str) -> str: stripped = text.strip() if stripped.startswith("```"): stripped = re.sub(r"^```[a-zA-Z0-9_-]*\s*", "", stripped) stripped = re.sub(r"\s*```$", "", stripped) return stripped.strip() def extract_json_object(text: str) -> str: start = text.find("{") if start == -1: return "" depth = 0 in_string = False escape = False for index in range(start, len(text)): char = text[index] if in_string: if escape: escape = False elif char == "\\": escape = True elif char == '"': in_string = False continue if char == '"': in_string = True elif char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return text[start : index + 1] return "" def build_fallback_preview( idea: str, requested_stack: dict[str, str], generation_mode: str, reason: str, generation_context: str | None = None, ) -> dict[str, Any]: preview = normalize_preview( {}, idea, requested_stack, generation_mode, requirements_context=generation_context or idea, ) fallback_note = ( "Fast Mode returned a complete template-based preview because AI generation was unavailable." if generation_mode == "fast" else "Deep Mode AI enrichment was unavailable, so a complete template-based preview was returned." ) preview["assumptions"] = dedupe_list( [ fallback_note, f"Template fallback preview was generated because the AI planner could not complete in time or returned invalid output: {reason}", *preview.get("assumptions", []), ] ) return preview def build_generation_context( idea: str, final_requirements: str = "", generation_mode: str = "fast", ) -> str: cleaned_idea = idea.strip() cleaned_requirements = final_requirements.strip() if not cleaned_requirements: return cleaned_idea if generation_mode == "fast": return ( f"Idea: {cleaned_idea}\n" "Final requirements:\n" f"{cleaned_requirements}" ).strip() return ( f"Original idea:\n{cleaned_idea}\n\n" "Finalized requirements from the agent conversation:\n" f"{cleaned_requirements}" ).strip() def analyze_project_idea(idea: str) -> dict[str, Any]: detected_choices = detect_user_choices(idea) suggested_stack = resolve_selected_stack( idea, normalize_stack_selection({}), None, detected_choices, ) project_kind = determine_project_kind(suggested_stack, infer_declared_project_type(idea)) questions = build_agent_questions(idea, suggested_stack, project_kind) return { "understanding": build_agent_understanding(idea, suggested_stack, project_kind), "assumptions": build_agent_analysis_assumptions(suggested_stack, project_kind, questions), "suggestedStack": suggested_stack, "stackReasons": build_stack_reasons(suggested_stack, project_kind), "questions": questions, "detectedProjectType": project_kind["label"], "confidence": compute_agent_confidence(idea, detected_choices, questions, project_kind), } def finalize_agent_requirements( idea: str, answers: Mapping[str, Any] | None, suggested_stack: Mapping[str, Any] | None, ) -> dict[str, Any]: normalized_answers = normalize_agent_answers(answers) selected_stack = normalize_stack_selection(suggested_stack) selected_stack = apply_agent_answers_to_stack(idea, selected_stack, normalized_answers) project_kind = determine_project_kind(selected_stack, normalized_answers.get("project_scope")) final_requirements = build_final_requirements_summary( idea, normalized_answers, selected_stack, project_kind, ) assumptions = build_agent_finalize_assumptions( normalized_answers, selected_stack, project_kind, ) return { "finalRequirements": final_requirements, "selectedStack": selected_stack, "assumptions": assumptions, } def infer_declared_project_type(idea: str) -> str: lowered = idea.lower() if any(token in lowered for token in ("full stack", "full-stack", "frontend and backend")): return "full-stack" if any(token in lowered for token in ("backend only", "api only", "service only")): return "backend-only" if any(token in lowered for token in ("frontend only", "landing page only", "static site")): return "frontend-only" return "" def build_agent_understanding( idea: str, suggested_stack: dict[str, str], project_kind: dict[str, Any], ) -> str: description = idea.strip().rstrip(".") if len(description) > 180: description = description[:177].rstrip() + "..." stack_bits: list[str] = [] if project_kind["hasFrontend"]: stack_bits.append(suggested_stack["frontend"]) if project_kind["hasBackend"]: stack_bits.append(suggested_stack["backend"]) stack_text = " + ".join(bit for bit in stack_bits if bit not in NONE_LIKE) or suggested_stack["language"] return ( f"I understood your idea as a {project_kind['label']} project focused on {description}. " f"My starting recommendation is {stack_text} with template-generated runtime structure so you can get to a runnable starter quickly." ) def build_agent_analysis_assumptions( suggested_stack: dict[str, str], project_kind: dict[str, Any], questions: list[dict[str, Any]], ) -> list[str]: assumptions = [ "Questions are limited to decisions that change architecture, dependencies, or required files.", "If you skip the questions, the suggested stack and defaults will be used to generate the project.", ] if project_kind["isFullStack"]: assumptions.append("The app currently looks like a full-stack build, so both frontend and backend are planned by default.") elif project_kind["hasBackend"]: assumptions.append("The current recommendation leans backend-first and will keep the frontend optional unless you add it.") else: assumptions.append("The current recommendation treats this as a frontend-focused starter unless you add backend needs.") if questions: assumptions.append("Each question starts with your own input first. If you leave it blank, the agent will suggest a default and explain the benefit.") if suggested_stack.get("deployment") == "Render": assumptions.append("Render is used as a deployment default when no target is mentioned explicitly.") return dedupe_list(assumptions) def build_stack_reasons( suggested_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: reasons: list[str] = [] backend = suggested_stack.get("backend", "Auto") frontend = suggested_stack.get("frontend", "Auto") database = suggested_stack.get("database", "Auto") deployment = suggested_stack.get("deployment", "Auto") ai_tools = suggested_stack.get("aiTools", "Auto") if backend == "FastAPI": reasons.append("FastAPI is a beginner-friendly Python API framework that pairs well with the current runnable starter templates.") elif backend == "Express": reasons.append("Express keeps a Node-based backend lightweight and easy to extend for API-first projects.") elif backend == "Spring Boot": reasons.append("Spring Boot was chosen because the idea strongly suggests a Java-based backend stack.") if frontend == "React": reasons.append("React with Vite gives a fast local frontend starter and aligns with the existing component templates.") elif frontend == "HTML/CSS/JavaScript": reasons.append("A lightweight HTML/CSS/JavaScript frontend keeps the starter simple when a framework was not clearly required.") elif frontend == "None": reasons.append("No frontend was selected because the current idea reads like an API, worker, or backend-only service.") if database == "SQLite": reasons.append("SQLite keeps local setup simple for a runnable starter when the app does not need heavier infrastructure yet.") elif database == "PostgreSQL": reasons.append("PostgreSQL is a strong default when the app looks multi-user, relational, or production-oriented.") elif database == "None": reasons.append("No database was selected because the current idea can start without persistent storage.") if ai_tools == "Ollama": reasons.append("Ollama stays the local-first AI default when the idea explicitly suggests AI features.") elif ai_tools == "None": reasons.append("No AI tool was added because the current idea does not require model integration.") if deployment == "Render": reasons.append("Render is the default deployment target because it is simple for web apps and matches the current starter flow.") elif deployment == "Docker": reasons.append("Docker is selected because the idea or deployment preference points toward containerized delivery.") if project_kind["isFullStack"]: reasons.append("Both frontend and backend are included because the idea appears to need a user interface and a service layer.") return dedupe_list(reasons) def build_agent_questions( idea: str, suggested_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, Any]]: lowered = idea.lower() questions: list[dict[str, Any]] = [] def add_question( question_id: str, question: str, question_type: str, default: str, reason: str, options: Sequence[str] | None = None, ) -> None: questions.append( { "id": question_id, "question": question, "type": question_type, "options": list(options or []), "default": default, "reason": reason, } ) has_explicit_frontend = bool(match_keyword("frontend", lowered)) has_explicit_backend = bool(match_keyword("backend", lowered)) if not infer_declared_project_type(idea) and not (has_explicit_frontend and has_explicit_backend): add_question( "project_scope", "Do you need both a frontend and a backend for this project?", "choice", project_kind["label"], "If you leave this blank, I will suggest the current project scope because it keeps the runnable starter aligned with the folders and runtime scripts your idea appears to need.", ["full-stack", "backend-only", "frontend-only"], ) if not match_keyword("backend", lowered) and project_kind["hasBackend"]: add_question( "backend_framework", "Which backend framework do you prefer?", "choice", suggested_stack["backend"], f"If you leave this blank, I will suggest {suggested_stack['backend']} because it matches the current starter templates and keeps backend setup straightforward.", ["FastAPI", "Flask", "Express", "NestJS", "Spring Boot"], ) if not match_keyword("frontend", lowered) and project_kind["hasFrontend"]: add_question( "frontend_framework", "Which frontend framework should the starter use?", "choice", suggested_stack["frontend"], f"If you leave this blank, I will suggest {suggested_stack['frontend']} because it gives the cleanest starter structure for the current UI needs.", ["React", "Next.js", "Vue", "HTML/CSS/JavaScript", "None"], ) if not match_keyword("language", lowered) and not match_keyword("backend", lowered): add_question( "language", "Do you want to lock in a preferred programming language?", "choice", suggested_stack["language"], f"If you leave this blank, I will suggest {suggested_stack['language']} because it fits the recommended stack and keeps the starter consistent.", ["Python", "JavaScript", "TypeScript", "Java"], ) if project_kind["hasBackend"] and not match_keyword("database", lowered): add_question( "database", "Which database should the starter prepare for?", "choice", suggested_stack["database"], f"If you leave this blank, I will suggest {suggested_stack['database']} because it gives the best balance of simple setup and starter reliability for this project.", ["SQLite", "PostgreSQL", "MySQL", "MongoDB", "None"], ) if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("auth", "authentication", "login", "signup", "jwt")): add_question( "authentication", "Do you need authentication in the starter?", "boolean", "No", "If you leave this blank, I will suggest No so the starter stays simpler unless your app clearly needs protected user flows.", ["Yes", "No"], ) if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("role", "roles", "permissions", "admin", "staff", "manager")): add_question( "user_roles", "Do you need multiple user roles?", "choice", "Single user / no roles", "If you leave this blank, I will suggest a single-user or no-roles setup so the starter stays easy to run before you add permissions logic.", ["Single user / no roles", "Basic roles (admin + user)", "Custom roles needed"], ) if project_kind["hasFrontend"] and not _context_mentions_any(lowered, ("admin dashboard", "admin panel", "back office")): add_question( "admin_dashboard", "Should the starter include an admin dashboard path?", "boolean", "No", "If you leave this blank, I will suggest No so the starter focuses on the main product flow before adding admin-specific pages.", ["Yes", "No"], ) if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("email", "smtp", "mail", "newsletter", "verification email", "contact form")): add_question( "email_smtp", "Will the project need email or SMTP support?", "boolean", "No", "If you leave this blank, I will suggest No so the starter avoids extra SMTP setup unless email is part of the core workflow.", ["Yes", "No"], ) if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("payment", "payments", "stripe", "checkout", "subscription", "billing")): add_question( "payment_system", "Will the project need payment processing?", "boolean", "No", "If you leave this blank, I will suggest No so the starter avoids payment keys and billing setup unless your app clearly needs them.", ["Yes", "No"], ) if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("api", "integration", "webhook", "third-party", "external service")): add_question( "external_apis", "Do you already know any external APIs or third-party services this project must connect to?", "text", "None", "If you leave this blank, I will assume no required external APIs so the starter stays self-contained and quicker to run locally.", ) if ( not match_keyword("aiTools", lowered) and _context_mentions_any( lowered, ("ai", "assistant", "agent", "chatbot", "llm", "openai", "ollama", "summar", "recommendation"), ) ): add_question( "ai_integration", "Do you want AI integration in the starter?", "choice", "None", "If you leave this blank, I will suggest no AI integration unless the idea clearly needs model tooling and provider keys.", ["None", "Ollama", "OpenAI API", "LangChain"], ) if not match_keyword("deployment", lowered): add_question( "deployment_target", "Where do you expect to deploy this project first?", "choice", suggested_stack["deployment"], f"If you leave this blank, I will suggest {suggested_stack['deployment']} because it matches the current deployment defaults and generated starter files.", ["Render", "Railway", "Vercel", "Docker", "None"], ) if len(re.findall(r"[A-Za-z0-9]+", idea)) < 18: add_question( "complexity_level", "How ambitious should the first starter be?", "choice", "Standard", "If you leave this blank, I will suggest Standard so the starter includes useful structure without becoming heavy too early.", ["Simple", "Standard", "Advanced"], ) return questions def compute_agent_confidence( idea: str, detected_choices: list[str], questions: list[dict[str, Any]], project_kind: dict[str, Any], ) -> int: score = 35 word_count = len(re.findall(r"[A-Za-z0-9]+", idea)) if word_count >= 12: score += 15 elif word_count >= 6: score += 8 score += min(len(detected_choices) * 8, 32) if infer_declared_project_type(idea): score += 8 if project_kind["isFullStack"]: score += 4 score -= min(len(questions) * 3, 24) return max(20, min(95, score)) def normalize_agent_answers(answers: Mapping[str, Any] | None) -> dict[str, str]: if not isinstance(answers, Mapping): return {} normalized: dict[str, str] = {} for key, value in answers.items(): question_id = str(key).strip() normalized[question_id] = normalize_agent_answer_value(question_id, value) return normalized def apply_agent_answers_to_stack( idea: str, selected_stack: dict[str, str], answers: Mapping[str, str], ) -> dict[str, str]: resolved = normalize_stack_selection(selected_stack) scope = normalize_project_scope( answers.get("project_scope", infer_declared_project_type(idea) or determine_project_kind(resolved)["label"]) ) scope_value = scope.strip().lower() frontend_choice = supported_answer(answers.get("frontend_framework"), SUPPORTED_FRONTEND_VALUES) backend_choice = supported_answer(answers.get("backend_framework"), SUPPORTED_BACKEND_VALUES) language_choice = supported_answer(answers.get("language"), SUPPORTED_LANGUAGE_VALUES) database_choice = supported_answer(answers.get("database"), SUPPORTED_DATABASE_VALUES) deployment_choice = supported_answer(answers.get("deployment_target"), SUPPORTED_DEPLOYMENT_VALUES) ai_choice = supported_answer(answers.get("ai_integration"), SUPPORTED_AI_VALUES) if frontend_choice: resolved["frontend"] = frontend_choice if backend_choice: resolved["backend"] = backend_choice if language_choice: resolved["language"] = language_choice if database_choice: resolved["database"] = database_choice if deployment_choice: resolved["deployment"] = deployment_choice if ai_choice: resolved["aiTools"] = ai_choice if scope_value == "backend-only": resolved["frontend"] = "None" if resolved["backend"] in NONE_LIKE: resolved["backend"] = infer_backend(idea.lower()) elif scope_value == "frontend-only": resolved["backend"] = "None" resolved["database"] = "None" if resolved["frontend"] in NONE_LIKE: resolved["frontend"] = infer_frontend(idea.lower(), "None") else: if resolved["frontend"] in NONE_LIKE: resolved["frontend"] = "React" if resolved["backend"] in NONE_LIKE: resolved["backend"] = "FastAPI" if resolved["database"] in {"Auto", ""}: resolved["database"] = infer_database(idea.lower(), resolved["backend"]) if resolved["language"] in {"Auto", ""}: resolved["language"] = infer_language(idea.lower(), resolved["frontend"], resolved["backend"]) return resolved def build_final_requirements_summary( idea: str, answers: Mapping[str, str], selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> str: lines = [ f"Original idea: {idea.strip()}", f"Project scope: {project_kind['label']}", "Selected stack:", f"- Language: {selected_stack['language']}", f"- Frontend: {selected_stack['frontend']}", f"- Backend: {selected_stack['backend']}", f"- Database: {selected_stack['database']}", f"- AI / Tools: {selected_stack['aiTools']}", f"- Deployment: {selected_stack['deployment']}", ] details: list[str] = [] if _is_yes_answer(answers.get("authentication")): details.append("Include authentication-ready structure and required secrets.") if _is_yes_answer(answers.get("admin_dashboard")): details.append("Include admin-oriented navigation or management surface in the starter.") if _is_yes_answer(answers.get("email_smtp")): details.append("Prepare SMTP/email configuration and service wiring.") if _is_yes_answer(answers.get("payment_system")): details.append("Prepare payment provider integration points and required keys.") if answers.get("user_roles") and answers.get("user_roles") != "Single user / no roles": details.append(f"Support user access model: {answers['user_roles']}.") if answers.get("external_apis") and answers.get("external_apis").lower() not in {"", "none", "no"}: details.append(f"Prepare integration boundaries for: {answers['external_apis']}.") if answers.get("complexity_level"): details.append(f"Target complexity level: {answers['complexity_level']}.") if answers.get("ai_integration") and answers.get("ai_integration") not in {"", "None"}: details.append(f"Prepare AI integration around: {answers['ai_integration']}.") details.extend(build_unmapped_preference_notes(answers, selected_stack)) if details: lines.append("Architectural requirements:") lines.extend(f"- {item}" for item in details) else: lines.append("Architectural requirements:") lines.append("- Use the suggested defaults for unspecified decisions and keep the starter runnable-first.") return "\n".join(lines).strip() def build_agent_finalize_assumptions( answers: Mapping[str, str], selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: assumptions = [ "Final requirements were synthesized from the original idea, your answers, and the selected stack.", "Suggested defaults were used only where you left a question blank, so generation can continue without extra back-and-forth.", ] if project_kind["isFullStack"]: assumptions.append("Frontend and backend will both be generated in separate folders.") if _is_yes_answer(answers.get("authentication")): assumptions.append("Authentication-related secrets and starter environment variables will be included where needed.") if selected_stack.get("deployment") == "Docker": assumptions.append("Docker-oriented deployment files will be included where the starter templates support them.") return dedupe_list(assumptions) def _is_yes_answer(value: str | None) -> bool: return str(value or "").strip().lower() in YES_ANSWERS def normalize_agent_answer_value(question_id: str, value: Any) -> str: text = str(value or "").strip() if not text: return "" lowered = text.lower() if question_id in BOOLEAN_QUESTION_IDS: if lowered in YES_ANSWERS: return "Yes" if lowered in NO_ANSWERS: return "No" if question_id == "project_scope": return normalize_project_scope(text) alias_map = CANONICAL_ANSWER_ALIASES.get(question_id, {}) if lowered in alias_map: return alias_map[lowered] if question_id == "user_roles": if "admin" in lowered and "user" in lowered: return "Basic roles (admin + user)" if "custom" in lowered or "manager" in lowered or "permission" in lowered: return "Custom roles needed" if "single" in lowered or "no role" in lowered: return "Single user / no roles" if question_id == "external_apis" and lowered in NO_ANSWERS: return "None" return text def normalize_project_scope(value: Any) -> str: text = str(value or "").strip() if not text: return "" lowered = text.lower() return CANONICAL_ANSWER_ALIASES["project_scope"].get(lowered, text) def supported_answer(value: str | None, supported_values: set[str]) -> str: text = str(value or "").strip() return text if text in supported_values else "" def build_unmapped_preference_notes( answers: Mapping[str, str], selected_stack: Mapping[str, str], ) -> list[str]: notes: list[str] = [] mappings = [ ("language", "Language", SUPPORTED_LANGUAGE_VALUES, "language"), ("frontend_framework", "Frontend framework", SUPPORTED_FRONTEND_VALUES, "frontend"), ("backend_framework", "Backend framework", SUPPORTED_BACKEND_VALUES, "backend"), ("database", "Database", SUPPORTED_DATABASE_VALUES, "database"), ("ai_integration", "AI integration", SUPPORTED_AI_VALUES, "aiTools"), ("deployment_target", "Deployment target", SUPPORTED_DEPLOYMENT_VALUES, "deployment"), ] for answer_key, label, supported_values, stack_key in mappings: raw_value = str(answers.get(answer_key, "")).strip() if not raw_value or raw_value in supported_values: continue notes.append( f"{label} preference noted: {raw_value}. The runnable starter keeps {selected_stack.get(stack_key, 'the suggested default')} as the safe template-backed default for now." ) return notes def preview_budget_seconds(generation_mode: str) -> float: return FAST_PLANNER_TIMEOUT_SECONDS if generation_mode == "fast" else DEEP_PREVIEW_TIMEOUT_SECONDS def validate_planner_payload(payload: Any) -> dict[str, Any]: if not isinstance(payload, Mapping): raise ValueError("Planner response was not a JSON object.") missing_keys = sorted(REQUIRED_PLANNER_KEYS.difference(payload.keys())) if missing_keys: raise ValueError(f"Planner response was partial and missed required keys: {', '.join(missing_keys)}") if not isinstance(payload.get("selectedStack"), Mapping): raise ValueError("Planner response did not include a valid selectedStack object.") if not isinstance(payload.get("modules"), Sequence) or isinstance(payload.get("modules"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid modules list.") if not isinstance(payload.get("packageRequirements"), Sequence) or isinstance(payload.get("packageRequirements"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid packageRequirements list.") if not isinstance(payload.get("installCommands"), Sequence) or isinstance(payload.get("installCommands"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid installCommands list.") if not isinstance(payload.get("runCommands"), Sequence) or isinstance(payload.get("runCommands"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid runCommands list.") if not isinstance(payload.get("requiredInputs"), Sequence) or isinstance(payload.get("requiredInputs"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid requiredInputs list.") if not isinstance(payload.get("envVariables"), Sequence) or isinstance(payload.get("envVariables"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid envVariables list.") if not isinstance(payload.get("customFiles"), Sequence) or isinstance(payload.get("customFiles"), (str, bytes, bytearray)): raise ValueError("Planner response did not include a valid customFiles list.") normalized_payload = dict(payload) normalized_payload["customFiles"] = list(payload.get("customFiles", []))[:MAX_CUSTOM_FILES] return normalized_payload def normalize_preview( raw_preview: Mapping[str, Any] | None, idea: str, requested_stack: dict[str, str] | None = None, generation_mode: str = "fast", requirements_context: str | None = None, ) -> dict[str, Any]: raw = dict(raw_preview or {}) mode = normalize_generation_mode(generation_mode) requested = normalize_stack_selection(requested_stack) planning_context = requirements_context or idea detected_choices = dedupe_list( normalize_string_list(raw.get("detectedUserChoices")) or detect_user_choices(idea) ) selected_stack = resolve_selected_stack( idea=idea, requested_stack=requested, model_stack=raw.get("selectedStack"), detected_choices=detected_choices, ) project_kind = determine_project_kind(selected_stack, raw.get("projectType")) project_name = clean_project_name(raw.get("projectName"), idea) modules = merge_modules( normalize_modules(raw.get("modules")), build_default_modules(selected_stack, project_kind), ) required_inputs = merge_required_inputs( normalize_required_inputs(raw.get("requiredInputs")), build_required_inputs(planning_context, selected_stack, project_kind, modules), ) env_variables = merge_env_variables( normalize_env_variables(raw.get("envVariables")), required_inputs_to_env_variables(required_inputs), ) package_requirements = dedupe_list( normalize_string_list(raw.get("packageRequirements")) + build_package_requirements(selected_stack, project_kind) ) install_commands = dedupe_list( normalize_string_list(raw.get("installCommands")) + build_install_commands(selected_stack, project_kind) ) run_commands = dedupe_list( normalize_string_list(raw.get("runCommands")) + build_run_commands(selected_stack, project_kind) ) custom_manifest = normalize_custom_manifest(raw.get("customFiles"), selected_stack, project_kind) validated_files = finalize_preview_files( project_name=project_name, selected_stack=selected_stack, project_kind=project_kind, custom_manifest=custom_manifest, raw_files=raw.get("files"), ) summary = ( str(raw.get("summary") or "").strip() or build_summary(project_name, project_kind, selected_stack, mode) ) problem_statement = ( str(raw.get("problemStatement") or "").strip() or idea.strip() or f"Build a starter project for {project_name}." ) assumptions = dedupe_list( normalize_string_list(raw.get("assumptions")) + build_assumptions(selected_stack, project_kind, requested, mode, bool(custom_manifest)) ) architecture = dedupe_list( normalize_string_list(raw.get("architecture")) + build_architecture(selected_stack, project_kind) ) chosen_stack = build_chosen_stack(selected_stack) file_tree = build_preview_file_tree(validated_files, include_env_example=bool(env_variables)) return { "projectName": project_name, "detectedUserChoices": detected_choices, "selectedStack": selected_stack, "chosenStack": chosen_stack, "assumptions": assumptions, "summary": summary, "problemStatement": problem_statement, "architecture": architecture, "modules": modules, "packageRequirements": package_requirements, "installCommands": install_commands, "runCommands": run_commands, "requiredInputs": required_inputs, "envVariables": env_variables, "fileTree": file_tree, "files": validated_files, } def apply_custom_file_overrides( preview: dict[str, Any], custom_files: list[dict[str, str]] ) -> dict[str, Any]: merged_files = merge_file_entries(preview.get("files", []), custom_files) selected_stack = normalize_stack_selection(preview.get("selectedStack")) project_kind = determine_project_kind(selected_stack) validated_files = finalize_preview_files( project_name=str(preview.get("projectName") or "Generated Project"), selected_stack=selected_stack, project_kind=project_kind, raw_files=merged_files, ) env_variables = normalize_env_variables(preview.get("envVariables")) preview["files"] = validated_files preview["fileTree"] = build_preview_file_tree( validated_files, include_env_example=bool(env_variables), ) return preview def normalize_generation_mode(value: Any) -> str: return "deep" if str(value or "").strip().lower() == "deep" else "fast" def remaining_time(deadline: float) -> float: return max(0.0, deadline - time.monotonic()) def normalize_stack_selection(selection: Any) -> dict[str, str]: data = selection if isinstance(selection, Mapping) else {} normalized: dict[str, str] = {} for field in STACK_FIELDS: value = str(data.get(field) or STACK_DEFAULTS[field]).strip() normalized[field] = value or STACK_DEFAULTS[field] return normalized def resolve_selected_stack( idea: str, requested_stack: dict[str, str], model_stack: Any, detected_choices: list[str], ) -> dict[str, str]: model_selection = normalize_stack_selection(model_stack) resolved = normalize_stack_selection(requested_stack) lowered_idea = idea.lower() backend = pick_stack_value( requested_stack.get("backend"), model_selection.get("backend"), infer_backend(lowered_idea), ) frontend = pick_stack_value( requested_stack.get("frontend"), model_selection.get("frontend"), infer_frontend(lowered_idea, backend), ) language = pick_stack_value( requested_stack.get("language"), model_selection.get("language"), infer_language(lowered_idea, frontend, backend), ) database = pick_stack_value( requested_stack.get("database"), model_selection.get("database"), infer_database(lowered_idea, backend), ) ai_tools = pick_stack_value( requested_stack.get("aiTools"), model_selection.get("aiTools"), infer_ai_tools(lowered_idea), ) deployment = pick_stack_value( requested_stack.get("deployment"), model_selection.get("deployment"), infer_deployment(lowered_idea), ) resolved.update( { "language": language, "frontend": frontend, "backend": backend, "database": database, "aiTools": ai_tools, "deployment": deployment, } ) if not detected_choices and all(value == "Auto" for value in requested_stack.values()): resolved["backend"] = resolved["backend"] if resolved["backend"] != "Auto" else "FastAPI" resolved["frontend"] = resolved["frontend"] if resolved["frontend"] != "Auto" else "React" resolved["language"] = resolved["language"] if resolved["language"] != "Auto" else "Python" resolved["database"] = resolved["database"] if resolved["database"] != "Auto" else "SQLite" resolved["aiTools"] = resolved["aiTools"] if resolved["aiTools"] != "Auto" else "None" resolved["deployment"] = resolved["deployment"] if resolved["deployment"] != "Auto" else "Render" return resolved def pick_stack_value(requested: str | None, suggested: str | None, inferred: str) -> str: for candidate in (requested, suggested): if candidate is not None and str(candidate).strip() not in {"", "Auto"}: return str(candidate).strip() return inferred def infer_language(idea: str, frontend: str, backend: str) -> str: detected = match_keyword("language", idea) if detected: return detected if backend in {"FastAPI", "Flask"}: return "Python" if backend == "Express": return "JavaScript" if backend == "NestJS": return "TypeScript" if backend == "Spring Boot": return "Java" if frontend in {"React", "Next.js", "Vue", "HTML/CSS/JavaScript"}: return "JavaScript" return "Python" def infer_frontend(idea: str, backend: str) -> str: detected = match_keyword("frontend", idea) if detected: return detected if any(token in idea for token in ("api only", "backend only", "worker", "cli", "automation")): return "None" if any(token in idea for token in ("dashboard", "web app", "portal", "browser", "frontend", "website")): return "React" if backend in {"FastAPI", "Express", "NestJS", "Spring Boot"}: return "React" return "None" def infer_backend(idea: str) -> str: detected = match_keyword("backend", idea) if detected: return detected if any(token in idea for token in ("frontend only", "static site", "landing page only")): return "None" if "spring" in idea or "java" in idea: return "Spring Boot" if "node" in idea or "express" in idea: return "Express" return "FastAPI" def infer_database(idea: str, backend: str) -> str: detected = match_keyword("database", idea) if detected: return detected if any(token in idea for token in ("no database", "without database", "in memory")): return "None" if backend in {"FastAPI", "Flask"}: return "SQLite" if backend in {"Express", "NestJS", "Spring Boot"}: return "PostgreSQL" return "None" def infer_ai_tools(idea: str) -> str: detected = match_keyword("aiTools", idea) if detected: return detected if any(token in idea for token in ("ai", "assistant", "agent", "generator", "chatbot", "llm")): return "Ollama" return "None" def infer_deployment(idea: str) -> str: detected = match_keyword("deployment", idea) return detected or "Render" def match_keyword(category: str, idea: str) -> str: for value, keywords in KEYWORD_MAP[category].items(): if any(keyword in idea for keyword in keywords): return value return "" def detect_user_choices(idea: str) -> list[str]: lowered = idea.lower() detected: list[str] = [] labels = { "language": "Language", "frontend": "Frontend", "backend": "Backend", "database": "Database", "aiTools": "AI / Tools", "deployment": "Deployment", } for category, mapping in KEYWORD_MAP.items(): for value, keywords in mapping.items(): if any(keyword in lowered for keyword in keywords): detected.append(f"{labels[category]}: {value}") break return dedupe_list(detected) def determine_project_kind( selected_stack: dict[str, str], declared_type: Any = None, ) -> dict[str, Any]: declared = str(declared_type or "").strip().lower() frontend = selected_stack.get("frontend", "None") backend = selected_stack.get("backend", "None") has_frontend = frontend not in NONE_LIKE has_backend = backend not in NONE_LIKE if declared == "frontend-only": has_backend = False elif declared == "backend-only": has_frontend = False elif declared == "full-stack": has_frontend = True has_backend = True is_full_stack = has_frontend and has_backend if is_full_stack: label = "full-stack" minimum_files = 15 elif has_backend: label = "backend-only" minimum_files = 8 else: label = "frontend-only" minimum_files = 6 return { "hasFrontend": has_frontend, "hasBackend": has_backend, "isFullStack": is_full_stack, "label": label, "minimumFiles": minimum_files, } def clean_project_name(raw_name: Any, idea: str) -> str: if isinstance(raw_name, str) and raw_name.strip(): return raw_name.strip()[:80] words = re.findall(r"[A-Za-z0-9]+", idea) if not words: return "Project Agent Output" return " ".join(word.capitalize() for word in words[:4]) def build_summary( project_name: str, project_kind: dict[str, Any], selected_stack: dict[str, str], generation_mode: str, ) -> str: stack_bits = [] if project_kind["hasFrontend"]: stack_bits.append(selected_stack["frontend"]) if project_kind["hasBackend"]: stack_bits.append(selected_stack["backend"]) stack_summary = " + ".join(bit for bit in stack_bits if bit not in NONE_LIKE) or selected_stack["language"] mode_label = "Fast Mode" if generation_mode == "fast" else "Deep Mode" return ( f"{project_name} is a 100% runnable starter project built around {stack_summary}. " f"{mode_label} uses backend templates for all standard project structure so output quality stays complete while generation remains fast." ) def build_assumptions( selected_stack: dict[str, str], project_kind: dict[str, Any], requested_stack: dict[str, str], generation_mode: str, has_custom_manifest: bool, ) -> list[str]: assumptions: list[str] = [] if all(value == "Auto" for value in requested_stack.values()): assumptions.append( "Some stack choices were inferred automatically to provide a complete beginner-friendly starter." ) if generation_mode == "fast": assumptions.append( "Fast Mode used backend templates for standard files so project completeness was preserved without waiting for the model to write boilerplate." ) else: assumptions.append( "Deep Mode keeps the same complete standard structure as Fast Mode and uses extra model time only for project-specific custom files." ) if project_kind["isFullStack"]: assumptions.append("The project is split into frontend and backend folders to keep the full-stack boundary explicit.") assumptions.append( "This output is a 100% runnable starter project after `.env` is filled, setup is run, and the run script is started. Business logic can still be customized afterward." ) if selected_stack.get("database") == "SQLite": assumptions.append("SQLite was chosen as a lightweight local-first database default.") if has_custom_manifest: assumptions.append("Project-specific custom files were layered on top of the standard stack templates.") return assumptions def build_architecture( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: architecture: list[str] = [] if project_kind["hasFrontend"]: architecture.append( f"{selected_stack['frontend']} handles the user-facing workflows, starter pages, and client-side integration points." ) if project_kind["hasBackend"]: architecture.append( f"{selected_stack['backend']} provides the API surface, routing, services, and configuration layer." ) if selected_stack.get("database") not in NONE_LIKE: architecture.append( f"{selected_stack['database']} is configured as the primary persistence layer through environment-driven settings." ) if selected_stack.get("aiTools") not in NONE_LIKE: architecture.append( f"{selected_stack['aiTools']} integration is isolated behind service boundaries so model/provider settings can evolve independently." ) architecture.append("Setup, run scripts, and dependency manifests are generated server-side for a consistent starter layout.") return architecture def build_default_modules( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, Any]]: modules: list[dict[str, Any]] = [] if project_kind["hasFrontend"]: base = "frontend/" if project_kind["isFullStack"] else "" modules.append( { "name": "Frontend Experience", "purpose": "Provides the main user interface, starter pages, and client-side state or API hooks.", "keyFiles": [ f"{base}src/App.jsx" if selected_stack["frontend"] in {"React", "Next.js", "Vue"} else f"{base}index.html", f"{base}src/pages/HomePage.jsx" if selected_stack["frontend"] in {"React", "Next.js", "Vue"} else f"{base}src/views/home.js", f"{base}src/services/api.js", ], } ) if project_kind["hasBackend"]: base = "backend/" if project_kind["isFullStack"] else "" if selected_stack["backend"] in {"FastAPI", "Flask"}: key_files = [f"{base}app/main.py", f"{base}app/routers/items.py", f"{base}app/services/item_service.py"] elif selected_stack["backend"] in {"Express", "NestJS"}: key_files = [f"{base}server.js", f"{base}src/routes/items.js", f"{base}src/services/itemService.js"] else: key_files = [f"{base}pom.xml", f"{base}src/main/java/com/example/demo/service/AppService.java"] modules.append( { "name": "Backend API", "purpose": "Provides the routes, services, and integration-ready backend surface.", "keyFiles": key_files, } ) if selected_stack.get("database") not in NONE_LIKE: base = "backend/" if project_kind["isFullStack"] else "" modules.append( { "name": "Persistence Layer", "purpose": "Supplies data models, configuration, and starter persistence wiring.", "keyFiles": [ f"{base}app/database.py" if selected_stack["backend"] in {"FastAPI", "Flask"} else f"{base}src/models/itemModel.js" ], } ) return modules def build_package_requirements( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: packages: list[str] = [] if project_kind["hasBackend"]: backend = selected_stack["backend"] database = selected_stack["database"] if backend == "FastAPI": packages.extend( ["fastapi", "uvicorn[standard]", "pydantic", "pydantic-settings", "sqlalchemy", "python-dotenv"] ) if database == "SQLite": packages.append("aiosqlite") elif database == "PostgreSQL": packages.append("psycopg[binary]") elif backend == "Flask": packages.extend(["flask", "python-dotenv", "sqlalchemy"]) elif backend in {"Express", "NestJS"}: packages.extend(["express", "cors", "dotenv"]) if database == "PostgreSQL": packages.append("pg") elif database == "MongoDB": packages.append("mongoose") elif backend == "Spring Boot": packages.extend( [ "org.springframework.boot:spring-boot-starter-web", "org.springframework.boot:spring-boot-starter-data-jpa", ] ) if project_kind["hasFrontend"]: frontend = selected_stack["frontend"] if frontend in {"React", "Next.js", "Vue"}: packages.extend(["react", "react-dom", "vite"]) elif frontend == "HTML/CSS/JavaScript": packages.append("vite") ai_tools = selected_stack.get("aiTools", "None") if ai_tools == "Ollama": packages.append("ollama") elif ai_tools == "LangChain": packages.append("langchain") return dedupe_list(packages) def build_install_commands( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: if project_kind["isFullStack"]: return ["setup.bat", "./setup.sh"] if project_kind["hasBackend"] and selected_stack["backend"] == "Spring Boot": return ["setup.bat", "./setup.sh", "mvn install"] if project_kind["hasFrontend"] and not project_kind["hasBackend"]: return ["setup.bat", "./setup.sh", "npm install"] return ["setup.bat", "./setup.sh"] def build_run_commands( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[str]: commands = ["run.bat", "./run.sh"] if project_kind["isFullStack"]: return commands if project_kind["hasBackend"]: backend = selected_stack["backend"] if backend in {"FastAPI", "Flask"}: commands.append("python -m uvicorn app.main:app --reload") elif backend in {"Express", "NestJS"}: commands.append("npm start") elif backend == "Spring Boot": commands.append("mvn spring-boot:run") elif project_kind["hasFrontend"]: commands.append("npm run dev") return commands def build_required_inputs( idea: str, selected_stack: dict[str, str], project_kind: dict[str, Any], modules: list[dict[str, Any]], ) -> list[dict[str, Any]]: context_parts = [idea] for module in modules: context_parts.append(str(module.get("name") or "")) context_parts.append(str(module.get("purpose") or "")) context_parts.extend(normalize_string_list(module.get("keyFiles"))) lowered = " ".join(part.lower() for part in context_parts if part) required_inputs: list[dict[str, Any]] = [] if project_kind["hasBackend"]: required_inputs.append( { "name": "APP_ENV", "required": False, "example": "development", "whereToAdd": ".env", "purpose": "Application environment name.", } ) database = selected_stack.get("database") if database == "SQLite": required_inputs.append( { "name": "DATABASE_URL", "required": True, "example": "sqlite:///./app.db", "whereToAdd": ".env", "purpose": "SQLite connection string for local development.", } ) elif database in {"PostgreSQL", "MySQL"}: example = ( "postgresql://postgres:postgres@localhost:5432/app_db" if database == "PostgreSQL" else "mysql://root:password@localhost:3306/app_db" ) required_inputs.append( { "name": "DATABASE_URL", "required": True, "example": example, "whereToAdd": ".env", "purpose": f"{database} connection string for the backend database.", } ) elif database == "MongoDB": required_inputs.append( { "name": "MONGODB_URI", "required": True, "example": "mongodb://localhost:27017/app_db", "whereToAdd": ".env", "purpose": "MongoDB connection string for the backend database.", } ) if _context_mentions_any(lowered, ("auth", "authentication", "login", "signup", "jwt", "token", "session")): required_inputs.append( { "name": "JWT_SECRET", "required": True, "example": "change-me-super-secret", "whereToAdd": ".env", "purpose": "Secret used to sign authentication tokens.", } ) if _context_mentions_any(lowered, ("email", "smtp", "mail", "newsletter", "verification email", "contact form")): required_inputs.extend( [ { "name": "SMTP_HOST", "required": True, "example": "smtp.gmail.com", "whereToAdd": ".env", "purpose": "SMTP email server host.", }, { "name": "SMTP_PORT", "required": True, "example": "587", "whereToAdd": ".env", "purpose": "SMTP email server port.", }, { "name": "SMTP_USER", "required": True, "example": "notifications@example.com", "whereToAdd": ".env", "purpose": "SMTP account username or sender login.", }, { "name": "SMTP_PASSWORD", "required": True, "example": "app-password", "whereToAdd": ".env", "purpose": "SMTP account password or app password.", }, ] ) if _context_mentions_any(lowered, ("payment", "payments", "stripe", "checkout", "billing", "subscription")): key_name = "STRIPE_SECRET_KEY" if "stripe" in lowered else "PAYMENT_SECRET_KEY" required_inputs.append( { "name": key_name, "required": True, "example": "sk_test_1234567890", "whereToAdd": ".env", "purpose": "Secret key for payment provider API requests.", } ) ai_tools = selected_stack.get("aiTools") if ai_tools == "Ollama" or _context_mentions_any(lowered, ("ollama", "local llm", "qwen", "llama")): required_inputs.extend( [ { "name": "OLLAMA_BASE_URL", "required": True, "example": "http://localhost:11434", "whereToAdd": ".env", "purpose": "Base URL for the local Ollama server.", }, { "name": "OLLAMA_MODEL", "required": True, "example": "qwen2.5-coder:latest", "whereToAdd": ".env", "purpose": "Ollama model name to use for local inference.", }, ] ) if ai_tools == "OpenAI API" or _context_mentions_any(lowered, ("openai", "gpt-4", "gpt", "chatgpt")): required_inputs.append( { "name": "OPENAI_API_KEY", "required": True, "example": "sk-proj-1234567890", "whereToAdd": ".env", "purpose": "API key for hosted OpenAI requests.", } ) if _context_mentions_any(lowered, ("oauth", "google login", "github login", "social login", "openid")): required_inputs.extend( [ { "name": "OAUTH_CLIENT_ID", "required": True, "example": "your-client-id", "whereToAdd": ".env", "purpose": "OAuth client identifier for external sign-in.", }, { "name": "OAUTH_CLIENT_SECRET", "required": True, "example": "your-client-secret", "whereToAdd": ".env", "purpose": "OAuth client secret for external sign-in.", }, ] ) if project_kind["isFullStack"]: required_inputs.append( { "name": "VITE_API_BASE_URL", "required": False, "example": "http://localhost:8000", "whereToAdd": ".env", "purpose": "Frontend base URL for backend API calls.", } ) return dedupe_required_inputs(required_inputs) def required_inputs_to_env_variables( required_inputs: list[dict[str, Any]], ) -> list[dict[str, str]]: env_vars: list[dict[str, str]] = [] for item in required_inputs: name = str(item.get("name") or "").strip() if not name: continue env_vars.append( { "name": name, "value": str(item.get("example") or "").strip(), "description": str(item.get("purpose") or "").strip(), } ) return env_vars def normalize_custom_manifest( value: Any, selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, str]]: manifest: list[dict[str, str]] = [] if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): for item in value: if len(manifest) >= MAX_CUSTOM_FILES: break if isinstance(item, str): path = clean_relative_path(item) purpose = "Project-specific custom logic." elif isinstance(item, Mapping): path = clean_relative_path(item.get("path")) purpose = str(item.get("purpose") or item.get("description") or "Project-specific custom logic.").strip() else: continue if not path or is_standard_file_path(path): continue manifest.append({"path": path, "purpose": purpose}) if manifest: return dedupe_manifest(manifest) defaults: list[dict[str, str]] = [] if project_kind["isFullStack"]: if selected_stack["frontend"] not in NONE_LIKE: defaults.append( { "path": "frontend/src/pages/DashboardPage.jsx", "purpose": "Primary project-specific frontend page for the main user workflow.", } ) if selected_stack["backend"] in {"FastAPI", "Flask"}: defaults.append( { "path": "backend/app/services/domain_service.py", "purpose": "Project-specific backend domain service with starter business rules.", } ) elif selected_stack["backend"] in {"Express", "NestJS"}: defaults.append( { "path": "backend/src/services/domainService.js", "purpose": "Project-specific backend domain service with starter business rules.", } ) elif project_kind["hasBackend"]: if selected_stack["backend"] in {"FastAPI", "Flask"}: defaults.append( { "path": "app/services/domain_service.py", "purpose": "Project-specific backend domain service with starter business rules.", } ) elif selected_stack["backend"] in {"Express", "NestJS"}: defaults.append( { "path": "src/services/domainService.js", "purpose": "Project-specific backend domain service with starter business rules.", } ) elif selected_stack["backend"] == "Spring Boot": defaults.append( { "path": "src/main/java/com/example/demo/service/DomainService.java", "purpose": "Project-specific backend service with starter business rules.", } ) elif project_kind["hasFrontend"]: if selected_stack["frontend"] in {"React", "Next.js", "Vue"}: defaults.append( { "path": "src/pages/DashboardPage.jsx", "purpose": "Primary project-specific frontend page for the main workflow.", } ) else: defaults.append( { "path": "src/views/dashboard.js", "purpose": "Primary project-specific frontend view for the main workflow.", } ) return defaults[:MAX_CUSTOM_FILES] def build_standard_files( project_name: str, selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, str]]: files: dict[str, str] = {} if project_kind["isFullStack"]: if project_kind["hasFrontend"]: files.update(build_frontend_files(selected_stack["frontend"], project_name, "frontend")) if project_kind["hasBackend"]: files.update(build_backend_files(selected_stack, project_name, "backend")) files.update(build_root_scripts(selected_stack, project_kind)) elif project_kind["hasBackend"]: files.update(build_backend_files(selected_stack, project_name, "")) files.update(build_root_scripts(selected_stack, project_kind)) else: files.update(build_frontend_files(selected_stack["frontend"], project_name, "")) files.update(build_root_scripts(selected_stack, project_kind)) return [{"path": path, "content": content} for path, content in files.items()] def build_custom_template_files( manifest: list[dict[str, str]], project_name: str, selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, str]]: files: list[dict[str, str]] = [] for item in manifest[:MAX_CUSTOM_FILES]: path = item["path"] purpose = item["purpose"] content = build_custom_template_content(path, purpose, project_name, selected_stack, project_kind) files.append({"path": path, "content": trim_content_lines(content)}) return files def build_custom_template_content( path: str, purpose: str, project_name: str, selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> str: filename = Path(path).name stem = Path(path).stem pretty_name = stem.replace("_", " ").replace("-", " ").title() extension = Path(path).suffix.lower() if extension in {".jsx", ".tsx"}: if "page" in stem.lower() or "/pages/" in path: return f"""const cards = [ {{ title: "{pretty_name} Overview", detail: "{purpose}" }}, {{ title: "Starter Workflow", detail: "Use this page to connect forms, API requests, and user-facing business actions." }} ]; export default function {safe_component_name(stem)}() {{ return (

{pretty_name}

{purpose}

); }} """ return f"""export default function {safe_component_name(stem)}() {{ return (

{pretty_name}

{purpose}

); }} """ if extension == ".py": if "router" in path or "/routers/" in path: return f"""from fastapi import APIRouter router = APIRouter() @router.get("/") def read_{safe_python_name(stem)}() -> dict[str, str]: return {{ "message": "{purpose}", "project": "{project_name}", }} """ if "schema" in path or "/schemas/" in path: return f"""from pydantic import BaseModel class {safe_component_name(stem)}(BaseModel): name: str description: str = "{purpose}" """ if "model" in path or "/models/" in path: return f"""from dataclasses import dataclass @dataclass class {safe_component_name(stem)}: name: str status: str = "ready" """ return f"""def {safe_python_name(stem)}_summary() -> dict[str, str]: return {{ "name": "{pretty_name}", "purpose": "{purpose}", "project": "{project_name}", }} """ if extension in {".js", ".mjs"}: if "service" in path.lower(): return f"""export function get{safe_component_name(stem)}Summary() {{ return {{ project: "{project_name}", purpose: "{purpose}" }}; }} """ if "controller" in path.lower(): return f"""export function {safe_js_name(stem)}(_req, res) {{ res.json({{ project: "{project_name}", purpose: "{purpose}" }}); }} """ return f"""export const {safe_js_name(stem)} = {{ project: "{project_name}", purpose: "{purpose}" }}; """ if extension == ".java": class_name = safe_component_name(stem) return f"""package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class {class_name} {{ public String summary() {{ return "{purpose}"; }} }} """ return f"# {pretty_name}\n\n{purpose}\n" def ensure_minimum_project_files( files: list[dict[str, str]], selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> list[dict[str, str]]: merged = {entry["path"]: entry["content"] for entry in files} for path, content in build_root_scripts(selected_stack, project_kind).items(): merged.setdefault(path, content) if project_kind["isFullStack"]: for path, content in build_frontend_files(selected_stack["frontend"], "App", "frontend").items(): merged.setdefault(path, content) for path, content in build_backend_files(selected_stack, "App", "backend").items(): merged.setdefault(path, content) elif project_kind["hasBackend"]: for path, content in build_backend_files(selected_stack, "App", "").items(): merged.setdefault(path, content) else: for path, content in build_frontend_files(selected_stack["frontend"], "App", "").items(): merged.setdefault(path, content) filler_index = 1 while len(merged) < project_kind["minimumFiles"]: filler_path = f"notes/starter-note-{filler_index}.md" merged.setdefault( filler_path, f"# Starter Note {filler_index}\n\nThis file preserves the complete minimum project structure while you continue iterating.\n", ) filler_index += 1 return [{"path": path, "content": content} for path, content in merged.items()] def build_backend_files( selected_stack: dict[str, str], project_name: str, prefix: str, ) -> dict[str, str]: backend = selected_stack.get("backend", "FastAPI") if backend in {"FastAPI", "Flask"}: return build_fastapi_backend_files(project_name, prefix) if backend in {"Express", "NestJS"}: return build_express_backend_files(project_name, prefix) if backend == "Spring Boot": return build_spring_backend_files(project_name, prefix) return build_fastapi_backend_files(project_name, prefix) def build_frontend_files(frontend: str, project_name: str, prefix: str) -> dict[str, str]: if frontend in {"React", "Next.js", "Vue"}: return build_react_frontend_files(project_name, prefix) return build_vanilla_frontend_files(project_name, prefix) def build_fastapi_backend_files(project_name: str, prefix: str) -> dict[str, str]: app_prefix = prefixed(prefix, "app") return { prefixed(prefix, "requirements.txt"): "\n".join( [ "fastapi", "uvicorn[standard]", "pydantic", "pydantic-settings", "sqlalchemy", "python-dotenv", "aiosqlite", "", ] ), prefixed(app_prefix, "__init__.py"): "", prefixed(app_prefix, "main.py"): f"""from fastapi import FastAPI from app.routers import health, items app = FastAPI(title="{project_name} API") app.include_router(health.router) app.include_router(items.router, prefix="/api/items", tags=["items"]) @app.get("/") def read_root() -> dict[str, str]: return {{"message": "{project_name} backend is running."}} """, prefixed(app_prefix, "routers/__init__.py"): "", prefixed(app_prefix, "routers/health.py"): """from fastapi import APIRouter from app.schemas.health import HealthResponse router = APIRouter(tags=["health"]) @router.get("/health", response_model=HealthResponse) def healthcheck() -> HealthResponse: return HealthResponse(status="ok") """, prefixed(app_prefix, "routers/items.py"): """from fastapi import APIRouter from app.schemas.item import Item from app.services.item_service import list_items router = APIRouter() @router.get("/", response_model=list[Item]) def get_items() -> list[Item]: return list_items() """, prefixed(app_prefix, "services/__init__.py"): "", prefixed(app_prefix, "services/app_service.py"): f"""def get_app_summary() -> str: return "{project_name} includes routes, services, schemas, and configuration for quick iteration." """, prefixed(app_prefix, "services/item_service.py"): """from app.schemas.item import Item def list_items() -> list[Item]: return [ Item(id=1, name="Starter task", status="ready"), Item(id=2, name="Next iteration", status="planned"), ] """, prefixed(app_prefix, "models/__init__.py"): "", prefixed(app_prefix, "models/base.py"): """from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass """, prefixed(app_prefix, "models/item.py"): """from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base class ItemModel(Base): __tablename__ = "items" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(120)) status: Mapped[str] = mapped_column(String(40), default="ready") """, prefixed(app_prefix, "schemas/__init__.py"): "", prefixed(app_prefix, "schemas/health.py"): """from pydantic import BaseModel class HealthResponse(BaseModel): status: str """, prefixed(app_prefix, "schemas/item.py"): """from pydantic import BaseModel class Item(BaseModel): id: int name: str status: str """, prefixed(app_prefix, "database.py"): """from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.config import settings engine = create_engine(settings.database_url, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) """, prefixed(app_prefix, "config.py"): """from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") app_env: str = "development" database_url: str = "sqlite:///./app.db" settings = Settings() """, } def build_express_backend_files(project_name: str, prefix: str) -> dict[str, str]: return { prefixed(prefix, "package.json"): json.dumps( { "name": project_name.lower().replace(" ", "-"), "version": "0.1.0", "private": True, "type": "module", "scripts": {"dev": "node --watch server.js", "start": "node server.js"}, "dependencies": {"cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2"}, }, indent=2, ) + "\n", prefixed(prefix, "server.js"): f"""import cors from "cors"; import dotenv from "dotenv"; import express from "express"; import indexRouter from "./src/routes/index.js"; import itemsRouter from "./src/routes/items.js"; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json()); app.use("/", indexRouter); app.use("/api/items", itemsRouter); const port = process.env.PORT || 8000; app.listen(port, () => {{ console.log("{project_name} API listening on port", port); }}); """, prefixed(prefix, "src/routes/index.js"): """import { Router } from "express"; import { getStatus } from "../controllers/appController.js"; const router = Router(); router.get("/", getStatus); export default router; """, prefixed(prefix, "src/routes/items.js"): """import { Router } from "express"; import { listItems } from "../controllers/itemController.js"; const router = Router(); router.get("/", listItems); export default router; """, prefixed(prefix, "src/controllers/appController.js"): """export function getStatus(_req, res) { res.json({ status: "ok" }); } """, prefixed(prefix, "src/controllers/itemController.js"): """import { getItems } from "../services/itemService.js"; export function listItems(_req, res) { res.json(getItems()); } """, prefixed(prefix, "src/services/appService.js"): f"""export function getAppSummary() {{ return "{project_name} includes routes, controllers, services, and starter configuration."; }} """, prefixed(prefix, "src/services/itemService.js"): """export function getItems() { return [ { id: 1, name: "Starter task", status: "ready" }, { id: 2, name: "Next iteration", status: "planned" } ]; } """, prefixed(prefix, "src/models/itemModel.js"): """export const itemShape = { id: "number", name: "string", status: "string" }; """, } def build_react_frontend_files(project_name: str, prefix: str) -> dict[str, str]: return { prefixed(prefix, "package.json"): json.dumps( { "name": project_name.lower().replace(" ", "-") + "-frontend", "private": True, "version": "0.1.0", "type": "module", "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"}, "dependencies": {"react": "^18.3.1", "react-dom": "^18.3.1"}, "devDependencies": {"vite": "^5.4.0", "@vitejs/plugin-react": "^4.3.1"}, }, indent=2, ) + "\n", prefixed(prefix, "index.html"): """ Project Starter
""", prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { port: 5173 } }); """, prefixed(prefix, "src/main.jsx"): """import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./styles.css"; ReactDOM.createRoot(document.getElementById("root")).render( ); """, prefixed(prefix, "src/App.jsx"): f"""import AppShell from "./components/AppShell"; import HomePage from "./pages/HomePage"; export default function App() {{ return ( ); }} """, prefixed(prefix, "src/components/AppShell.jsx"): """export default function AppShell({ title, children }) { return (

Generated by Project Agent

{title}

{children}
); } """, prefixed(prefix, "src/pages/HomePage.jsx"): """import { getProjectHealth } from "../services/api"; export default function HomePage() { const projectHealth = getProjectHealth(); return (

Starter Overview

This frontend is ready for your first feature slice.

API health source: {projectHealth}

); } """, prefixed(prefix, "src/services/api.js"): """export function getProjectHealth() { return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; } """, prefixed(prefix, "src/styles.css"): """:root { color-scheme: light; font-family: "Segoe UI", Arial, sans-serif; background: #f5f7fb; color: #132238; } body { margin: 0; } .app-shell { max-width: 960px; margin: 0 auto; padding: 32px 20px 56px; } .hero { margin-bottom: 24px; } .eyebrow { font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: #57657d; } .card { background: white; border-radius: 16px; padding: 24px; box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08); } """, } def build_vanilla_frontend_files(project_name: str, prefix: str) -> dict[str, str]: return { prefixed(prefix, "package.json"): json.dumps( { "name": project_name.lower().replace(" ", "-") + "-frontend", "private": True, "version": "0.1.0", "type": "module", "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"}, "devDependencies": {"vite": "^5.4.0"}, }, indent=2, ) + "\n", prefixed(prefix, "index.html"): """ Project Starter
""", prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite"; export default defineConfig({ server: { port: 5173 } }); """, prefixed(prefix, "src/main.js"): f"""import {{ renderHomePage }} from "./views/home.js"; import "./styles.css"; document.querySelector("#app").innerHTML = renderHomePage("{project_name}"); """, prefixed(prefix, "src/views/home.js"): """export function renderHomePage(title) { return `

Generated by Project Agent

${title}

This starter is ready for your first feature slice.

`; } """, prefixed(prefix, "src/services/api.js"): """export function getApiBaseUrl() { return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; } """, prefixed(prefix, "src/styles.css"): """body { margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: #f5f7fb; color: #132238; } .app-shell { max-width: 960px; margin: 0 auto; padding: 32px 20px 56px; } .card { background: white; border-radius: 16px; padding: 24px; box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08); } .eyebrow { font-size: 0.75rem; text-transform: uppercase; color: #57657d; } """, } def build_spring_backend_files(project_name: str, prefix: str) -> dict[str, str]: java_base = prefixed(prefix, "src/main/java/com/example/demo") resources_base = prefixed(prefix, "src/main/resources") return { prefixed(prefix, "pom.xml"): """ 4.0.0 com.example demo 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.3.2 17 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-maven-plugin """, prefixed(java_base, "Application.java"): """package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } """, prefixed(java_base, "controller/AppController.java"): """package com.example.demo.controller; import com.example.demo.service.AppService; import java.util.Map; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AppController { private final AppService appService; public AppController(AppService appService) { this.appService = appService; } @GetMapping("/") public Map status() { return Map.of("message", appService.status()); } } """, prefixed(java_base, "service/AppService.java"): f"""package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class AppService {{ public String status() {{ return "{project_name} backend is running."; }} }} """, prefixed(java_base, "model/AppModel.java"): """package com.example.demo.model; public class AppModel { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } """, prefixed(java_base, "repository/AppRepository.java"): """package com.example.demo.repository; import com.example.demo.model.AppModel; import java.util.List; import org.springframework.stereotype.Repository; @Repository public class AppRepository { public List findAll() { return List.of(); } } """, prefixed(resources_base, "application.properties"): """spring.application.name=demo server.port=8080 """, } def build_root_scripts( selected_stack: dict[str, str], project_kind: dict[str, Any], ) -> dict[str, str]: if project_kind["isFullStack"]: return build_fullstack_scripts(selected_stack) if project_kind["hasBackend"]: backend = selected_stack["backend"] if backend in {"FastAPI", "Flask"}: return build_python_scripts(".") if backend in {"Express", "NestJS"}: return build_node_scripts(".") if backend == "Spring Boot": return build_java_scripts(".") return build_node_scripts(".") def build_fullstack_scripts(selected_stack: dict[str, str]) -> dict[str, str]: backend_setup = "" backend_run_windows = "echo No backend runtime configured.\n" backend_run_unix = 'echo "No backend runtime configured."\n' if selected_stack["backend"] in {"FastAPI", "Flask"}: backend_setup = ( "if exist backend\\requirements.txt (\n" " python -m pip install -r backend\\requirements.txt\n" ")\n" ) backend_run_windows = ( 'start "Backend" cmd /k "cd backend && python -m uvicorn app.main:app --reload"\n' ) backend_run_unix = ( '(cd backend && python -m uvicorn app.main:app --reload) &\n' ) elif selected_stack["backend"] in {"Express", "NestJS"}: backend_setup = 'if exist backend\\package.json (\n pushd backend\n call npm install\n popd\n)\n' backend_run_windows = 'start "Backend" cmd /k "cd backend && npm run dev"\n' backend_run_unix = '(cd backend && npm run dev) &\n' elif selected_stack["backend"] == "Spring Boot": backend_setup = ( 'where mvn >nul 2>nul && (\n' ' pushd backend\n' ' call mvn install\n' ' popd\n' ') || echo Maven not found. Skipping backend install.\n' ) backend_run_windows = 'start "Backend" cmd /k "cd backend && mvn spring-boot:run"\n' backend_run_unix = '(cd backend && mvn spring-boot:run) &\n' return { "setup.bat": f"""@echo off setlocal {backend_setup}if exist frontend\\package.json ( pushd frontend call npm install popd ) echo Setup complete. """, "setup.sh": """#!/usr/bin/env bash set -e if [ -f backend/requirements.txt ]; then python -m pip install -r backend/requirements.txt fi if [ -f backend/package.json ]; then (cd backend && npm install) fi if [ -f backend/pom.xml ]; then if command -v mvn >/dev/null 2>&1; then (cd backend && mvn install) else echo "Maven not found. Skipping backend install." fi fi if [ -f frontend/package.json ]; then (cd frontend && npm install) fi echo "Setup complete." """, "run.bat": f"""@echo off setlocal {backend_run_windows}start "Frontend" cmd /k "cd frontend && npm run dev" """, "run.sh": f"""#!/usr/bin/env bash set -e {backend_run_unix}(cd frontend && npm run dev) & wait """, } def build_python_scripts(target_dir: str) -> dict[str, str]: directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/" windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\" return { "setup.bat": f"""@echo off setlocal python -m pip install -r {windows_prefix}requirements.txt echo Setup complete. """, "setup.sh": f"""#!/usr/bin/env bash set -e python -m pip install -r {directory_prefix}requirements.txt echo "Setup complete." """, "run.bat": f"""@echo off setlocal python -m uvicorn app.main:app --reload """, "run.sh": f"""#!/usr/bin/env bash set -e python -m uvicorn app.main:app --reload """, } def build_node_scripts(target_dir: str) -> dict[str, str]: directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/" windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\" return { "setup.bat": f"""@echo off setlocal pushd {windows_prefix or "."} call npm install popd echo Setup complete. """, "setup.sh": f"""#!/usr/bin/env bash set -e (cd {directory_prefix or "."} && npm install) echo "Setup complete." """, "run.bat": f"""@echo off setlocal pushd {windows_prefix or "."} call npm run dev popd """, "run.sh": f"""#!/usr/bin/env bash set -e (cd {directory_prefix or "."} && npm run dev) """, } def build_java_scripts(target_dir: str) -> dict[str, str]: directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/" windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\" return { "setup.bat": f"""@echo off setlocal where mvn >nul 2>nul && ( pushd {windows_prefix or "."} call mvn install popd ) || ( echo Maven not found. Skipping install. ) """, "setup.sh": f"""#!/usr/bin/env bash set -e if command -v mvn >/dev/null 2>&1; then (cd {directory_prefix or "."} && mvn install) else echo "Maven not found. Skipping install." fi """, "run.bat": f"""@echo off setlocal pushd {windows_prefix or "."} call mvn spring-boot:run popd """, "run.sh": f"""#!/usr/bin/env bash set -e (cd {directory_prefix or "."} && mvn spring-boot:run) """, } def normalize_string_list(value: Any) -> list[str]: if value is None: return [] if isinstance(value, str): lines = [item.strip(" -*\t") for item in value.splitlines()] return [line for line in lines if line] if isinstance(value, Mapping): return [f"{key}: {str(item).strip()}" for key, item in value.items() if str(item).strip()] if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return [str(item).strip() for item in value if str(item).strip()] text = str(value).strip() return [text] if text else [] def normalize_modules(value: Any) -> list[dict[str, Any]]: if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): return [] modules: list[dict[str, Any]] = [] for item in value: if not isinstance(item, Mapping): continue name = str(item.get("name") or "").strip() if not name: continue modules.append( { "name": name, "purpose": str(item.get("purpose") or "").strip(), "keyFiles": normalize_string_list(item.get("keyFiles")), } ) return modules def merge_modules( primary: list[dict[str, Any]], secondary: list[dict[str, Any]], ) -> list[dict[str, Any]]: merged: dict[str, dict[str, Any]] = {} for collection in (secondary, primary): for module in collection: key = module["name"].strip().lower() current = merged.setdefault( key, {"name": module["name"], "purpose": "", "keyFiles": []}, ) if module.get("purpose"): current["purpose"] = module["purpose"] current["keyFiles"] = dedupe_list(current["keyFiles"] + normalize_string_list(module.get("keyFiles"))) return list(merged.values()) def normalize_env_variables(value: Any) -> list[dict[str, str]]: if isinstance(value, Mapping): value = [{"name": key, "value": str(item), "description": ""} for key, item in value.items()] if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): return [] env_vars: list[dict[str, str]] = [] for item in value: if not isinstance(item, Mapping): continue name = str(item.get("name") or "").strip() if not name: continue env_vars.append( { "name": name, "value": str(item.get("value") or "").strip(), "description": str(item.get("description") or "").strip(), } ) return env_vars def normalize_required_inputs(value: Any) -> list[dict[str, Any]]: if isinstance(value, Mapping): value = [value] if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): return [] required_inputs: list[dict[str, Any]] = [] for item in value: if not isinstance(item, Mapping): continue name = str(item.get("name") or "").strip() if not name: continue required_flag = item.get("required") required_inputs.append( { "name": name, "required": _coerce_required_flag(required_flag), "example": str(item.get("example") or item.get("value") or "").strip(), "whereToAdd": str(item.get("whereToAdd") or ".env").strip() or ".env", "purpose": str(item.get("purpose") or item.get("description") or "").strip(), } ) return dedupe_required_inputs(required_inputs) def merge_required_inputs( primary: list[dict[str, Any]], secondary: list[dict[str, Any]], ) -> list[dict[str, Any]]: merged: dict[str, dict[str, Any]] = {} for collection in (secondary, primary): for item in collection: name = str(item.get("name") or "").strip() if not name: continue merged[name] = { "name": name, "required": _coerce_required_flag(item.get("required", True)), "example": str(item.get("example") or "").strip(), "whereToAdd": str(item.get("whereToAdd") or ".env").strip() or ".env", "purpose": str(item.get("purpose") or "").strip(), } return list(merged.values()) def merge_env_variables( primary: list[dict[str, str]], secondary: list[dict[str, str]], ) -> list[dict[str, str]]: merged: dict[str, dict[str, str]] = {} for collection in (secondary, primary): for item in collection: merged[item["name"]] = { "name": item["name"], "value": item.get("value", ""), "description": item.get("description", ""), } return list(merged.values()) def normalize_files(value: Any) -> list[dict[str, str]]: if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): return [] files: list[dict[str, str]] = [] for item in value: if not isinstance(item, Mapping): continue path = clean_relative_path(item.get("path")) if not path: continue files.append({"path": path, "content": trim_content_lines(str(item.get("content") or ""), allow_long=True)}) return files def merge_file_entries( primary: list[dict[str, str]], secondary: list[dict[str, str]], ) -> list[dict[str, str]]: merged: dict[str, dict[str, str]] = {} for file_entry in primary: merged[file_entry["path"]] = file_entry for file_entry in secondary: merged[file_entry["path"]] = file_entry return list(merged.values()) def build_chosen_stack(selected_stack: dict[str, str]) -> list[str]: labels = { "language": "Language", "frontend": "Frontend", "backend": "Backend", "database": "Database", "aiTools": "AI / Tools", "deployment": "Deployment", } return [ f"{labels[field]}: {selected_stack[field]}" for field in STACK_FIELDS if selected_stack.get(field) not in {"", "Auto"} ] def clean_relative_path(value: Any) -> str: path = str(value or "").replace("\\", "/").strip().strip("/") if not path or path.startswith(".") or ".." in path.split("/"): return "" return path def is_standard_file_path(path: str) -> bool: standard_names = { "readme.md", "requirements.txt", "package.json", "pom.xml", ".env.example", "setup.bat", "setup.sh", "run.bat", "run.sh", "vite.config.js", "server.js", "index.html", "main.py", "main.jsx", "main.js", "app.jsx", "application.java", } return Path(path).name.lower() in standard_names def dedupe_manifest(items: list[dict[str, str]]) -> list[dict[str, str]]: seen: set[str] = set() result: list[dict[str, str]] = [] for item in items: path = item["path"] if path in seen: continue seen.add(path) result.append(item) return result def dedupe_required_inputs(items: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]: seen: set[str] = set() result: list[dict[str, Any]] = [] for item in items: name = str(item.get("name") or "").strip() if not name: continue key = name.lower() if key in seen: continue seen.add(key) result.append( { "name": name, "required": _coerce_required_flag(item.get("required", True)), "example": str(item.get("example") or "").strip(), "whereToAdd": str(item.get("whereToAdd") or ".env").strip() or ".env", "purpose": str(item.get("purpose") or "").strip(), } ) return result def trim_content_lines(content: str, allow_long: bool = False) -> str: if allow_long: return content lines = content.splitlines() if len(lines) <= MAX_CUSTOM_FILE_LINES: return content return "\n".join(lines[:MAX_CUSTOM_FILE_LINES]).rstrip() + "\n" def safe_component_name(value: str) -> str: parts = re.findall(r"[A-Za-z0-9]+", value) return "".join(part.capitalize() for part in parts) or "GeneratedComponent" def safe_python_name(value: str) -> str: cleaned = re.sub(r"[^a-zA-Z0-9_]+", "_", value).strip("_").lower() return cleaned or "generated_item" def safe_js_name(value: str) -> str: parts = re.findall(r"[A-Za-z0-9]+", value) if not parts: return "generatedItem" head = parts[0].lower() tail = "".join(part.capitalize() for part in parts[1:]) return head + tail def prefixed(prefix: str, path: str) -> str: base = Path(prefix) if prefix else Path() return (base / path).as_posix() def dedupe_list(items: Sequence[str]) -> list[str]: seen: set[str] = set() result: list[str] = [] for item in items: cleaned = str(item).strip() if not cleaned: continue key = cleaned.lower() if key in seen: continue seen.add(key) result.append(cleaned) return result def _context_mentions_any(lowered_text: str, keywords: Sequence[str]) -> bool: return any(keyword in lowered_text for keyword in keywords) def _coerce_required_flag(value: Any) -> bool: if isinstance(value, bool): return value if value is None: return True if isinstance(value, str): return value.strip().lower() not in {"false", "0", "no", "optional"} return bool(value)