Skip to content

Commit df4c6c3

Browse files
committed
langgraph implementation
1 parent 5083bb7 commit df4c6c3

12 files changed

Lines changed: 527 additions & 18 deletions

File tree

app/src/agents/game_dev_agent.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ class GameDevAgent:
2020

2121
def __init__(self):
2222

23-
self.active_environments = {}
24-
self.session_to_env = {}
23+
self.SESSIONS = {}
24+
2525

2626
def init_mgx_env(self):
2727
context = Context()
2828
env = MGXEnv(context=context)
2929
return env, context
30+
31+
async def run_env_to_idle(self, env : MGXEnv, allow_idle_time: int = 180):
32+
start = time.time()
33+
while not env.is_idle and (time.time() - start) < allow_idle_time:
34+
await env.run()
3035

3136
async def generate_game(self,
3237
requirement="",
@@ -41,23 +46,21 @@ async def generate_game(self,
4146
Path(workspace_path).mkdir(parents=True, exist_ok=True)
4247

4348
try:
44-
if env_id and env_id in self.active_environments:
45-
env = self.active_environments[env_id]
46-
print(f"🔄 Reusing existing environment: {env_id}")
47-
else:
4849

49-
env,context = self.init_mgx_env()
50-
env.add_roles(
51-
[
52-
TeamLeader(context=context),
53-
Engineer2(context=context),
54-
QaEngineer(context=context),
55-
]
56-
)
57-
58-
if env_id:
59-
self.active_environments[env_id] = env
60-
print(f"🆕 Created new environment: {env_id}")
50+
51+
52+
env,context = self.init_mgx_env()
53+
env.add_roles(
54+
[
55+
TeamLeader(context=context),
56+
Engineer2(context=context),
57+
QaEngineer(context=context),
58+
]
59+
)
60+
61+
if env_id:
62+
self.active_environments[env_id] = env
63+
print(f"🆕 Created new environment: {env_id}")
6164

6265
msg = Message(content=requirement)
6366
env.attach_images(msg)

app/src/llm/factory.py

Whitespace-only changes.

app/src/llm/interface.py

Whitespace-only changes.

app/src/llm/providers/__init__.py

Whitespace-only changes.

app/src/llm/providers/openai_chain.py

Whitespace-only changes.

app/src/pipelines/__init__.py

Whitespace-only changes.

app/src/pipelines/langgraph_pipeline.py

Whitespace-only changes.

app/src/schemas/dsl_schema.yml

Whitespace-only changes.

app/tests/data_models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
from pydantic import BaseModel, Field
3+
from typing import List, Optional, Literal, Dict, Any
4+
5+
class PackageInfo(BaseModel):
6+
"""Information about an npm package to install."""
7+
name: str = Field(description="Package name")
8+
dev: bool = Field(default=False, description="Whether it's a dev dependency")
9+
10+
class FileInfo(BaseModel):
11+
"""Information about a file to create."""
12+
path: str = Field(description="File path relative to project root")
13+
description: str = Field(description="What this file does")
14+
15+
class ProjectPlan(BaseModel):
16+
"""Complete project plan for Next.js application."""
17+
description: str = Field(description="Brief project description")
18+
development_steps : List[str] = Field(description="List of development steps to provide to engineer")
19+
directories: List[str] = Field(description="List of directories to create")
20+
files: List[FileInfo] = Field(description="List of files to create with content")
21+
packages: List[PackageInfo] = Field(description="List of packages to install")
22+
23+
class BuilderAction(BaseModel):
24+
command: Literal["write_file", "read_file", "install_package", "run_script"] = Field(
25+
description="Which tool to call."
26+
)
27+
path: Optional[str] = Field(default=None, description="Target path for file ops (write/read).")
28+
content: Optional[str] = Field(default=None, description="Content for write_file.")
29+
args: Optional[Dict[str, Any]] = Field(default=None, description="Extra args, e.g. {'name': 'framer-motion', 'dev': False} or {'script': 'build'}")
30+
note: str = Field(description="Brief rationale for this action.")
31+
32+
class BuilderBatch(BaseModel):
33+
step_index: int = Field(description="Index of the development step this batch addresses.")
34+
step_name: str = Field(description="The development step description.")
35+
actions: List[BuilderAction] = Field(description="Actions to execute now for this step.")
36+
mark_step_complete: bool = Field(description="True when this step is completed by the actions above.")
37+
notes: Optional[str] = Field(default=None, description="Optional notes for the planner.")

app/tests/test.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)