1+ import os
2+ import json
3+ from pathlib import Path
4+ from typing import List , TypedDict , Annotated , Dict , Optional , Any
5+ from langchain_anthropic import ChatAnthropic
6+ from langgraph .graph import StateGraph , START , END
7+ from langgraph .graph .message import add_messages
8+ from langgraph .prebuilt import ToolNode
9+ from langchain .tools import tool
10+ from langchain_core .messages import AIMessage , ToolMessage , BaseMessage , HumanMessage
11+ from langchain_core .messages import ToolMessage
12+ from utils .command_utils import CommandUtils
13+ from tools import execute_plan
14+ from data_models import ProjectPlan , BuilderBatch
15+
16+
17+ WORKSPACE_DIR = Path (os .path .join (os .getcwd (), "workspace" ))
18+ PACKAGE_MANAGER = "npm"
19+ MIN_NODE = (18 , 18 , 0 )
20+
21+ command_utils = CommandUtils (workspace = WORKSPACE_DIR , min_node = MIN_NODE , package_manager = PACKAGE_MANAGER )
22+
23+ class BuildState (TypedDict ):
24+
25+ requirements : str
26+
27+ description : str
28+ development_steps : List [str ]
29+ directories : List [str ]
30+ files : List [dict ] # [{path, description, (maybe content)}]
31+ packages : List [dict ]
32+
33+ messages : Annotated [List [BaseMessage ], add_messages ]
34+ last_tool_result : Optional [Dict [str , Any ]]
35+ execution_success : Optional [bool ]
36+
37+
38+ def get_project_plan (requirements : str ):
39+ """Get a project plan from LLM."""
40+ print ("🤖 Getting project plan from LLM..." )
41+
42+ prompt = f"""
43+ You are an expert Next.js developer. Create a detailed project plan for: "{ requirements } " . In development setps no need to mention create files and folders.
44+
45+ Return a JSON object with this structure:
46+ {{
47+ "description": "Brief project description",
48+ "Development_steps" : list of steps to develop the project by engineer.
49+ "directories": ["list", "of", "directories", "to", "create" it should be inside the scr folder],
50+ "files": [
51+ {{
52+ "path": "file/path.tsx",
53+ "description": "Brief explanation to what could be in this file",
54+ }}
55+ ],
56+ "packages": [
57+ {{
58+ "name": "package-name",
59+ "dev": false
60+ }}
61+ ]
62+ }}
63+
64+ Focus on:
65+ - Next.js 15+ with App Router
66+ - TypeScript and Tailwind CSS
67+ - Clean, scalable folder structure
68+ - Working example code
69+ - Best practices
70+ - Must be use modern and professional UI/UX componenets, gradients, animation, etc
71+ - Should implement game features like player name input, scoreboard, Playing with AI, start new game, game logic handling (win, loss draw)
72+
73+ Make sure all file content is complete and functional.
74+ """
75+
76+ try :
77+ llm = ChatAnthropic (model = "claude-opus-4-1-20250805" , temperature = 0.1 , api_key = "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" , max_tokens_to_sample = 12000 )
78+ structured_llm = llm .with_structured_output (ProjectPlan )
79+ response : ProjectPlan = structured_llm .invoke ([HumanMessage (content = prompt )])
80+ return response
81+ except Exception as e :
82+ print (f"❌ Error getting project plan: { str (e )} " )
83+ import traceback
84+ traceback .print_exc ()
85+ return None
86+
87+ @tool ("execute_project_plan" )
88+ def execute_project_plan_tool (plan : Dict ) -> Dict :
89+ """
90+ Execute a project plan (dict).
91+ Returns: {"success": bool, "error"?: str}
92+ """
93+ try :
94+ ok = execute_plan (ProjectPlan (** plan ))
95+ return {"success" : bool (ok )}
96+ except Exception as e :
97+ return {"success" : False , "error" : str (e )}
98+
99+ @tool ("write_file" )
100+ def write_file_tool (path : str , content : str ) -> Dict :
101+ """Create/overwrite a file with content (relative to workspace)."""
102+ try :
103+ command_utils .create_file (path , content or "" )
104+ return {"ok" : True , "path" : path }
105+ except Exception as e :
106+ return {"ok" : False , "error" : str (e ), "path" : path }
107+
108+ @tool ("read_file" )
109+ def read_file_tool (path : str ) -> Dict :
110+ """Read a file content (relative to workspace)."""
111+ try :
112+ data = command_utils .read_file (path ) # ensure you have this helper
113+ return {"ok" : True , "path" : path , "content" : data }
114+ except Exception as e :
115+ return {"ok" : False , "error" : str (e ), "path" : path }
116+
117+ @tool ("install_package" )
118+ def install_package_tool (name : str , dev : bool = False ) -> Dict :
119+ """Install an npm package."""
120+ try :
121+ command_utils .install_package (name , dev )
122+ return {"ok" : True , "name" : name , "dev" : dev }
123+ except Exception as e :
124+ return {"ok" : False , "error" : str (e ), "name" : name , "dev" : dev }
125+
126+ @tool ("run_script" )
127+ def run_script_tool (script : str ) -> Dict :
128+ """Run an npm script, e.g., 'build' or 'dev'."""
129+ try :
130+
131+ command_utils .run_script (script )
132+ return {"ok" : True , "script" : script }
133+ except Exception as e :
134+ return {"ok" : False , "error" : str (e ), "script" : script }
135+
136+
137+ def plan_node (state : BuildState ):
138+ """Call LLM once, fill state with the plan, and enqueue a tool call."""
139+
140+ plan_obj = get_project_plan (state ["requirements" ])
141+ if plan_obj is None :
142+
143+ return {
144+ "messages" : [AIMessage (content = "Failed to get plan; skipping tool call." )],
145+ }
146+ plan = plan_obj .model_dump ()
147+ print (f"state inside plan : { plan } " )
148+
149+ return {
150+ "description" : plan ["description" ],
151+ "development_steps" : plan ["development_steps" ],
152+ "directories" : plan ["directories" ],
153+ "files" : plan ["files" ],
154+ "packages" : plan ["packages" ],
155+ "messages" : [
156+ AIMessage (
157+ content = "Executing generated project plan." ,
158+ tool_calls = [{
159+ "name" : "execute_project_plan" ,
160+ "args" : {"plan" : plan },
161+ "id" : "call-0" ,
162+ }],
163+ )
164+ ],
165+ }
166+
167+ _scaffold_tool_node = ToolNode ([execute_project_plan_tool ])
168+
169+ def tools_node (state : BuildState ):
170+ out = _scaffold_tool_node .invoke ({"messages" : state ["messages" ]})
171+ msgs : List [BaseMessage ] = out ["messages" ] if isinstance (out , dict ) else out
172+
173+ tool_msg = next ((m for m in reversed (msgs ) if isinstance (m , ToolMessage )), None )
174+
175+ if not tool_msg :
176+ result : Dict [str , Any ] = {"success" : False , "error" : "No ToolMessage returned" }
177+ else :
178+ payload = tool_msg .content
179+ if isinstance (payload , dict ):
180+ result = payload
181+ else :
182+ try :
183+ result = json .loads (payload )
184+ except Exception :
185+ result = {"success" : False , "error" : f"Non-JSON tool output: { payload } " }
186+
187+ updates : Dict [str , Any ] = {
188+ "messages" : msgs ,
189+ "last_tool_result" : result ,
190+ "execution_success" : bool (result .get ("success" )),
191+ }
192+ if result .get ("success" ):
193+ files_list = state .get ("files" , [])
194+ updates ["file_states" ] = {f ["path" ]: {"written" : False } for f in files_list }
195+
196+ return updates
197+
198+ _builder_tool_node = ToolNode ([write_file_tool , read_file_tool , install_package_tool , run_script_tool ])
199+
200+
201+ def route_after_scaffold (state : BuildState ):
202+ return "builder" if state .get ("execution_success" ) else "end"
203+
204+ def build_app ():
205+ g = StateGraph (BuildState )
206+
207+ g .add_node ("plan" , plan_node )
208+ g .add_node ("tools" , tools_node )
209+
210+ g .add_edge (START , "plan" )
211+ g .add_edge ("plan" , "tools" )
212+ g .add_conditional_edges ("tools" , route_after_scaffold , {
213+ "builder" : "builder" ,
214+ "end" : END
215+ })
216+
217+ return g .compile ()
218+
219+
220+ # ---------- Example run ----------
221+ if __name__ == "__main__" :
222+ app = build_app ()
223+
224+ requirements = """Create a tic-tac-toe game with:
225+ - React components with TypeScript
226+ - Game state using useState
227+ - Winner detection
228+ - Reset game
229+ - Responsive Tailwind UI
230+ - Clean, modern UI
231+ """
232+
233+ init_state : BuildState = {
234+ "requirements" : requirements ,
235+ "description" : "" ,
236+ "development_steps" : [],
237+ "directories" : [],
238+ "files" : [],
239+ "packages" : [],
240+ "messages" : [],
241+ "last_tool_result" : None ,
242+ "execution_success" : None ,
243+ "current_step_idx" : 0 ,
244+ "file_states" : {},
245+ "last_action_batch" : None ,
246+ "last_action_ids" : [],
247+ }
248+
249+
250+ final_state = app .invoke (init_state )
251+
252+ print ("---- RESULT ----" )
253+ print ("Execution success:" , final_state .get ("execution_success" ))
254+ print ("Tool result:" , final_state .get ("last_tool_result" ))
0 commit comments