prthm11 commited on
Commit
8add79a
·
verified ·
1 Parent(s): 554abd8

Delete utils/action_node.py

Browse files
Files changed (1) hide show
  1. utils/action_node.py +0 -452
utils/action_node.py DELETED
@@ -1,452 +0,0 @@
1
- import json
2
- import uuid
3
- import logging
4
- from typing import Dict, Any, List
5
-
6
- # Assume GameState is a TypedDict or similar for clarity
7
- # from typing import TypedDict
8
- # class GameState(TypedDict):
9
- # description: str
10
- # project_json: Dict[str, Any]
11
- # action_plan: Dict[str, Any]
12
- # sprite_initial_positions: Dict[str, Any]
13
-
14
- # Placeholder for actual GameState in your application
15
- GameState = Dict[str, Any]
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- # --- Mock Agent for demonstration ---
20
- class MockAgent:
21
- def invoke(self, payload: Dict[str, Any]) -> Dict[str, Any]:
22
- """
23
- Mocks an LLM agent invocation. In a real scenario, this would call your
24
- actual LLM API (e.g., through LangChain, LlamaIndex, etc.).
25
- """
26
- user_message = payload["messages"][-1]["content"]
27
- print(f"\n--- Mock Agent Received Prompt (partial) ---\n{user_message[:500]}...\n------------------------------------------")
28
-
29
- # Simplified mock responses for demonstration purposes
30
- # In a real scenario, the LLM would generate actual Scratch block JSON
31
- if "Propose a high-level action flow" in user_message:
32
- return {
33
- "messages": [{
34
- "content": json.dumps({
35
- "action_overall_flow": {
36
- "Sprite1": {
37
- "description": "Basic movement and interaction",
38
- "plans": [
39
- {
40
- "event": "when flag clicked",
41
- "logic": "forever loop: move 10 steps, if touching Edge then turn 15 degrees"
42
- },
43
- {
44
- "event": "when space key pressed",
45
- "logic": "say Hello! for 2 seconds"
46
- }
47
- ]
48
- },
49
- "Ball": {
50
- "description": "Simple bouncing behavior",
51
- "plans": [
52
- {
53
- "event": "when flag clicked",
54
- "logic": "move 5 steps, if on edge bounce"
55
- }
56
- ]
57
- }
58
- }
59
- })
60
- }]
61
- }
62
- elif "You are an AI assistant generating Scratch 3.0 block JSON" in user_message:
63
- # This mock response is highly simplified. A real LLM would generate
64
- # valid Scratch blocks based on the provided relevant catalog and plan.
65
- # We're just demonstrating the *mechanism* of filtering the catalog.
66
- if "Sprite1" in user_message:
67
- return {
68
- "messages": [{
69
- "content": json.dumps({
70
- f"block_id_{generate_block_id()}": {
71
- "opcode": "event_whenflagclicked",
72
- "next": f"block_id_{generate_block_id()}_forever",
73
- "parent": None,
74
- "inputs": {}, "fields": {}, "shadow": False, "topLevel": True, "x": 100, "y": 100
75
- },
76
- f"block_id_{generate_block_id()}_forever": {
77
- "opcode": "control_forever",
78
- "next": None,
79
- "parent": f"block_id_{generate_block_id()}",
80
- "inputs": {
81
- "SUBSTACK": [2, f"block_id_{generate_block_id()}_move"]
82
- }, "fields": {}, "shadow": False, "topLevel": False
83
- },
84
- f"block_id_{generate_block_id()}_move": {
85
- "opcode": "motion_movesteps",
86
- "next": f"block_id_{generate_block_id()}_if",
87
- "parent": f"block_id_{generate_block_id()}_forever",
88
- "inputs": {
89
- "STEPS": [1, [4, "10"]]
90
- }, "fields": {}, "shadow": False, "topLevel": False
91
- },
92
- f"block_id_{generate_block_id()}_if": {
93
- "opcode": "control_if",
94
- "next": None,
95
- "parent": f"block_id_{generate_block_id()}_forever",
96
- "inputs": {
97
- "CONDITION": [2, f"block_id_{generate_block_id()}_touching"],
98
- "SUBSTACK": [2, f"block_id_{generate_block_id()}_turn"]
99
- }, "fields": {}, "shadow": False, "topLevel": False
100
- },
101
- f"block_id_{generate_block_id()}_touching": {
102
- "opcode": "sensing_touchingobject",
103
- "next": None,
104
- "parent": f"block_id_{generate_block_id()}_if",
105
- "inputs": {
106
- "TOUCHINGOBJECTMENU": [1, f"block_id_{generate_block_id()}_touching_menu"]
107
- }, "fields": {}, "shadow": False, "topLevel": False
108
- },
109
- f"block_id_{generate_block_id()}_touching_menu": {
110
- "opcode": "sensing_touchingobjectmenu",
111
- "next": None,
112
- "parent": f"block_id_{generate_block_id()}_touching",
113
- "inputs": {},
114
- "fields": {"TOUCHINGOBJECTMENU": ["_edge_", None]},
115
- "shadow": True, "topLevel": False
116
- },
117
- f"block_id_{generate_block_id()}_turn": {
118
- "opcode": "motion_turnright",
119
- "next": None,
120
- "parent": f"block_id_{generate_block_id()}_if",
121
- "inputs": {
122
- "DEGREES": [1, [4, "15"]]
123
- }, "fields": {}, "shadow": False, "topLevel": False
124
- },
125
- f"block_id_{generate_block_id()}_say": {
126
- "opcode": "looks_sayforsecs",
127
- "next": None,
128
- "parent": None, # This block would typically be part of a separate script
129
- "inputs": {
130
- "MESSAGE": [1, [10, "Hello!"]],
131
- "SECS": [1, [4, "2"]]
132
- }, "fields": {}, "shadow": False, "topLevel": True, "x": 300, "y": 100
133
- }
134
- })
135
- }]
136
- }
137
- elif "Ball" in user_message:
138
- return {
139
- "messages": [{
140
- "content": json.dumps({
141
- f"block_id_{generate_block_id()}": {
142
- "opcode": "event_whenflagclicked",
143
- "next": f"block_id_{generate_block_id()}_moveball",
144
- "parent": None,
145
- "inputs": {}, "fields": {}, "shadow": False, "topLevel": True, "x": 100, "y": 100
146
- },
147
- f"block_id_{generate_block_id()}_moveball": {
148
- "opcode": "motion_movesteps",
149
- "next": f"block_id_{generate_block_id()}_edgebounce",
150
- "parent": f"block_id_{generate_block_id()}",
151
- "inputs": {
152
- "STEPS": [1, [4, "5"]]
153
- }, "fields": {}, "shadow": False, "topLevel": False
154
- },
155
- f"block_id_{generate_block_id()}_edgebounce": {
156
- "opcode": "motion_ifonedgebounce",
157
- "next": None,
158
- "parent": f"block_id_{generate_block_id()}_moveball",
159
- "inputs": {}, "fields": {}, "shadow": False, "topLevel": False
160
- }
161
- })
162
- }]
163
- }
164
- return {"messages": [{"content": "[]"}]} # Default empty response
165
-
166
- agent = MockAgent()
167
-
168
- # Helper function to generate a unique block ID
169
- def generate_block_id():
170
- return str(uuid.uuid4())[:10].replace('-', '') # Shorten for readability, ensure uniqueness
171
-
172
- # Placeholder for your extract_json_from_llm_response function
173
- def extract_json_from_llm_response(response_string):
174
- try:
175
- # Assuming the LLM response is ONLY the JSON string within triple backticks
176
- json_match = response_string.strip().replace("```json", "").replace("```", "").strip()
177
- return json.loads(json_match)
178
- except json.JSONDecodeError as e:
179
- logger.error(f"Failed to decode JSON from LLM response: {e}")
180
- logger.error(f"Raw response: {response_string}")
181
- raise ValueError("Invalid JSON response from LLM")
182
-
183
- # --- GLOBAL CATALOG OF ALL SCRATCH BLOCKS ---
184
- # This is where you would load your block_content.json
185
- # For demonstration, I'm using your provided snippets and adding some common ones.
186
- # In a real application, you'd load this once at startup.
187
- ALL_SCRATCH_BLOCKS_CATALOG = {
188
- "motion_movesteps": {
189
- "opcode": "motion_movesteps", "next": None, "parent": None,
190
- "inputs": {"STEPS": [1, [4, "10"]]}, "fields": {}, "shadow": False, "topLevel": True, "x": 464, "y": -416
191
- },
192
- "motion_turnright": {
193
- "opcode": "motion_turnright", "next": None, "parent": None,
194
- "inputs": {"DEGREES": [1, [4, "15"]]}, "fields": {}, "shadow": False, "topLevel": True, "x": 467, "y": -316
195
- },
196
- "motion_ifonedgebounce": {
197
- "opcode": "motion_ifonedgebounce", "next": None, "parent": None,
198
- "inputs": {}, "fields": {}, "shadow": False, "topLevel": True, "x": 467, "y": -316
199
- },
200
- "event_whenflagclicked": {
201
- "opcode": "event_whenflagclicked", "next": None, "parent": None,
202
- "inputs": {}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
203
- },
204
- "event_whenkeypressed": {
205
- "opcode": "event_whenkeypressed", "next": None, "parent": None,
206
- "inputs": {}, "fields": {"KEY_OPTION": ["space", None]}, "shadow": False, "topLevel": True, "x": 10, "y": 10
207
- },
208
- "control_forever": {
209
- "opcode": "control_forever", "next": None, "parent": None,
210
- "inputs": {"SUBSTACK": [2, "some_id"]}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
211
- },
212
- "control_if": {
213
- "opcode": "control_if", "next": None, "parent": None,
214
- "inputs": {"CONDITION": [2, "some_id"], "SUBSTACK": [2, "some_id_2"]}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
215
- },
216
- "looks_sayforsecs": {
217
- "opcode": "looks_sayforsecs", "next": None, "parent": None,
218
- "inputs": {"MESSAGE": [1, [10, "Hello!"]], "SECS": [1, [4, "2"]]}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
219
- },
220
- "looks_say": {
221
- "opcode": "looks_say", "next": None, "parent": None,
222
- "inputs": {"MESSAGE": [1, [10, "Hello!"]]}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
223
- },
224
- "sensing_touchingobject": {
225
- "opcode": "sensing_touchingobject", "next": None, "parent": None,
226
- "inputs": {"TOUCHINGOBJECTMENU": [1, "some_id"]}, "fields": {}, "shadow": False, "topLevel": True, "x": 10, "y": 10
227
- },
228
- "sensing_touchingobjectmenu": {
229
- "opcode": "sensing_touchingobjectmenu", "next": None, "parent": None,
230
- "inputs": {}, "fields": {"TOUCHINGOBJECTMENU": ["_mouse_", None]}, "shadow": True, "topLevel": True, "x": 10, "y": 10
231
- },
232
- # Add more blocks from your block_content.json here...
233
- }
234
-
235
- # --- Heuristic-based block selection ---
236
- def get_relevant_blocks_for_plan(action_plan: Dict[str, Any], all_blocks_catalog: Dict[str, Any]) -> Dict[str, Any]:
237
- """
238
- Analyzes the natural language action plan and selects relevant Scratch blocks
239
- from the comprehensive catalog. This is a heuristic approach and might need
240
- to be refined based on your specific use cases and LLM capabilities.
241
- """
242
- relevant_opcodes = set()
243
-
244
- # Always include common event blocks
245
- relevant_opcodes.add("event_whenflagclicked")
246
- relevant_opcodes.add("event_whenkeypressed") # Could be more specific if key is mentioned
247
-
248
- # Keyword to opcode mapping (can be expanded)
249
- keyword_map = {
250
- "move": "motion_movesteps",
251
- "steps": "motion_movesteps",
252
- "turn": "motion_turnright",
253
- "rotate": "motion_turnright",
254
- "bounce": "motion_ifonedgebounce",
255
- "edge": "motion_ifonedgebounce",
256
- "forever": "control_forever",
257
- "loop": "control_forever",
258
- "if": "control_if",
259
- "condition": "control_if",
260
- "say": "looks_say",
261
- "hello": "looks_say", # Simple example, might need more context
262
- "touching": "sensing_touchingobject",
263
- "mouse pointer": "sensing_touchingobjectmenu",
264
- "edge": "sensing_touchingobjectmenu", # For touching edge
265
- }
266
-
267
- # Iterate through the action plan to find keywords
268
- for sprite_name, sprite_actions in action_plan.get("action_overall_flow", {}).items():
269
- for plan in sprite_actions.get("plans", []):
270
- event_logic = plan.get("event", "").lower() + " " + plan.get("logic", "").lower()
271
-
272
- # Check for direct opcode matches (if the LLM somehow outputs opcodes in its plan)
273
- for opcode in all_blocks_catalog.keys():
274
- if opcode in event_logic:
275
- relevant_opcodes.add(opcode)
276
-
277
- # Check for keywords
278
- for keyword, opcode in keyword_map.items():
279
- if keyword in event_logic:
280
- relevant_opcodes.add(opcode)
281
- # Add associated shadow blocks if known
282
- if opcode == "sensing_touchingobject":
283
- relevant_opcodes.add("sensing_touchingobjectmenu")
284
- if opcode == "event_whenkeypressed":
285
- relevant_opcodes.add("event_whenkeypressed") # It's already there but good to be explicit
286
-
287
- # Construct the filtered catalog
288
- relevant_blocks_catalog = {
289
- opcode: all_blocks_catalog[opcode]
290
- for opcode in relevant_opcodes if opcode in all_blocks_catalog
291
- }
292
- return relevant_blocks_catalog
293
-
294
- # --- New Action Planning Node ---
295
- def plan_sprite_actions(state: GameState):
296
- logger.info("--- Running PlanSpriteActionsNode ---")
297
-
298
- planning_prompt = (
299
- f"You are an AI assistant tasked with planning Scratch 3.0 block code for a game. "
300
- f"The game description is: '{state['description']}'.\n\n"
301
- f"Here are the sprites currently in the project: {', '.join(target['name'] for target in state['project_json']['targets'] if not target['isStage']) if len(state['project_json']['targets']) > 1 else 'None'}.\n"
302
- f"Initial positions: {json.dumps(state.get('sprite_initial_positions', {}), indent=2)}\n\n"
303
- f"Consider the main actions and interactions required for each sprite. "
304
- f"Think step-by-step about what each sprite needs to *do*, *when* it needs to do it (events), "
305
- f"and if any actions need to *repeat* or depend on *conditions*.\n\n"
306
- f"Propose a high-level action flow for each sprite in the following JSON format. "
307
- f"Do NOT generate Scratch block JSON yet. Only describe the logic using natural language or simplified pseudo-code.\n\n"
308
- f"Example format:\n"
309
- f"```json\n"
310
- f"{{\n"
311
- f" \"action_overall_flow\": {{\n"
312
- f" \"Sprite1\": {{\n"
313
- f" \"description\": \"Main character actions\",\n"
314
- f" \"plans\": [\n"
315
- f" {{\n"
316
- f" \"event\": \"when flag clicked\",\n"
317
- f" \"logic\": \"forever loop: move 10 steps, if on edge bounce\"\n"
318
- f" }},\n"
319
- f" {{\n"
320
- f" \"event\": \"when space key pressed\",\n"
321
- f" \"logic\": \"change y by 10, wait 0.1 seconds, change y by -10\"\n"
322
- f" }}\n"
323
- f" ]\n"
324
- f" }},\n"
325
- f" \"Ball\": {{\n"
326
- f" \"description\": \"Projectile movement\",\n"
327
- f" \"plans\": [\n"
328
- f" {{\n"
329
- f" \"event\": \"when I start as a clone\",\n"
330
- f" \"logic\": \"glide 1 sec to random position, if touching Sprite1 then stop this script\"\n"
331
- f" }}\n"
332
- f" ]\n"
333
- f" }}\n"
334
- f" }}\n"
335
- f"}}\n"
336
- f"```\n\n"
337
- f"Return ONLY the JSON object for the action overall flow."
338
- )
339
-
340
- try:
341
- response = agent.invoke({"messages": [{"role": "user", "content": planning_prompt}]})
342
- raw_response = response["messages"][-1].content
343
- print("Raw response from LLM [PlanSpriteActionsNode]:", raw_response)
344
- action_plan = extract_json_from_llm_response(raw_response)
345
- logger.info("Sprite action plan generated by PlanSpriteActionsNode.")
346
- return {"action_plan": action_plan}
347
- except Exception as e:
348
- logger.error(f"Error in PlanSpriteActionsNode: {e}")
349
- raise
350
-
351
- # --- Updated Action Node Builder (to consume the plan and build blocks) ---
352
- def build_action_nodes(state: GameState):
353
- logger.info("--- Running ActionNodeBuilder ---")
354
-
355
- action_plan = state.get("action_plan", {})
356
- if not action_plan:
357
- raise ValueError("No action plan found in state. Run PlanSpriteActionsNode first.")
358
-
359
- # Convert the Scratch project JSON to a mutable Python object
360
- project_json = state["project_json"]
361
- targets = project_json["targets"]
362
-
363
- # We need a way to map sprite names to their actual target objects in project_json
364
- sprite_map = {target["name"]: target for target in targets if not target["isStage"]}
365
-
366
- # --- NEW: Get only the relevant blocks for the entire action plan ---
367
- relevant_scratch_blocks_catalog = get_relevant_blocks_for_plan(action_plan, ALL_SCRATCH_BLOCKS_CATALOG)
368
- logger.info(f"Filtered {len(relevant_scratch_blocks_catalog)} relevant blocks out of {len(ALL_SCRATCH_BLOCKS_CATALOG)} total.")
369
-
370
-
371
- # Iterate through the planned actions for each sprite
372
- for sprite_name, sprite_actions in action_plan.get("action_overall_flow", {}).items():
373
- if sprite_name in sprite_map:
374
- current_sprite_target = sprite_map[sprite_name]
375
- # Ensure 'blocks' field exists for the sprite
376
- if "blocks" not in current_sprite_target:
377
- current_sprite_target["blocks"] = {}
378
-
379
- # Generate block JSON based on the detailed action plan for this sprite
380
- # This is where the LLM's role becomes crucial: translating logic to blocks
381
- llm_block_generation_prompt = (
382
- f"You are an AI assistant generating Scratch 3.0 block JSON based on a provided plan. "
383
- f"The current sprite is '{sprite_name}'.\n"
384
- f"Its planned actions are:\n"
385
- f"```json\n{json.dumps(sprite_actions, indent=2)}\n```\n\n"
386
- f"Here is a **curated catalog of only the most relevant Scratch 3.0 blocks** for this plan:\n"
387
- f"```json\n{json.dumps(relevant_scratch_blocks_catalog, indent=2)}\n```\n\n"
388
- f"Current Scratch project JSON (for context, specifically this sprite's existing blocks if any):\n"
389
- f"```json\n{json.dumps(current_sprite_target, indent=2)}\n```\n\n"
390
- f"**Instructions:**\n"
391
- f"1. For each planned event and its associated logic, generate the corresponding Scratch 3.0 block JSON.\n"
392
- f"2. **Generate unique block IDs** for every new block. Use a format like 'block_id_abcdef12'.\n" # Updated ID format hint
393
- f"3. Properly link blocks using `next` and `parent` fields to form execution stacks. Hat blocks (`topLevel: true`, `parent: null`).\n"
394
- f"4. Correctly fill `inputs` and `fields` based on the catalog and the plan's logic (e.g., specific values for motion, keys for events, conditions for controls).\n"
395
- f"5. For C-blocks (like `control_repeat`, `control_forever`, `control_if`), use the `SUBSTACK` input to link to the first block inside its loop/conditional.\n"
396
- f"6. If the plan involves operators (e.g., 'if touching Sprite1'), use the appropriate operator blocks from the catalog and link them correctly as `CONDITION` inputs.\n"
397
- f"7. Ensure that any shadow blocks (e.g., for dropdowns like `motion_goto_menu`, `sensing_touchingobjectmenu`) are generated with `shadow: true` and linked correctly as inputs to their parent block.\n"
398
- f"8. Return ONLY the **updated 'blocks' dictionary** for this specific sprite. Do NOT return the full project JSON. ONLY the `blocks` dictionary."
399
- )
400
-
401
- try:
402
- response = agent.invoke({"messages": [{"role": "user", "content": llm_block_generation_prompt}]})
403
- raw_response = response["messages"][-1].content
404
- print(f"Raw response from LLM [ActionNodeBuilder - {sprite_name}]:", raw_response)
405
- generated_blocks = extract_json_from_llm_response(raw_response)
406
- current_sprite_target["blocks"].update(generated_blocks) # Merge new blocks
407
- logger.info(f"Action blocks added for sprite '{sprite_name}' by ActionNodeBuilder.")
408
- except Exception as e:
409
- logger.error(f"Error generating blocks for sprite '{sprite_name}': {e}")
410
- # Depending on robustness needed, you might continue or re-raise
411
- raise
412
-
413
- return {"project_json": project_json}
414
-
415
- # --- Example Usage (to demonstrate the flow) ---
416
- if __name__ == "__main__":
417
- # Initialize a mock game state
418
- initial_game_state = {
419
- "description": "A simple game where a sprite moves and says hello.",
420
- "project_json": {
421
- "targets": [
422
- {"isStage": True, "name": "Stage", "blocks": {}},
423
- {"isStage": False, "name": "Sprite1", "blocks": {}},
424
- {"isStage": False, "name": "Ball", "blocks": {}}
425
- ]
426
- },
427
- "sprite_initial_positions": {}
428
- }
429
-
430
- # Step 1: Plan Sprite Actions
431
- try:
432
- state_after_planning = plan_sprite_actions(initial_game_state)
433
- initial_game_state.update(state_after_planning)
434
- print("\n--- Game State After Planning ---")
435
- print(json.dumps(initial_game_state, indent=2))
436
- except Exception as e:
437
- print(f"Planning failed: {e}")
438
- exit()
439
-
440
- # Step 2: Build Action Nodes (Generate Blocks)
441
- try:
442
- state_after_building = build_action_nodes(initial_game_state)
443
- initial_game_state.update(state_after_building)
444
- print("\n--- Game State After Building Blocks ---")
445
- # Print only the blocks for a specific sprite to keep output manageable
446
- for target in initial_game_state["project_json"]["targets"]:
447
- if not target["isStage"]:
448
- print(f"\nBlocks for {target['name']}:")
449
- print(json.dumps(target.get('blocks', {}), indent=2))
450
-
451
- except Exception as e:
452
- print(f"Building blocks failed: {e}")