owenkaplinsky commited on
Commit
0fc972f
·
1 Parent(s): 0ca2c7f

Add functions

Browse files
project/src/blocks/text.js CHANGED
@@ -1,80 +1,73 @@
1
  import * as Blockly from 'blockly/core';
2
  import { pythonGenerator } from 'blockly/python';
3
 
4
- // Function to create a unique block type for an input
5
  function createInputRefBlockType(inputName) {
6
  const blockType = `input_reference_${inputName}`;
7
-
8
- // Only create if not already defined
9
  if (!Blockly.Blocks[blockType]) {
10
- // Define block
11
  Blockly.Blocks[blockType] = {
12
  init: function () {
13
  this.jsonInit({
14
- "type": blockType,
15
- "message0": "%1",
16
- "args0": [
17
  {
18
- "type": "field_label_serializable",
19
- "name": "VARNAME",
20
- "text": inputName
21
  }
22
  ],
23
- "output": null,
24
- "colour": 210,
25
- "outputShape": 1 // Oval shape (1 = round)
26
  });
27
  }
28
  };
29
-
30
- // Define Python generator for this block type
31
- pythonGenerator.forBlock[blockType] = function (block) {
32
- // Just return the input name as a variable reference
33
  return [inputName, pythonGenerator.ORDER_ATOMIC];
34
  };
35
  }
36
  return blockType;
37
  }
38
 
39
- // Keep track of input reference blocks
40
- const inputRefs = new Map(); // Maps input name to block ID
41
 
 
42
  Blockly.Extensions.registerMutator(
43
  'test_mutator',
44
  {
45
- // Initialize tracking of input reference blocks
46
  initialize: function () {
47
  if (!this.initialized_) {
48
  this.inputCount_ = 0;
49
  this.inputNames_ = [];
50
  this.inputTypes_ = [];
51
- this.inputRefBlocks_ = new Map(); // Maps input name to block reference
52
  this.initialized_ = true;
 
 
53
  }
54
  },
55
 
56
  decompose: function (workspace) {
57
- var containerBlock = workspace.newBlock('container');
58
  containerBlock.initSvg();
59
- var connection = containerBlock.getInput('STACK').connection;
60
 
61
- // Ensure inputCount_ is defined
62
  this.inputCount_ = this.inputCount_ || 0;
63
  this.inputNames_ = this.inputNames_ || [];
64
  this.inputTypes_ = this.inputTypes_ || [];
65
 
66
- // Dynamically add input blocks based on inputCount_
67
- for (var i = 0; i < this.inputCount_; i++) {
68
- var itemBlock = workspace.newBlock('container_input');
69
  itemBlock.initSvg();
 
 
 
 
70
 
71
- var typeVal = this.inputTypes_[i] || 'string';
72
- var nameVal = this.inputNames_[i] || typeVal;
73
- itemBlock.setFieldValue(typeVal, 'TYPE'); // Set data type
74
- itemBlock.setFieldValue(nameVal, 'NAME'); // Set name from stored name
75
-
76
- // Preserve any existing connection from this block's input
77
- var input = this.getInput('X' + i);
78
  if (input && input.connection && input.connection.targetConnection) {
79
  itemBlock.valueConnection_ = input.connection.targetConnection;
80
  }
@@ -87,55 +80,49 @@ Blockly.Extensions.registerMutator(
87
  },
88
 
89
  compose: function (containerBlock) {
90
- // Disable events during mutator updates to prevent duplication
91
  Blockly.Events.disable();
92
-
93
  try {
94
- // Ensure initialization
95
- if (!this.initialized_) {
96
- this.initialize();
97
- }
98
 
99
- var itemBlock = containerBlock.getInputTargetBlock('STACK');
100
- var connections = [];
101
- var oldNames = this.inputNames_ ? [...this.inputNames_] : []; // Copy old names for cleanup
102
 
103
- // Collect input connections
104
  while (itemBlock) {
105
  connections.push(itemBlock.valueConnection_);
106
  itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock();
107
  }
108
 
109
- var newCount = connections.length;
110
  this.inputCount_ = newCount;
111
  this.inputNames_ = this.inputNames_ || [];
112
  this.inputTypes_ = this.inputTypes_ || [];
113
 
114
- var idx = 0;
115
- var it = containerBlock.getInputTargetBlock('STACK');
116
- var newNames = [];
 
117
  while (it) {
118
  this.inputTypes_[idx] = it.getFieldValue('TYPE') || 'string';
119
- this.inputNames_[idx] = it.getFieldValue('NAME') || ('arg' + idx);
120
  newNames.push(this.inputNames_[idx]);
121
  it = it.nextConnection && it.nextConnection.targetBlock();
122
  idx++;
123
  }
124
 
125
- // Clean up removed input reference blocks only if count decreased
126
  if (newCount < oldNames.length) {
127
- // Only remove blocks for inputs that were deleted (beyond new count)
128
  for (let i = newCount; i < oldNames.length; i++) {
129
  const oldName = oldNames[i];
130
  const block = this.inputRefBlocks_.get(oldName);
131
- if (block && !block.disposed) {
132
- block.dispose(true); // true = skip gap
133
- }
134
  this.inputRefBlocks_.delete(oldName);
135
  }
136
  }
137
 
138
- // Handle renamed variables - update ALL instances (main block + clones)
 
139
  for (let i = 0; i < Math.min(oldNames.length, newNames.length); i++) {
140
  const oldName = oldNames[i];
141
  const newName = newNames[i];
@@ -143,227 +130,278 @@ Blockly.Extensions.registerMutator(
143
  const oldBlockType = `input_reference_${oldName}`;
144
  const newBlockType = `input_reference_${newName}`;
145
 
146
- // Update the reference block in the MCP block
147
  if (this.inputRefBlocks_.has(oldName)) {
148
  const refBlock = this.inputRefBlocks_.get(oldName);
149
  if (refBlock && !refBlock.disposed) {
150
  this.inputRefBlocks_.delete(oldName);
151
  this.inputRefBlocks_.set(newName, refBlock);
152
  refBlock.setFieldValue(newName, 'VARNAME');
153
- refBlock.type = newBlockType; // Update the block type
 
 
 
 
 
 
 
154
  }
155
  }
156
 
157
- // Find and update ALL clones of this reference block in the workspace
158
- const workspace = this.workspace;
159
- const allBlocks = workspace.getAllBlocks(false);
160
- for (const block of allBlocks) {
161
- if (block.type === oldBlockType && block !== this.inputRefBlocks_.get(newName)) {
162
- // This is a clone - update it too
163
- block.type = newBlockType;
164
- block.setFieldValue(newName, 'VARNAME');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
166
  }
167
 
168
- // Update the Python generator for the new block type
169
- pythonGenerator.forBlock[newBlockType] = function (block) {
170
  return [newName, pythonGenerator.ORDER_ATOMIC];
171
  };
172
  }
173
  }
174
 
175
- // Remove only the dynamic inputs (X0, X1, etc.)
176
- var i = 0;
177
- while (this.getInput('X' + i)) {
178
- this.removeInput('X' + i);
179
- i++;
 
 
 
 
 
 
 
 
180
  }
181
 
182
- // Now add dynamic inputs at the correct position
183
- for (var j = 0; j < newCount; j++) {
184
- var type = this.inputTypes_[j] || 'string';
185
- var name = this.inputNames_[j] || type;
186
- var check = null;
187
  if (type === 'integer') check = 'Number';
188
  if (type === 'string') check = 'String';
189
- // For list, leave check as null (no restriction)
190
 
191
- // Get existing reference block if any
192
  const existingRefBlock = this.inputRefBlocks_.get(name);
193
-
194
- // Insert inputs at the beginning (after the static text)
195
- var input = this.appendValueInput('X' + j);
196
  if (check) input.setCheck(check);
197
- input.appendField(type); // Display the type instead of the name
198
-
199
- // Move the input to the correct position (after dummy inputs, before BODY)
200
  this.moveInputBefore('X' + j, 'BODY');
201
 
202
- // Create or reuse reference block
203
  const blockType = createInputRefBlockType(name);
204
  if (!existingRefBlock) {
205
- // Create new reference block
206
- const workspace = this.workspace;
207
- const refBlock = workspace.newBlock(blockType);
208
  refBlock.initSvg();
209
- refBlock.setDeletable(false); // Can't be deleted directly
210
  refBlock.render();
 
 
211
  this.inputRefBlocks_.set(name, refBlock);
212
-
213
- // Connect new block
214
- if (input && input.connection && refBlock.outputConnection) {
215
  input.connection.connect(refBlock.outputConnection);
216
  }
217
- } else {
218
- // Reuse existing block - connect it to new input
219
- if (input && input.connection && existingRefBlock.outputConnection) {
220
- input.connection.connect(existingRefBlock.outputConnection);
221
- }
222
-
223
- // Update the Python generator for renamed variables
224
- pythonGenerator.forBlock[blockType] = function (block) {
225
- return [name, pythonGenerator.ORDER_ATOMIC];
226
- };
227
  }
 
 
 
 
228
  }
229
 
230
- // Reconnect preserved connections
231
- for (var k = 0; k < newCount; k++) {
232
- var conn = connections[k];
233
  if (conn) {
234
  try {
235
  conn.connect(this.getInput('X' + k).connection);
236
- } catch (e) {
237
- // ignore failed reconnects
238
- }
239
  }
240
  }
241
 
242
- // Force workspace to update
243
  this.workspace.render();
244
-
245
  } finally {
246
- // Re-enable events
247
  Blockly.Events.enable();
248
  }
249
  },
250
 
251
  saveExtraState: function () {
252
- const state = {
253
  inputCount: this.inputCount_,
254
  inputNames: this.inputNames_,
255
- inputTypes: this.inputTypes_
 
256
  };
257
- return state;
258
  },
259
 
260
  loadExtraState: function (state) {
261
- this.inputCount_ = state['inputCount'];
262
- this.inputNames_ = state['inputNames'] || [];
263
- this.inputTypes_ = state['inputTypes'] || [];
 
264
  }
265
  },
266
- null, // No helper function needed
267
  ['container_input']
268
  );
269
 
270
- // Define the test block with mutator
271
- const create_mcp = {
272
- "type": "create_mcp",
273
- "message0": "create MCP with inputs: %1 %2 and return %3",
274
- "args0": [
275
- {
276
- "type": "input_dummy"
277
- },
278
- {
279
- "type": "input_statement",
280
- "name": "BODY"
281
- },
282
- {
283
- "type": "input_value",
284
- "name": "RETURN",
285
- },
286
- ],
287
- "colour": 160,
288
- "inputsInline": true,
289
- "mutator": "test_mutator",
290
- "inputCount_": 0, // Start with no inputs
291
- "deletable": false, // Make the block non-deletable
292
-
293
- // Override the dispose function to clean up reference blocks
294
- "extensions": ["test_cleanup_extension"]
295
- };
296
-
297
- // Define the container block for the mutator
298
  const container = {
299
- "type": "container",
300
- "message0": "inputs %1 %2",
301
- "args0": [
302
- {
303
- "type": "input_dummy",
304
- "name": "title"
305
- },
306
- {
307
- "type": "input_statement",
308
- "name": "STACK"
309
- }
310
  ],
311
- "colour": 160,
312
- "inputsInline": false
313
- }
314
-
315
 
316
- // Define the input block for the mutator
317
  const container_input = {
318
- type: 'container_input',
319
- message0: '%1 %2',
320
  args0: [
321
  {
322
- type: 'field_dropdown',
323
- name: 'TYPE',
324
  options: [
325
  ["String", "string"],
326
  ["Integer", "integer"],
327
  ["List", "list"],
328
  ]
329
  },
330
- {
331
- type: 'field_input',
332
- name: 'NAME',
333
- },
334
  ],
335
  previousStatement: null,
336
  nextStatement: null,
337
  colour: 210,
338
  };
339
 
340
- // Register an extension to handle cleanup when the block is deleted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  Blockly.Extensions.register('test_cleanup_extension', function () {
342
- // Store the original dispose function
343
  const oldDispose = this.dispose;
344
-
345
- // Override the dispose function
346
  this.dispose = function (healStack, recursive) {
347
- // Clean up all reference blocks first
348
  if (this.inputRefBlocks_) {
349
- for (const [name, refBlock] of this.inputRefBlocks_) {
350
- if (refBlock && !refBlock.disposed) {
351
- refBlock.dispose(false); // Don't heal stack for reference blocks
352
- }
353
  }
354
  this.inputRefBlocks_.clear();
355
  }
356
-
357
- // Call the original dispose function
358
- if (oldDispose) {
359
- oldDispose.call(this, healStack, recursive);
360
- }
361
  };
362
  });
363
 
364
- // Create block definitions from the JSON
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
366
  create_mcp,
367
  container,
368
  container_input,
 
369
  ]);
 
1
  import * as Blockly from 'blockly/core';
2
  import { pythonGenerator } from 'blockly/python';
3
 
4
+ // Utility to create a unique input reference block type
5
  function createInputRefBlockType(inputName) {
6
  const blockType = `input_reference_${inputName}`;
 
 
7
  if (!Blockly.Blocks[blockType]) {
 
8
  Blockly.Blocks[blockType] = {
9
  init: function () {
10
  this.jsonInit({
11
+ type: blockType,
12
+ message0: "%1",
13
+ args0: [
14
  {
15
+ type: "field_label_serializable",
16
+ name: "VARNAME",
17
+ text: inputName
18
  }
19
  ],
20
+ output: null,
21
+ colour: 210,
22
+ outputShape: 2
23
  });
24
  }
25
  };
26
+ pythonGenerator.forBlock[blockType] = function () {
 
 
 
27
  return [inputName, pythonGenerator.ORDER_ATOMIC];
28
  };
29
  }
30
  return blockType;
31
  }
32
 
33
+ // Global input reference tracking map
34
+ const inputRefs = new Map();
35
 
36
+ // Core mutator registration for dynamic tool and input creation
37
  Blockly.Extensions.registerMutator(
38
  'test_mutator',
39
  {
 
40
  initialize: function () {
41
  if (!this.initialized_) {
42
  this.inputCount_ = 0;
43
  this.inputNames_ = [];
44
  this.inputTypes_ = [];
45
+ this.inputRefBlocks_ = new Map();
46
  this.initialized_ = true;
47
+ // Mark all reference blocks with their owner for later identification
48
+ this._ownerBlockId = this.id;
49
  }
50
  },
51
 
52
  decompose: function (workspace) {
53
+ const containerBlock = workspace.newBlock('container');
54
  containerBlock.initSvg();
55
+ let connection = containerBlock.getInput('STACK').connection;
56
 
 
57
  this.inputCount_ = this.inputCount_ || 0;
58
  this.inputNames_ = this.inputNames_ || [];
59
  this.inputTypes_ = this.inputTypes_ || [];
60
 
61
+ // Restore dynamically added input items
62
+ for (let i = 0; i < this.inputCount_; i++) {
63
+ const itemBlock = workspace.newBlock('container_input');
64
  itemBlock.initSvg();
65
+ const typeVal = this.inputTypes_[i] || 'string';
66
+ const nameVal = this.inputNames_[i] || typeVal;
67
+ itemBlock.setFieldValue(typeVal, 'TYPE');
68
+ itemBlock.setFieldValue(nameVal, 'NAME');
69
 
70
+ const input = this.getInput('X' + i);
 
 
 
 
 
 
71
  if (input && input.connection && input.connection.targetConnection) {
72
  itemBlock.valueConnection_ = input.connection.targetConnection;
73
  }
 
80
  },
81
 
82
  compose: function (containerBlock) {
 
83
  Blockly.Events.disable();
 
84
  try {
85
+ if (!this.initialized_) this.initialize();
 
 
 
86
 
87
+ const oldNames = [...(this.inputNames_ || [])];
88
+ const connections = [];
89
+ let itemBlock = containerBlock.getInputTargetBlock('STACK');
90
 
91
+ // Collect all child connections from mutator stack
92
  while (itemBlock) {
93
  connections.push(itemBlock.valueConnection_);
94
  itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock();
95
  }
96
 
97
+ const newCount = connections.length;
98
  this.inputCount_ = newCount;
99
  this.inputNames_ = this.inputNames_ || [];
100
  this.inputTypes_ = this.inputTypes_ || [];
101
 
102
+ // Rebuild the new list of input names and types
103
+ let idx = 0;
104
+ let it = containerBlock.getInputTargetBlock('STACK');
105
+ const newNames = [];
106
  while (it) {
107
  this.inputTypes_[idx] = it.getFieldValue('TYPE') || 'string';
108
+ this.inputNames_[idx] = it.getFieldValue('NAME') || 'arg' + idx;
109
  newNames.push(this.inputNames_[idx]);
110
  it = it.nextConnection && it.nextConnection.targetBlock();
111
  idx++;
112
  }
113
 
114
+ // Dispose of removed input reference blocks when inputs shrink
115
  if (newCount < oldNames.length) {
 
116
  for (let i = newCount; i < oldNames.length; i++) {
117
  const oldName = oldNames[i];
118
  const block = this.inputRefBlocks_.get(oldName);
119
+ if (block && !block.disposed) block.dispose(true);
 
 
120
  this.inputRefBlocks_.delete(oldName);
121
  }
122
  }
123
 
124
+ // Rename reference blocks when variable names change
125
+ // Only update reference blocks that belong to THIS block
126
  for (let i = 0; i < Math.min(oldNames.length, newNames.length); i++) {
127
  const oldName = oldNames[i];
128
  const newName = newNames[i];
 
130
  const oldBlockType = `input_reference_${oldName}`;
131
  const newBlockType = `input_reference_${newName}`;
132
 
 
133
  if (this.inputRefBlocks_.has(oldName)) {
134
  const refBlock = this.inputRefBlocks_.get(oldName);
135
  if (refBlock && !refBlock.disposed) {
136
  this.inputRefBlocks_.delete(oldName);
137
  this.inputRefBlocks_.set(newName, refBlock);
138
  refBlock.setFieldValue(newName, 'VARNAME');
139
+ // Properly update the block type in workspace tracking
140
+ if (refBlock.workspace && refBlock.workspace.removeTypedBlock) {
141
+ refBlock.workspace.removeTypedBlock(refBlock);
142
+ refBlock.type = newBlockType;
143
+ refBlock.workspace.addTypedBlock(refBlock);
144
+ } else {
145
+ refBlock.type = newBlockType;
146
+ }
147
  }
148
  }
149
 
150
+ // Update all clones of this reference block that share the same owner
151
+ // (i.e., all clones that were created from the same parent block)
152
+ const refBlock = this.inputRefBlocks_.get(newName);
153
+ const ownerBlockId = this.id;
154
+
155
+ if (refBlock && !refBlock.disposed) {
156
+ const allBlocks = this.workspace.getAllBlocks(false);
157
+ for (const block of allBlocks) {
158
+ if (block.type === oldBlockType) {
159
+ // Update if this block has the same owner as our reference block
160
+ // This includes both connected and cloned blocks
161
+ if (block._ownerBlockId === ownerBlockId) {
162
+ // Properly update the block type in workspace tracking
163
+ if (block.workspace && block.workspace.removeTypedBlock) {
164
+ block.workspace.removeTypedBlock(block);
165
+ block.type = newBlockType;
166
+ block.workspace.addTypedBlock(block);
167
+ } else {
168
+ block.type = newBlockType;
169
+ }
170
+ block.setFieldValue(newName, 'VARNAME');
171
+ }
172
+ }
173
  }
174
  }
175
 
176
+ pythonGenerator.forBlock[newBlockType] = function () {
 
177
  return [newName, pythonGenerator.ORDER_ATOMIC];
178
  };
179
  }
180
  }
181
 
182
+ // Remove all dynamic and temporary inputs before reconstruction
183
+ let i = 0;
184
+ while (this.getInput('X' + i)) this.removeInput('X' + i++);
185
+ let t = 0;
186
+ while (this.getInput('T' + t)) this.removeInput('T' + t++);
187
+ ['INPUTS_TEXT', 'TOOLS_TEXT'].forEach(name => {
188
+ if (this.getInput(name)) this.removeInput(name);
189
+ });
190
+
191
+ if (newCount > 0) {
192
+ const inputsText = this.appendDummyInput('INPUTS_TEXT');
193
+ inputsText.appendField('with inputs:');
194
+ this.moveInputBefore('INPUTS_TEXT', 'BODY');
195
  }
196
 
197
+ // Add each dynamic input, reconnecting to reference blocks
198
+ for (let j = 0; j < newCount; j++) {
199
+ const type = this.inputTypes_[j] || 'string';
200
+ const name = this.inputNames_[j] || type;
201
+ let check = null;
202
  if (type === 'integer') check = 'Number';
203
  if (type === 'string') check = 'String';
 
204
 
 
205
  const existingRefBlock = this.inputRefBlocks_.get(name);
206
+ const input = this.appendValueInput('X' + j);
 
 
207
  if (check) input.setCheck(check);
208
+ input.appendField(type);
 
 
209
  this.moveInputBefore('X' + j, 'BODY');
210
 
 
211
  const blockType = createInputRefBlockType(name);
212
  if (!existingRefBlock) {
213
+ const refBlock = this.workspace.newBlock(blockType);
 
 
214
  refBlock.initSvg();
215
+ refBlock.setDeletable(false);
216
  refBlock.render();
217
+ // Mark the reference block with its owner
218
+ refBlock._ownerBlockId = this.id;
219
  this.inputRefBlocks_.set(name, refBlock);
220
+ if (input.connection && refBlock.outputConnection) {
 
 
221
  input.connection.connect(refBlock.outputConnection);
222
  }
223
+ } else if (input.connection && existingRefBlock.outputConnection) {
224
+ input.connection.connect(existingRefBlock.outputConnection);
 
 
 
 
 
 
 
 
225
  }
226
+
227
+ pythonGenerator.forBlock[blockType] = function () {
228
+ return [name, pythonGenerator.ORDER_ATOMIC];
229
+ };
230
  }
231
 
232
+ // Reconnect preserved connections to new structure
233
+ for (let k = 0; k < newCount; k++) {
234
+ const conn = connections[k];
235
  if (conn) {
236
  try {
237
  conn.connect(this.getInput('X' + k).connection);
238
+ } catch { }
 
 
239
  }
240
  }
241
 
 
242
  this.workspace.render();
 
243
  } finally {
 
244
  Blockly.Events.enable();
245
  }
246
  },
247
 
248
  saveExtraState: function () {
249
+ return {
250
  inputCount: this.inputCount_,
251
  inputNames: this.inputNames_,
252
+ inputTypes: this.inputTypes_,
253
+ toolCount: this.toolCount_ || 0
254
  };
 
255
  },
256
 
257
  loadExtraState: function (state) {
258
+ this.inputCount_ = state.inputCount;
259
+ this.inputNames_ = state.inputNames || [];
260
+ this.inputTypes_ = state.inputTypes || [];
261
+ this.toolCount_ = state.toolCount || 0;
262
  }
263
  },
264
+ null,
265
  ['container_input']
266
  );
267
 
268
+ // Base block definitions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  const container = {
270
+ type: "container",
271
+ message0: "inputs %1 %2",
272
+ args0: [
273
+ { type: "input_dummy", name: "title" },
274
+ { type: "input_statement", name: "STACK" },
 
 
 
 
 
 
275
  ],
276
+ colour: 160,
277
+ inputsInline: false
278
+ };
 
279
 
 
280
  const container_input = {
281
+ type: "container_input",
282
+ message0: "input %1 %2",
283
  args0: [
284
  {
285
+ type: "field_dropdown",
286
+ name: "TYPE",
287
  options: [
288
  ["String", "string"],
289
  ["Integer", "integer"],
290
  ["List", "list"],
291
  ]
292
  },
293
+ { type: "field_input", name: "NAME" },
 
 
 
294
  ],
295
  previousStatement: null,
296
  nextStatement: null,
297
  colour: 210,
298
  };
299
 
300
+ const llm_call = {
301
+ type: "llm_call",
302
+ message0: "call model %1 with prompt %2",
303
+ args0: [
304
+ {
305
+ type: "field_dropdown",
306
+ name: "MODEL",
307
+ options: [
308
+ ["gpt-3.5-turbo", "gpt-3.5-turbo-0125"],
309
+ ["gpt-5-mini", "gpt-5-mini-2025-08-07"],
310
+ ]
311
+ },
312
+ { type: "input_value", name: "PROMPT", check: "String" },
313
+ ],
314
+ inputsInline: true,
315
+ output: "String",
316
+ colour: 230,
317
+ tooltip: "Call the selected OpenAI model to get a response.",
318
+ helpUrl: "",
319
+ };
320
+
321
+ const create_mcp = {
322
+ type: "create_mcp",
323
+ message0: "create MCP %1 %2 and return %3",
324
+ args0: [
325
+ { type: "input_dummy" },
326
+ { type: "input_statement", name: "BODY" },
327
+ { type: "input_value", name: "RETURN" },
328
+ ],
329
+ colour: 160,
330
+ inputsInline: true,
331
+ mutator: "test_mutator",
332
+ inputCount_: 0,
333
+ deletable: false,
334
+ extensions: ["test_cleanup_extension"]
335
+ };
336
+
337
+ const tool_def = {
338
+ type: "tool_def",
339
+ message0: "function %1 %2 %3 and return %4",
340
+ args0: [
341
+ { type: "field_input", name: "NAME", text: "newFunction" },
342
+ { type: "input_dummy" },
343
+ { type: "input_statement", name: "BODY" },
344
+ { type: "input_value", name: "RETURN" },
345
+ ],
346
+ colour: 160,
347
+ inputsInline: true,
348
+ mutator: "test_mutator",
349
+ inputCount_: 0,
350
+ deletable: true,
351
+ extensions: ["test_cleanup_extension"]
352
+ };
353
+
354
+ // Cleanup extension ensures that dynamic reference blocks are deleted when parent is
355
  Blockly.Extensions.register('test_cleanup_extension', function () {
 
356
  const oldDispose = this.dispose;
 
 
357
  this.dispose = function (healStack, recursive) {
 
358
  if (this.inputRefBlocks_) {
359
+ for (const [, refBlock] of this.inputRefBlocks_) {
360
+ if (refBlock && !refBlock.disposed) refBlock.dispose(false);
 
 
361
  }
362
  this.inputRefBlocks_.clear();
363
  }
364
+ if (oldDispose) oldDispose.call(this, healStack, recursive);
 
 
 
 
365
  };
366
  });
367
 
368
+ // Function to generate a unique tool name
369
+ function generateUniqueToolName(workspace, excludeBlock) {
370
+ const existingNames = new Set();
371
+ const allBlocks = workspace.getAllBlocks(false);
372
+
373
+ // Collect all existing tool names, excluding the block being created
374
+ for (const block of allBlocks) {
375
+ if (block.type === 'tool_def' && block !== excludeBlock && block.getFieldValue('NAME')) {
376
+ existingNames.add(block.getFieldValue('NAME'));
377
+ }
378
+ }
379
+
380
+ // Generate a unique name
381
+ let baseName = 'newTool';
382
+ let name = baseName;
383
+ let counter = 1;
384
+
385
+ while (existingNames.has(name)) {
386
+ counter++;
387
+ name = `${baseName}${counter}`;
388
+ }
389
+
390
+ return name;
391
+ }
392
+
393
+ // Register tool_def block separately to include custom init logic
394
+ Blockly.Blocks['tool_def'] = {
395
+ init: function () {
396
+ this.jsonInit(tool_def);
397
+ // Apply extensions
398
+ Blockly.Extensions.apply('test_cleanup_extension', this, false);
399
+ }
400
+ };
401
+
402
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
403
  create_mcp,
404
  container,
405
  container_input,
406
+ llm_call,
407
  ]);
project/src/generators/python.js CHANGED
@@ -3,17 +3,16 @@ import { Order } from 'blockly/python';
3
  export const forBlock = Object.create(null);
4
 
5
  forBlock['create_mcp'] = function (block, generator) {
6
- // Get all inputs with their types
7
  const typedInputs = [];
8
  let i = 0;
 
 
9
  while (block.getInput('X' + i)) {
10
  const input = block.getInput('X' + i);
11
  if (input && input.connection && input.connection.targetBlock()) {
12
- // Get the actual parameter name from inputNames_ array
13
  const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i);
14
- // Get the type from inputTypes_ array
15
  const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string';
16
- // Convert type to Python type annotation
17
  let pyType;
18
  switch (type) {
19
  case 'integer':
@@ -33,13 +32,82 @@ forBlock['create_mcp'] = function (block, generator) {
33
  i++;
34
  }
35
 
36
- // Get the code for blocks inside the BODY statement input
 
 
 
 
 
 
 
 
 
 
 
37
  let body = generator.statementToCode(block, 'BODY');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- // Get the return value if any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  let returnValue = generator.valueToCode(block, 'RETURN', Order.ATOMIC);
41
 
42
- // Replace arg references with actual parameter names
43
  if (returnValue && block.inputNames_) {
44
  for (let j = 0; j < block.inputNames_.length; j++) {
45
  const paramName = block.inputNames_[j];
@@ -49,11 +117,23 @@ forBlock['create_mcp'] = function (block, generator) {
49
 
50
  let returnStatement = returnValue ? ` return ${returnValue}\n` : ' return\n';
51
 
52
- // Create the function with all typed inputs
 
53
  if (typedInputs.length > 0) {
54
- const code = `def create_mcp(${typedInputs.join(', ')}):\n${body}${returnStatement}\n`;
55
- return code;
56
  } else {
57
- return `def create_mcp():\n${body}${returnStatement}`;
58
  }
59
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  export const forBlock = Object.create(null);
4
 
5
  forBlock['create_mcp'] = function (block, generator) {
 
6
  const typedInputs = [];
7
  let i = 0;
8
+
9
+ // Build list of typed input parameters with inferred Python types
10
  while (block.getInput('X' + i)) {
11
  const input = block.getInput('X' + i);
12
  if (input && input.connection && input.connection.targetBlock()) {
 
13
  const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i);
 
14
  const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string';
15
+
16
  let pyType;
17
  switch (type) {
18
  case 'integer':
 
32
  i++;
33
  }
34
 
35
+ // Gather any tool definitions connected to this block
36
+ let toolDefs = [];
37
+ let t = 0;
38
+ while (block.getInput('T' + t)) {
39
+ const toolCode = generator.valueToCode(block, 'T' + t, Order.NONE);
40
+ if (toolCode) {
41
+ toolDefs.push(toolCode);
42
+ }
43
+ t++;
44
+ }
45
+
46
+ // Main function body and return value
47
  let body = generator.statementToCode(block, 'BODY');
48
+ let returnValue = generator.valueToCode(block, 'RETURN', Order.ATOMIC);
49
+
50
+ // Replace placeholder args (arg0, arg1...) with actual names in return statement
51
+ if (returnValue && block.inputNames_) {
52
+ for (let j = 0; j < block.inputNames_.length; j++) {
53
+ const paramName = block.inputNames_[j];
54
+ returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
55
+ }
56
+ }
57
+
58
+ let returnStatement = returnValue ? ` return ${returnValue}\n` : ' return\n';
59
+ let code = '';
60
+
61
+ // Tool definitions come before main function
62
+ if (toolDefs.length > 0) {
63
+ code += toolDefs.join('\n') + '\n\n';
64
+ }
65
+
66
+ // Create the main function definition
67
+ if (typedInputs.length > 0) {
68
+ code += `def create_mcp(${typedInputs.join(', ')}):\n${body}${returnStatement}\n`;
69
+ } else {
70
+ code += `def create_mcp():\n${body}${returnStatement}`;
71
+ }
72
+
73
+ return code;
74
+ };
75
+
76
+ forBlock['tool_def'] = function (block, generator) {
77
+ const name = block.getFieldValue('NAME');
78
+ const typedInputs = [];
79
+ let i = 0;
80
+
81
+ // Build function signature with typed arguments
82
+ while (block.getInput('X' + i)) {
83
+ const input = block.getInput('X' + i);
84
+ if (input && input.connection && input.connection.targetBlock()) {
85
+ const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i);
86
+ const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string';
87
 
88
+ let pyType;
89
+ switch (type) {
90
+ case 'integer':
91
+ pyType = 'int';
92
+ break;
93
+ case 'string':
94
+ pyType = 'str';
95
+ break;
96
+ case 'list':
97
+ pyType = 'list';
98
+ break;
99
+ default:
100
+ pyType = 'Any';
101
+ }
102
+ typedInputs.push(`${paramName}: ${pyType}`);
103
+ }
104
+ i++;
105
+ }
106
+
107
+ let body = generator.statementToCode(block, 'BODY');
108
  let returnValue = generator.valueToCode(block, 'RETURN', Order.ATOMIC);
109
 
110
+ // Ensure return expression uses correct parameter names
111
  if (returnValue && block.inputNames_) {
112
  for (let j = 0; j < block.inputNames_.length; j++) {
113
  const paramName = block.inputNames_[j];
 
117
 
118
  let returnStatement = returnValue ? ` return ${returnValue}\n` : ' return\n';
119
 
120
+ // Construct the function definition
121
+ let code;
122
  if (typedInputs.length > 0) {
123
+ code = `def ${name}(${typedInputs.join(', ')}):\n${body}${returnStatement}`;
 
124
  } else {
125
+ code = `def ${name}():\n${body}${returnStatement}`;
126
  }
127
+
128
+ // Return function definition as a string value (not executed immediately)
129
+ return code;
130
+ };
131
+
132
+ forBlock['llm_call'] = function (block, generator) {
133
+ const model = block.getFieldValue('MODEL');
134
+ const prompt = generator.valueToCode(block, 'PROMPT', Order.NONE) || "''";
135
+
136
+ // Generate code to call an LLM model with a prompt
137
+ const code = `llm_call(${prompt}, model="${model}")`;
138
+ return [code, Order.NONE];
139
+ };
project/src/index.js CHANGED
@@ -57,8 +57,26 @@ const updateCode = () => {
57
  let code = pythonGenerator.workspaceToCode(ws);
58
  const codeEl = document.querySelector('#generatedCode code');
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  const blocks = ws.getAllBlocks(false);
61
- const hasBlock = blocks.some(block => block.type === 'block');
 
 
 
 
62
 
63
  if (codeEl) {
64
  codeEl.textContent = code;
@@ -118,8 +136,7 @@ ws.addChangeListener((event) => {
118
  if (
119
  removedBlock &&
120
  oldParent &&
121
- removedBlock.type.startsWith('input_reference_') &&
122
- oldParent.type === 'create_mcp'
123
  ) {
124
  // Only duplicate if removed from a mutator input (X0, X1, X2, etc.)
125
  // NOT from other inputs like RETURN, BODY, or title input
@@ -133,6 +150,8 @@ ws.addChangeListener((event) => {
133
  const newRefBlock = ws.newBlock(removedBlock.type);
134
  newRefBlock.initSvg();
135
  newRefBlock.setDeletable(false); // This one stays in the MCP block
 
 
136
 
137
  const input = oldParent.getInput(inputName);
138
  if (input) {
@@ -140,9 +159,11 @@ ws.addChangeListener((event) => {
140
  }
141
 
142
  // Update the parent block's reference tracking
143
- const varName = removedBlock.type.replace('input_reference_', '');
144
- if (oldParent.inputRefBlocks_) {
145
- oldParent.inputRefBlocks_.set(varName, newRefBlock);
 
 
146
  }
147
 
148
  // Make the dragged-out block deletable
 
57
  let code = pythonGenerator.workspaceToCode(ws);
58
  const codeEl = document.querySelector('#generatedCode code');
59
 
60
+ const call = `def llm_call(prompt, model):
61
+ global history
62
+ from openai import OpenAI
63
+ import os
64
+
65
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
66
+
67
+ messages = [{"role": "user", "content": prompt}]
68
+
69
+ completion = client.chat.completions.create(model=model, messages=messages)
70
+ return completion.choices[0].message.content.strip()
71
+
72
+ `;
73
+
74
  const blocks = ws.getAllBlocks(false);
75
+ const hasCall = blocks.some(block => block.type === 'llm_call');
76
+
77
+ if (hasCall) {
78
+ code = call + code;
79
+ }
80
 
81
  if (codeEl) {
82
  codeEl.textContent = code;
 
136
  if (
137
  removedBlock &&
138
  oldParent &&
139
+ (removedBlock.type.startsWith('input_reference_') && (oldParent.type === 'create_mcp' || oldParent.type === 'tool_def'))
 
140
  ) {
141
  // Only duplicate if removed from a mutator input (X0, X1, X2, etc.)
142
  // NOT from other inputs like RETURN, BODY, or title input
 
150
  const newRefBlock = ws.newBlock(removedBlock.type);
151
  newRefBlock.initSvg();
152
  newRefBlock.setDeletable(false); // This one stays in the MCP block
153
+ // Mark the new reference block with its owner (same as the parent)
154
+ newRefBlock._ownerBlockId = oldParent.id;
155
 
156
  const input = oldParent.getInput(inputName);
157
  if (input) {
 
159
  }
160
 
161
  // Update the parent block's reference tracking
162
+ if (removedBlock.type.startsWith('input_reference_')) {
163
+ const varName = removedBlock.type.replace('input_reference_', '');
164
+ if (oldParent.inputRefBlocks_) {
165
+ oldParent.inputRefBlocks_.set(varName, newRefBlock);
166
+ }
167
  }
168
 
169
  // Make the dragged-out block deletable
project/src/toolbox.js CHANGED
@@ -9,7 +9,24 @@ export const toolbox = {
9
  {
10
  'kind': 'sep',
11
  },
12
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  {
14
  kind: 'category',
15
  name: 'Logic',
@@ -533,7 +550,7 @@ export const toolbox = {
533
  },
534
  {
535
  kind: 'category',
536
- name: 'Tools',
537
  categorystyle: 'procedure_category',
538
  custom: 'PROCEDURE',
539
  },
 
9
  {
10
  'kind': 'sep',
11
  },
12
+ {
13
+ kind: 'category',
14
+ name: 'Custom',
15
+ categorystyle: 'logic_category',
16
+ contents: [
17
+ {
18
+ kind: 'block',
19
+ type: 'llm_call',
20
+ },
21
+ {
22
+ kind: 'block',
23
+ type: 'tool_def',
24
+ },
25
+ ]
26
+ },
27
+ {
28
+ 'kind': 'sep',
29
+ },
30
  {
31
  kind: 'category',
32
  name: 'Logic',
 
550
  },
551
  {
552
  kind: 'category',
553
+ name: 'Functions',
554
  categorystyle: 'procedure_category',
555
  custom: 'PROCEDURE',
556
  },