owenkaplinsky commited on
Commit
d9f156a
·
1 Parent(s): 60456b4

Add function calling

Browse files
project/src/blocks/text.js CHANGED
@@ -372,7 +372,7 @@ const container = {
372
  { type: "input_dummy", name: "title2" },
373
  { type: "input_statement", name: "STACK2" },
374
  ],
375
- colour: 160,
376
  inputsInline: false
377
  };
378
 
@@ -432,11 +432,47 @@ const llm_call = {
432
  ],
433
  inputsInline: true,
434
  output: "String",
435
- colour: 230,
436
  tooltip: "Call the selected OpenAI model to get a response.",
437
  helpUrl: "",
438
  };
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  const create_mcp = {
441
  type: "create_mcp",
442
  message0: "create MCP %1 %2",
@@ -444,7 +480,7 @@ const create_mcp = {
444
  { type: "input_dummy" },
445
  { type: "input_statement", name: "BODY" },
446
  ],
447
- colour: 160,
448
  inputsInline: true,
449
  mutator: "test_mutator",
450
  inputCount_: 0,
@@ -460,7 +496,7 @@ const func_def = {
460
  { type: "input_dummy" },
461
  { type: "input_statement", name: "BODY" },
462
  ],
463
- colour: 160,
464
  inputsInline: true,
465
  mutator: "test_mutator",
466
  inputCount_: 0,
@@ -482,6 +518,188 @@ Blockly.Extensions.register('test_cleanup_extension', function () {
482
  };
483
  });
484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  // Function to generate a unique tool name
486
  function generateUniqueToolName(workspace, excludeBlock) {
487
  const existingNames = new Set();
@@ -533,6 +751,33 @@ Blockly.Blocks['func_def'] = {
533
  }
534
  };
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
537
  container,
538
  container_input,
 
372
  { type: "input_dummy", name: "title2" },
373
  { type: "input_statement", name: "STACK2" },
374
  ],
375
+ colour: 210,
376
  inputsInline: false
377
  };
378
 
 
432
  ],
433
  inputsInline: true,
434
  output: "String",
435
+ colour: 160,
436
  tooltip: "Call the selected OpenAI model to get a response.",
437
  helpUrl: "",
438
  };
439
 
440
+ // Dynamic function call block
441
+ const func_call = {
442
+ type: "func_call",
443
+ message0: "func %1",
444
+ args0: [
445
+ {
446
+ type: "field_dropdown",
447
+ name: "FUNC_NAME",
448
+ options: function () {
449
+ // This will be populated dynamically
450
+ const options = [["<no functions>", "NONE"]];
451
+ if (this.sourceBlock_) {
452
+ const workspace = this.sourceBlock_.workspace;
453
+ const funcBlocks = workspace.getAllBlocks(false).filter(b => b.type === 'func_def');
454
+ if (funcBlocks.length > 0) {
455
+ options.length = 0;
456
+ funcBlocks.forEach(block => {
457
+ const name = block.getFieldValue('NAME');
458
+ if (name) {
459
+ options.push([name, name]);
460
+ }
461
+ });
462
+ }
463
+ }
464
+ return options;
465
+ }
466
+ }
467
+ ],
468
+ inputsInline: true,
469
+ output: null,
470
+ colour: 210,
471
+ tooltip: "Call a function defined in the workspace",
472
+ helpUrl: "",
473
+ extensions: ["func_call_dynamic"]
474
+ };
475
+
476
  const create_mcp = {
477
  type: "create_mcp",
478
  message0: "create MCP %1 %2",
 
480
  { type: "input_dummy" },
481
  { type: "input_statement", name: "BODY" },
482
  ],
483
+ colour: 210,
484
  inputsInline: true,
485
  mutator: "test_mutator",
486
  inputCount_: 0,
 
496
  { type: "input_dummy" },
497
  { type: "input_statement", name: "BODY" },
498
  ],
499
+ colour: 210,
500
  inputsInline: true,
501
  mutator: "test_mutator",
502
  inputCount_: 0,
 
518
  };
519
  });
520
 
521
+ // Extension for dynamic function call blocks
522
+ Blockly.Extensions.register('func_call_dynamic', function () {
523
+ const block = this;
524
+
525
+ // Store current function being called
526
+ block.currentFunction_ = null;
527
+ block.paramCount_ = 0;
528
+
529
+ // Function to update the block based on selected function
530
+ block.updateShape_ = function () {
531
+ const funcName = this.getFieldValue('FUNC_NAME');
532
+
533
+ // Temporarily disable events to prevent recursive updates
534
+ const eventsEnabled = Blockly.Events.isEnabled();
535
+ if (eventsEnabled) {
536
+ Blockly.Events.disable();
537
+ }
538
+
539
+ try {
540
+ // Remove all existing parameter inputs
541
+ let i = 0;
542
+ while (this.getInput('ARG' + i)) {
543
+ this.removeInput('ARG' + i);
544
+ i++;
545
+ }
546
+
547
+ if (funcName && funcName !== 'NONE') {
548
+ // Find the function definition block
549
+ const workspace = this.workspace;
550
+ const funcBlock = workspace.getAllBlocks(false).find(b =>
551
+ b.type === 'func_def' && b.getFieldValue('NAME') === funcName
552
+ );
553
+
554
+ if (funcBlock) {
555
+ this.currentFunction_ = funcName;
556
+
557
+ // Get the function's parameters
558
+ const inputCount = funcBlock.inputCount_ || 0;
559
+ const inputNames = funcBlock.inputNames_ || [];
560
+ const inputTypes = funcBlock.inputTypes_ || [];
561
+
562
+ this.paramCount_ = inputCount;
563
+
564
+ // Add parameter inputs matching the function definition
565
+ for (let j = 0; j < inputCount; j++) {
566
+ const paramName = inputNames[j] || ('arg' + j);
567
+ const paramType = inputTypes[j] || 'string';
568
+
569
+ let check = null;
570
+ if (paramType === 'integer') check = 'Number';
571
+ if (paramType === 'string') check = 'String';
572
+
573
+ const input = this.appendValueInput('ARG' + j);
574
+ if (check) input.setCheck(check);
575
+ input.appendField(`${paramType} "${paramName}"`);
576
+ }
577
+
578
+ // Set output type based on function's return type
579
+ if (funcBlock.outputCount_ && funcBlock.outputCount_ > 0) {
580
+ const outputType = funcBlock.outputTypes_[0] || 'string';
581
+ if (outputType === 'integer') {
582
+ this.setOutput(true, 'Number');
583
+ } else if (outputType === 'string') {
584
+ this.setOutput(true, 'String');
585
+ } else {
586
+ this.setOutput(true, null);
587
+ }
588
+ } else {
589
+ this.setOutput(true, null);
590
+ }
591
+ } else {
592
+ this.currentFunction_ = null;
593
+ this.paramCount_ = 0;
594
+ }
595
+ } else {
596
+ this.currentFunction_ = null;
597
+ this.paramCount_ = 0;
598
+ }
599
+ } finally {
600
+ // Re-enable events if they were enabled before
601
+ if (eventsEnabled) {
602
+ Blockly.Events.enable();
603
+ }
604
+ }
605
+ };
606
+
607
+ // Defer initial shape update to ensure block is fully initialized
608
+ setTimeout(() => {
609
+ block.updateShape_();
610
+ }, 0);
611
+
612
+ // Listen for dropdown changes
613
+ block.getField('FUNC_NAME').setValidator(function (newValue) {
614
+ const block = this.getSourceBlock();
615
+ // Ensure the update happens after the dropdown value is set
616
+ setTimeout(() => {
617
+ block.updateShape_();
618
+ }, 0);
619
+ return newValue;
620
+ });
621
+
622
+ // Listen for workspace changes to update when functions are modified
623
+ const workspaceListener = function (event) {
624
+ // Skip if block has been disposed
625
+ if (block.disposed) {
626
+ return;
627
+ }
628
+
629
+ if (event.type === Blockly.Events.BLOCK_CHANGE ||
630
+ event.type === Blockly.Events.BLOCK_DELETE ||
631
+ event.type === Blockly.Events.BLOCK_CREATE ||
632
+ event.type === Blockly.Events.BLOCK_MOVE) {
633
+
634
+ // Check if a func_def block was changed
635
+ const changedBlock = block.workspace.getBlockById(event.blockId);
636
+
637
+ if (event.type === Blockly.Events.BLOCK_DELETE) {
638
+ // Delay check to ensure workspace has been updated
639
+ setTimeout(() => {
640
+ if (!block.disposed && block.currentFunction_ &&
641
+ !block.workspace.getAllBlocks(false).some(b =>
642
+ b.type === 'func_def' && b.getFieldValue('NAME') === block.currentFunction_)) {
643
+ block.dispose(true);
644
+ }
645
+ }, 0);
646
+ } else if (changedBlock && changedBlock.type === 'func_def') {
647
+ // If the function being called was modified, update shape
648
+ const funcName = changedBlock.getFieldValue('NAME');
649
+ if (funcName === block.currentFunction_ ||
650
+ (event.oldValue && event.oldValue === block.currentFunction_)) {
651
+ // Handle renaming
652
+ if (event.type === Blockly.Events.BLOCK_CHANGE &&
653
+ event.name === 'NAME' && event.oldValue === block.currentFunction_) {
654
+ block.currentFunction_ = funcName;
655
+ block.setFieldValue(funcName, 'FUNC_NAME');
656
+ }
657
+ setTimeout(() => {
658
+ if (!block.disposed) {
659
+ block.updateShape_();
660
+ }
661
+ }, 0);
662
+ }
663
+ }
664
+
665
+ // Update dropdown options
666
+ const dropdown = block.getField('FUNC_NAME');
667
+ if (dropdown) {
668
+ const workspace = block.workspace;
669
+ const funcBlocks = workspace.getAllBlocks(false).filter(b => b.type === 'func_def');
670
+ const options = funcBlocks.length > 0
671
+ ? funcBlocks.map(b => [b.getFieldValue('NAME'), b.getFieldValue('NAME')])
672
+ : [["<no functions>", "NONE"]];
673
+
674
+ dropdown.menuGenerator_ = options;
675
+
676
+ // If current function no longer exists, reset
677
+ if (block.currentFunction_ && !options.some(opt => opt[1] === block.currentFunction_)) {
678
+ block.setFieldValue('NONE', 'FUNC_NAME');
679
+ setTimeout(() => {
680
+ if (!block.disposed) {
681
+ block.updateShape_();
682
+ }
683
+ }, 0);
684
+ }
685
+ }
686
+ }
687
+ };
688
+
689
+ block.workspace.addChangeListener(workspaceListener);
690
+
691
+ // Clean up the listener when block is disposed
692
+ const oldDispose = block.dispose;
693
+ block.dispose = function (healStack) {
694
+ if (block.workspace) {
695
+ block.workspace.removeChangeListener(workspaceListener);
696
+ }
697
+ if (oldDispose) {
698
+ oldDispose.call(this, healStack);
699
+ }
700
+ };
701
+ });
702
+
703
  // Function to generate a unique tool name
704
  function generateUniqueToolName(workspace, excludeBlock) {
705
  const existingNames = new Set();
 
751
  }
752
  };
753
 
754
+ // Register func_call block separately to include custom logic
755
+ Blockly.Blocks['func_call'] = {
756
+ init: function () {
757
+ this.jsonInit(func_call);
758
+ },
759
+
760
+ // Save the current function name and parameter count for serialization
761
+ saveExtraState: function () {
762
+ return {
763
+ currentFunction: this.currentFunction_ || null,
764
+ paramCount: this.paramCount_ || 0
765
+ };
766
+ },
767
+
768
+ // Load the saved state and rebuild the block
769
+ loadExtraState: function (state) {
770
+ this.currentFunction_ = state.currentFunction;
771
+ this.paramCount_ = state.paramCount;
772
+ // The shape will be updated by the extension after loading
773
+ if (this.updateShape_) {
774
+ setTimeout(() => {
775
+ this.updateShape_();
776
+ }, 0);
777
+ }
778
+ }
779
+ };
780
+
781
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
782
  container,
783
  container_input,
project/src/generators/python.js CHANGED
@@ -79,9 +79,10 @@ forBlock['create_mcp'] = function (block, generator) {
79
  }
80
 
81
  // Map Python types to Gradio components for inputs
 
82
  const gradioInputs = [];
83
- if (block.inputTypes_) {
84
- for (let k = 0; k < block.inputTypes_.length; k++) {
85
  const type = block.inputTypes_[k];
86
  switch (type) {
87
  case 'integer':
@@ -102,7 +103,7 @@ forBlock['create_mcp'] = function (block, generator) {
102
  // Map Python types to Gradio components for outputs
103
  const gradioOutputs = [];
104
  // Only add outputs if they actually exist in the block (R0, R1, etc.)
105
- if (block.outputTypes_ && block.outputCount_ > 0 && block.getInput('R0')) {
106
  // Use outputCount_ to ensure we only process actual outputs
107
  for (let k = 0; k < block.outputCount_; k++) {
108
  const type = block.outputTypes_[k];
@@ -199,12 +200,8 @@ forBlock['func_def'] = function (block, generator) {
199
  returnStatement = ` return (${returnValues.join(', ')})\n`;
200
  }
201
  } else {
202
- // No outputs defined, add pass only if body is empty
203
- if (!body || body.trim() === '') {
204
- returnStatement = ' pass\n';
205
- } else {
206
- returnStatement = '';
207
- }
208
  }
209
 
210
  // Construct the function definition
@@ -212,7 +209,7 @@ forBlock['func_def'] = function (block, generator) {
212
  if (typedInputs.length > 0) {
213
  code = `def ${name}(${typedInputs.join(', ')}):\n${body}${returnStatement}`;
214
  } else {
215
- code = `def ${name}():\n${body || ' pass\n'}${returnStatement}`;
216
  }
217
 
218
  // Add output type hints as comments if outputs are defined
@@ -236,8 +233,7 @@ forBlock['func_def'] = function (block, generator) {
236
  pyType = 'Any';
237
  }
238
  outputTypes.push(`${outName}: ${pyType}`);
239
- }
240
- code = code.slice(0, -1) + ` # Returns: ${outputTypes.join(', ')}\n`;
241
  }
242
 
243
  // Return function definition as a string value (not executed immediately)
@@ -252,3 +248,36 @@ forBlock['llm_call'] = function (block, generator) {
252
  const code = `llm_call(${prompt}, model="${model}")`;
253
  return [code, Order.NONE];
254
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
 
81
  // Map Python types to Gradio components for inputs
82
+ // Only add inputs if they actually exist in the block (X0, X1, etc.)
83
  const gradioInputs = [];
84
+ if (block.inputCount_ && block.inputCount_ > 0 && block.getInput('X0')) {
85
+ for (let k = 0; k < block.inputCount_; k++) {
86
  const type = block.inputTypes_[k];
87
  switch (type) {
88
  case 'integer':
 
103
  // Map Python types to Gradio components for outputs
104
  const gradioOutputs = [];
105
  // Only add outputs if they actually exist in the block (R0, R1, etc.)
106
+ if (block.outputCount_ && block.outputCount_ > 0 && block.getInput('R0')) {
107
  // Use outputCount_ to ensure we only process actual outputs
108
  for (let k = 0; k < block.outputCount_; k++) {
109
  const type = block.outputTypes_[k];
 
200
  returnStatement = ` return (${returnValues.join(', ')})\n`;
201
  }
202
  } else {
203
+ // No outputs defined, return None
204
+ returnStatement = ' return None\n';
 
 
 
 
205
  }
206
 
207
  // Construct the function definition
 
209
  if (typedInputs.length > 0) {
210
  code = `def ${name}(${typedInputs.join(', ')}):\n${body}${returnStatement}`;
211
  } else {
212
+ code = `def ${name}():\n${body}${returnStatement}`;
213
  }
214
 
215
  // Add output type hints as comments if outputs are defined
 
233
  pyType = 'Any';
234
  }
235
  outputTypes.push(`${outName}: ${pyType}`);
236
+ };
 
237
  }
238
 
239
  // Return function definition as a string value (not executed immediately)
 
248
  const code = `llm_call(${prompt}, model="${model}")`;
249
  return [code, Order.NONE];
250
  };
251
+
252
+ forBlock['func_call'] = function (block, generator) {
253
+ const funcName = block.getFieldValue('FUNC_NAME');
254
+
255
+ if (!funcName || funcName === 'NONE') {
256
+ return ['None', Order.ATOMIC];
257
+ }
258
+
259
+ // Find the function definition to get parameter info
260
+ const workspace = block.workspace;
261
+ const funcBlock = workspace.getAllBlocks(false).find(b =>
262
+ b.type === 'func_def' && b.getFieldValue('NAME') === funcName
263
+ );
264
+
265
+ if (!funcBlock) {
266
+ return ['None', Order.ATOMIC];
267
+ }
268
+
269
+ // Build the argument list based on actual inputs on the block
270
+ const args = [];
271
+ let i = 0;
272
+
273
+ // Check for inputs that actually exist on the block
274
+ while (block.getInput('ARG' + i)) {
275
+ const argValue = generator.valueToCode(block, 'ARG' + i, Order.NONE);
276
+ args.push(argValue || 'None');
277
+ i++;
278
+ }
279
+
280
+ // Generate the function call
281
+ const code = `${funcName}(${args.join(', ')})`;
282
+ return [code, Order.FUNCTION_CALL];
283
+ };
project/src/toolbox.js CHANGED
@@ -37,6 +37,10 @@ export const toolbox = {
37
  kind: 'block',
38
  type: 'func_def',
39
  },
 
 
 
 
40
  ]
41
  },
42
  {
@@ -364,36 +368,6 @@ export const toolbox = {
364
  },
365
  },
366
  },
367
- {
368
- kind: 'block',
369
- type: 'text_indexOf',
370
- inputs: {
371
- VALUE: {
372
- block: {
373
- type: 'variables_get',
374
- },
375
- },
376
- FIND: {
377
- shadow: {
378
- type: 'text',
379
- fields: {
380
- TEXT: 'abc',
381
- },
382
- },
383
- },
384
- },
385
- },
386
- {
387
- kind: 'block',
388
- type: 'text_charAt',
389
- inputs: {
390
- VALUE: {
391
- block: {
392
- type: 'variables_get',
393
- },
394
- },
395
- },
396
- },
397
  {
398
  kind: 'block',
399
  type: 'text_getSubstring',
@@ -563,11 +537,5 @@ export const toolbox = {
563
  categorystyle: 'variable_category',
564
  custom: 'VARIABLE',
565
  },
566
- {
567
- kind: 'category',
568
- name: 'Functions',
569
- categorystyle: 'procedure_category',
570
- custom: 'PROCEDURE',
571
- },
572
  ],
573
  };
 
37
  kind: 'block',
38
  type: 'func_def',
39
  },
40
+ {
41
+ kind: 'block',
42
+ type: 'func_call',
43
+ },
44
  ]
45
  },
46
  {
 
368
  },
369
  },
370
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  {
372
  kind: 'block',
373
  type: 'text_getSubstring',
 
537
  categorystyle: 'variable_category',
538
  custom: 'VARIABLE',
539
  },
 
 
 
 
 
 
540
  ],
541
  };