diff --git a/app/configs.py b/app/configs.py index e5d41a3..bea86f1 100644 --- a/app/configs.py +++ b/app/configs.py @@ -1,302 +1,23 @@ import os from pathlib import Path -from prompt_store import PromptStore from dotenv import load_dotenv + + load_dotenv() -MONGO_URI = "mongodb+srv://lahiruprabhath099:qYVDTCA22Mds96KV@cluster1.diq5rte.mongodb.net/?retryWrites=true&w=majority&appName=Cluster1&tlsAllowInvalidCertificates=true" -ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") + WORKSPACE_DIR = Path(os.path.join(os.getcwd(), "workspace")) TEMPLATE_DIR = os.path.join(os.getcwd(), "templates/workspace") PACKAGE_MANAGER = "npm" MIN_NODE = (18, 18, 0) -store = PromptStore(MONGO_URI, db_name="GameOnAI", collection_name="prompts") - -prompt_template = store.get_prompt("project_planner_prompt") - -INSTRUCTIONS= """ -**Game Logic & Features:** -- AI opponents with multiple difficulty levels (Easy, Medium, Hard) -- Single-player vs AI -- 2 players multiplayer option -- Single-Player vs AI feature should work without connecting to web Socket or not -- page.tsx should inside the app folder. -- Multiplayer game isn't not playing from the same device. -- Win/lose/draw condition detection with pixel-perfect accuracy -- Comprehensive error handling for all edge cases -- Player statistics and scoring system -- Eliminate lynting error and type defining errors - -## UI/UX PRIORITY SPECIFICATIONS: - -**Visual Design:** -- Modern, sleek interface with smooth micro-animations -- CRITICAL : Text colors, UI componenet allignments, spacing, utilizing the space as UX professionals. -- the playing areas should designed perfectly as match per the requested game. -- Space utilisation, deciding the size of each component, allignments, positioning should be world class -- No instructions and No dcumentaions. -- Eye catching Gradient backgrounds with dynamic color transitions -- Eye-catching color palettes with strategic color mixing -- Glassmorphism effects and card-based layouts -- Subtle shadows, borders, and depth effects -- Hover animations and interactive feedback -- Loading spinners, success states, and error notifications -- Minimal text, no guidelines or explanations -- Player turn indicators must be clear and immediate - -##CRITICAL for MULTIPLAYER IMPLEMENTATION: - -- Multiplayer game feature is only allowed to play via web soket implementaion in multiple devices. -- But must not create join room UI feature in multiplayer option. You can assume when game is opened it is created already. -- Understand that multiplayer game option is not playing in the same device. It is playing from two devices -- UI States are update through the websocket in two players screen accordingly. - -CRITICAL : UI/UX components positioning , color mixing ,player turns return should be world class -Use filled color game objects. Less Text , no guidelines or explanations in the UI -""" - -summarizer_prompt = """You are a game development requirements analyzer. Your task is to extract and summarize the core game concept and requirements from user queries, focusing only on gameplay mechanics, features, and user experience elements. - -INSTRUCTIONS: -1. Extract the main game concept and identify the specific game type -2. Summarize ONLY the gameplay requirements, features, and mechanics -3. Ignore all technical implementation details including: - - Database implementations - - WebSocket connections - - API specifications - - Server architecture - - Programming languages - - Framework choices - - Deployment details - - Technical stack mentions - -4. Focus on: - - Game mechanics and rules - - Player interactions - - Game objectives and win conditions - - UI/UX requirements - - Visual elements and themes - - Game modes or difficulty levels - - Player progression systems - - Core features and functionality - -5. Return your response in this exact JSON format: -{{ - "summarized_text": "Clear, concise summary of the game requirements focusing on gameplay mechanics, features, and user experience", - "game_name": "Identified game type or name (e.g., 'tic_tac_toe', 'snake_game', 'puzzle_game', 'platformer', etc.)" -}} - -EXAMPLES: - -Input: "I want to build a tic-tac-toe game with React and Node.js backend, using WebSocket for real-time multiplayer and MongoDB for storing game states" -Output: {{ - "summarized_text": "A classic tic-tac-toe game where two players take turns placing X's and O's on a 3x3 grid, with the goal of getting three marks in a row (horizontally, vertically, or diagonally). Features real-time multiplayer gameplay.", - "game_name": "tic_tac_toe" -}} - -Input: "Create a snake game using Python with pygame, store high scores in SQLite database and implement collision detection algorithms" -Output: {{ - "summarized_text": "A snake game where the player controls a growing snake to eat food while avoiding collisions with walls and the snake's own body. Features score tracking and high score system.", - "game_name": "snake_game" -}} - -Now analyze the following user query and return the JSON response: - -{user_query}""" - - -sample_prompt = """You are an expert Next.js developer. continue a incomplete game development project following the guidelines. - -**user_query:** {requirements} - -Return a JSON object with this exact structure. No string literal output. The files field should be a list not a string literal. Also the object inside the file list should not be string json text. It should be a JSON type object: - -```json -{{ - "description": "Brief project description", - "development_plan": "Development plan overview", - "directories": ["app/components", "app/hooks", "app/types"], - "files": [ - {{ - "path": "app/file1.tsx", - "description": "File description", - "content": "complete functional code" - }} - ], - "packages": [ - {{ - "name": "package-name", - "dev": false - }} - ] -}} -``` - -***Guidelines*** -***CRITICAL*** -Your goal is to continue from the avialble web socket connection. - - -**Game Logic & Features:** -- continue game implementaion. creat components like, types and page.tsx, -- db connection already implemented. -- AI opponents with multiple difficulty levels (Easy, Medium, Hard) -- Single-Player vs AI and - Multi player options -- multiplayer option feature should work without connecting to web Socket -- Create seperate componenets, type defenitions -- componenets folder, types folder, hooks folder, page.tsx must be inside the app folder. -- Multiplayer game isn't not playing from the same device. It is playing from two devices -- Win/lose/draw condition detection with pixel-perfect accuracy -- Comprehensive error handling for all edge cases -- Player statistics and scoring system -- Eliminate lynting error and type defining errors - -## UI/UX PRIORITY SPECIFICATIONS: - -**Visual Design:** -- Modern, sleek interface with smooth micro-animations -- CRITICAL : Text colors, UI componenet allignments, spacing, utilizing the space as UX professionals. -- the playing areas should designed perfectly as match per the requested game. -- Space utilisation, deciding the size of each component, allignments, positioning should be world class -- No instructions and No dcumentaions. -- Eye catching Gradient backgrounds with dynamic color transitions -- Eye-catching color palettes with strategic color mixing -- Glassmorphism effects and card-based layouts -- Subtle shadows, borders, and depth effects -- Hover animations and interactive feedback -- Loading spinners, success states, and error notifications -- Minimal text, no guidelines or explanations -- Player turn indicators must be clear and immediate - - -##Important for MULTIPLAYER IMPLEMENTATION: -I'll provide an example codes how the multiplayer game is implemented. you want to use exact websocket and api related concept variables names as exactly as same. -see the example code below to handle states in each player device with multiplayer support. The example is to guioide to the multiplayer connection setup. the game play and componenet should design according the required game - -{main_page} - - - -## **CRITICAL FOR MULTIPLAYER OPTION - ** -- Multiplayer games ONLY via WebSocket on multiple devices (not same device) -- NO join room UI - assume game room already created when opened -- Use EXACTLY the same payload variables, state variables, API names, event names as reference example -- Handle all socket events in page.tsx according to requested game -- gameSessionUUID value must be parsed from params in page.tsx -- Define all type definitions in page.tsx -- Do NOT create additional APIs -- Player names should display by actual name and profile accordingly - -## PLAYER TURN DISPLAY -- When player turns change, display logically updates on each user's screen -- UI states update through WebSocket across both players' screens simultaneously - - -## IMPLEMENTATION -- Extract gameSessionUUID by parsing URL parameters in page.tsx -- Create game components in component folder -- Game logic and UI adapt to requested game while maintaining WebSocket structure -- Real-time state synchronization between separate devices -""" - - - -fixer_prompt = """You are a Next.js/TypeScript code fixer. You MUST generate fix actions. - -BUILD ERROR: {error} -ROOT CAUSE: {root_cause} -CURRENT CODE: {current_code} - -CRITICAL RULE: Since an error exists, you MUST provide at least one action. Empty actions = system failure. - -MANDATORY OUTPUT STRUCTURE: -{{ - "summary": "Error type and fix method", - "actions": [REQUIRED - CANNOT BE EMPTY] -}} - -FORCE ACTION GENERATION: -If you identify the fix but hesitate to provide actions, you MUST still provide them. -If you're unsure of exact content, provide your best attempt rather than empty actions. -If you can't determine the exact file, use the most likely file path from the error. - -COMMON ERROR → ACTION MAPPING: -- Type error → write_file with type fix -- Import error → write_file with correct imports -- Missing package → install_package -- Syntax error → write_file with syntax fix -- Any other error → write_file with attempted fix- -- Hardly skip warnings. No need to fix -- You have two task mainly - 1 - fix the error. - 2 - Must provide the rewrite content with fix in actions key - -EMERGENCY FALLBACK (if no specific fix identified): -Always include at least this action: -{{"tool": "write_file", "args": {{"path": "[most_likely_file_from_error]", "content": "[current_code_with_obvious_fix_attempt]"}} - -VALIDATION CHECK: -- Before responding, verify actions array is NOT empty -- If actions is empty, add a fallback write_file action -- Never explain why you can't fix - just provide your best fix attempt - -RESPONSE (JSON only): -{{ - "summary": "Brief fix description", - "actions": [{{"tool": "write_file", "args": {{"path": "file.ts", "content": "complete fixed file content"}}] -}}""" - - -improver_prompt = """ -You are an expert Next.js developer with 10+ years of experience building production-grade applications. You specialize in game development and have a track record of maintaining high-quality, stable codebases. - -## CONTEXT -- Initial Project: {instructions} -- Change Request: {requirement} -- Target Code: {codes} - -## YOUR MISSION -Analyze the provided code and implement the requested changes with surgical precision. This is a WORKING system that requires minimal, targeted modifications. - -## CRITICAL CONSTRAINTS -1. **Preserve Functionality**: The current code is stable and functional - make only necessary changes -2. **Minimal Impact**: Avoid refactoring or "improvements" beyond the specific requirement -3. **Precision Over Perfection**: Focus on the exact change requested, not code optimization -4. **Risk Assessment**: If the change could break existing functionality, explain the risks - -## ANALYSIS PROCESS -1. **Understand**: What specific functionality needs to change? -2. **Locate**: Which exact files/sections require modification? -3. **Isolate**: What's the minimal change that satisfies the requirement? -4. **Implement**: Apply changes while preserving existing code patterns -5. **Do not** change any route.ts file if provided in any case. They are provided to as helpers to change other required files - -## RESPONSE FORMAT -The √ name should return as it is provided in the Target code section. -You MUST respond with a valid JSON object in this exact format: - -{{ - "changes": [ - {{ - "file_path": "file_path_1", - "content": "complete_rewritten_file_content_with_changes_applied" - }}, - {{ - "file_path": "file_path", - "content": "complete_rewritten_file_content_with_changes_applied" - }} - ], - "description": "Brief explanation of changes made or additional information needed" -}} +MONGO_URI = "mongodb+srv://lahiruprabhath099:qYVDTCA22Mds96KV@cluster1.diq5rte.mongodb.net/?retryWrites=true&w=majority&appName=Cluster1&tlsAllowInvalidCertificates=true" +ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") -## SPECIAL CASES -If the change requested cannot be achieved by modifying the target code (it might require additional files, dependencies, or information not provided), respond with an empty changes list and use the description field to request the additional information needed to complete the task. +os.environ["VERCEL_SCOPE"] = "sameera-pereras-projects" +os.environ["VERCEL_TOKEN"] = "y93uhh2AXWPgkno0HquUsTNj" -Example for cases requiring additional information: -{{ - "changes": [], - "description": "Cannot complete the requested change. Additional information needed: [specify what's missing - e.g., missing component files, required dependencies, configuration files, etc.]" -}} -""" \ No newline at end of file +VERCEL_TOKEN = os.getenv("VERCEL_TOKEN") # "y93uhh2AXWPgkno0HquUsTNj" # required for API/CLI +VERCEL_SCOPE = os.getenv("VERCEL_SCOPE") # "sameera-pereras-projects" # optional team slug +VERCEL_TIMEOUT_SECONDS = int(os.getenv("VERCEL_TIMEOUT_SECONDS", "30")) \ No newline at end of file diff --git a/app/connectors/mongodb_connector.py b/app/connectors/mongodb_connector.py index e69de29..596b38f 100644 --- a/app/connectors/mongodb_connector.py +++ b/app/connectors/mongodb_connector.py @@ -0,0 +1,30 @@ +from pymongo import MongoClient +from typing import Optional + +class MongoConnection: + def __init__(self, mongo_uri: str, db_name: str = "GameOnAI", collection_name: str = "Prompts"): + self.client = MongoClient(mongo_uri) + self.db = self.client[db_name] + self.collection = self.db[collection_name] + + def save_prompt(self, name: str, content: str, metadata: Optional[dict] = None) -> str: + existing = self.collection.find_one({"name": name}) + if existing: + self.collection.update_one( + {"_id": existing["_id"]}, + {"$set": {"content": content, "metadata": metadata or {}}} + ) + return str(existing["_id"]) + result = self.collection.insert_one({ + "name": name, + "content": content, + "metadata": metadata or {} + }) + return str(result.inserted_id) + + def get_prompt(self, name: str) -> Optional[str]: + result = self.collection.find_one({"name": name}) + return result["content"] if result else None + + def list_prompts(self): + return [doc["name"] for doc in self.collection.find({}, {"name": 1})] diff --git a/app/main.py b/app/main.py index fc534fb..59dbf58 100644 --- a/app/main.py +++ b/app/main.py @@ -1,72 +1,121 @@ from typing import Dict, Any from flask import Flask, request, jsonify +from flask_socketio import SocketIO +from flask_cors import CORS from langgraph.checkpoint.memory import MemorySaver from src.agents.game_dev_agent.graph_builder import GraphBuilder -from langchain_core.messages import HumanMessage -from src.utils.deploy import push_workspace_to_github +from langchain_core.messages import HumanMessage, AIMessage +from src.utils.deploy import push_workspace_to_github, deploy_workspace_to_github_and_vercel +from src.services.conversation_service import WebSocketHandler, setup_websocket_events app = Flask(__name__) +app.config['SECRET_KEY'] = 'your-secret-key' + +CORS(app, origins=["http://localhost:3000", "http://127.0.0.1:3000" ,"https://gameon-ai.vercel.app"]) +socketio = SocketIO(app, cors_allowed_origins=["http://localhost:3000", "http://127.0.0.1:3000", "https://gameon-ai.vercel.app"]) checkpointer = MemorySaver() graph_builder = GraphBuilder() app_graph = graph_builder.build_app(checkpointer=checkpointer) +ws_handler = WebSocketHandler(socketio) + +setup_websocket_events(socketio) + +@app.route("/api/build", methods=["POST", "OPTIONS"]) +def build_game() -> tuple[Any, int]: -@app.route("/build", methods=["POST"]) -def build_game(): + if request.method == "OPTIONS": + return "", 200 + try: payload: Dict[str, Any] = request.get_json(force=True) except Exception: return jsonify({"ok": False, "error": "Invalid JSON body"}), 400 - thread_id = payload.get("thread_id") - requirements = payload.get("requirements") + thread_id: str = payload.get("thread_id") + requirements: str = payload.get("requirements") if not isinstance(thread_id, str) or not thread_id: return jsonify({"ok": False, "error": "`thread_id` (string) is required"}), 400 if requirements is not None and not isinstance(requirements, str): return jsonify({"ok": False, "error": "`requirements` must be a string"}), 400 - config = {"configurable": {"thread_id": thread_id}} + config: Dict[str, Any] = {"configurable": {"thread_id": thread_id}, "recursion_limit": 100} try: snapshot = app_graph.get_state(config) - has_state = bool(getattr(snapshot, "values", None)) + has_state: bool = bool(getattr(snapshot, "values", None)) except Exception: has_state = False snapshot = None - if not has_state: - + print(f"initializing graph") input_state = graph_builder.init_state(requirements=requirements) else: - input_state = { "messages": HumanMessage(content=requirements), } - - - for chunk in app_graph.stream(input=input_state, config=config): - print("---- CHUNK ----") - print("---------------") - - return jsonify({"Success": True, "message": "Build completed successfully"}), 200 - - -@app.route("/deploy" , methods = ["POST"]) -def deploy_to_github(): + print(f"input_state : {input_state}") try: - payload: Dict[str, Any] = request.get_json(force=True) - except Exception: - return jsonify({"ok": False, "error": "Invalid JSON body"}), 400 - github_repo = payload.get("thread_id") - branch = payload.get("requirements") - commit_message = payload.get("commit_message") - - push_workspace_to_github(branch_name=branch , repo_url=github_repo, commit_message=commit_message) + for chunk in app_graph.stream(input=input_state, config=config): + print("---- CHUNK ----") + + for node_name, node_data in chunk.items(): + if isinstance(node_data, dict) and "messages" in node_data: + messages = node_data["messages"] + if isinstance(messages, list): + for message in messages: + if isinstance(message, AIMessage) and hasattr(message, 'content'): + ws_handler.emit_build_progress( + thread_id=thread_id, + content=message.content, + node_name=node_name + ) + elif isinstance(messages, AIMessage) and hasattr(messages, 'content'): + ws_handler.emit_build_progress( + thread_id=thread_id, + content=messages.content, + node_name=node_name + ) + + + ws_handler.emit_build_complete( + thread_id=thread_id, + success=True, + message="Build completed successfully" + ) + + return jsonify({"Success": True, "message": "Build completed successfully"}), 200 + + except Exception as e: + ws_handler.emit_build_complete( + thread_id=thread_id, + success=False, + message=f"Build failed: {str(e)}" + ) + return jsonify({"ok": False, "error": str(e)}), 500 + + +@app.route("/deploy", methods=["POST", "OPTIONS"]) +def deploy_to_github_and_vercel(): + if request.method == "OPTIONS": + return "", 200 + + payload = request.get_json(force=True) + repo = payload.get("repository") + branch = payload.get("branch") + message = payload.get("commit_message") or "Add workspace files" + + ok, prod_url, err = deploy_workspace_to_github_and_vercel(repo, branch, message) + if ok: + return jsonify({"Success": True, "message": "Deployment completed successfully", "production_url": prod_url}), 200 + else: + return jsonify({"ok": False, "error": err}), 500 + -if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000, debug=False) +if __name__ == "__main__": + socketio.run(app, host="0.0.0.0", port=8000, debug=False) \ No newline at end of file diff --git a/app/prompt_store.py b/app/prompt_store.py index b488ccc..e69de29 100644 --- a/app/prompt_store.py +++ b/app/prompt_store.py @@ -1,30 +0,0 @@ -from pymongo import MongoClient -from typing import Optional - -class PromptStore: - def __init__(self, mongo_uri: str, db_name: str = "GameOnAI", collection_name: str = "Prompts"): - self.client = MongoClient(mongo_uri) - self.db = self.client[db_name] - self.collection = self.db[collection_name] - - def save_prompt(self, name: str, content: str, metadata: Optional[dict] = None) -> str: - existing = self.collection.find_one({"name": name}) - if existing: - self.collection.update_one( - {"_id": existing["_id"]}, - {"$set": {"content": content, "metadata": metadata or {}}} - ) - return str(existing["_id"]) - result = self.collection.insert_one({ - "name": name, - "content": content, - "metadata": metadata or {} - }) - return str(result.inserted_id) - - def get_prompt(self, name: str) -> Optional[str]: - result = self.collection.find_one({"name": name}) - return result["content"] if result else None - - def list_prompts(self): - return [doc["name"] for doc in self.collection.find({}, {"name": 1})] diff --git a/app/src/agents/game_dev_agent/codeEmbedding.py b/app/src/agents/game_dev_agent/codeEmbedding.py index 39ed39a..cb4e1b5 100644 --- a/app/src/agents/game_dev_agent/codeEmbedding.py +++ b/app/src/agents/game_dev_agent/codeEmbedding.py @@ -10,7 +10,6 @@ from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter from configs import WORKSPACE_DIR, MIN_NODE, PACKAGE_MANAGER -from prompt_store import PromptStore from src.utils.command_utils import CommandUtils print(f"workspace_inside_nodes : {WORKSPACE_DIR}") diff --git a/app/src/agents/game_dev_agent/graph_builder.py b/app/src/agents/game_dev_agent/graph_builder.py index 712814f..7d5eda3 100644 --- a/app/src/agents/game_dev_agent/graph_builder.py +++ b/app/src/agents/game_dev_agent/graph_builder.py @@ -78,6 +78,7 @@ def build_app(self, checkpointer=None): ) graph.add_edge("summarizer","plan") graph.add_edge("retriever", "enhancer") + graph.add_edge("enhancer" , "build") graph.add_edge('plan','tools' ) graph.add_conditional_edges( "tools", diff --git a/app/src/agents/game_dev_agent/nodes.py b/app/src/agents/game_dev_agent/nodes.py index a9e1050..8e841ff 100644 --- a/app/src/agents/game_dev_agent/nodes.py +++ b/app/src/agents/game_dev_agent/nodes.py @@ -3,7 +3,7 @@ import json import shutil from pathlib import Path -from typing import List, TypedDict, Annotated, Dict, Optional, Any +from typing import List, Dict, Any from langchain_anthropic import ChatAnthropic from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode @@ -11,22 +11,17 @@ from langchain_core.messages import ToolMessage from langchain.tools import tool -from configs import ANTHROPIC_API_KEY -from src.data_models.schemas import ProjectPlan, FixPlan, BuildState, Summarize -from src.agents.game_dev_agent.tools import read_file_tool, execute_project_plan_tool -from src.utils.command_utils import CommandUtils -from src.agents.game_dev_agent.tool_helpers import execute_plan -from src.data_models.schemas import ProjectPlan, ErrorFiles, UpdatedCode -from configs import WORKSPACE_DIR, TEMPLATE_DIR, MIN_NODE, PACKAGE_MANAGER, prompt_template, sample_prompt, fixer_prompt, improver_prompt, INSTRUCTIONS, summarizer_prompt -from prompt_store import PromptStore +from configs import ANTHROPIC_API_KEY,WORKSPACE_DIR, TEMPLATE_DIR, MIN_NODE, PACKAGE_MANAGER, MONGO_URI +from connectors.mongodb_connector import MongoConnection +from src.agents.game_dev_agent.tools import read_file_tool, execute_project_plan_tool, execute_plan from src.agents.game_dev_agent.codeEmbedding import CodeEmbeddings +from src.data_models.schemas import ProjectPlan, FixPlan, BuildState, Summarize, ErrorFiles, UpdatedCode +from src.utils.command_utils import CommandUtils -MONGO_URI = "mongodb+srv://lahiruprabhath099:qYVDTCA22Mds96KV@cluster1.diq5rte.mongodb.net/?retryWrites=true&w=majority&appName=Cluster1&tlsAllowInvalidCertificates=true" -store = PromptStore(MONGO_URI, db_name="GameOnAI", collection_name="prompts") -print(f"workspace_inside_nodes : {WORKSPACE_DIR}") - + +prompt_collection = MongoConnection(MONGO_URI, db_name="GameOnAI", collection_name="prompt_templates") command_utils = CommandUtils(workspace=WORKSPACE_DIR, min_node=MIN_NODE, package_manager=PACKAGE_MANAGER) @@ -92,14 +87,21 @@ def summarizer(state : BuildState): print("Graph running started" ) llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0.1, api_key=ANTHROPIC_API_KEY, max_tokens_to_sample=32000) structured_llm = llm.with_structured_output(Summarize) + summarizer_prompt = prompt_collection.get_prompt(name="summarizer_prompt") formatted_prompt = summarizer_prompt.format(user_query = state.get("requirements")) response : Summarize = structured_llm.invoke([HumanMessage(content=formatted_prompt)]) response = response.model_dump() print(response) return { "game_name" : response["game_name"], + "messages" :[ + AIMessage( + content=response["reply"] + ) + ], "summary" : response["summarized_text"] } + def get_project_plan(requirements: str): """Get a project plan from LLM.""" print("🤖 Getting project plan from LLM...") @@ -109,23 +111,25 @@ def get_project_plan(requirements: str): if os.path.exists(os.path.join(os.getcwd(), 'workspace')): shutil.rmtree(os.path.join(os.getcwd(), 'workspace')) - print("Removing workspace !") + print("Removing existing workspace !") main_page = sub_comm_utils.read_file('app/page.tsx') command_utils.move_file(TEMPLATE_DIR, os.path.join(os.getcwd() , 'workspace')) - - command_utils.remove_file(path = "app/page.tsx") + # command_utils.remove_file(path = "app/page.tsx") + if os.path.exists(os.path.join(os.getcwd(), "app/page.tsx")): + command_utils.remove_file(path="app/page.tsx") llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0.1, api_key=ANTHROPIC_API_KEY, max_tokens_to_sample=32000) - structured_llm = llm.with_structured_output(ProjectPlan) - - formatted_prompt = sample_prompt.format(requirements=requirements, main_page=main_page) + developer_prompt = prompt_collection.get_prompt("developer_prompt") + formatted_prompt = developer_prompt.format(requirements=requirements, main_page=main_page) response = llm.invoke([HumanMessage(content=formatted_prompt)]) - raw = response.content # from llm.invoke(...) + + raw = response.content parsed_json = extract_json_object(raw) response = ProjectPlan(**parsed_json) - print(f"plan : {response}") - return response + + return response + except Exception as e: print(f"❌ Error getting project plan: {str(e)}") import traceback @@ -138,7 +142,6 @@ def extract_json_object(text: str) -> dict: s = text.strip() - # Strip ```json ... ``` or ``` ... ``` if s.startswith("```"): s = re.sub(r"^```(?:json)?\s*|\s*```$", "", s, flags=re.DOTALL).strip() @@ -152,7 +155,7 @@ def extract_json_object(text: str) -> dict: def plan_node(state: BuildState): """Call LLM once, fill state with the plan, and enqueue a tool call.""" - print(f"round {state["n_rounds"]}") + print(f"round {state['n_rounds']}") plan_obj = get_project_plan(state["summary"]) if plan_obj is None: @@ -169,7 +172,7 @@ def plan_node(state: BuildState): "packages": plan["packages"], "messages": [ AIMessage( - content="Executing generated project plan.", + content=f"Executing generated project plan : \n{plan['description']}", tool_calls=[{ "name": "execute_project_plan", "args": {"plan" : plan}, @@ -186,7 +189,6 @@ def project_tools_node(state: BuildState): msgs: List[BaseMessage] = out["messages"] if isinstance(out, dict) else out tool_msg = next((m for m in reversed(msgs) if isinstance(m, ToolMessage)), None) - print(f"project tool message: {tool_msg}") if not tool_msg: result: Dict[str, Any] = {"success": False, "error": "No ToolMessage returned"} else: @@ -220,7 +222,7 @@ def project_tools_node(state: BuildState): return updates def build_tool_node(state: BuildState): - print(f"🏗️ Running build for the {state.get("fix_attempts")+1} time...") + print(f"🏗️ Running build for the {state.get('fix_attempts')+1} time...") build_tool = ToolNode([run_script_tool]) out = build_tool.invoke({"messages": state.get("messages", [])}) @@ -250,12 +252,11 @@ def build_tool_node(state: BuildState): def error_analyzer(state: BuildState): - prompt = store.get_prompt("error_analyzer_prompt") - + error_analyzer_prompt = prompt_collection.get_prompt("error_analyzer_prompt") llm = ChatAnthropic(model="claude-3-5-sonnet-20241022", temperature=0.1, api_key=ANTHROPIC_API_KEY, max_tokens_to_sample=8192) structured_llm = llm.with_structured_output(ErrorFiles) - print(f" Build output : {state.get("build_output")}") - formatted_prompt = prompt.format(error=state.get("build_output"), files = state.get("files")) + formatted_prompt = error_analyzer_prompt.format(error=state.get("build_output"), files = state.get("files")) + response : ErrorFiles = structured_llm.invoke([HumanMessage(content=formatted_prompt)]) response = response.model_dump() @@ -265,10 +266,14 @@ def error_analyzer(state: BuildState): state["fix_attempts"] = 0 print(f"Build is completed successfully") - print(state["messages"]) - + ai_message = "successfully Completed the project, Ready to deployment" + else : + ai_message = f"building attempt : {state['fix_attempts']+1} \nBuild failed due to an error, fixing error and rebuilding" return { "is_error" : response["is_error"], + "messages" : [AIMessage( + content=ai_message, + )], "error_files" : response["files"], "root_cause" : response["root_cause"], "n_rounds" : state["n_rounds"], @@ -287,7 +292,7 @@ def code_fixer(state: BuildState): "fix_plan": None, "messages": state.get("messages", []) + [AIMessage(content="No build output to diagnose.")], } - prompt_template = store.get_prompt("code_fixer_prompt") + fixer_prompt = prompt_collection.get_prompt("code_fixer_prompt") try: llm = ChatAnthropic( model="claude-sonnet-4-20250514", @@ -309,8 +314,7 @@ def code_fixer(state: BuildState): formatted_prompt = fixer_prompt.format(error=state.get("build_output",""), root_cause = state.get("root_cause"), current_code=errro_codes) - fixer_response : FixPlan = structured_llm.invoke([HumanMessage(content=formatted_prompt)]) - print(f"fixing plan : {fixer_response}") + fixer_response : FixPlan = structured_llm.invoke([HumanMessage(content=formatted_prompt)]) return fixer_response except Exception as e: print(f"❌ Error fixing code: {str(e)}") @@ -325,14 +329,16 @@ def fixer_node(state : BuildState): } fix_plan = fixer_response.model_dump() return { + "summary" : fix_plan["summary"], "fix_plan": fix_plan, - "messages": state.get("messages", []) + [AIMessage(content="Fix plan generated.")], + "messages": [AIMessage(content="Fix plan generated.")], } def apply_fix_actions_node(state: BuildState): plan = state.get("fix_plan") or {} - actions: List[Dict[str, Any]] = plan.get("actions", []) print(f'fix plan inside apply : {plan}') + actions: List[Dict[str, Any]] = plan.get("actions", []) + results: List[Dict[str, Any]] = [] TOOL_REGISTRY = { "write_file": command_utils.create_file, @@ -410,17 +416,21 @@ def code_retriever(state: BuildState, score_threshold=0.3): def enhancement_node(state: BuildState): - print("calling enhancement node !") + improver_prompt = prompt_collection.get_prompt("improver_prompt") + instrcutions = prompt_collection.get_prompt("instructions") + for message in reversed(state["messages"]): if isinstance(message, HumanMessage): change_request = message.content + print(f"calling enhancement node ! : \nchange request : {change_request}") if not change_request : raise "No human messages found !" + retrieved_files = state.get("retrieved_files") target_codes = " " for chunk in retrieved_files: - code = f'\n\n{chunk["file_path"]}' + f"\n{chunk["content"]}" + code = f"\n\n{chunk['file_path']}" + f"\n{chunk['content']}" target_codes = target_codes + code try: @@ -434,18 +444,19 @@ def enhancement_node(state: BuildState): structured_llm = llm.with_structured_output(UpdatedCode) - formatted_prompt = improver_prompt.format(instructions=INSTRUCTIONS, requirement = change_request, codes = target_codes) + formatted_prompt = improver_prompt.format(instructions=instrcutions, requirement = change_request, codes = target_codes) changed_response : UpdatedCode = structured_llm.invoke([HumanMessage(content=formatted_prompt)]) response = changed_response.model_dump() msg = apply_changes(response) print(msg) return { + "n_rounds": 0, "messages": AIMessage( - content=f"re-building the project.", + content=f"Apply changes, re-building the project.", tool_calls=[{ "name": "run_script", "args": {"script" : "build"}, - "id": "call-1", + "id": "call-2", }], ), } diff --git a/app/src/data_models/schemas.py b/app/src/data_models/schemas.py index b649d48..9cfbd04 100644 --- a/app/src/data_models/schemas.py +++ b/app/src/data_models/schemas.py @@ -54,6 +54,8 @@ class UpdatedCode(BaseModel) : class Summarize(BaseModel): summarized_text : str game_name : str + reply : str + is_game : bool class BuildState(TypedDict): @@ -73,7 +75,7 @@ class BuildState(TypedDict): build_ok: Optional[bool] build_output: Optional[str] - is_error : [Optional[bool]] + is_error : Optional[bool] error_files : Optional[List[str]] root_cause : Optional[str] fix_plan: Optional[Dict[str, Any]] @@ -84,4 +86,3 @@ class BuildState(TypedDict): - diff --git a/app/src/services/conversation_service.py b/app/src/services/conversation_service.py new file mode 100644 index 0000000..d375228 --- /dev/null +++ b/app/src/services/conversation_service.py @@ -0,0 +1,86 @@ +from typing import Dict, Any, Optional +from flask_socketio import SocketIO, emit, join_room, leave_room # ✅ Add these imports +from flask import request +import uuid +from datetime import datetime + +class WebSocketHandler: + def __init__(self, socketio: SocketIO) -> None: + self.socketio = socketio + + def emit_build_progress( + self, + thread_id: str, + content: str, + node_name: Optional[str] = None + ) -> None: + """Emit build progress to connected clients""" + message_data = { + "id": str(uuid.uuid4()), + "thread_id": thread_id, + "content": content, + "node_name": node_name, + "timestamp": datetime.utcnow().isoformat() + } + + self.socketio.emit('build_progress', message_data, room=thread_id) + + def emit_build_complete(self, thread_id: str, success: bool, message: str) -> None: + """Emit build completion status""" + completion_data = { + "id": str(uuid.uuid4()), + "thread_id": thread_id, + "success": success, + "message": message, + "timestamp": datetime.utcnow().isoformat(), + "type": "completion" + } + + self.socketio.emit('build_complete', completion_data, room=thread_id) + +def setup_websocket_events(socketio: SocketIO) -> None: + """Setup WebSocket event handlers""" + + @socketio.on('connect') + def handle_connect() -> None: + """Handle client connection""" + print(f"Client connected: {request.sid}") + emit('connected', {'status': 'Connected to build server'}) + + @socketio.on('disconnect') + def handle_disconnect() -> None: + """Handle client disconnection""" + print(f"Client disconnected: {request.sid}") + + @socketio.on('join_thread') + def handle_join_thread(data: Dict[str, Any]) -> None: + """Join a thread room for updates""" + thread_id = data.get('thread_id') + if thread_id: + # ✅ Fixed: Use imported join_room function + join_room(thread_id) + emit('joined', {'thread_id': thread_id}) + print(f"Client {request.sid} joined thread: {thread_id}") + else: + emit('error', {'message': 'thread_id is required'}) + + @socketio.on('leave_thread') + def handle_leave_thread(data: Dict[str, Any]) -> None: + """Leave a thread room""" + thread_id = data.get('thread_id') + if thread_id: + + leave_room(thread_id) + emit('left', {'thread_id': thread_id}) + print(f"Client {request.sid} left thread: {thread_id}") + + @socketio.on('ping') + def handle_ping() -> None: + """Handle ping for connection health check""" + emit('pong', {'timestamp': datetime.utcnow().isoformat()}) + + @socketio.on_error_default + def default_error_handler(e) -> None: + """Handle WebSocket errors""" + print(f"WebSocket error: {e}") + emit('error', {'message': 'An error occurred'}) \ No newline at end of file diff --git a/app/src/utils/command_utils.py b/app/src/utils/command_utils.py index a51a885..031ff5b 100644 --- a/app/src/utils/command_utils.py +++ b/app/src/utils/command_utils.py @@ -161,7 +161,6 @@ def read_file(self, file_path: str) -> str: """Read and return the content of a file inside the workspace.""" print(f"📖 Reading file: {file_path}") full_path = self.workspace / file_path - print("full path : {}") if not full_path.exists(): raise FileNotFoundError(f"File not found: {file_path}") with open(full_path, 'r', encoding='utf-8') as f: @@ -189,7 +188,6 @@ def _has_script(self, name: str) -> bool: data = json.load(f) return bool(data.get("scripts", {}).get(name)) except Exception: - # If anything goes wrong, don't hard fail—let run attempt anyway return False diff --git a/app/src/utils/deploy.py b/app/src/utils/deploy.py index 524ab8a..3c64d6a 100644 --- a/app/src/utils/deploy.py +++ b/app/src/utils/deploy.py @@ -1,9 +1,15 @@ import os from src.utils.command_utils import CommandUtils -from configs import WORKSPACE_DIR, MIN_NODE, PACKAGE_MANAGER +from configs import WORKSPACE_DIR, MIN_NODE, PACKAGE_MANAGER, VERCEL_TOKEN, VERCEL_SCOPE, VERCEL_TIMEOUT_SECONDS +import sys, json, re +from pathlib import Path +from urllib import request, parse, error +from typing import Tuple, Optional + +VERCEL_BIN = "vercel.cmd" if sys.platform.startswith("win") else "vercel" command_utils = CommandUtils(workspace=WORKSPACE_DIR, min_node=MIN_NODE, package_manager=PACKAGE_MANAGER) -def push_workspace_to_github(branch_name, repo_url=None, commit_message="Add workspace files"): +def push_workspace_to_github(branch_name, repo_url, commit_message="Add workspace files"): """ Push workspace folder to GitHub on a new branch @@ -13,53 +19,141 @@ def push_workspace_to_github(branch_name, repo_url=None, commit_message="Add wor commit_message: Commit message for the changes """ workspace_path = os.path.join(os.getcwd(), "workspace") - + print(f"branch : {branch_name} \nrepo: {repo_url} \ncommit : {commit_message} ") if not os.path.exists(workspace_path): print("Error: workspace folder not found") return False - try: - # Change to workspace directory - os.chdir(workspace_path) - - # Initialize git if not already initialized - stdout, stderr = command_utils.run_command(["git", "status"], cwd=workspace_path) - if "not a git repository" in stderr: - print("Initializing git repository...") - command_utils.run_command(["git", "init"], cwd=workspace_path) - - # Add remote if repo_url is provided - if repo_url: - command_utils.run_command(["git", "remote", "add", "origin", repo_url], cwd=workspace_path) - - # Create and checkout new branch - print(f"Creating and switching to branch: {branch_name}") - command_utils.run_command(["git", "checkout", "-b", branch_name], cwd=workspace_path) - - # Add all files - print("Adding all files...") - command_utils.run_command(["git", "add", "."], cwd=workspace_path) - - # Commit changes - print("Committing changes...") - command_utils.run_command(["git", "commit", "-m", commit_message], cwd=workspace_path) - - # Push to GitHub - print(f"Pushing to GitHub on branch {branch_name}...") - stdout, stderr = command_utils.run_command(["git", "push", "-u", "origin", branch_name], cwd=workspace_path) + + os.chdir(workspace_path) + + if not os.path.exists(os.path.join(workspace_path, ".git")): + print("Initializing independent git repository inside workspace...") + command_utils.run_command(["git", "init"], cwd=workspace_path) + + if repo_url: + command_utils.run_command(["git", "remote", "add", "origin", repo_url], cwd=workspace_path) + else: + print("Git repo already exists in workspace") + - if stderr and "error" in stderr.lower(): - print(f"Push failed: {stderr}") - return False - else: - print(f"Successfully pushed workspace to GitHub on branch {branch_name}") - return True - - except Exception as e: - print(f"Error: {e}") + print(f"Creating and switching to branch: {branch_name}") + command_utils.run_command(["git", "checkout", "-b", branch_name], cwd=workspace_path) + + + print("Adding all files...") + command_utils.run_command(["git", "add", "."], cwd=workspace_path) + + + print("Committing changes...") + command_utils.run_command(["git", "commit", "-m", commit_message], cwd=workspace_path) + + + print(f"Pushing to GitHub on branch {branch_name}...") + stdout, stderr = command_utils.run_command(["git", "push", "-u", "origin", branch_name], cwd=workspace_path) + + if stderr and "error" in stderr.lower(): + print(f"Push failed: {stderr}") return False - finally: - # Go back to original directory - os.chdir(os.path.dirname(workspace_path)) + else: + print(f"git response : {stdout}") + print(f"Successfully pushed workspace to GitHub on branch {branch_name}") + + return True + + # except Exception as e: + # print(f"Error: {e}") + # return False + # finally: + # # Go back to original directory + # os.chdir(os.path.dirname(workspace_path)) +# ---- Vercel deployment helpers ---- +def _require_vercel_env() -> None: + if not VERCEL_TOKEN: + raise RuntimeError("VERCEL_TOKEN is not set (configure it in configs.py or environment).") + +def _ensure_linked_workspace() -> None: + """Create .vercel/project.json by running a non-interactive link.""" + pj = WORKSPACE_DIR / ".vercel" / "project.json" + if pj.is_file(): + return + cmd = [VERCEL_BIN, "--yes"] + if VERCEL_SCOPE: + cmd += ["--scope", VERCEL_SCOPE] + if VERCEL_TOKEN: + cmd += ["--token", VERCEL_TOKEN] + print("Linking workspace to Vercel project...") + command_utils.run_command(cmd, cwd=str(WORKSPACE_DIR)) + +def _deploy_workspace_prod() -> Optional[str]: + """Deploy workspace/ to Production; return the final https://*.vercel.app URL.""" + cmd = [VERCEL_BIN, "--prod", "--yes"] + if VERCEL_SCOPE: + cmd += ["--scope", VERCEL_SCOPE] + if VERCEL_TOKEN: + cmd += ["--token", VERCEL_TOKEN] + print("Deploying workspace to Vercel Production...") + stdout, stderr = command_utils.run_command(cmd, cwd=str(WORKSPACE_DIR)) + combined = f"{stdout}\n{stderr}" + urls = re.findall(r"https://[a-zA-Z0-9.-]+\.vercel\.app", combined) + return urls[-1] if urls else None + +def _read_workspace_project_ref() -> str: + pj = WORKSPACE_DIR / ".vercel" / "project.json" + try: + data = json.loads(pj.read_text(encoding="utf-8")) + return data.get("projectId") or WORKSPACE_DIR.name + except Exception: + return WORKSPACE_DIR.name + +def _disable_vercel_auth_for_workspace() -> None: + """Disable Vercel Authentication + Password Protection via Projects API.""" + _require_vercel_env() + id_or_name = _read_workspace_project_ref() + qs = f"?slug={parse.quote(VERCEL_SCOPE)}" if VERCEL_SCOPE else "" + url = f"https://api.vercel.com/v9/projects/{parse.quote(id_or_name)}{qs}" + body = {"ssoProtection": None, "passwordProtection": None} + req = request.Request( + url, + method="PATCH", + data=json.dumps(body).encode("utf-8"), + headers={"Authorization": f"Bearer {VERCEL_TOKEN}", "Content-Type": "application/json"}, + ) + print("Disabling Vercel Authentication for workspace project...") + with request.urlopen(req, timeout=VERCEL_TIMEOUT_SECONDS) as resp: + if not (200 <= resp.status < 300): + raise RuntimeError(f"Unexpected Vercel API status: {resp.status}") + +def deploy_workspace_to_github_and_vercel( + repo_url: str, + branch_name: str, + commit_message: str = "Add workspace files", +) -> Tuple[bool, Optional[str], Optional[str]]: + """ + 1) Push 'workspace/' to GitHub (uses your existing function). + 2) Deploy 'workspace/' to Vercel Production. + 3) Disable Vercel Authentication so the link is public. + Returns: (success, production_url, error_message) + """ + if not WORKSPACE_DIR.is_dir(): + return False, None, "workspace folder not found" + + ok = push_workspace_to_github(branch_name=branch_name, repo_url=repo_url, commit_message=commit_message) + if not ok: + return False, None, "GitHub push failed" + + try: + _require_vercel_env() + _ensure_linked_workspace() + prod_url = _deploy_workspace_prod() + if not prod_url: + return False, None, "Could not determine Vercel production URL" + _disable_vercel_auth_for_workspace() + return True, prod_url, None + except error.HTTPError as e: + detail = e.read().decode("utf-8", errors="ignore") + return False, None, f"Vercel API error {e.code}: {detail}" + except Exception as e: + return False, None, str(e) diff --git a/requirements.txt b/requirements.txt index 720c9fc..f12ea3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ faiss-cpu==1.12.0 -huggingface-hub==0.34. +huggingface-hub==0.34.0 Flask==3.1.1 +Flask-SocketIO==5.5.1 +flask-cors==6.0.1 langgraph==0.6.3 langchain==0.3.27 langchain-anthropic==0.3.18 @@ -9,6 +11,6 @@ langchain-core==0.3.72 langchain-openai==0.3.28 langchain-xai==0.2.5 scikit-learn==1.7.1 -numpy==2.3.2 +numpy==2.1.3 pymongo==4.14.0 python-dotenv==1.1.1