From 927b6640bf50b8f3f61c5b2ff7e5531a8f7b7078 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 16 Oct 2024 22:45:42 +0200
Subject: [PATCH 01/95] #3481307 Add submodule skeleton

---
 modules/agents/ai_eca_agents.info.yml | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 modules/agents/ai_eca_agents.info.yml

diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
new file mode 100644
index 0000000..33f2bdb
--- /dev/null
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -0,0 +1,8 @@
+name: 'AI ECA - Agents'
+type: module
+description: 'Enable an LLM to answer questions about models and ECA-components.'
+core_version_requirement: ^10.3 || ^11
+package: AI
+dependencies:
+  - ai_eca:ai_eca
+  - ai_agents:ai_agents
-- 
GitLab


From 023692ec677cf2b462849286f934ad4a185f23f9 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 16 Oct 2024 22:48:57 +0200
Subject: [PATCH 02/95] #3481307 Add DataProvider-service

---
 modules/agents/ai_eca_agents.services.yml     |   8 +
 .../Services/DataProvider/DataProvider.php    | 203 ++++++++++++++++++
 .../DataProvider/DataProviderInterface.php    |  70 ++++++
 .../DataProvider/DataViewModeEnum.php         |  10 +
 4 files changed, 291 insertions(+)
 create mode 100644 modules/agents/ai_eca_agents.services.yml
 create mode 100644 modules/agents/src/Services/DataProvider/DataProvider.php
 create mode 100644 modules/agents/src/Services/DataProvider/DataProviderInterface.php
 create mode 100644 modules/agents/src/Services/DataProvider/DataViewModeEnum.php

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
new file mode 100644
index 0000000..08d711f
--- /dev/null
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -0,0 +1,8 @@
+services:
+  ai_eca_agents.services.data_provider:
+    class: Drupal\ai_eca_agents\Services\DataProvider\DataProvider
+    arguments:
+      - '@eca.service.modeller'
+      - '@eca.service.condition'
+      - '@eca.service.action'
+      - '@entity_type.manager'
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
new file mode 100644
index 0000000..67484a3
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\eca\Attributes\Token;
+use Drupal\eca\Entity\Eca;
+use Drupal\eca\Service\Actions;
+use Drupal\eca\Service\Conditions;
+use Drupal\eca\Service\Modellers;
+
+/**
+ * Class for providing ECA-data.
+ */
+class DataProvider implements DataProviderInterface {
+
+  /**
+   * The view mode which determines how many details are returned.
+   *
+   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum
+   */
+  protected DataViewModeEnum $viewMode = DataViewModeEnum::Teaser;
+
+  /**
+   * Constructs an DataProvider instance.
+   *
+   * @param \Drupal\eca\Service\Modellers $modellers
+   *   The service for ECA modellers.
+   * @param \Drupal\eca\Service\Conditions $conditions
+   *   The conditions.
+   * @param \Drupal\eca\Service\Actions $actions
+   *   The actions.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   */
+  public function __construct(
+    protected Modellers $modellers,
+    protected Conditions $conditions,
+    protected Actions $actions,
+    protected EntityTypeManagerInterface $entityTypeManager,
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEvents(): array {
+    $output = [];
+
+    foreach ($this->modellers->events() as $event) {
+      $info = [
+        'plugin_id' => $event->getPluginId(),
+        'name' => $event->getPluginDefinition()['label'],
+        'module' => $event->getPluginDefinition()['provider'],
+      ];
+
+      if ($this->viewMode === DataViewModeEnum::Full) {
+        $info['tokens'] = array_reduce($event->getTokens(), function (array $carry, Token $token) {
+          $info = [
+            'name' => $token->name,
+            'description' => $token->description,
+          ];
+          if (!empty($token->aliases)) {
+            $info['aliases'] = implode(', ', $token->aliases);
+          }
+          $carry[] = $info;
+
+          return $carry;
+        }, []);
+      }
+
+      $output[] = $info;
+    }
+
+    return $output;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConditions(): array {
+    return $this->convertPlugins($this->conditions->conditions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActions(): array {
+    return $this->convertPlugins($this->actions->actions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getComponents(array $filterIds = []): array {
+    return [
+      'events' => empty($filterIds) ? $this->getEvents() : array_filter($this->getEvents(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
+      'conditions' => empty($filterIds) ? $this->getConditions() : array_filter($this->getConditions(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
+      'actions' => empty($filterIds) ? $this->getActions() : array_filter($this->getActions(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getModels(array $filterIds = []): array {
+    /** @var \Drupal\eca\Entity\EcaStorage $storage */
+    $storage = $this->entityTypeManager->getStorage('eca');
+    $models = $storage->loadMultiple();
+
+    if (!empty($filterIds)) {
+      $models = array_filter($models, function (Eca $model) use ($filterIds) {
+        return in_array($model->id(), $filterIds, TRUE);
+      });
+    }
+
+    return array_reduce($models, function (array $carry, Eca $eca) {
+      $info = [
+        'model_id' => $eca->id(),
+        'label' => $eca->label(),
+      ];
+      if (!empty($eca->getModel()->getDocumentation())) {
+        $info['description'] = $eca->getModel()->getDocumentation();
+      }
+
+      if ($this->viewMode === DataViewModeEnum::Full) {
+        $info['dependencies'] = $eca->getDependencies();
+        $info['events'] = $eca->getEventInfos();
+        $info['conditions'] = $eca->getConditions();
+        $info['actions'] = $eca->getActions();
+      }
+      $carry[] = $info;
+
+      return $carry;
+    }, []);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setViewMode(DataViewModeEnum $viewMode): DataProviderInterface {
+    $this->viewMode = $viewMode;
+
+    return $this;
+  }
+
+  /**
+   * Converts a list of plugins to an array of essential information.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface[] $plugins
+   *   The list of plugins.
+   *
+   * @return array
+   *   Returns a converted list of plugins with essential information.
+   */
+  protected function convertPlugins(array $plugins): array {
+    $output = [];
+
+    foreach ($plugins as $plugin) {
+      $info = [
+        'plugin_id' => $plugin->getPluginId(),
+        'name' => (string) $plugin->getPluginDefinition()['label'],
+      ];
+
+      if ($this->viewMode === DataViewModeEnum::Full) {
+        if (!$plugin instanceof ConfigurableInterface && !$plugin instanceof PluginFormInterface) {
+          continue;
+        }
+
+        $configKeys = array_keys($plugin->defaultConfiguration());
+        $elements = array_filter($plugin->buildConfigurationForm([], new FormState()), function ($key) use ($configKeys) {
+          return in_array($key, $configKeys);
+        }, ARRAY_FILTER_USE_KEY);
+
+        $info['configuration'] = array_reduce(array_keys($elements), function (array $carry, $key) use ($elements) {
+          $config = [
+            'config_id' => $key,
+            'name' => (string) $elements[$key]['#title'],
+          ];
+          if (!empty($elements[$key]['#description'])) {
+            $config['description'] = (string) $elements[$key]['#description'];
+          }
+
+          $carry[] = $config;
+
+          return $carry;
+        }, []);
+      }
+
+      if (!empty($plugin->getPluginDefinition()['description'])) {
+        $info['description'] = (string) $plugin->getPluginDefinition()['description'];
+      }
+
+      $output[] = $info;
+    }
+
+    return $output;
+  }
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataProviderInterface.php b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
new file mode 100644
index 0000000..9e3c5a9
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+/**
+ * Interface for ECA data provider.
+ */
+interface DataProviderInterface {
+
+  /**
+   * Returns the events.
+   *
+   * @return array
+   *   The events.
+   */
+  public function getEvents(): array;
+
+  /**
+   * Returns the conditions.
+   *
+   * @return array
+   *   The conditions.
+   */
+  public function getConditions(): array;
+
+  /**
+   * Returns the actions.
+   *
+   * @return array
+   *   The actions.
+   */
+  public function getActions(): array;
+
+  /**
+   * A wrapper that returns the events, conditions and actions.
+   *
+   * @param array $filterIds
+   *   An optional array of IDs to filter by.
+   *
+   * @return array
+   *   The collection of events, conditions and actions.
+   */
+  public function getComponents(array $filterIds = []): array;
+
+  /**
+   * Returns the models.
+   *
+   * @param array $filterIds
+   *   An optional array of IDs to filter by.
+   *
+   * @return array
+   *   The models.
+   */
+  public function getModels(array $filterIds = []): array;
+
+  /**
+   * Allows overriding the view mode.
+   *
+   * This is used for controlling how much details are exposed for the different
+   * components.
+   *
+   * @param \Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum $viewMode
+   *   The view mode.
+   *
+   * @return \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
+   *   Returns the altered instance.
+   */
+  public function setViewMode(DataViewModeEnum $viewMode): DataProviderInterface;
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataViewModeEnum.php b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
new file mode 100644
index 0000000..2100ab4
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+enum DataViewModeEnum: string {
+
+  case Teaser = 'teaser';
+  case Full = 'full';
+
+}
-- 
GitLab


From c96c7a8bcc7e2ffd5de49a9d02a44a2233c2cac7 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 16 Oct 2024 22:51:01 +0200
Subject: [PATCH 03/95] #3481307 Add prompts

---
 modules/agents/prompts/eca/answerQuestion.yml | 21 ++++++
 modules/agents/prompts/eca/determineTask.yml  | 68 +++++++++++++++++++
 2 files changed, 89 insertions(+)
 create mode 100644 modules/agents/prompts/eca/answerQuestion.yml
 create mode 100644 modules/agents/prompts/eca/determineTask.yml

diff --git a/modules/agents/prompts/eca/answerQuestion.yml b/modules/agents/prompts/eca/answerQuestion.yml
new file mode 100644
index 0000000..056fa76
--- /dev/null
+++ b/modules/agents/prompts/eca/answerQuestion.yml
@@ -0,0 +1,21 @@
+introduction: |
+  You are a Drupal developer that can answer questions about a possible model of the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+  
+  Based on a previous prompt, you will given information about a model or technical details regarding the events, conditions or actions. The input might also be a combination of those things, try to specify your answer based on those. 
+
+  Be helpful, elaborate and answer in the best way you can. Try to re-use as much information about the components and which technical information they expose. This can be the tokens that they expose or the module that implements it.
+  If you do not have enough information to answer the question, please let the user know.
+
+  You can answer with multiple objects if needed.
+preferred_model: gpt-4o
+preferred_llm: openai
+possible_actions:
+  info: The answer to the question or the feedback that you do not have enough information to answer that.
+formats:
+  - action: Action ID from the list
+    answer: The answer to the question
+one_shot_learning_examples:
+  - action: info
+    answer: The model 'User login' can redirect a user to the correct page.
+  - action: info
+    answer: There are a couple of actions that can be used to do something with an entity, like unpublishing, updating the URL alias etc.
diff --git a/modules/agents/prompts/eca/determineTask.yml b/modules/agents/prompts/eca/determineTask.yml
new file mode 100644
index 0000000..ec6a56c
--- /dev/null
+++ b/modules/agents/prompts/eca/determineTask.yml
@@ -0,0 +1,68 @@
+introduction: |
+  You are a Drupal developer that can create or edit configuration for the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+
+  Based on the following context of a task description and comments, you should figure out if they are trying to create a new model, edit an existing one or just asking a question that requires no action. Any question that was already answered, will not be marked as a question.
+
+  You will be given a list of existing models and any events, condition- or action-plugins that are present in the website.
+  
+  If the action is "create", "edit" or "info", include the IDs of the existing components you think are required to execute the action as "component_ids" in your answer. There can be multiple but do not generate new IDs.
+  If the action is "edit", include the ID of the model as "model_id" in your answer. There can only be one model ID and do not generate a new model ID.
+  If the action is "info" and you believe that an existing model might be the subject of the answer, provide the model ID as "model_id". There can only be one model ID and do not generate a new model ID.
+  
+  If you have suggestions on how the determined task type (eg. "create" or "info") should be executed, you can provide that feedback as "feedback" in your answer. Be precise.
+  If you can't find the answer to the question, you can ask for more information or say that you can't find the answer.
+
+  Only give back one long answer.
+preferred_model: gpt-4o
+preferred_llm: openai
+possible_actions:
+  create: They are trying to create an Event-Condition-Action model.
+  edit: They are trying to edit an existing Event-Condition-Action model.
+  info: They want information about an existing model or one or more existing events, conditions or actions, without any further action.
+  fail: It failed due to missing information or being ambivalent.
+formats:
+  -
+    action: Action ID from the list
+    model_id: The ID of the existing model when the action is "edit" or "info"
+    component_ids: A list of IDs of components when the action is "create", "edit" or "info"
+    feedback: An optional message
+one_shot_learning_examples:
+  -
+    action: create
+    component_ids:
+      - 'user:login'
+      - 'content_entity:insert'
+      - 'eca_current_user_role'
+      - 'action_goto_action'
+  -
+    action: edit
+    model_id: user_login
+    component_ids:
+      - 'user:login'
+      - 'content_entity:insert'
+      - 'eca_current_user_role'
+      - 'action_goto_action'
+  -
+    action: info
+    model_id: user_login
+  -
+    action: info
+    component_ids:
+      - 'user:login'
+      - 'content_entity:insert'
+      - 'eca_current_user_role'
+      - 'action_goto_action'
+  -
+    action: info
+    model_id: user_login
+    component_ids:
+      - 'user:login'
+      - 'content_entity:insert'
+      - 'eca_current_user_role'
+      - 'action_goto_action'
+  -
+    action: fail
+    feedback: The model "User login" does not exist
+  -
+    action: fail
+    feedback: There is no condition plugin about checking the age of the user.
-- 
GitLab


From 6880bde319972103663066f7d82f5a76d6039ad5 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 23 Oct 2024 16:26:49 +0200
Subject: [PATCH 04/95] #3481307 Move prompts

---
 composer.json                                       | 2 +-
 modules/agents/prompts/{eca => }/answerQuestion.yml | 0
 modules/agents/prompts/{eca => }/determineTask.yml  | 0
 3 files changed, 1 insertion(+), 1 deletion(-)
 rename modules/agents/prompts/{eca => }/answerQuestion.yml (100%)
 rename modules/agents/prompts/{eca => }/determineTask.yml (100%)

diff --git a/composer.json b/composer.json
index 4d16e3e..27bab96 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,6 @@
     "require": {
         "drupal/eca": "^2.0",
         "drupal/ai": "1.0.x-dev@dev",
-        "drupal/core": "^10.3||^11"
+        "drupal/core": "^10.3 || ^11"
     }
 }
diff --git a/modules/agents/prompts/eca/answerQuestion.yml b/modules/agents/prompts/answerQuestion.yml
similarity index 100%
rename from modules/agents/prompts/eca/answerQuestion.yml
rename to modules/agents/prompts/answerQuestion.yml
diff --git a/modules/agents/prompts/eca/determineTask.yml b/modules/agents/prompts/determineTask.yml
similarity index 100%
rename from modules/agents/prompts/eca/determineTask.yml
rename to modules/agents/prompts/determineTask.yml
-- 
GitLab


From 7443da2526aa6d208d9d9f2da97d5c9d92e61b4f Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 23 Oct 2024 16:28:16 +0200
Subject: [PATCH 05/95] #3481307 Expose tokens via the DataProvider

---
 modules/agents/ai_eca_agents.services.yml     |  2 ++
 .../Services/DataProvider/DataProvider.php    | 26 +++++++++++++++++++
 .../DataProvider/DataProviderInterface.php    | 24 +++++++++++------
 3 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index 08d711f..c58ba3c 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -6,3 +6,5 @@ services:
       - '@eca.service.condition'
       - '@eca.service.action'
       - '@entity_type.manager'
+      - '@eca.token_services'
+      - '@token.tree_builder'
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 67484a3..9e8c2b1 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -11,6 +11,7 @@ use Drupal\eca\Entity\Eca;
 use Drupal\eca\Service\Actions;
 use Drupal\eca\Service\Conditions;
 use Drupal\eca\Service\Modellers;
+use Drupal\token\TreeBuilderInterface;
 
 /**
  * Class for providing ECA-data.
@@ -35,12 +36,15 @@ class DataProvider implements DataProviderInterface {
    *   The actions.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
    *   The entity type manager.
+   * @param \Drupal\token\TreeBuilderInterface $treeBuilder
+   *   The token tree builder.
    */
   public function __construct(
     protected Modellers $modellers,
     protected Conditions $conditions,
     protected Actions $actions,
     protected EntityTypeManagerInterface $entityTypeManager,
+    protected TreeBuilderInterface $treeBuilder,
   ) {
   }
 
@@ -138,6 +142,28 @@ class DataProvider implements DataProviderInterface {
     }, []);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTokens(): array {
+    $output = [];
+
+    $render = $this->treeBuilder->buildAllRenderable();
+
+    foreach ($render['#token_tree'] as $typeInfo) {
+      foreach ($typeInfo['tokens'] as $token => $tokenInfo) {
+        $output[$token] = [
+          'name' => $tokenInfo['name'],
+        ];
+        if (!empty($tokenInfo['description'])) {
+          $output[$token]['description'] = $tokenInfo['description'];
+        }
+      }
+    }
+
+    return $output;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/modules/agents/src/Services/DataProvider/DataProviderInterface.php b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
index 9e3c5a9..85ce273 100644
--- a/modules/agents/src/Services/DataProvider/DataProviderInterface.php
+++ b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
@@ -8,26 +8,26 @@ namespace Drupal\ai_eca_agents\Services\DataProvider;
 interface DataProviderInterface {
 
   /**
-   * Returns the events.
+   * Get all the events.
    *
    * @return array
-   *   The events.
+   *   Returns the events.
    */
   public function getEvents(): array;
 
   /**
-   * Returns the conditions.
+   * Get all the conditions.
    *
    * @return array
-   *   The conditions.
+   *   Returns the conditions.
    */
   public function getConditions(): array;
 
   /**
-   * Returns the actions.
+   * Get all the actions.
    *
    * @return array
-   *   The actions.
+   *   Returns the actions.
    */
   public function getActions(): array;
 
@@ -43,16 +43,24 @@ interface DataProviderInterface {
   public function getComponents(array $filterIds = []): array;
 
   /**
-   * Returns the models.
+   * Get models.
    *
    * @param array $filterIds
    *   An optional array of IDs to filter by.
    *
    * @return array
-   *   The models.
+   *   Returns the models.
    */
   public function getModels(array $filterIds = []): array;
 
+  /**
+   * Get the available tokens.
+   *
+   * @return array
+   *   Return all the available tokens.
+   */
+  public function getTokens(): array;
+
   /**
    * Allows overriding the view mode.
    *
-- 
GitLab


From b8b6b9e47adf1ba27bdb4d17b34e9dadd3eda37a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 26 Oct 2024 09:39:42 +0200
Subject: [PATCH 06/95] #3481307 Expose modelers via DataProvider

---
 modules/agents/ai_eca_agents.services.yml                 | 1 -
 modules/agents/src/Services/DataProvider/DataProvider.php | 7 +++++++
 .../src/Services/DataProvider/DataProviderInterface.php   | 8 ++++++++
 3 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index c58ba3c..dc5ceca 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -6,5 +6,4 @@ services:
       - '@eca.service.condition'
       - '@eca.service.action'
       - '@entity_type.manager'
-      - '@eca.token_services'
       - '@token.tree_builder'
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 9e8c2b1..8b8dd0c 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -96,6 +96,13 @@ class DataProvider implements DataProviderInterface {
     return $this->convertPlugins($this->actions->actions());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getModellers(): array {
+    return $this->modellers->getModellerDefinitions();
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/modules/agents/src/Services/DataProvider/DataProviderInterface.php b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
index 85ce273..9a7c961 100644
--- a/modules/agents/src/Services/DataProvider/DataProviderInterface.php
+++ b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
@@ -31,6 +31,14 @@ interface DataProviderInterface {
    */
   public function getActions(): array;
 
+  /**
+   * Get all the modellers.
+   *
+   * @return array
+   *   Returns a list of all the modellers.
+   */
+  public function getModellers(): array;
+
   /**
    * A wrapper that returns the events, conditions and actions.
    *
-- 
GitLab


From c7c17a9299617932146327d49e007b2958d08a0a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 26 Oct 2024 09:42:26 +0200
Subject: [PATCH 07/95] #3481307 Create ECA-agent

---
 modules/agents/prompts/getConfig.yml      |  81 +++++
 modules/agents/src/Plugin/AiAgent/Eca.php | 369 ++++++++++++++++++++++
 2 files changed, 450 insertions(+)
 create mode 100644 modules/agents/prompts/getConfig.yml
 create mode 100644 modules/agents/src/Plugin/AiAgent/Eca.php

diff --git a/modules/agents/prompts/getConfig.yml b/modules/agents/prompts/getConfig.yml
new file mode 100644
index 0000000..80e0a06
--- /dev/null
+++ b/modules/agents/prompts/getConfig.yml
@@ -0,0 +1,81 @@
+introduction: |
+  You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
+
+  If you can't create the configuration because it's lacking information, just answer with the "no_info" action.
+
+  You will receive information about the available plugins and their details, as well as a list of generally available tokens. You can use them how you see fit, but do not generate new tokens.
+  There should be at least one Event-related component.
+preferred_model: gpt-4o
+preferred_llm: openai
+possible_actions:
+  set_title: sets the title of the configuration item
+  set_description: sets the description of the configuration item
+  no_info: if there's not enough information to create the configuration item, just answer with this action
+formats:
+  - id: unique random ID of component, it should consist of 7 characters and start with "Event_" for an event, "Activity_" for an activity or "Flow_" for a condition
+    plugin: an existing plugin ID
+    label: human readable label
+    config: optional setup to configure the component
+    successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
+  - id: unique random ID of the Gateway, it should consist of 7 characters and start with "Gateway_"
+    successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
+  - type: type of action
+    value: value of the action
+one_shot_learning_examples:
+  - id: Event_0erz1e4
+    plugin: 'user:login'
+    label: 'User Login'
+    successors:
+      - id: Gateway_0hd8858
+        condition: Flow_1o433l9
+  - id: Flow_
+    plugin: eca_scalar
+    config:
+      case: false
+      left: '[current-page:url:path]'
+      right: /user/reset
+      operator: beginswith
+      type: value
+      negate: true
+  - id: Gateway_0hd8858
+    successors:
+      - id: Activity_0l4w3fc
+        condition: Flow_1hqinah
+      - id: Activity_182vndw
+        condition: Flow_0047zve
+  - id: Activity_0l4w3fc
+    plugin: action_goto_action
+    label: 'Redirect to content overview'
+    config:
+      replace_tokens: false
+      url: /admin/content
+    successors: { }
+  - id: Flow_1hqinah
+    plugin: eca_current_user_role
+    config:
+      negate: false
+      role: content_editor
+  - id: Activity_182vndw
+    plugin: action_goto_action
+    label: 'Redirect to admin overview'
+    config:
+      replace_tokens: false
+      url: /admin
+  - id: Flow_0047zve
+    plugin: eca_current_user_role
+    config:
+      negate: false
+      role: administrator
+  - type: set_title
+    value: 'ECA Feature Demo'
+  - type: set_description
+    value: |
+      This model demonstrates a number of smart features around user accounts:
+
+      1. When a user registers themselves or gets created by an existing user, then all existing users with the admin role get informed by email. If the current user has the admin role, a message also get displayed with a link to the mailhog application to review the emails.
+      2. When a user logs in, a number of actions applies: depending on their role, different redirect destinations will be used after login. Also, the assigment of the internal user role gets executed, see below.
+      3. When a user gets updated, the assigment of the internal user role also gets executed.
+
+      The assignment of the internal user role assigns that role to the current user if their email domain contains @example.com and removes it otherwise. It does that only if the situation had changed and also displays an according message on screen.
+  - type: no_info
+    value: Not enough information to create the configuration item
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
new file mode 100644
index 0000000..45df4f1
--- /dev/null
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -0,0 +1,369 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\AiAgent;
+
+use Drupal\ai_agents\Attribute\AiAgent;
+use Drupal\ai_agents\PluginBase\AiAgentBase;
+use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
+use Drupal\ai_agents\Task\TaskInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
+use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\eca\Entity\Eca as EcaEntity;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The ECA agent.
+ */
+#[AiAgent(
+  id: 'eca',
+  label: new TranslatableMarkup('ECA Agent'),
+)]
+class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * Questions to ask.
+   *
+   * @var array
+   */
+  protected array $questions;
+
+  /**
+   * The full data of the initial task.
+   *
+   * @var array
+   */
+  protected array $data;
+
+  /**
+   * The ECA entity.
+   *
+   * @var \Drupal\eca\Entity\Eca|NULL
+   */
+  protected ?EcaEntity $model;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The ECA data provider.
+   *
+   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
+   */
+  protected DataProviderInterface $dataProvider;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = new static();
+    $instance->entityTypeManager = $container->get('entity_type.manager');
+    $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
+
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId() {
+    return 'eca';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function agentsNames() {
+    return ['Event-Condition-Action agent'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAvailable() {
+    return $this->agentHelper->isModuleEnabled('eca');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRetries() {
+    return 2;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAccess() {
+    if (!$this->currentUser->hasPermission('administer eca')) {
+      return AccessResult::forbidden();
+    }
+
+    return parent::hasAccess();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function agentsCapabilities(): array {
+    return [
+      'eca' => [
+        'name' => 'Event-Condition-Action Agent',
+        'description' => 'This is agent is capable of adding, editing or informing about Event-Condition-Action models on a Drupal website. Note that it does not add events, conditions or actions as those require a specific implementation via code.',
+        'inputs' => [
+          'free_text' => [
+            'name' => 'Prompt',
+            'type' => 'string',
+            'description' => 'The prompt to create, edit or ask questions about Event-Condition-Actions models.',
+            'default_value' => '',
+          ],
+        ],
+        'outputs' => [
+          'answers' => [
+            'description' => 'The answers to the questions asked about the Event-Condition-Action model.',
+            'type' => 'string',
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function determineSolvability(): int {
+    $type = $this->determineTypeOfTask();
+
+    return match ($type) {
+      'create', 'edit' => AiAgentInterface::JOB_SOLVABLE,
+      'info' => AiAgentInterface::JOB_SHOULD_ANSWER_QUESTION,
+      'fail' => AiAgentInterface::JOB_NEEDS_ANSWERS,
+      default => AiAgentInterface::JOB_NOT_SOLVABLE,
+    };
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function solve(): array|string {
+    $messages = [''];
+
+    switch ($this->data[0]['action']) {
+      case 'create':
+        $this->createConfig();
+        break;
+
+      case 'edit':
+        $messages[] = 'edit';
+        break;
+    }
+
+    return implode("\n", $messages);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function answerQuestion(): string|TranslatableMarkup {
+    $this->dataProvider->setViewMode(DataViewModeEnum::Full);
+
+    $userPrompts = [];
+    if (!empty($this->model)) {
+      $userPrompts['The information of the model, in JSON format'] = sprintf("```json\n%s", json_encode($this->dataProvider->getModels([$this->model->id()])));
+    }
+
+    if (!empty($this->data[0]['component_ids'])) {
+      $userPrompts['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+    }
+
+    if (empty($userPrompts)) {
+      return $this->t('Sorry, I could not answer your question without anymore context.');
+    }
+
+    $actionPrompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'answerQuestion', $userPrompts);
+
+    // Perform the prompt.
+    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($actionPrompt['prompt']));
+
+    if (isset($data[0]['answer'])) {
+      $answer = array_map(function ($dataPoint) {
+        return $dataPoint['answer'];
+      }, $data);
+
+      return implode("\n", $answer);
+    }
+
+    return $this->t('Sorry, I got no answers for you.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHelp() {
+    return $this->t('This agent can figure out event-condition-action models of a file. Just upload and ask.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function askQuestion() {
+    return $this->questions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function approveSolution() {
+    $this->data[0]['action'] = 'create';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTask(TaskInterface $task) {
+    parent::setTask($task);
+
+    if (!empty($this->getState()['models'][0])) {
+      $this->model = $this->getState()['models'][0];
+    }
+  }
+
+  /**
+   * Determine the type of task.
+   *
+   * @return string
+   *   Returns the type of task.
+   *
+   * @throws \Exception
+   */
+  protected function determineTypeOfTask() {
+    $context = $this->getFullContextOfTask($this->task);
+    if (!empty($this->model)) {
+      $context .= "\n\nA model already exists, so creation is not possible.";
+    }
+
+    // Prepare the prompt by fetching all the relevant info.
+    $actionPrompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'determineTask', [
+      'Task description and if available comments description' => $context,
+      'The list of existing models, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getModels())),
+      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getComponents())),
+    ]);
+    // Perform the prompt.
+    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($actionPrompt['prompt']));
+
+    // Quit early if the returned response isn't what we expected.
+    if (empty($data[0]['action'])) {
+      $this->questions[] = 'Sorry, we could not understand what you wanted to do, please try again.';
+
+      return 'fail';
+    }
+
+    if (in_array($data[0]['action'], [
+      'create',
+      'edit',
+      'info',
+    ])) {
+      if (!empty($data[0]['model_id'])) {
+        $this->model = $this->entityTypeManager->getStorage('eca')->load($data[0]['model_id']);
+      }
+
+      if (!empty($data[0]['feedback'])) {
+        $this->questions[] = $data[0]['feedback'];
+      }
+
+      $this->data = $data;
+
+      return $data[0]['action'];
+    }
+
+    throw new \Exception('Invalid action determined for ECA');
+  }
+
+  /**
+   * Create a configuration item for ECA.
+   */
+  protected function createConfig(): void {
+    $this->dataProvider->setViewMode(DataViewModeEnum::Full);
+
+    // Prepare the prompt.
+    $userPrompts = [
+      //          'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
+      'The available modellers' => sprintf("```json\n%s", json_encode($this->dataProvider->getModellers())),
+    ];
+    if (!empty($this->data[0]['component_ids'])) {
+      $userPrompts['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+    }
+    if (!empty($this->data[0]['feedback'])) {
+      $userPrompts['Guidelines'] = $this->data[0]['feedback'];
+    }
+    $prompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'getConfig', $userPrompts);
+
+    // Execute it.
+    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($prompt['prompt']));
+
+    if (empty($data) || (!empty($data[0]['type']) && $data[0]['type'] === 'no_info')) {
+      throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
+    }
+
+    // Map response to entity properties.
+    /** @var \Drupal\eca\Entity\Eca $eca */
+    $eca = $this->entityTypeManager->getStorage('eca')
+      ->create();
+
+    $random = new Random();
+    $eca->set('id', md5($random->string(16, TRUE)));
+    $eca->set('label', $random->string(16));
+    $eca->set('modeller', 'core');
+    $eca->set('version', '0.0.1');
+
+    foreach ($data as $entry) {
+      // Events, Conditions ("Flows"), Actions and Gateways.
+      if (!empty($entry['id'])) {
+        switch (TRUE) {
+          case str_starts_with($entry['id'], 'Event_'):
+            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Flow_'):
+            $eca->addCondition($entry['id'], $entry['plugin'], $entry['config'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Activity_'):
+            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Gateway_'):
+            $eca->addGateway($entry['id'], 0, $entry['successors'] ?? []);
+            break;
+        }
+
+        continue;
+      }
+
+      if (!empty($entry['type'])) {
+        switch ($entry['type']) {
+          case 'set_title':
+            $eca->set('label', $entry['value']);
+            break;
+        }
+      }
+    }
+
+    // Validate the entity.
+    if (empty($eca->getUsedEvents())) {
+      throw new \Exception('No events registered.');
+    }
+
+    // Save the entity.
+    $eca->save();
+  }
+
+}
-- 
GitLab


From ecc9d32ece7a4cc53d8f88fb61ca650b45ceb0ac Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 15:22:57 +0100
Subject: [PATCH 08/95] #3481307 Replace deprecated methods

---
 composer.json                                 |  3 +-
 modules/agents/ai_eca_agents.info.yml         |  1 +
 modules/agents/prompts/answerQuestion.yml     | 21 -----
 modules/agents/prompts/determineTask.yml      | 68 ---------------
 modules/agents/prompts/eca/answerQuestion.yml | 23 +++++
 modules/agents/prompts/eca/determineTask.yml  | 55 ++++++++++++
 modules/agents/prompts/eca/getConfig.yml      | 83 +++++++++++++++++++
 modules/agents/prompts/getConfig.yml          | 81 ------------------
 modules/agents/src/Plugin/AiAgent/Eca.php     | 43 +++++-----
 9 files changed, 185 insertions(+), 193 deletions(-)
 delete mode 100644 modules/agents/prompts/answerQuestion.yml
 delete mode 100644 modules/agents/prompts/determineTask.yml
 create mode 100644 modules/agents/prompts/eca/answerQuestion.yml
 create mode 100644 modules/agents/prompts/eca/determineTask.yml
 create mode 100644 modules/agents/prompts/eca/getConfig.yml
 delete mode 100644 modules/agents/prompts/getConfig.yml

diff --git a/composer.json b/composer.json
index 27bab96..1f9b8b1 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
     "require": {
         "drupal/eca": "^2.0",
         "drupal/ai": "1.0.x-dev@dev",
-        "drupal/core": "^10.3 || ^11"
+        "drupal/core": "^10.3 || ^11",
+        "drupal/token": "^1.15"
     }
 }
diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
index 33f2bdb..6d3246a 100644
--- a/modules/agents/ai_eca_agents.info.yml
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -6,3 +6,4 @@ package: AI
 dependencies:
   - ai_eca:ai_eca
   - ai_agents:ai_agents
+  - token:token
diff --git a/modules/agents/prompts/answerQuestion.yml b/modules/agents/prompts/answerQuestion.yml
deleted file mode 100644
index 056fa76..0000000
--- a/modules/agents/prompts/answerQuestion.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-introduction: |
-  You are a Drupal developer that can answer questions about a possible model of the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
-  
-  Based on a previous prompt, you will given information about a model or technical details regarding the events, conditions or actions. The input might also be a combination of those things, try to specify your answer based on those. 
-
-  Be helpful, elaborate and answer in the best way you can. Try to re-use as much information about the components and which technical information they expose. This can be the tokens that they expose or the module that implements it.
-  If you do not have enough information to answer the question, please let the user know.
-
-  You can answer with multiple objects if needed.
-preferred_model: gpt-4o
-preferred_llm: openai
-possible_actions:
-  info: The answer to the question or the feedback that you do not have enough information to answer that.
-formats:
-  - action: Action ID from the list
-    answer: The answer to the question
-one_shot_learning_examples:
-  - action: info
-    answer: The model 'User login' can redirect a user to the correct page.
-  - action: info
-    answer: There are a couple of actions that can be used to do something with an entity, like unpublishing, updating the URL alias etc.
diff --git a/modules/agents/prompts/determineTask.yml b/modules/agents/prompts/determineTask.yml
deleted file mode 100644
index ec6a56c..0000000
--- a/modules/agents/prompts/determineTask.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-introduction: |
-  You are a Drupal developer that can create or edit configuration for the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
-
-  Based on the following context of a task description and comments, you should figure out if they are trying to create a new model, edit an existing one or just asking a question that requires no action. Any question that was already answered, will not be marked as a question.
-
-  You will be given a list of existing models and any events, condition- or action-plugins that are present in the website.
-  
-  If the action is "create", "edit" or "info", include the IDs of the existing components you think are required to execute the action as "component_ids" in your answer. There can be multiple but do not generate new IDs.
-  If the action is "edit", include the ID of the model as "model_id" in your answer. There can only be one model ID and do not generate a new model ID.
-  If the action is "info" and you believe that an existing model might be the subject of the answer, provide the model ID as "model_id". There can only be one model ID and do not generate a new model ID.
-  
-  If you have suggestions on how the determined task type (eg. "create" or "info") should be executed, you can provide that feedback as "feedback" in your answer. Be precise.
-  If you can't find the answer to the question, you can ask for more information or say that you can't find the answer.
-
-  Only give back one long answer.
-preferred_model: gpt-4o
-preferred_llm: openai
-possible_actions:
-  create: They are trying to create an Event-Condition-Action model.
-  edit: They are trying to edit an existing Event-Condition-Action model.
-  info: They want information about an existing model or one or more existing events, conditions or actions, without any further action.
-  fail: It failed due to missing information or being ambivalent.
-formats:
-  -
-    action: Action ID from the list
-    model_id: The ID of the existing model when the action is "edit" or "info"
-    component_ids: A list of IDs of components when the action is "create", "edit" or "info"
-    feedback: An optional message
-one_shot_learning_examples:
-  -
-    action: create
-    component_ids:
-      - 'user:login'
-      - 'content_entity:insert'
-      - 'eca_current_user_role'
-      - 'action_goto_action'
-  -
-    action: edit
-    model_id: user_login
-    component_ids:
-      - 'user:login'
-      - 'content_entity:insert'
-      - 'eca_current_user_role'
-      - 'action_goto_action'
-  -
-    action: info
-    model_id: user_login
-  -
-    action: info
-    component_ids:
-      - 'user:login'
-      - 'content_entity:insert'
-      - 'eca_current_user_role'
-      - 'action_goto_action'
-  -
-    action: info
-    model_id: user_login
-    component_ids:
-      - 'user:login'
-      - 'content_entity:insert'
-      - 'eca_current_user_role'
-      - 'action_goto_action'
-  -
-    action: fail
-    feedback: The model "User login" does not exist
-  -
-    action: fail
-    feedback: There is no condition plugin about checking the age of the user.
diff --git a/modules/agents/prompts/eca/answerQuestion.yml b/modules/agents/prompts/eca/answerQuestion.yml
new file mode 100644
index 0000000..e474391
--- /dev/null
+++ b/modules/agents/prompts/eca/answerQuestion.yml
@@ -0,0 +1,23 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: false
+prompt:
+  introduction: |
+    You are a Drupal developer that can answer questions about a possible model of the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+
+    Based on a previous prompt, you will given information about a model or technical details regarding the events, conditions or actions. The input might also be a combination of those things, try to specify your answer based on those.
+
+    Be helpful, elaborate and answer in the best way you can. Try to re-use as much information about the components and which technical information they expose. This can be the tokens that they expose or the module that implements it.
+    If you do not have enough information to answer the question, please let the user know.
+
+    You can answer with multiple objects if needed.
+  possible_actions:
+    info: The answer to the question or the feedback that you do not have enough information to answer that.
+  formats:
+    - action: Action ID from the list
+      answer: The answer to the question
+  one_shot_learning_examples:
+    - action: info
+      answer: The model 'User login' can redirect a user to the correct page.
+    - action: info
+      answer: There are a couple of actions that can be used to do something with an entity, like unpublishing, updating the URL alias etc.
diff --git a/modules/agents/prompts/eca/determineTask.yml b/modules/agents/prompts/eca/determineTask.yml
new file mode 100644
index 0000000..87f5c70
--- /dev/null
+++ b/modules/agents/prompts/eca/determineTask.yml
@@ -0,0 +1,55 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: true
+prompt:
+  introduction: |
+    You are a Drupal developer that can create or edit configuration for the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+
+    Based on the following context of a task description and comments, you should figure out if they are trying to create a new model, edit an existing one or just asking a question that requires no action. Any question that was already answered, will not be marked as a question.
+
+    You will be given a list of existing models and any events, condition- or action-plugins that are present in the website.
+
+    If the action is "create", "edit" or "info", include the IDs of the existing components you think are required to execute the action as "component_ids" in your answer. There can be multiple but do not generate new IDs.
+    If the action is "edit", include the ID of the model as "model_id" in your answer. There can only be one model ID and do not generate a new model ID.
+    If the action is "info" and you believe that an existing model might be the subject of the answer, provide the model ID as "model_id". There can only be one model ID and do not generate a new model ID.
+
+    If you have suggestions on how the determined task type (eg. "create" or "info") should be executed, you can provide that feedback as "feedback" in your answer. Be precise.
+    If you can't find the answer to the question, you can ask for more information or say that you can't find the answer.
+
+    Only give back one long answer.
+  possible_actions:
+    create: They are trying to create an Event-Condition-Action model.
+    edit: They are trying to edit an existing Event-Condition-Action model.
+    info: They want information about an existing model or one or more existing events, conditions or actions, without any further action.
+    fail: It failed due to missing information or being ambivalent.
+  formats:
+    - action: Action ID from the list
+      model_id: The ID of the existing model when the action is "edit" or "info"
+      component_ids: A list of IDs of components when the action is "create", "edit" or "info"
+      feedback: An optional message
+  one_shot_learning_examples:
+    - action: create
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: edit
+      model_id: user_login
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: info
+      model_id: user_login
+    - action: info
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: fail
+      feedback: The model "User login" does not exist
+    - action: fail
+      feedback: There is no condition plugin about checking the age of the user.
diff --git a/modules/agents/prompts/eca/getConfig.yml b/modules/agents/prompts/eca/getConfig.yml
new file mode 100644
index 0000000..5273887
--- /dev/null
+++ b/modules/agents/prompts/eca/getConfig.yml
@@ -0,0 +1,83 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: false
+prompt:
+  introduction: |
+    You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
+
+    If you can't create the configuration because it's lacking information, just answer with the "no_info" action.
+
+    You will receive information about the available plugins and their details, as well as a list of generally available tokens. You can use them how you see fit, but do not generate new tokens.
+    There should be at least one Event-related component.
+  possible_actions:
+    set_title: sets the title of the configuration item
+    set_description: sets the description of the configuration item
+    no_info: if there's not enough information to create the configuration item, just answer with this action
+  formats:
+    - id: unique random ID of component, it should consist of 7 characters and start with "Event_" for an event, "Activity_" for an activity or "Flow_" for a condition
+      plugin: an existing plugin ID
+      label: human readable label
+      config: optional setup to configure the component
+      successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
+    - id: unique random ID of the Gateway, it should consist of 7 characters and start with "Gateway_"
+      successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
+    - type: type of action
+      value: value of the action
+  one_shot_learning_examples:
+    - id: Event_0erz1e4
+      plugin: 'user:login'
+      label: 'User Login'
+      successors:
+        - id: Gateway_0hd8858
+          condition: Flow_1o433l9
+    - id: Flow_
+      plugin: eca_scalar
+      config:
+        case: false
+        left: '[current-page:url:path]'
+        right: /user/reset
+        operator: beginswith
+        type: value
+        negate: true
+    - id: Gateway_0hd8858
+      successors:
+        - id: Activity_0l4w3fc
+          condition: Flow_1hqinah
+        - id: Activity_182vndw
+          condition: Flow_0047zve
+    - id: Activity_0l4w3fc
+      plugin: action_goto_action
+      label: 'Redirect to content overview'
+      config:
+        replace_tokens: false
+        url: /admin/content
+      successors: { }
+    - id: Flow_1hqinah
+      plugin: eca_current_user_role
+      config:
+        negate: false
+        role: content_editor
+    - id: Activity_182vndw
+      plugin: action_goto_action
+      label: 'Redirect to admin overview'
+      config:
+        replace_tokens: false
+        url: /admin
+    - id: Flow_0047zve
+      plugin: eca_current_user_role
+      config:
+        negate: false
+        role: administrator
+    - type: set_title
+      value: 'ECA Feature Demo'
+    - type: set_description
+      value: |
+        This model demonstrates a number of smart features around user accounts:
+
+        1. When a user registers themselves or gets created by an existing user, then all existing users with the admin role get informed by email. If the current user has the admin role, a message also get displayed with a link to the mailhog application to review the emails.
+        2. When a user logs in, a number of actions applies: depending on their role, different redirect destinations will be used after login. Also, the assigment of the internal user role gets executed, see below.
+        3. When a user gets updated, the assigment of the internal user role also gets executed.
+
+        The assignment of the internal user role assigns that role to the current user if their email domain contains @example.com and removes it otherwise. It does that only if the situation had changed and also displays an according message on screen.
+    - type: no_info
+      value: Not enough information to create the configuration item
diff --git a/modules/agents/prompts/getConfig.yml b/modules/agents/prompts/getConfig.yml
deleted file mode 100644
index 80e0a06..0000000
--- a/modules/agents/prompts/getConfig.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-introduction: |
-  You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
-
-  If you can't create the configuration because it's lacking information, just answer with the "no_info" action.
-
-  You will receive information about the available plugins and their details, as well as a list of generally available tokens. You can use them how you see fit, but do not generate new tokens.
-  There should be at least one Event-related component.
-preferred_model: gpt-4o
-preferred_llm: openai
-possible_actions:
-  set_title: sets the title of the configuration item
-  set_description: sets the description of the configuration item
-  no_info: if there's not enough information to create the configuration item, just answer with this action
-formats:
-  - id: unique random ID of component, it should consist of 7 characters and start with "Event_" for an event, "Activity_" for an activity or "Flow_" for a condition
-    plugin: an existing plugin ID
-    label: human readable label
-    config: optional setup to configure the component
-    successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
-  - id: unique random ID of the Gateway, it should consist of 7 characters and start with "Gateway_"
-    successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
-  - type: type of action
-    value: value of the action
-one_shot_learning_examples:
-  - id: Event_0erz1e4
-    plugin: 'user:login'
-    label: 'User Login'
-    successors:
-      - id: Gateway_0hd8858
-        condition: Flow_1o433l9
-  - id: Flow_
-    plugin: eca_scalar
-    config:
-      case: false
-      left: '[current-page:url:path]'
-      right: /user/reset
-      operator: beginswith
-      type: value
-      negate: true
-  - id: Gateway_0hd8858
-    successors:
-      - id: Activity_0l4w3fc
-        condition: Flow_1hqinah
-      - id: Activity_182vndw
-        condition: Flow_0047zve
-  - id: Activity_0l4w3fc
-    plugin: action_goto_action
-    label: 'Redirect to content overview'
-    config:
-      replace_tokens: false
-      url: /admin/content
-    successors: { }
-  - id: Flow_1hqinah
-    plugin: eca_current_user_role
-    config:
-      negate: false
-      role: content_editor
-  - id: Activity_182vndw
-    plugin: action_goto_action
-    label: 'Redirect to admin overview'
-    config:
-      replace_tokens: false
-      url: /admin
-  - id: Flow_0047zve
-    plugin: eca_current_user_role
-    config:
-      negate: false
-      role: administrator
-  - type: set_title
-    value: 'ECA Feature Demo'
-  - type: set_description
-    value: |
-      This model demonstrates a number of smart features around user accounts:
-
-      1. When a user registers themselves or gets created by an existing user, then all existing users with the admin role get informed by email. If the current user has the admin role, a message also get displayed with a link to the mailhog application to review the emails.
-      2. When a user logs in, a number of actions applies: depending on their role, different redirect destinations will be used after login. Also, the assigment of the internal user role gets executed, see below.
-      3. When a user gets updated, the assigment of the internal user role also gets executed.
-
-      The assignment of the internal user role assigns that role to the current user if their email domain contains @example.com and removes it otherwise. It does that only if the situation had changed and also displays an according message on screen.
-  - type: no_info
-    value: Not enough information to create the configuration item
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 45df4f1..9ff9e2f 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -64,7 +64,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    $instance = new static();
+    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
     $instance->entityTypeManager = $container->get('entity_type.manager');
     $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
 
@@ -140,6 +140,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * {@inheritdoc}
    */
   public function determineSolvability(): int {
+    parent::determineSolvability();
     $type = $this->determineTypeOfTask();
 
     return match ($type) {
@@ -173,25 +174,26 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * {@inheritdoc}
    */
   public function answerQuestion(): string|TranslatableMarkup {
+    if (!$this->currentUser->hasPermission('administer eca')) {
+      return $this->t('You do not have permission to do this.');
+    }
+
     $this->dataProvider->setViewMode(DataViewModeEnum::Full);
 
-    $userPrompts = [];
+    $context = [];
     if (!empty($this->model)) {
-      $userPrompts['The information of the model, in JSON format'] = sprintf("```json\n%s", json_encode($this->dataProvider->getModels([$this->model->id()])));
+      $context['The information of the model, in JSON format'] = sprintf("```json\n%s", json_encode($this->dataProvider->getModels([$this->model->id()])));
     }
-
-    if (!empty($this->data[0]['component_ids'])) {
-      $userPrompts['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+    else if (!empty($this->data[0]['component_ids'])) {
+      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
     }
 
-    if (empty($userPrompts)) {
+    if (empty($context)) {
       return $this->t('Sorry, I could not answer your question without anymore context.');
     }
 
-    $actionPrompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'answerQuestion', $userPrompts);
-
     // Perform the prompt.
-    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($actionPrompt['prompt']));
+    $data = $this->agentHelper->runSubAgent('answerQuestion', $context);
 
     if (isset($data[0]['answer'])) {
       $answer = array_map(function ($dataPoint) {
@@ -231,8 +233,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   public function setTask(TaskInterface $task) {
     parent::setTask($task);
 
-    if (!empty($this->getState()['models'][0])) {
-      $this->model = $this->getState()['models'][0];
+    if (!empty($this->state['models'][0])) {
+      $this->model = $this->state['models'][0];
     }
   }
 
@@ -250,14 +252,12 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       $context .= "\n\nA model already exists, so creation is not possible.";
     }
 
-    // Prepare the prompt by fetching all the relevant info.
-    $actionPrompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'determineTask', [
+    // Prepare and run the prompt by fetching all the relevant info.
+    $data = $this->agentHelper->runSubAgent('determineTask', [
       'Task description and if available comments description' => $context,
       'The list of existing models, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getModels())),
       'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getComponents())),
     ]);
-    // Perform the prompt.
-    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($actionPrompt['prompt']));
 
     // Quit early if the returned response isn't what we expected.
     if (empty($data[0]['action'])) {
@@ -294,20 +294,19 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $this->dataProvider->setViewMode(DataViewModeEnum::Full);
 
     // Prepare the prompt.
-    $userPrompts = [
+    $context = [
       //          'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
       'The available modellers' => sprintf("```json\n%s", json_encode($this->dataProvider->getModellers())),
     ];
     if (!empty($this->data[0]['component_ids'])) {
-      $userPrompts['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
     }
     if (!empty($this->data[0]['feedback'])) {
-      $userPrompts['Guidelines'] = $this->data[0]['feedback'];
+      $context['Guidelines'] = $this->data[0]['feedback'];
     }
-    $prompt = $this->agentHelper->actionPrompts('ai_eca_agents', 'getConfig', $userPrompts);
 
     // Execute it.
-    $data = $this->cleanupAndDecodeJsonResponse($this->runAiProvider($prompt['prompt']));
+    $data = $this->agentHelper->runSubAgent('getConfig', $context);
 
     if (empty($data) || (!empty($data[0]['type']) && $data[0]['type'] === 'no_info')) {
       throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
@@ -321,7 +320,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $random = new Random();
     $eca->set('id', md5($random->string(16, TRUE)));
     $eca->set('label', $random->string(16));
-    $eca->set('modeller', 'core');
+    $eca->set('modeller', 'fallback');
     $eca->set('version', '0.0.1');
 
     foreach ($data as $entry) {
-- 
GitLab


From 4067be9320af187748b76260acb8e8767824f9b4 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 16:55:43 +0100
Subject: [PATCH 09/95] #3481307 AI can't decide which modeller to use

---
 modules/agents/src/Plugin/AiAgent/Eca.php                 | 3 +--
 modules/agents/src/Services/DataProvider/DataProvider.php | 7 -------
 .../src/Services/DataProvider/DataProviderInterface.php   | 8 --------
 3 files changed, 1 insertion(+), 17 deletions(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 9ff9e2f..36bc251 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -295,8 +295,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Prepare the prompt.
     $context = [
-      //          'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
-      'The available modellers' => sprintf("```json\n%s", json_encode($this->dataProvider->getModellers())),
+//      'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
     ];
     if (!empty($this->data[0]['component_ids'])) {
       $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 8b8dd0c..9e8c2b1 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -96,13 +96,6 @@ class DataProvider implements DataProviderInterface {
     return $this->convertPlugins($this->actions->actions());
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getModellers(): array {
-    return $this->modellers->getModellerDefinitions();
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/modules/agents/src/Services/DataProvider/DataProviderInterface.php b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
index 9a7c961..85ce273 100644
--- a/modules/agents/src/Services/DataProvider/DataProviderInterface.php
+++ b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
@@ -31,14 +31,6 @@ interface DataProviderInterface {
    */
   public function getActions(): array;
 
-  /**
-   * Get all the modellers.
-   *
-   * @return array
-   *   Returns a list of all the modellers.
-   */
-  public function getModellers(): array;
-
   /**
    * A wrapper that returns the events, conditions and actions.
    *
-- 
GitLab


From ff974de3685181890fea350ebb01e483440539d2 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 16:56:32 +0100
Subject: [PATCH 10/95] #3481307 Redirect to ECA-overview when entity was
 created

---
 modules/agents/ai_eca_agents.info.yml     |  1 +
 modules/agents/src/Plugin/AiAgent/Eca.php | 20 ++++++++++++++------
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
index 6d3246a..4f26343 100644
--- a/modules/agents/ai_eca_agents.info.yml
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -6,4 +6,5 @@ package: AI
 dependencies:
   - ai_eca:ai_eca
   - ai_agents:ai_agents
+  - eca:eca_ui
   - token:token
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 36bc251..edd0f79 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -13,6 +13,7 @@ use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
 use Drupal\eca\Entity\Eca as EcaEntity;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -32,6 +33,11 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    */
   protected array $questions;
 
+  /**
+   * A list of messages for the user.
+   */
+  protected array $messages = [];
+
   /**
    * The full data of the initial task.
    *
@@ -154,20 +160,17 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * {@inheritdoc}
    */
-  public function solve(): array|string {
-    $messages = [''];
-
-    switch ($this->data[0]['action']) {
+  public function solve(): array|string {switch ($this->data[0]['action']) {
       case 'create':
         $this->createConfig();
         break;
 
       case 'edit':
-        $messages[] = 'edit';
+        $this->messages[] = 'edit';
         break;
     }
 
-    return implode("\n", $messages);
+    return implode("\n", $this->messages);
   }
 
   /**
@@ -362,6 +365,11 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Save the entity.
     $eca->save();
+    $this->messages[] = $this->t('The model \'@name\' has been created. You can find it here: %link.', [
+      '@name' => $eca->label(),
+      '%link' => Url::fromRoute('entity.eca.collection')->toString(),
+    ]);
+    $this->messages[] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
   }
 
 }
-- 
GitLab


From b8cd0d7aa1699915a7d6c77f32b25a509db76269 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 16:56:54 +0100
Subject: [PATCH 11/95] #3481307 Always set ECA models as unpublished

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index edd0f79..a743daa 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -324,6 +324,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $eca->set('label', $random->string(16));
     $eca->set('modeller', 'fallback');
     $eca->set('version', '0.0.1');
+    $eca->setStatus(FALSE);
 
     foreach ($data as $entry) {
       // Events, Conditions ("Flows"), Actions and Gateways.
-- 
GitLab


From 45b794930a0668fe663877ab37488bf2c6f9e6f3 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 16:57:14 +0100
Subject: [PATCH 12/95] #3481307 ECA entities can't contain description

---
 modules/agents/prompts/eca/getConfig.yml | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/modules/agents/prompts/eca/getConfig.yml b/modules/agents/prompts/eca/getConfig.yml
index 5273887..d86133b 100644
--- a/modules/agents/prompts/eca/getConfig.yml
+++ b/modules/agents/prompts/eca/getConfig.yml
@@ -11,7 +11,6 @@ prompt:
     There should be at least one Event-related component.
   possible_actions:
     set_title: sets the title of the configuration item
-    set_description: sets the description of the configuration item
     no_info: if there's not enough information to create the configuration item, just answer with this action
   formats:
     - id: unique random ID of component, it should consist of 7 characters and start with "Event_" for an event, "Activity_" for an activity or "Flow_" for a condition
@@ -70,14 +69,5 @@ prompt:
         role: administrator
     - type: set_title
       value: 'ECA Feature Demo'
-    - type: set_description
-      value: |
-        This model demonstrates a number of smart features around user accounts:
-
-        1. When a user registers themselves or gets created by an existing user, then all existing users with the admin role get informed by email. If the current user has the admin role, a message also get displayed with a link to the mailhog application to review the emails.
-        2. When a user logs in, a number of actions applies: depending on their role, different redirect destinations will be used after login. Also, the assigment of the internal user role gets executed, see below.
-        3. When a user gets updated, the assigment of the internal user role also gets executed.
-
-        The assignment of the internal user role assigns that role to the current user if their email domain contains @example.com and removes it otherwise. It does that only if the situation had changed and also displays an according message on screen.
     - type: no_info
       value: Not enough information to create the configuration item
-- 
GitLab


From 6e64e1ca49fa98d6ef14ec07874fc7abc1140e94 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 23 Nov 2024 17:19:50 +0100
Subject: [PATCH 13/95] #3481307 Create debug-command for the data provider

---
 modules/agents/drush.services.yml             |  7 ++
 .../src/Command/DebugDataProviderCommand.php  | 68 +++++++++++++++++++
 2 files changed, 75 insertions(+)
 create mode 100644 modules/agents/drush.services.yml
 create mode 100644 modules/agents/src/Command/DebugDataProviderCommand.php

diff --git a/modules/agents/drush.services.yml b/modules/agents/drush.services.yml
new file mode 100644
index 0000000..572d0cd
--- /dev/null
+++ b/modules/agents/drush.services.yml
@@ -0,0 +1,7 @@
+services:
+  ai_eca_agents.debug_data_provider:
+    class: Drupal\ai_eca_agents\Command\DebugDataProviderCommand
+    arguments:
+      - '@ai_eca_agents.services.data_provider'
+    tags:
+      - { name: console.command }
diff --git a/modules/agents/src/Command/DebugDataProviderCommand.php b/modules/agents/src/Command/DebugDataProviderCommand.php
new file mode 100644
index 0000000..a954145
--- /dev/null
+++ b/modules/agents/src/Command/DebugDataProviderCommand.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ai_eca_agents\Command;
+
+use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
+use Drupal\Component\Serialization\Json;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * A console command to debug the data provider.
+ */
+#[AsCommand(
+  name: 'ai-agents-eca:debug:data',
+  description: 'Debug the data provider',
+)]
+final class DebugDataProviderCommand extends Command {
+
+  /**
+   * Constructs an DebugCommand object.
+   */
+  public function __construct(
+    protected readonly DataProviderInterface $dataProvider
+  ) {
+    parent::__construct();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure(): void {
+    $this
+      ->addOption('filter', NULL, InputOption::VALUE_OPTIONAL, 'The filter to apply to the provider. Options are "components" or "models"')
+      ->addOption('vm', NULL, InputOption::VALUE_OPTIONAL, 'The view mode to use. Options are teaser or full.', DataViewModeEnum::Teaser->value)
+      ->addUsage('ai-agents-eca:debug:data --vm=full')
+      ->setHelp('Debug the data provider');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output): int {
+    $this->dataProvider->setViewMode(DataViewModeEnum::from($input->getOption('vm')));
+    $response = [
+      'components' => $this->dataProvider->getComponents(),
+      'models' => $this->dataProvider->getModels(),
+    ];
+    if (!empty($input->getOption('filter'))) {
+      $filter = $input->getOption('filter');
+      $response = array_filter($response, function (string $key) use ($filter) {
+        return $key === $filter;
+      }, ARRAY_FILTER_USE_KEY);
+    }
+
+    $response = Json::decode(Json::encode($response));
+
+    $output->writeln(print_r($response, TRUE));
+
+    return self::SUCCESS;
+  }
+
+}
-- 
GitLab


From 5c398031fa8388b63d93beeb4e015c55a576d1af Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 20:59:11 +0100
Subject: [PATCH 14/95] #3481307 Provide possible options of actions

---
 modules/agents/src/Services/DataProvider/DataProvider.php | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 9e8c2b1..28d74c4 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -62,7 +62,7 @@ class DataProvider implements DataProviderInterface {
       ];
 
       if ($this->viewMode === DataViewModeEnum::Full) {
-        $info['tokens'] = array_reduce($event->getTokens(), function (array $carry, Token $token) {
+        $info['exposed_tokens'] = array_reduce($event->getTokens(), function (array $carry, Token $token) {
           $info = [
             'name' => $token->name,
             'description' => $token->description,
@@ -209,6 +209,9 @@ class DataProvider implements DataProviderInterface {
           if (!empty($elements[$key]['#description'])) {
             $config['description'] = (string) $elements[$key]['#description'];
           }
+          if (!empty($elements[$key]['#options'])) {
+            $config['options'] = array_keys($elements[$key]['#options']);
+          }
 
           $carry[] = $config;
 
-- 
GitLab


From ecdef678d19ce744ca1a67a083e4f5bd1ca8cabe Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 20:59:38 +0100
Subject: [PATCH 15/95] #3481307 Use the default value of the element to
 determine the type

---
 .../agents/src/Services/DataProvider/DataProvider.php  | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 28d74c4..c643b85 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -196,12 +196,13 @@ class DataProvider implements DataProviderInterface {
           continue;
         }
 
-        $configKeys = array_keys($plugin->defaultConfiguration());
+        $defaultConfig = $plugin->defaultConfiguration();
+        $configKeys = array_keys($defaultConfig);
         $elements = array_filter($plugin->buildConfigurationForm([], new FormState()), function ($key) use ($configKeys) {
           return in_array($key, $configKeys);
         }, ARRAY_FILTER_USE_KEY);
 
-        $info['configuration'] = array_reduce(array_keys($elements), function (array $carry, $key) use ($elements) {
+        $info['configuration'] = array_reduce(array_keys($elements), function (array $carry, $key) use ($elements, $defaultConfig) {
           $config = [
             'config_id' => $key,
             'name' => (string) $elements[$key]['#title'],
@@ -212,6 +213,9 @@ class DataProvider implements DataProviderInterface {
           if (!empty($elements[$key]['#options'])) {
             $config['options'] = array_keys($elements[$key]['#options']);
           }
+          if (isset($defaultConfig[$key])) {
+            $config['value_type'] = gettype($defaultConfig[$key]);
+          }
 
           $carry[] = $config;
 
@@ -223,7 +227,7 @@ class DataProvider implements DataProviderInterface {
         $info['description'] = (string) $plugin->getPluginDefinition()['description'];
       }
 
-      $output[] = $info;
+      $output[] = array_filter($info);
     }
 
     return $output;
-- 
GitLab


From 7d2cb1911ad4fdbafa00f3f29ed8d24065729ad8 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 21:15:39 +0100
Subject: [PATCH 16/95] #3481307 Refactor building the config details for
 plugins

---
 .../Services/DataProvider/DataProvider.php    | 74 +++++++++++--------
 1 file changed, 45 insertions(+), 29 deletions(-)

diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index c643b85..de1d743 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -3,6 +3,7 @@
 namespace Drupal\ai_eca_agents\Services\DataProvider;
 
 use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Plugin\PluginFormInterface;
@@ -74,6 +75,8 @@ class DataProvider implements DataProviderInterface {
 
           return $carry;
         }, []);
+
+        $info['configuration'] = $this->buildConfig($event);
       }
 
       $output[] = $info;
@@ -192,35 +195,7 @@ class DataProvider implements DataProviderInterface {
       ];
 
       if ($this->viewMode === DataViewModeEnum::Full) {
-        if (!$plugin instanceof ConfigurableInterface && !$plugin instanceof PluginFormInterface) {
-          continue;
-        }
-
-        $defaultConfig = $plugin->defaultConfiguration();
-        $configKeys = array_keys($defaultConfig);
-        $elements = array_filter($plugin->buildConfigurationForm([], new FormState()), function ($key) use ($configKeys) {
-          return in_array($key, $configKeys);
-        }, ARRAY_FILTER_USE_KEY);
-
-        $info['configuration'] = array_reduce(array_keys($elements), function (array $carry, $key) use ($elements, $defaultConfig) {
-          $config = [
-            'config_id' => $key,
-            'name' => (string) $elements[$key]['#title'],
-          ];
-          if (!empty($elements[$key]['#description'])) {
-            $config['description'] = (string) $elements[$key]['#description'];
-          }
-          if (!empty($elements[$key]['#options'])) {
-            $config['options'] = array_keys($elements[$key]['#options']);
-          }
-          if (isset($defaultConfig[$key])) {
-            $config['value_type'] = gettype($defaultConfig[$key]);
-          }
-
-          $carry[] = $config;
-
-          return $carry;
-        }, []);
+        $info['configuration'] = $this->buildConfig($plugin);
       }
 
       if (!empty($plugin->getPluginDefinition()['description'])) {
@@ -233,4 +208,45 @@ class DataProvider implements DataProviderInterface {
     return $output;
   }
 
+  /**
+   * Build the configuration details for the given plugin.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
+   *   The plugin.
+   *
+   * @return array
+   *   Returns the configuration details.
+   */
+  protected function buildConfig(PluginInspectionInterface $plugin): array {
+    if (!$plugin instanceof ConfigurableInterface && !$plugin instanceof PluginFormInterface) {
+      return [];
+    }
+
+    $defaultConfig = $plugin->defaultConfiguration();
+    $configKeys = array_keys($defaultConfig);
+    $elements = array_filter($plugin->buildConfigurationForm([], new FormState()), function ($key) use ($configKeys) {
+      return in_array($key, $configKeys);
+    }, ARRAY_FILTER_USE_KEY);
+
+    return array_reduce(array_keys($elements), function (array $carry, $key) use ($elements, $defaultConfig) {
+      $config = [
+        'config_id' => $key,
+        'name' => (string) $elements[$key]['#title'],
+      ];
+      if (!empty($elements[$key]['#description'])) {
+        $config['description'] = (string) $elements[$key]['#description'];
+      }
+      if (!empty($elements[$key]['#options'])) {
+        $config['options'] = array_keys($elements[$key]['#options']);
+      }
+      if (isset($defaultConfig[$key])) {
+        $config['value_type'] = gettype($defaultConfig[$key]);
+      }
+
+      $carry[] = $config;
+
+      return $carry;
+    }, []);
+  }
+
 }
-- 
GitLab


From 512ca036e5ab11a7dd94bec59d46d94f79c09f6b Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 21:15:58 +0100
Subject: [PATCH 17/95] #3481307 Rename the buildModel-subagent

---
 .../agents/prompts/eca/{getConfig.yml => buildModel.yml}  | 8 ++++++++
 modules/agents/src/Plugin/AiAgent/Eca.php                 | 2 +-
 2 files changed, 9 insertions(+), 1 deletion(-)
 rename modules/agents/prompts/eca/{getConfig.yml => buildModel.yml} (92%)

diff --git a/modules/agents/prompts/eca/getConfig.yml b/modules/agents/prompts/eca/buildModel.yml
similarity index 92%
rename from modules/agents/prompts/eca/getConfig.yml
rename to modules/agents/prompts/eca/buildModel.yml
index d86133b..dd44411 100644
--- a/modules/agents/prompts/eca/getConfig.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -29,6 +29,14 @@ prompt:
       successors:
         - id: Gateway_0hd8858
           condition: Flow_1o433l9
+    - id: Event_0erz1e4
+      plugin: 'content_entity:insert'
+      label: 'User Register'
+      configuration:
+        type: 'user user'
+      successors:
+        - id: Gateway_0hd8858
+          condition: Flow_1o433l9
     - id: Flow_
       plugin: eca_scalar
       config:
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index a743daa..8fed91f 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -308,7 +308,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     }
 
     // Execute it.
-    $data = $this->agentHelper->runSubAgent('getConfig', $context);
+    $data = $this->agentHelper->runSubAgent('buildModel', $context);
 
     if (empty($data) || (!empty($data[0]['type']) && $data[0]['type'] === 'no_info')) {
       throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
-- 
GitLab


From 78c081938ba4a4c87fbabd2088d667f22e91e7ea Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 21:23:27 +0100
Subject: [PATCH 18/95] #3481307 Describe prompts

---
 modules/agents/prompts/eca/answerQuestion.yml | 2 ++
 modules/agents/prompts/eca/buildModel.yml     | 2 ++
 modules/agents/prompts/eca/determineTask.yml  | 2 ++
 3 files changed, 6 insertions(+)

diff --git a/modules/agents/prompts/eca/answerQuestion.yml b/modules/agents/prompts/eca/answerQuestion.yml
index e474391..451f0e7 100644
--- a/modules/agents/prompts/eca/answerQuestion.yml
+++ b/modules/agents/prompts/eca/answerQuestion.yml
@@ -1,6 +1,8 @@
 preferred_model: gpt-4o
 preferred_llm: openai
 is_triage: false
+name: Answer question
+description: This sub-agent is capable of answering questions about existing models, events, conditions or actions that are available in the website.
 prompt:
   introduction: |
     You are a Drupal developer that can answer questions about a possible model of the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index dd44411..1ecf564 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -1,6 +1,8 @@
 preferred_model: gpt-4o
 preferred_llm: openai
 is_triage: false
+name: Build model
+description: This sub-agent is capable of creating ECA models.
 prompt:
   introduction: |
     You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
diff --git a/modules/agents/prompts/eca/determineTask.yml b/modules/agents/prompts/eca/determineTask.yml
index 87f5c70..d41e855 100644
--- a/modules/agents/prompts/eca/determineTask.yml
+++ b/modules/agents/prompts/eca/determineTask.yml
@@ -1,6 +1,8 @@
 preferred_model: gpt-4o
 preferred_llm: openai
 is_triage: true
+name: Determine task
+Description: This agent is the initial agent that determines if the user is trying to manipulate ECA models, wants information or ask questions about models or plugins.
 prompt:
   introduction: |
     You are a Drupal developer that can create or edit configuration for the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
-- 
GitLab


From 8edad6357818e0576ddc023d2e4a71178f23bea3 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 25 Nov 2024 22:00:51 +0100
Subject: [PATCH 19/95] #3481307 Add ECA validation

---
 modules/agents/prompts/eca/buildModel.yml     |  2 +
 .../AiAgentValidation/EcaValidation.php       | 66 +++++++++++++++++++
 2 files changed, 68 insertions(+)
 create mode 100644 modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 1ecf564..7a39b52 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -3,6 +3,8 @@ preferred_llm: openai
 is_triage: false
 name: Build model
 description: This sub-agent is capable of creating ECA models.
+validation:
+  - [eca_validation]
 prompt:
   introduction: |
     You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
new file mode 100644
index 0000000..3db984a
--- /dev/null
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\AiAgentValidation;
+
+use Drupal\ai\OperationType\Chat\ChatMessage;
+use Drupal\ai_agents\Attribute\AiAgentValidation;
+use Drupal\ai_agents\Exception\AgentRetryableValidationException;
+use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * The validation of the ECA agent.
+ */
+#[AiAgentValidation(
+  id: 'eca_validation',
+  label: new TranslatableMarkup('ECA Validation'),
+)]
+class EcaValidation extends AiAgentValidationPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValidation(mixed $data): bool {
+    $data = $this->decodeData($data);
+    if (empty($data)) {
+      throw new AgentRetryableValidationException('The LLM response failed validation.', 0, NULL, 'You MUST only provide a RFC8259 compliant JSON response.');
+    }
+
+    // Validate that at least 1 part of the response sets the title.
+    $setsTitle = (bool) count(array_filter($data, function ($component) {
+      return !empty($component['type']) && $component['type'] === 'set_title';
+    }));
+    if (!$setsTitle) {
+      throw new AgentRetryableValidationException('The LLM response failed validation.', 0, NULL, 'You MUST only provide a title for the model.');
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Decodes the source data.
+   *
+   * @param mixed $source
+   *   The source.
+   *
+   * @return array|null
+   *   Returns the decoded data or NULL.
+   */
+  protected function decodeData(mixed $source): ?array {
+    $text = NULL;
+
+    switch (TRUE) {
+      case $source instanceof ChatMessage:
+        $text = $source->getText();
+        break;
+
+      case is_string($source):
+        $text = $source;
+        break;
+    }
+
+    return Json::decode($text ?? '');
+  }
+
+}
-- 
GitLab


From ece710f70e00c663e50b18985aa1844a0fa08c92 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 14:35:51 +0100
Subject: [PATCH 20/95] #3481307 Re-use functionality of BPMN.io to generate id
 of model

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 8fed91f..539cb21 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -320,7 +320,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       ->create();
 
     $random = new Random();
-    $eca->set('id', md5($random->string(16, TRUE)));
+    $eca->set('id', sprintf('Process_%s', $random->name(7)));
     $eca->set('label', $random->string(16));
     $eca->set('modeller', 'fallback');
     $eca->set('version', '0.0.1');
-- 
GitLab


From bfefc3f0c350063b564d4ddd4446466c7f68218c Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 14:36:12 +0100
Subject: [PATCH 21/95] #3481307 Specify error messages

---
 modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index 3db984a..8bb925e 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -24,7 +24,7 @@ class EcaValidation extends AiAgentValidationPluginBase {
   public function defaultValidation(mixed $data): bool {
     $data = $this->decodeData($data);
     if (empty($data)) {
-      throw new AgentRetryableValidationException('The LLM response failed validation.', 0, NULL, 'You MUST only provide a RFC8259 compliant JSON response.');
+      throw new AgentRetryableValidationException('The LLM response failed validation: could not decode from JSON.', 0, NULL, 'You MUST only provide a RFC8259 compliant JSON response.');
     }
 
     // Validate that at least 1 part of the response sets the title.
@@ -32,7 +32,7 @@ class EcaValidation extends AiAgentValidationPluginBase {
       return !empty($component['type']) && $component['type'] === 'set_title';
     }));
     if (!$setsTitle) {
-      throw new AgentRetryableValidationException('The LLM response failed validation.', 0, NULL, 'You MUST only provide a title for the model.');
+      throw new AgentRetryableValidationException('The LLM response failed validation: no title was provided for the model.', 0, NULL, 'You MUST only provide a title for the model.');
     }
 
     return TRUE;
-- 
GitLab


From 247890bf7234039023364aa5ca2cc8e8f1c14282 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 14:52:00 +0100
Subject: [PATCH 22/95] #3481307 Refactor to an ECA-entity repository service

---
 modules/agents/ai_eca_agents.services.yml     |  5 +
 modules/agents/src/Plugin/AiAgent/Eca.php     | 72 ++-------------
 .../Services/EcaRepository/EcaRepository.php  | 92 +++++++++++++++++++
 .../EcaRepository/EcaRepositoryInterface.php  | 34 +++++++
 4 files changed, 141 insertions(+), 62 deletions(-)
 create mode 100644 modules/agents/src/Services/EcaRepository/EcaRepository.php
 create mode 100644 modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index dc5ceca..8fe2ffe 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -7,3 +7,8 @@ services:
       - '@eca.service.action'
       - '@entity_type.manager'
       - '@token.tree_builder'
+
+  ai_eca_agents.services.eca_repository:
+    class: Drupal\ai_eca_agents\Services\EcaRepository\EcaRepository
+    arguments:
+      - '@entity_type.manager'
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 539cb21..12aa04d 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -8,9 +8,8 @@ use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
 use Drupal\ai_agents\Task\TaskInterface;
 use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
 use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
-use Drupal\Component\Utility\Random;
+use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
@@ -53,26 +52,26 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   protected ?EcaEntity $model;
 
   /**
-   * The entity type manager.
+   * The ECA data provider.
    *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
    */
-  protected EntityTypeManagerInterface $entityTypeManager;
+  protected DataProviderInterface $dataProvider;
 
   /**
-   * The ECA data provider.
+   * The ECA helper.
    *
-   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
+   * @var \Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface
    */
-  protected DataProviderInterface $dataProvider;
+  protected EcaRepositoryInterface $ecaRepository;
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
-    $instance->entityTypeManager = $container->get('entity_type.manager');
     $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
+    $instance->ecaRepository = $container->get('ai_eca_agents.services.eca_repository');
 
     return $instance;
   }
@@ -275,7 +274,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       'info',
     ])) {
       if (!empty($data[0]['model_id'])) {
-        $this->model = $this->entityTypeManager->getStorage('eca')->load($data[0]['model_id']);
+        $this->model = $this->ecaRepository->get($data[0]['model_id']);
       }
 
       if (!empty($data[0]['feedback'])) {
@@ -314,58 +313,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
     }
 
-    // Map response to entity properties.
-    /** @var \Drupal\eca\Entity\Eca $eca */
-    $eca = $this->entityTypeManager->getStorage('eca')
-      ->create();
-
-    $random = new Random();
-    $eca->set('id', sprintf('Process_%s', $random->name(7)));
-    $eca->set('label', $random->string(16));
-    $eca->set('modeller', 'fallback');
-    $eca->set('version', '0.0.1');
-    $eca->setStatus(FALSE);
-
-    foreach ($data as $entry) {
-      // Events, Conditions ("Flows"), Actions and Gateways.
-      if (!empty($entry['id'])) {
-        switch (TRUE) {
-          case str_starts_with($entry['id'], 'Event_'):
-            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
-            break;
-
-          case str_starts_with($entry['id'], 'Flow_'):
-            $eca->addCondition($entry['id'], $entry['plugin'], $entry['config'] ?? []);
-            break;
-
-          case str_starts_with($entry['id'], 'Activity_'):
-            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
-            break;
-
-          case str_starts_with($entry['id'], 'Gateway_'):
-            $eca->addGateway($entry['id'], 0, $entry['successors'] ?? []);
-            break;
-        }
-
-        continue;
-      }
-
-      if (!empty($entry['type'])) {
-        switch ($entry['type']) {
-          case 'set_title':
-            $eca->set('label', $entry['value']);
-            break;
-        }
-      }
-    }
-
-    // Validate the entity.
-    if (empty($eca->getUsedEvents())) {
-      throw new \Exception('No events registered.');
-    }
-
-    // Save the entity.
-    $eca->save();
+    $eca = $this->ecaRepository->build($data);
     $this->messages[] = $this->t('The model \'@name\' has been created. You can find it here: %link.', [
       '@name' => $eca->label(),
       '%link' => Url::fromRoute('entity.eca.collection')->toString(),
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
new file mode 100644
index 0000000..19dbf1b
--- /dev/null
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\EcaRepository;
+
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Repository for the ECA entity type.
+ */
+class EcaRepository implements EcaRepositoryInterface {
+
+  /**
+   * Constructs an EcaHelper-instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   */
+  public function __construct(
+    protected EntityTypeManagerInterface $entityTypeManager
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(string $id): ?Eca {
+    return $this->entityTypeManager->getStorage('eca')
+      ->load($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $data): Eca {
+    /** @var \Drupal\eca\Entity\Eca $eca */
+    $eca = $this->entityTypeManager->getStorage('eca')
+      ->create();
+
+    $random = new Random();
+    $eca->set('id', sprintf('Process_%s', $random->name(7)));
+    $eca->set('label', $random->string(16));
+    $eca->set('modeller', 'fallback');
+    $eca->set('version', '0.0.1');
+    $eca->setStatus(FALSE);
+
+    foreach ($data as $entry) {
+      // Events, Conditions ("Flows"), Actions and Gateways.
+      if (!empty($entry['id'])) {
+        switch (TRUE) {
+          case str_starts_with($entry['id'], 'Event_'):
+            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Flow_'):
+            $eca->addCondition($entry['id'], $entry['plugin'], $entry['config'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Activity_'):
+            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            break;
+
+          case str_starts_with($entry['id'], 'Gateway_'):
+            $eca->addGateway($entry['id'], 0, $entry['successors'] ?? []);
+            break;
+        }
+
+        continue;
+      }
+
+      if (!empty($entry['type'])) {
+        switch ($entry['type']) {
+          case 'set_title':
+            $eca->set('label', $entry['value']);
+            break;
+        }
+      }
+    }
+
+    // Validate the entity.
+    if (empty($eca->getUsedEvents())) {
+      throw new \Exception('No events registered.');
+    }
+
+    // Save the entity.
+    $eca->save();
+
+    return $eca;
+  }
+
+}
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
new file mode 100644
index 0000000..a82ffe9
--- /dev/null
+++ b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\EcaRepository;
+
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Repository for the ECA entity type.
+ */
+interface EcaRepositoryInterface {
+
+  /**
+   * Load a model by ID.
+   *
+   * @param string $id
+   *   The ID to look for.
+   *
+   * @return \Drupal\eca\Entity\Eca|null
+   *   The ECA-entity or NULL.
+   */
+  public function get(string $id): ?Eca;
+
+  /**
+   * Build the ECA-model.
+   *
+   * @param array $data
+   *   The data to use.
+   *
+   * @return \Drupal\eca\Entity\Eca
+   *   Returns the ECA-model.
+   */
+  public function build(array $data): Eca;
+
+}
-- 
GitLab


From 263936a955400f4797eb8506d8c05a6f7105f61e Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 16:10:47 +0100
Subject: [PATCH 23/95] #3481307 Add kernel test for repository

---
 .../tests/src/Kernel/EcaRepositoryTest.php    | 187 ++++++++++++++++++
 1 file changed, 187 insertions(+)
 create mode 100644 modules/agents/tests/src/Kernel/EcaRepositoryTest.php

diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
new file mode 100644
index 0000000..fa3dc2d
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+use Drupal\TestTools\Random;
+
+/**
+ * Tests various input data for generating ECA models.
+ *
+ * @group ai_eca_agents
+ */
+class EcaRepositoryTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'system',
+  ];
+
+  /**
+   * The ECA repository.
+   *
+   * @var \Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface|null
+   */
+  protected ?EcaRepositoryInterface $ecaRepository;
+
+  /**
+   * Generate different sets of data.
+   *
+   * @return \Generator
+   */
+  public static function dataProvider(): \Generator {
+    $random = Random::getGenerator();
+
+    yield [
+      [],
+      [],
+      'No events registered.',
+    ];
+
+    yield [
+      [
+        [
+          'id' => 'Activity_2f4g6h8',
+          'plugin' => 'eca_new_entity',
+          'label' => 'Create New Article',
+          'config' => [
+            'token_name' => 'new_article',
+            'type' => 'node article',
+            'langcode' => 'en',
+            'label' => '[entity:title] Article',
+            'published' => TRUE,
+            'owner' => '[entity:uid]',
+          ],
+        ],
+      ],
+      [],
+      'No events registered.',
+    ];
+
+    $label = $random->name();
+    yield [
+      [
+        [
+          'id' => 'Event_1a3b5c7',
+          'plugin' => 'content_entity:insert',
+          'label' => 'Insert Page Event',
+          'config' => [
+            'type' => 'node page',
+          ],
+          'successors' => [
+            ['id' => 'Activity_2f4g6h8'],
+          ],
+        ],
+        [
+          'id' => 'Activity_2f4g6h8',
+          'plugin' => 'eca_new_entity',
+          'label' => 'Create New Article',
+          'config' => [
+            'token_name' => 'new_article',
+            'type' => 'node article',
+            'langcode' => 'en',
+            'label' => '[entity:title] Article',
+            'published' => TRUE,
+            'owner' => '[entity:uid]',
+          ],
+          'successors' => [
+            ['id' => 'Activity_3i7j9k0'],
+          ],
+        ],
+        [
+          'id' => 'Activity_3i7j9k0',
+          'plugin' => 'eca_set_field_value',
+          'label' => 'Set Article Body',
+          'config' => [
+            'method' => 'remove',
+            'field_name' => 'body.value',
+            'field_value' => 'Page by [entity:author] - Learn more about the topic discussed in [entity:title].',
+            'strip_tags' => FALSE,
+            'trim' => TRUE,
+            'save_entity' => TRUE,
+          ],
+          'successors' => [],
+        ],
+        [
+          'type' => 'set_title',
+          'value' => $label,
+        ]
+      ],
+      [
+        'events' => 1,
+        'conditions' => 0,
+        'actions' => 2,
+        'label' => $label,
+      ],
+    ];
+
+    
+  }
+
+  /**
+   * Build an ECA-model with the provided data.
+   *
+   * @dataProvider dataProvider
+   */
+  public function testBuildModel(array $data, array $assertions, ?string $errorMessage = NULL): void {
+    if (!empty($errorMessage)) {
+      $this->expectExceptionMessage($errorMessage);
+    }
+
+    $eca = $this->ecaRepository->build($data);
+
+    foreach ($assertions as $property => $expected) {
+      switch (TRUE) {
+        case is_int($expected):
+          $this->assertCount($expected, $eca->get($property));
+          break;
+
+        case is_string($expected):
+          $this->assertEquals($expected, $eca->get($property));
+          break;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('eca');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    // Create the Page content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    // Create the Article content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    $this->ecaRepository = \Drupal::service('ai_eca_agents.services.eca_repository');
+  }
+
+}
-- 
GitLab


From 2ab87492eba40afd0ac2f534c6112f851bd79530 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 16:17:42 +0100
Subject: [PATCH 24/95] #3481307 Cleanup code

---
 modules/agents/tests/src/Kernel/EcaRepositoryTest.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index fa3dc2d..9c0eed4 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -119,7 +119,7 @@ class EcaRepositoryTest extends KernelTestBase {
         [
           'type' => 'set_title',
           'value' => $label,
-        ]
+        ],
       ],
       [
         'events' => 1,
@@ -128,8 +128,6 @@ class EcaRepositoryTest extends KernelTestBase {
         'label' => $label,
       ],
     ];
-
-    
   }
 
   /**
-- 
GitLab


From 57a82b9f35c8f2172842c3c4d4de094b8ae745d0 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 16:25:59 +0100
Subject: [PATCH 25/95] #3481307 Configure GitLabCI

---
 .cspell-project-words.txt |  4 ++++
 .gitlab-ci.yml            | 27 +++++++++++++++++++++++----
 2 files changed, 27 insertions(+), 4 deletions(-)
 create mode 100644 .cspell-project-words.txt

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
new file mode 100644
index 0000000..c67d11d
--- /dev/null
+++ b/.cspell-project-words.txt
@@ -0,0 +1,4 @@
+beginswith
+hqinah
+Retryable
+vndw
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e5384a8..c208590 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,7 +23,26 @@ include:
 # https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
 # Uncomment the lines below if you want to override any of the variables. The following is just an example.
 ################
-# variables:
-#   SKIP_ESLINT: '1'
-#   OPT_IN_TEST_NEXT_MAJOR: '1'
-#   _CURL_TEMPLATES_REF: 'main'
+variables:
+  OPT_IN_TEST_PREVIOUS_MAJOR: '1'
+  OPT_IN_TEST_NEXT_MINOR: '1'
+  OPT_IN_TEST_NEXT_MAJOR: '1'
+  RUN_JOB_UPGRADE_STATUS: '0'
+cspell:
+  allow_failure: false
+phpcs:
+  allow_failure: false
+phpstan:
+  allow_failure: false
+phpstan (next minor):
+  allow_failure: false
+phpstan (next major):
+  allow_failure: false
+phpunit (previous major):
+  allow_failure: false
+phpunit (next minor):
+  allow_failure: false
+phpunit (next major):
+  allow_failure: false
+upgrade status:
+  allow_failure: false
-- 
GitLab


From e3f77c6b347ecb954a38679c88030a38caf470a6 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 16:30:57 +0100
Subject: [PATCH 26/95] #3481307 Fix phpcs issues

---
 .../src/Command/DebugDataProviderCommand.php  |  4 ++--
 modules/agents/src/Plugin/AiAgent/Eca.php     | 19 ++++++++++---------
 .../AiAgentValidation/EcaValidation.php       |  4 ++--
 .../DataProvider/DataViewModeEnum.php         |  3 +++
 .../Services/EcaRepository/EcaRepository.php  |  5 ++---
 .../tests/src/Kernel/EcaRepositoryTest.php    |  7 ++++---
 6 files changed, 23 insertions(+), 19 deletions(-)

diff --git a/modules/agents/src/Command/DebugDataProviderCommand.php b/modules/agents/src/Command/DebugDataProviderCommand.php
index a954145..105af68 100644
--- a/modules/agents/src/Command/DebugDataProviderCommand.php
+++ b/modules/agents/src/Command/DebugDataProviderCommand.php
@@ -4,9 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\ai_eca_agents\Command;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
 use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
-use Drupal\Component\Serialization\Json;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
@@ -26,7 +26,7 @@ final class DebugDataProviderCommand extends Command {
    * Constructs an DebugCommand object.
    */
   public function __construct(
-    protected readonly DataProviderInterface $dataProvider
+    protected readonly DataProviderInterface $dataProvider,
   ) {
     parent::__construct();
   }
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 12aa04d..c8331b9 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\ai_eca_agents\Plugin\AiAgent;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
 use Drupal\ai_agents\Attribute\AiAgent;
 use Drupal\ai_agents\PluginBase\AiAgentBase;
 use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
@@ -9,10 +13,6 @@ use Drupal\ai_agents\Task\TaskInterface;
 use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
 use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\Url;
 use Drupal\eca\Entity\Eca as EcaEntity;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -47,7 +47,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * The ECA entity.
    *
-   * @var \Drupal\eca\Entity\Eca|NULL
+   * @var \Drupal\eca\Entity\Eca|null
    */
   protected ?EcaEntity $model;
 
@@ -159,7 +159,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * {@inheritdoc}
    */
-  public function solve(): array|string {switch ($this->data[0]['action']) {
+  public function solve(): array|string {
+    switch ($this->data[0]['action']) {
       case 'create':
         $this->createConfig();
         break;
@@ -186,7 +187,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     if (!empty($this->model)) {
       $context['The information of the model, in JSON format'] = sprintf("```json\n%s", json_encode($this->dataProvider->getModels([$this->model->id()])));
     }
-    else if (!empty($this->data[0]['component_ids'])) {
+    elseif (!empty($this->data[0]['component_ids'])) {
       $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
     }
 
@@ -297,7 +298,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Prepare the prompt.
     $context = [
-//      'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
+   // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
     ];
     if (!empty($this->data[0]['component_ids'])) {
       $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
@@ -314,7 +315,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     }
 
     $eca = $this->ecaRepository->build($data);
-    $this->messages[] = $this->t('The model \'@name\' has been created. You can find it here: %link.', [
+    $this->messages[] = $this->t("The model '@name' has been created. You can find it here: %link.", [
       '@name' => $eca->label(),
       '%link' => Url::fromRoute('entity.eca.collection')->toString(),
     ]);
diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index 8bb925e..a75f55f 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -2,12 +2,12 @@
 
 namespace Drupal\ai_eca_agents\Plugin\AiAgentValidation;
 
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ai\OperationType\Chat\ChatMessage;
 use Drupal\ai_agents\Attribute\AiAgentValidation;
 use Drupal\ai_agents\Exception\AgentRetryableValidationException;
 use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * The validation of the ECA agent.
diff --git a/modules/agents/src/Services/DataProvider/DataViewModeEnum.php b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
index 2100ab4..1a63783 100644
--- a/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
+++ b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\ai_eca_agents\Services\DataProvider;
 
+/**
+ * Enumeration determining the view mode of the data.
+ */
 enum DataViewModeEnum: string {
 
   case Teaser = 'teaser';
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index 19dbf1b..5b53b78 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -18,9 +18,8 @@ class EcaRepository implements EcaRepositoryInterface {
    *   The entity type manager.
    */
   public function __construct(
-    protected EntityTypeManagerInterface $entityTypeManager
-  ) {
-  }
+    protected EntityTypeManagerInterface $entityTypeManager,
+  ) {}
 
   /**
    * {@inheritdoc}
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index 9c0eed4..e8f98fc 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
-use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 use Drupal\KernelTests\KernelTestBase;
-use Drupal\node\Entity\NodeType;
 use Drupal\TestTools\Random;
+use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
+use Drupal\node\Entity\NodeType;
 
 /**
  * Tests various input data for generating ECA models.
@@ -39,9 +39,10 @@ class EcaRepositoryTest extends KernelTestBase {
   protected ?EcaRepositoryInterface $ecaRepository;
 
   /**
-   * Generate different sets of data.
+   * Generate different sets of data points.
    *
    * @return \Generator
+   *   Returns a collection of data points.
    */
   public static function dataProvider(): \Generator {
     $random = Random::getGenerator();
-- 
GitLab


From 0061360810c308ffc73d7c16fb0f2c7d03ab346e Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 26 Nov 2024 16:33:59 +0100
Subject: [PATCH 27/95] #3481307 Add drupal/ai_agents as dependency

---
 composer.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/composer.json b/composer.json
index 1f9b8b1..8ec1589 100644
--- a/composer.json
+++ b/composer.json
@@ -8,9 +8,10 @@
         "source": "https://drupal.org/project/ai_eca"
     },
     "require": {
-        "drupal/eca": "^2.0",
         "drupal/ai": "1.0.x-dev@dev",
+        "drupal/ai_agents": "1.0.x-dev@dev",
         "drupal/core": "^10.3 || ^11",
+        "drupal/eca": "^2.0",
         "drupal/token": "^1.15"
     }
 }
-- 
GitLab


From d77a249348baeea13bfecbfa8ea955755092f4ce Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 27 Nov 2024 08:49:16 +0100
Subject: [PATCH 28/95] #3481307 Fix order of imports

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index c8331b9..901034c 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -10,8 +10,8 @@ use Drupal\ai_agents\Attribute\AiAgent;
 use Drupal\ai_agents\PluginBase\AiAgentBase;
 use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
 use Drupal\ai_agents\Task\TaskInterface;
-use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
 use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 use Drupal\eca\Entity\Eca as EcaEntity;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -298,7 +298,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Prepare the prompt.
     $context = [
-   // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
+      // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
     ];
     if (!empty($this->data[0]['component_ids'])) {
       $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
-- 
GitLab


From 4739d010a6eefe5494fa6458f08a10230e8f294b Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 28 Nov 2024 10:52:46 +0100
Subject: [PATCH 29/95] #3481307 Specify number of retries for buildModel

---
 modules/agents/prompts/eca/buildModel.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 7a39b52..cea9c7b 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -5,6 +5,7 @@ name: Build model
 description: This sub-agent is capable of creating ECA models.
 validation:
   - [eca_validation]
+retries: 2
 prompt:
   introduction: |
     You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
-- 
GitLab


From c73bd5935a31c3fe3a07b8be74d20a9495e17ea5 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 28 Nov 2024 10:54:22 +0100
Subject: [PATCH 30/95] #3481307 Use PromptJsonDecoder for validation

---
 .../AiAgentValidation/EcaValidation.php       | 28 ++++++++++++++++---
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index a75f55f..b224bf4 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -3,11 +3,15 @@
 namespace Drupal\ai_eca_agents\Plugin\AiAgentValidation;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ai\OperationType\Chat\ChatMessage;
+use Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface;
 use Drupal\ai_agents\Attribute\AiAgentValidation;
 use Drupal\ai_agents\Exception\AgentRetryableValidationException;
 use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
+use Drupal\ai_agents\PluginInterfaces\AiAgentValidationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * The validation of the ECA agent.
@@ -16,7 +20,24 @@ use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
   id: 'eca_validation',
   label: new TranslatableMarkup('ECA Validation'),
 )]
-class EcaValidation extends AiAgentValidationPluginBase {
+class EcaValidation extends AiAgentValidationPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The JSON-decoder of the prompt.
+   *
+   * @var \Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface
+   */
+  protected PromptJsonDecoderInterface $promptJsonDecoder;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): AiAgentValidationInterface {
+    $instance = new static($configuration, $plugin_id, $plugin_definition);
+    $instance->promptJsonDecoder = $container->get('ai.prompt_json_decode');
+
+    return $instance;
+  }
 
   /**
    * {@inheritdoc}
@@ -28,7 +49,7 @@ class EcaValidation extends AiAgentValidationPluginBase {
     }
 
     // Validate that at least 1 part of the response sets the title.
-    $setsTitle = (bool) count(array_filter($data, function ($component) {
+    $setsTitle = (bool) count(array_filter($data, function($component) {
       return !empty($component['type']) && $component['type'] === 'set_title';
     }));
     if (!$setsTitle) {
@@ -52,8 +73,7 @@ class EcaValidation extends AiAgentValidationPluginBase {
 
     switch (TRUE) {
       case $source instanceof ChatMessage:
-        $text = $source->getText();
-        break;
+        return $this->promptJsonDecoder->decode($source);
 
       case is_string($source):
         $text = $source;
-- 
GitLab


From 74490983117bf2490bac80cabfe91c387d74d372 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 28 Nov 2024 10:54:40 +0100
Subject: [PATCH 31/95] #3481307 Small optimisations

---
 modules/agents/src/Plugin/AiAgent/Eca.php                   | 4 ++++
 modules/agents/src/Services/EcaRepository/EcaRepository.php | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 901034c..649815b 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -168,6 +168,10 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       case 'edit':
         $this->messages[] = 'edit';
         break;
+
+      case 'info':
+        $this->messages[] = $this->answerQuestion();
+        break;
     }
 
     return implode("\n", $this->messages);
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index 5b53b78..c17ebc6 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -38,7 +38,7 @@ class EcaRepository implements EcaRepositoryInterface {
       ->create();
 
     $random = new Random();
-    $eca->set('id', sprintf('Process_%s', $random->name(7)));
+    $eca->set('id', sprintf('process_%s', $random->name(7)));
     $eca->set('label', $random->string(16));
     $eca->set('modeller', 'fallback');
     $eca->set('version', '0.0.1');
-- 
GitLab


From 068a2a2b30d622ed90c98cb062d96f1f919b10c5 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 28 Nov 2024 22:07:02 +0100
Subject: [PATCH 32/95] #3481307 Use DTO-object for internal data and data
 sharing

---
 composer.json                             |   3 +-
 modules/agents/src/Plugin/AiAgent/Eca.php | 104 +++++++++++++---------
 2 files changed, 65 insertions(+), 42 deletions(-)

diff --git a/composer.json b/composer.json
index 8ec1589..33bbc92 100644
--- a/composer.json
+++ b/composer.json
@@ -12,6 +12,7 @@
         "drupal/ai_agents": "1.0.x-dev@dev",
         "drupal/core": "^10.3 || ^11",
         "drupal/eca": "^2.0",
-        "drupal/token": "^1.15"
+        "drupal/token": "^1.15",
+        "illuminate/support": "^11.34"
     }
 }
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 649815b..24b947e 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\Plugin\AiAgent;
 
+use Drupal\Component\Render\MarkupInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -14,6 +15,7 @@ use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
 use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 use Drupal\eca\Entity\Eca as EcaEntity;
+use Illuminate\Support\Arr;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -26,23 +28,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
   /**
-   * Questions to ask.
+   * The DTO.
    *
    * @var array
    */
-  protected array $questions;
-
-  /**
-   * A list of messages for the user.
-   */
-  protected array $messages = [];
-
-  /**
-   * The full data of the initial task.
-   *
-   * @var array
-   */
-  protected array $data;
+  protected array $dto;
 
   /**
    * The ECA entity.
@@ -72,6 +62,13 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
     $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
     $instance->ecaRepository = $container->get('ai_eca_agents.services.eca_repository');
+    $instance->dto = [
+      'task_description' => '',
+      'feedback' => '',
+      'questions' => [],
+      'data' => [],
+      'logs' => [],
+    ];
 
     return $instance;
   }
@@ -79,28 +76,28 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * {@inheritdoc}
    */
-  public function getId() {
+  public function getId(): string {
     return 'eca';
   }
 
   /**
    * {@inheritdoc}
    */
-  public function agentsNames() {
+  public function agentsNames(): array {
     return ['Event-Condition-Action agent'];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function isAvailable() {
+  public function isAvailable(): bool {
     return $this->agentHelper->isModuleEnabled('eca');
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getRetries() {
+  public function getRetries(): int {
     return 2;
   }
 
@@ -160,21 +157,26 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * {@inheritdoc}
    */
   public function solve(): array|string {
-    switch ($this->data[0]['action']) {
+    if (isset($this->dto['setup_agent']) && $this->dto['setup_agent'] === TRUE) {
+      parent::determineSolvability();
+    }
+
+    switch (Arr::get($this->dto, 'data.0.action')) {
       case 'create':
         $this->createConfig();
         break;
 
       case 'edit':
-        $this->messages[] = 'edit';
+        $this->dto['logs'][] = 'edit';
         break;
 
       case 'info':
-        $this->messages[] = $this->answerQuestion();
+        $log = $this->answerQuestion();
+        $this->dto['logs'][] = $log;
         break;
     }
 
-    return implode("\n", $this->messages);
+    return Arr::join($this->dto['logs'], "\n");
   }
 
   /**
@@ -216,33 +218,51 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * {@inheritdoc}
    */
-  public function getHelp() {
+  public function getHelp(): MarkupInterface {
     return $this->t('This agent can figure out event-condition-action models of a file. Just upload and ask.');
   }
 
   /**
    * {@inheritdoc}
    */
-  public function askQuestion() {
-    return $this->questions;
+  public function askQuestion(): array {
+    return (array) Arr::get($this->dto, 'questions');
   }
 
   /**
    * {@inheritdoc}
    */
-  public function approveSolution() {
-    $this->data[0]['action'] = 'create';
+  public function approveSolution(): void {
+    $this->dto = Arr::set($this->dto, 'data.0.action', 'create');
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setTask(TaskInterface $task) {
+  public function setTask(TaskInterface $task): void {
     parent::setTask($task);
 
-    if (!empty($this->state['models'][0])) {
-      $this->model = $this->state['models'][0];
-    }
+    $this->dto['task_description'] = $task->getDescription();
+  }
+
+  /**
+   * Get the data transfer object.
+   *
+   * @return array
+   */
+  public function getDto(): array {
+    return $this->dto;
+  }
+
+  /**
+   * Set the data transfer object.
+   *
+   * @param array $dto
+   */
+  public function setDto(array $dto): void {
+    $this->dto = $dto;
+
+    $this->model = $this->ecaRepository->get(Arr::get($this->dto, 'model_id'));
   }
 
   /**
@@ -253,7 +273,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    *
    * @throws \Exception
    */
-  protected function determineTypeOfTask() {
+  protected function determineTypeOfTask(): string {
     $context = $this->getFullContextOfTask($this->task);
     if (!empty($this->model)) {
       $context .= "\n\nA model already exists, so creation is not possible.";
@@ -268,7 +288,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Quit early if the returned response isn't what we expected.
     if (empty($data[0]['action'])) {
-      $this->questions[] = 'Sorry, we could not understand what you wanted to do, please try again.';
+      $this->dto['questions'][] = 'Sorry, we could not understand what you wanted to do, please try again.';
 
       return 'fail';
     }
@@ -279,14 +299,15 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       'info',
     ])) {
       if (!empty($data[0]['model_id'])) {
+        $this->dto['model_id'] = $data[0]['model_id'];
         $this->model = $this->ecaRepository->get($data[0]['model_id']);
       }
 
       if (!empty($data[0]['feedback'])) {
-        $this->questions[] = $data[0]['feedback'];
+        $this->dto['feedback'] = $data[0]['feedback'];
       }
 
-      $this->data = $data;
+      $this->dto['data'] = $data;
 
       return $data[0]['action'];
     }
@@ -304,11 +325,12 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $context = [
       // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
     ];
-    if (!empty($this->data[0]['component_ids'])) {
-      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+    if (Arr::has($this->dto, 'data.0.component_ids')) {
+      $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
+      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($componentIds)));
     }
-    if (!empty($this->data[0]['feedback'])) {
-      $context['Guidelines'] = $this->data[0]['feedback'];
+    if (Arr::has($this->dto, 'data.0.feedback')) {
+      $context['Guidelines'] = Arr::get($this->dto, 'data.0.feedback');
     }
 
     // Execute it.
@@ -319,11 +341,11 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     }
 
     $eca = $this->ecaRepository->build($data);
-    $this->messages[] = $this->t("The model '@name' has been created. You can find it here: %link.", [
+    $this->dto['messages'][] = $this->t("The model '@name' has been created. You can find it here: %link.", [
       '@name' => $eca->label(),
       '%link' => Url::fromRoute('entity.eca.collection')->toString(),
     ]);
-    $this->messages[] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
+    $this->dto['messages'][] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
   }
 
 }
-- 
GitLab


From 1dbf5d94354fabd301b1679c6a1d04a4d7080018 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 28 Nov 2024 22:13:57 +0100
Subject: [PATCH 33/95] #3481307 Use AI agent via form in popup

---
 modules/agents/ai_eca_agents.links.action.yml |   6 +
 modules/agents/ai_eca_agents.routing.yml      |   7 +
 modules/agents/src/Form/AskAiForm.php         | 171 ++++++++++++++++++
 .../AiEcaAgentsDialogLocalAction.php          |  35 ++++
 4 files changed, 219 insertions(+)
 create mode 100644 modules/agents/ai_eca_agents.links.action.yml
 create mode 100644 modules/agents/ai_eca_agents.routing.yml
 create mode 100644 modules/agents/src/Form/AskAiForm.php
 create mode 100644 modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php

diff --git a/modules/agents/ai_eca_agents.links.action.yml b/modules/agents/ai_eca_agents.links.action.yml
new file mode 100644
index 0000000..311fb77
--- /dev/null
+++ b/modules/agents/ai_eca_agents.links.action.yml
@@ -0,0 +1,6 @@
+ai_eca_agents.ask_ai:
+  route_name: ai_eca_agents.ask_ai
+  title: 'Ask AI'
+  class: '\Drupal\ai_eca_agents\Plugin\Menu\LocalAction\AiEcaAgentsDialogLocalAction'
+  appears_on:
+    - entity.eca.collection
diff --git a/modules/agents/ai_eca_agents.routing.yml b/modules/agents/ai_eca_agents.routing.yml
new file mode 100644
index 0000000..4e11c91
--- /dev/null
+++ b/modules/agents/ai_eca_agents.routing.yml
@@ -0,0 +1,7 @@
+ai_eca_agents.ask_ai:
+  path: '/admin/config/workflow/eca/ask-ai'
+  defaults:
+    _title: 'Ask AI'
+    _form: '\Drupal\ai_eca_agents\Form\AskAiForm'
+  requirements:
+    _permission: 'administer eca'
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
new file mode 100644
index 0000000..f53c781
--- /dev/null
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Form;
+
+use Drupal\Core\Batch\BatchBuilder;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
+use Drupal\ai_agents\Task\Task;
+use Drupal\ai_eca_agents\Plugin\AiAgent\Eca;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A form to propose a question to AI.
+ */
+class AskAiForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId(): string {
+    return 'ai_eca_agents_ask_ai';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state): array {
+    $form['question'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Propose your question'),
+      '#required' => TRUE,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Ask'),
+      '#button_type' => 'primary',
+    ];
+    $form['#theme'] = 'confirm_form';
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state): void {
+    $batch = new BatchBuilder();
+    $batch->setTitle($this->t('Solving ECA question with AI.'));
+    $batch->setInitMessage($this->t('Contacting the AI...'));
+    $batch->setErrorMessage($this->t('An error occurred during processing.'));
+    $batch->setFinishCallback([self::class, 'batchFinished']);
+
+    $batch->addOperation([self::class, 'determineTask'], [$form_state->getValue('question')]);
+    $batch->addOperation([self::class, 'executeTask']);
+
+    batch_set($batch->toArray());
+  }
+
+  /**
+   * Batch operation for determining the task to execute.
+   *
+   * @param string $question
+   *   The question of the user.
+   * @param array $context
+   *   The context.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public static function determineTask(string $question, array &$context): void {
+    $context['message'] = t('Task determined.');
+
+    $context['sandbox']['question'] = $question;
+    $agent = self::initAgent($context);
+
+    // Let the agent decide how it can answer the question.
+    $solvability = $agent->determineSolvability();
+    if ($solvability === AiAgentInterface::JOB_NOT_SOLVABLE) {
+      $context['results']['error'] = t('The AI agent could not solve the task');
+
+      return;
+    }
+
+    $context['results']['dto'] = $agent->getDto();
+  }
+
+  /**
+   * Batch operation for executing the task.
+   *
+   * @param array $context
+   *   The context.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public static function executeTask(array &$context): void {
+    $context['message'] = t('Task executed.');
+
+    $agent = self::initAgent($context);
+
+    // Solve the question.
+    $response = $agent->solve();
+    $context['results']['response'] = $response;
+  }
+
+  /**
+   * Callback for when the batch is finished.
+   *
+   * @param bool $success
+   *   A boolean indicating a successful execution.
+   * @param array $results
+   *   The collection of results.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   Returns a http-response.
+   */
+  public static function batchFinished(bool $success, array $results): Response {
+    if (!empty($results['response'])) {
+      \Drupal::messenger()->addStatus($results['response']);
+    }
+
+    return new RedirectResponse(Url::fromRoute('entity.eca.collection')->toString());
+  }
+
+  /**
+   * Initialize the AI agent.
+   *
+   * @param array $context
+   *   The context.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\AiAgent\Eca
+   *   Returns the AI agent.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  protected static function initAgent(array $context): Eca {
+    // Initiate the agent.
+    /** @var \Drupal\ai_agents\PluginInterfaces\AiAgentInterface $agent */
+    $agent = \Drupal::service('plugin.manager.ai_agents')
+      ->createInstance('eca');
+
+    $dto = [];
+    if (!empty($context['results']['dto']) && is_array($context['results']['dto'])) {
+      $dto = $context['results']['dto'];
+      $dto['setup_agent'] = TRUE;
+      $agent->setDto($dto);
+    }
+
+    // Prepare the task.
+    $question = $context['sandbox']['question'] ?? NULL;
+    $question = $dto['task_description'] ?? $question;
+    $task = new Task($question);
+    $agent->setTask($task);
+
+    // Set the provider.
+    /** @var \Drupal\ai\AiProviderPluginManager $providerManager */
+    $providerManager = \Drupal::service('ai.provider');
+    $provider = $providerManager->getDefaultProviderForOperationType('chat');
+    $agent->setAiProvider($providerManager->createInstance($provider['provider_id']));
+    $agent->setModelName($provider['model_id']);
+    $agent->setAiConfiguration([]);
+
+    return $agent;
+  }
+
+}
diff --git a/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php b/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php
new file mode 100644
index 0000000..fdcf975
--- /dev/null
+++ b/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Menu\LocalAction;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Defines a local action plugin with the needed dialog attributes.
+ */
+class AiEcaAgentsDialogLocalAction extends LocalActionDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions(RouteMatchInterface $route_match) {
+    $options = parent::getOptions($route_match);
+
+    $attributes = [
+      'class' => ['use-ajax'],
+      'data-dialog-type' => 'modal',
+      'data-dialog-options' => Json::encode([
+        'width' => 1000,
+        'dialogClass' => 'ui-dialog-off-canvas',
+      ]),
+    ];
+    $options['attributes'] = $this->pluginDefinition['attributes'] ?? [];
+    $options['attributes'] = NestedArray::mergeDeep($options['attributes'], $attributes);
+
+    return $options;
+  }
+
+}
-- 
GitLab


From bf25171294268adf4dde7434c556bc14092e7ec5 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 30 Nov 2024 08:58:50 +0100
Subject: [PATCH 34/95] #3481307 Adjust batch messages

---
 modules/agents/src/Form/AskAiForm.php     | 4 ++--
 modules/agents/src/Plugin/AiAgent/Eca.php | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index f53c781..15bbc69 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -74,7 +74,7 @@ class AskAiForm extends FormBase {
    * @throws \Drupal\Component\Plugin\Exception\PluginException
    */
   public static function determineTask(string $question, array &$context): void {
-    $context['message'] = t('Task determined.');
+    $context['message'] = t('Task determined. Executing...');
 
     $context['sandbox']['question'] = $question;
     $agent = self::initAgent($context);
@@ -99,7 +99,7 @@ class AskAiForm extends FormBase {
    * @throws \Drupal\Component\Plugin\Exception\PluginException
    */
   public static function executeTask(array &$context): void {
-    $context['message'] = t('Task executed.');
+    $context['message'] = t('Task executed. Gathering feedback...');
 
     $agent = self::initAgent($context);
 
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 24b947e..4589304 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -341,11 +341,11 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     }
 
     $eca = $this->ecaRepository->build($data);
-    $this->dto['messages'][] = $this->t("The model '@name' has been created. You can find it here: %link.", [
+    $this->dto['logs'][] = $this->t("The model '@name' has been created. You can find it here: %link.", [
       '@name' => $eca->label(),
       '%link' => Url::fromRoute('entity.eca.collection')->toString(),
     ]);
-    $this->dto['messages'][] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
+    $this->dto['logs'][] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
   }
 
 }
-- 
GitLab


From 69bff0a93a6b937ca8f232a870ac10e2d01242b9 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 30 Nov 2024 08:59:07 +0100
Subject: [PATCH 35/95] #3481307 Only load model if it's present in the DTO

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 4589304..14e9255 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -262,7 +262,9 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   public function setDto(array $dto): void {
     $this->dto = $dto;
 
-    $this->model = $this->ecaRepository->get(Arr::get($this->dto, 'model_id'));
+    if (!empty($this->dto['model_id'])) {
+      $this->model = $this->ecaRepository->get($this->dto['model_id']);
+    }
   }
 
   /**
-- 
GitLab


From 8f7bd1b2c87a127cc1d787551c7f5e58fe6aa4ce Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 2 Dec 2024 23:02:14 +0100
Subject: [PATCH 36/95] #3481307 Ensure that a condition-key is present

---
 modules/agents/prompts/eca/buildModel.yml             |  1 +
 .../src/Services/EcaRepository/EcaRepository.php      | 11 ++++++++---
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index cea9c7b..223966d 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -75,6 +75,7 @@ prompt:
       config:
         replace_tokens: false
         url: /admin
+      successors: { }
     - id: Flow_0047zve
       plugin: eca_current_user_role
       config:
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index c17ebc6..f200f15 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -47,9 +47,14 @@ class EcaRepository implements EcaRepositoryInterface {
     foreach ($data as $entry) {
       // Events, Conditions ("Flows"), Actions and Gateways.
       if (!empty($entry['id'])) {
+        $successors = $entry['successors'] ?? [];
+        foreach ($successors as &$successor) {
+          $successor['condition'] ??= '';
+        }
+
         switch (TRUE) {
           case str_starts_with($entry['id'], 'Event_'):
-            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $successors);
             break;
 
           case str_starts_with($entry['id'], 'Flow_'):
@@ -57,11 +62,11 @@ class EcaRepository implements EcaRepositoryInterface {
             break;
 
           case str_starts_with($entry['id'], 'Activity_'):
-            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $entry['successors'] ?? []);
+            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $successors);
             break;
 
           case str_starts_with($entry['id'], 'Gateway_'):
-            $eca->addGateway($entry['id'], 0, $entry['successors'] ?? []);
+            $eca->addGateway($entry['id'], 0, $successors);
             break;
         }
 
-- 
GitLab


From 63c4c1aa937b6492291ea22ee2085cddf22d4293 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 9 Dec 2024 20:16:16 +0100
Subject: [PATCH 37/95] #3481307 Use entity validation before model is saved

---
 modules/agents/ai_eca_agents.services.yml     |  1 +
 .../agents/src/EntityViolationException.php   | 58 +++++++++++++++++++
 modules/agents/src/MissingEventException.php  | 12 ++++
 .../Services/EcaRepository/EcaRepository.php  | 23 +++++++-
 .../EcaRepository/EcaRepositoryInterface.php  |  4 +-
 5 files changed, 94 insertions(+), 4 deletions(-)
 create mode 100644 modules/agents/src/EntityViolationException.php
 create mode 100644 modules/agents/src/MissingEventException.php

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index 8fe2ffe..1cd4b9d 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -12,3 +12,4 @@ services:
     class: Drupal\ai_eca_agents\Services\EcaRepository\EcaRepository
     arguments:
       - '@entity_type.manager'
+      - '@typed_data_manager'
diff --git a/modules/agents/src/EntityViolationException.php b/modules/agents/src/EntityViolationException.php
new file mode 100644
index 0000000..ed407ab
--- /dev/null
+++ b/modules/agents/src/EntityViolationException.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+use Drupal\Core\Config\ConfigException;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * An exception thrown when violations against the schema are captured.
+ */
+class EntityViolationException extends ConfigException {
+
+  /**
+   * The constraint violations associated with this exception.
+   *
+   * @var \Symfony\Component\Validator\ConstraintViolationListInterface
+   */
+  protected ConstraintViolationListInterface $violations;
+
+  /**
+   * EntityViolationException constructor.
+   *
+   * @param string $message
+   *   The Exception message to throw.
+   * @param int $code
+   *   The Exception code.
+   * @param \Throwable|null $previous
+   *   The previous throwable used for the exception chaining.
+   */
+  public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = NULL) {
+    if (empty($message)) {
+      $message = 'Validation of the entity failed.';
+    }
+
+    parent::__construct($message, $code, $previous);
+  }
+
+  /**
+   * Gets the constraint violations associated with this exception.
+   *
+   * @return \Symfony\Component\Validator\ConstraintViolationListInterface
+   *   The constraint violations.
+   */
+  public function getViolations(): ConstraintViolationListInterface {
+    return $this->violations;
+  }
+
+  /**
+   * Sets the constraint violations associated with this exception.
+   *
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
+   *   The constraint violations.
+   */
+  public function setViolations(ConstraintViolationListInterface $violations): void {
+    $this->violations = $violations;
+  }
+
+}
diff --git a/modules/agents/src/MissingEventException.php b/modules/agents/src/MissingEventException.php
new file mode 100644
index 0000000..401a071
--- /dev/null
+++ b/modules/agents/src/MissingEventException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+use Drupal\Core\Config\ConfigException;
+
+/**
+ * An exception thrown when no events are present.
+ */
+class MissingEventException extends ConfigException {
+
+}
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index f200f15..9bdf650 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\ai_eca_agents\Services\EcaRepository;
 
+use Drupal\ai_eca_agents\EntityViolationException;
+use Drupal\ai_eca_agents\MissingEventException;
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
 use Drupal\eca\Entity\Eca;
 
 /**
@@ -16,9 +19,12 @@ class EcaRepository implements EcaRepositoryInterface {
    *
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
    *   The entity type manager.
+   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
+   *   The typed data manager.
    */
   public function __construct(
     protected EntityTypeManagerInterface $entityTypeManager,
+    protected TypedDataManagerInterface $typedDataManager,
   ) {}
 
   /**
@@ -32,7 +38,7 @@ class EcaRepository implements EcaRepositoryInterface {
   /**
    * {@inheritdoc}
    */
-  public function build(array $data): Eca {
+  public function build(array $data, bool $save = TRUE): Eca {
     /** @var \Drupal\eca\Entity\Eca $eca */
     $eca = $this->entityTypeManager->getStorage('eca')
       ->create();
@@ -83,12 +89,23 @@ class EcaRepository implements EcaRepositoryInterface {
     }
 
     // Validate the entity.
+    $definition = $this->typedDataManager->createDataDefinition(sprintf('entity:%s', $eca->getEntityTypeId()));
+    $violations = $this->typedDataManager->create($definition, $eca)
+      ->validate();
+    if ($violations->count()) {
+      $exception = new EntityViolationException();
+      $exception->setViolations($violations);
+
+      throw $exception;
+    }
     if (empty($eca->getUsedEvents())) {
-      throw new \Exception('No events registered.');
+      throw new MissingEventException('No events registered.');
     }
 
     // Save the entity.
-    $eca->save();
+    if ($save) {
+      $eca->save();
+    }
 
     return $eca;
   }
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
index a82ffe9..3a9c64f 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
@@ -25,10 +25,12 @@ interface EcaRepositoryInterface {
    *
    * @param array $data
    *   The data to use.
+   * @param bool $save
+   *   Toggle to save the model.
    *
    * @return \Drupal\eca\Entity\Eca
    *   Returns the ECA-model.
    */
-  public function build(array $data): Eca;
+  public function build(array $data, bool $save = TRUE): Eca;
 
 }
-- 
GitLab


From ad33ed25e0f1131ef27acf4cd63272beebc0d52b Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 14 Dec 2024 13:47:15 +0100
Subject: [PATCH 38/95] #3481307 Decorate the DataDefinition normalizer from
 schemata

---
 modules/agents/ai_eca_agents.info.yml         |  1 +
 modules/agents/ai_eca_agents.services.yml     |  7 ++
 .../json/DataDefinitionNormalizer.php         | 66 +++++++++++++++++++
 3 files changed, 74 insertions(+)
 create mode 100644 modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php

diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
index 4f26343..3be10c6 100644
--- a/modules/agents/ai_eca_agents.info.yml
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -7,4 +7,5 @@ dependencies:
   - ai_eca:ai_eca
   - ai_agents:ai_agents
   - eca:eca_ui
+  - schemata_json_schema:schemata_json_schema
   - token:token
diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index 1cd4b9d..c841908 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -13,3 +13,10 @@ services:
     arguments:
       - '@entity_type.manager'
       - '@typed_data_manager'
+
+  # Typed data definitions in general can take many forms. This handles final items.
+  serializer.normalizer.data_definition.schema_json_ai_eca_agents.json:
+    class: Drupal\ai_eca_agents\Normalizer\json\DataDefinitionNormalizer
+    decorates: serializer.normalizer.data_definition.schema_json.json
+    arguments:
+      - '@serializer.normalizer.data_definition.schema_json_ai_eca_agents.json.inner'
diff --git a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
new file mode 100644
index 0000000..afad803
--- /dev/null
+++ b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Normalizer\json;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\schemata_json_schema\Normalizer\json\JsonNormalizerBase;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Normalizer for DataDefinitionInterface instances.
+ *
+ * DataDefinitionInterface is the ultimate parent to all data definitions. This
+ * service must always be low priority for data definitions, otherwise the
+ * simpler normalization process it supports will take precedence over all the
+ * complexities most entity properties contain before reaching this level.
+ *
+ * DataDefinitionNormalizer produces scalar value definitions.
+ *
+ * Unlike the other Normalizer services in the JSON Schema module, this one is
+ * used by the hal_schemata normalizer. It is unlikely divergent requirements
+ * will develop.
+ *
+ * All the TypedData normalizers extend from this class.
+ */
+class DataDefinitionNormalizer extends JsonNormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected string $supportedInterfaceOrClass = DataDefinitionInterface::class;
+
+  /**
+   * The inner normalizer.
+   *
+   * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface
+   */
+  protected NormalizerInterface $inner;
+
+  /**
+   * Constructs a DataDefinitionNormalizer object.
+   *
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   */
+  public function __construct(NormalizerInterface $normalizer) {
+    $this->inner = $normalizer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($entity, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
+    $normalized = $this->inner->normalize($entity, $format, $context);
+
+    if (
+      empty($entity->getSetting('allowed_values_function'))
+      && !empty($entity->getSetting('allowed_values'))
+    ) {
+      $normalized['properties'][$context['name']]['enum'] = array_keys($entity->getSetting('allowed_values'));
+    }
+
+    return $normalized;
+  }
+
+}
-- 
GitLab


From aff1040906e6ca51e9917ffef47f8fcbd4335c3a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 14 Dec 2024 13:48:05 +0100
Subject: [PATCH 39/95] #3481307 Close the json-part of the prompts

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 14e9255..f4d141b 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -191,10 +191,10 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     $context = [];
     if (!empty($this->model)) {
-      $context['The information of the model, in JSON format'] = sprintf("```json\n%s", json_encode($this->dataProvider->getModels([$this->model->id()])));
+      $context['The information of the model, in JSON format'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getModels([$this->model->id()])));
     }
     elseif (!empty($this->data[0]['component_ids'])) {
-      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+      $context['The details about the components'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
     }
 
     if (empty($context)) {
@@ -284,8 +284,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     // Prepare and run the prompt by fetching all the relevant info.
     $data = $this->agentHelper->runSubAgent('determineTask', [
       'Task description and if available comments description' => $context,
-      'The list of existing models, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getModels())),
-      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s", json_encode($this->dataProvider->getComponents())),
+      'The list of existing models, in JSON format' => sprintf("```json\n%s\n```", json_encode($this->dataProvider->getModels())),
+      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents())),
     ]);
 
     // Quit early if the returned response isn't what we expected.
@@ -329,7 +329,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     ];
     if (Arr::has($this->dto, 'data.0.component_ids')) {
       $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
-      $context['The details about the components'] = sprintf("```json\n%s", json_encode($this->dataProvider->getComponents($componentIds)));
+      $context['The details about the components'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents($componentIds)));
     }
     if (Arr::has($this->dto, 'data.0.feedback')) {
       $context['Guidelines'] = Arr::get($this->dto, 'data.0.feedback');
-- 
GitLab


From da3ca9d9ce7c322941ed9f29195aa0d5001851af Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 14 Dec 2024 15:26:07 +0100
Subject: [PATCH 40/95] #3481307 Create Typed Data API definitions for ECA
 models

---
 .../agents/src/Plugin/DataType/EcaAction.php  | 17 ++++
 .../src/Plugin/DataType/EcaCondition.php      | 17 ++++
 .../agents/src/Plugin/DataType/EcaEvent.php   | 17 ++++
 .../agents/src/Plugin/DataType/EcaGateway.php | 17 ++++
 .../agents/src/Plugin/DataType/EcaModel.php   | 17 ++++
 .../src/Plugin/DataType/EcaSuccessor.php      | 17 ++++
 .../src/TypedData/EcaActionDefinition.php     | 31 ++++++++
 .../src/TypedData/EcaConditionDefinition.php  | 38 +++++++++
 .../src/TypedData/EcaEventDefinition.php      | 31 ++++++++
 .../src/TypedData/EcaGatewayDefinition.php    | 38 +++++++++
 .../src/TypedData/EcaModelDefinition.php      | 57 +++++++++++++
 .../src/TypedData/EcaPluginDefinition.php     | 79 +++++++++++++++++++
 .../src/TypedData/EcaSuccessorDefinition.php  | 31 ++++++++
 13 files changed, 407 insertions(+)
 create mode 100644 modules/agents/src/Plugin/DataType/EcaAction.php
 create mode 100644 modules/agents/src/Plugin/DataType/EcaCondition.php
 create mode 100644 modules/agents/src/Plugin/DataType/EcaEvent.php
 create mode 100644 modules/agents/src/Plugin/DataType/EcaGateway.php
 create mode 100644 modules/agents/src/Plugin/DataType/EcaModel.php
 create mode 100644 modules/agents/src/Plugin/DataType/EcaSuccessor.php
 create mode 100644 modules/agents/src/TypedData/EcaActionDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaConditionDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaEventDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaGatewayDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaModelDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaPluginDefinition.php
 create mode 100644 modules/agents/src/TypedData/EcaSuccessorDefinition.php

diff --git a/modules/agents/src/Plugin/DataType/EcaAction.php b/modules/agents/src/Plugin/DataType/EcaAction.php
new file mode 100644
index 0000000..8b2c68b
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaAction.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaActionDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_action',
+  label: new TranslatableMarkup('ECA Action'),
+  definition_class: EcaActionDefinition::class,
+)]
+class EcaAction extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaCondition.php b/modules/agents/src/Plugin/DataType/EcaCondition.php
new file mode 100644
index 0000000..3b6baa0
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaCondition.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaConditionDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_condition',
+  label: new TranslatableMarkup('ECA Condition'),
+  definition_class: EcaConditionDefinition::class,
+)]
+class EcaCondition extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaEvent.php b/modules/agents/src/Plugin/DataType/EcaEvent.php
new file mode 100644
index 0000000..160a490
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaEvent.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaEventDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_event',
+  label: new TranslatableMarkup('ECA Event'),
+  definition_class: EcaEventDefinition::class,
+)]
+class EcaEvent extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaGateway.php b/modules/agents/src/Plugin/DataType/EcaGateway.php
new file mode 100644
index 0000000..026b97f
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaGateway.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaGatewayDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_gateway',
+  label: new TranslatableMarkup('ECA Gateway'),
+  definition_class: EcaGatewayDefinition::class,
+)]
+class EcaGateway extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaModel.php b/modules/agents/src/Plugin/DataType/EcaModel.php
new file mode 100644
index 0000000..f158c3a
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaModel.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_model',
+  label: new TranslatableMarkup('ECA Model'),
+  definition_class: EcaModelDefinition::class,
+)]
+class EcaModel extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaSuccessor.php b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
new file mode 100644
index 0000000..539c674
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaSuccessorDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+#[DataType(
+  id: 'eca_successor',
+  label: new TranslatableMarkup('ECA Successor'),
+  definition_class: EcaSuccessorDefinition::class
+)]
+class EcaSuccessor extends Map {
+
+}
diff --git a/modules/agents/src/TypedData/EcaActionDefinition.php b/modules/agents/src/TypedData/EcaActionDefinition.php
new file mode 100644
index 0000000..e9fec03
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaActionDefinition.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\Action\ActionInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+
+class EcaActionDefinition extends EcaPluginDefinition {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_action'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginManagerId(): string {
+    return 'plugin.manager.action';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginInterface(): string {
+    return ActionInterface::class;
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaConditionDefinition.php b/modules/agents/src/TypedData/EcaConditionDefinition.php
new file mode 100644
index 0000000..35f3c05
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaConditionDefinition.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
+
+class EcaConditionDefinition extends EcaPluginDefinition {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_condition'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginManagerId(): string {
+    return 'plugin.manager.eca.condition';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginInterface(): string {
+    return ConditionInterface::class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEnabledProperties(): array {
+    return [];
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaEventDefinition.php b/modules/agents/src/TypedData/EcaEventDefinition.php
new file mode 100644
index 0000000..efeb545
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaEventDefinition.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\eca\Plugin\ECA\Event\EventInterface;
+
+class EcaEventDefinition extends EcaPluginDefinition {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_event'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginManagerId(): string {
+    return 'plugin.manager.eca.event';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginInterface(): string {
+    return EventInterface::class;
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
new file mode 100644
index 0000000..4efd8cc
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\ListDataDefinition;
+
+class EcaGatewayDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_gateway'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+    $properties['type'] = DataDefinition::create('integer')
+      ->setLabel(new TranslatableMarkup('Type'))
+      ->setSetting('allowed_values_function', '')
+      ->setSetting('allowed_values', ['0'])
+      ->addConstraint('Choice', [
+        'choices' => [0]
+      ]);
+    $properties['successors'] = ListDataDefinition::create('eca_successor')
+      ->setLabel('Successors');
+
+    return $properties;
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
new file mode 100644
index 0000000..7da875e
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\ListDataDefinition;
+
+class EcaModelDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_model'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+    $properties['id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.'
+      ]);
+    $properties['label'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Label'))
+      ->setRequired(TRUE)
+      ->addConstraint('Length', ['max' => 255])
+      ->addConstraint('NotBlank');
+
+    $properties['events'] = ListDataDefinition::create('eca_event')
+      ->setLabel(new TranslatableMarkup('Events'))
+      ->setRequired(TRUE)
+      ->addConstraint('NotNull');
+
+    $properties['conditions'] = ListDataDefinition::create('eca_condition')
+      ->setLabel(new TranslatableMarkup('Conditions'));
+
+    $properties['gateways'] = ListDataDefinition::create('eca_gateway')
+      ->setLabel(new TranslatableMarkup('Gateways'));
+
+    $properties['actions'] = ListDataDefinition::create('eca_action')
+      ->setLabel(new TranslatableMarkup('Actions'))
+      ->setRequired(TRUE)
+      ->addConstraint('NotNull');
+
+    return $properties;
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
new file mode 100644
index 0000000..3055957
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\ListDataDefinition;
+
+abstract class EcaPluginDefinition extends ComplexDataDefinitionBase {
+
+  protected const PROP_LABEL = 'label';
+
+  protected const PROP_SUCCESSORS = 'successors';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+    $properties['plugin'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Plugin ID'))
+      ->setRequired(TRUE)
+      ->addConstraint('PluginExists', [
+        'manager' => $this->getPluginManagerId(),
+        'interface' => $this->getPluginInterface(),
+      ]);
+
+    if (in_array(self::PROP_LABEL, $this->getEnabledProperties(), TRUE)) {
+      $properties['label'] = DataDefinition::create('string')
+        ->setLabel(new TranslatableMarkup('Label'))
+        ->setRequired(TRUE)
+        ->addConstraint('Regex', [
+          'pattern' => '/([^\PC\x09\x0a\x0d])/u',
+          'match' => FALSE,
+          'message' => 'Text is not allowed to contain control characters, only visible characters.',
+        ]);
+    }
+
+    $properties['configuration'] = ListDataDefinition::create('any');
+
+    if (in_array(self::PROP_SUCCESSORS, $this->getEnabledProperties(), TRUE)) {
+      $properties['successors'] = ListDataDefinition::create('eca_successor')
+        ->setLabel(new TranslatableMarkup('Successors'));
+    }
+
+    return $properties;
+  }
+
+  /**
+   * Get the plugin manager ID.
+   *
+   * @return string
+   *   Returns the plugin manager ID.
+   */
+  abstract protected function getPluginManagerId(): string;
+
+  /**
+   * Get the plugin interface.
+   *
+   * @return string
+   *   Returns the plugin interface.
+   */
+  abstract protected function getPluginInterface(): string;
+
+  /**
+   * Get a list of enabled properties.
+   *
+   * @return string[]
+   *   The list of enabled properties.
+   */
+  protected function getEnabledProperties(): array {
+    return [
+      self::PROP_LABEL,
+      self::PROP_SUCCESSORS,
+    ];
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
new file mode 100644
index 0000000..c0f41a0
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+
+class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+    $properties['id'] = DataDefinition::create('string')
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+    $properties['condition'] = DataDefinition::create('string')
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    return $properties;
+  }
+
+}
-- 
GitLab


From 94ae62ef031545098fd88b9818043f8f7ba4138c Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 14 Dec 2024 15:27:28 +0100
Subject: [PATCH 41/95] #3481307 Add support for serializing the ECA model as
 JSON Schema

---
 modules/agents/src/Schema/Eca.php | 85 +++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)
 create mode 100644 modules/agents/src/Schema/Eca.php

diff --git a/modules/agents/src/Schema/Eca.php b/modules/agents/src/Schema/Eca.php
new file mode 100644
index 0000000..a5fb7c7
--- /dev/null
+++ b/modules/agents/src/Schema/Eca.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Schema;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\schemata\Schema\SchemaInterface;
+
+class Eca implements SchemaInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The data definition.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface
+   */
+  protected DataDefinitionInterface $definition;
+
+  /**
+   * Metadata values that describe the schema.
+   *
+   * @var array
+   */
+  protected array $metadata = [
+    'title' => 'ECA Model Schema',
+    'description' => 'The schema describing the properties of an ECA model.',
+  ];
+
+  /**
+   * Typed Data objects for all properties.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface[]
+   */
+  protected array $properties = [];
+
+  /**
+   * Constructs an ECA schema.
+   *
+   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
+   *   The Typed Data definition.
+   * @param array $properties
+   *   The properties.
+   */
+  public function __construct(DataDefinitionInterface $definition, array $properties = []) {
+    $this->definition = $definition;
+    $this->addProperties($properties);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addProperties(array $properties): void {
+    $this->properties += $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId(): string {
+    return 'eca_model';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleId(): string {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProperties(): array {
+    return $this->properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata(): array {
+    return $this->metadata;
+  }
+
+}
-- 
GitLab


From d58edef65be419197fe6adf6b9e91bc1413dfe27 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 14 Dec 2024 15:32:35 +0100
Subject: [PATCH 42/95] #3481307 Add drupal/schemata as dev-dependency

---
 composer.json | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/composer.json b/composer.json
index 33bbc92..e82a8f5 100644
--- a/composer.json
+++ b/composer.json
@@ -14,5 +14,8 @@
         "drupal/eca": "^2.0",
         "drupal/token": "^1.15",
         "illuminate/support": "^11.34"
+    },
+    "require-dev": {
+        "drupal/schemata": "^1.0"
     }
 }
-- 
GitLab


From 63cb431a03bc14f2c7c059b122f39aee94dc31a2 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 16 Dec 2024 15:13:55 +0100
Subject: [PATCH 43/95] #3481307 Refactor event-, condition- and action-defs to
 single class

---
 .../src/Plugin/DataType/EcaCondition.php      | 17 ---------
 .../agents/src/Plugin/DataType/EcaEvent.php   | 17 ---------
 .../DataType/{EcaAction.php => EcaPlugin.php} | 10 ++---
 .../src/TypedData/EcaActionDefinition.php     | 31 ---------------
 .../src/TypedData/EcaConditionDefinition.php  | 38 -------------------
 .../src/TypedData/EcaEventDefinition.php      | 31 ---------------
 .../src/TypedData/EcaModelDefinition.php      | 11 ++++--
 .../src/TypedData/EcaPluginDefinition.php     | 38 +++++++++++++++++--
 8 files changed, 46 insertions(+), 147 deletions(-)
 delete mode 100644 modules/agents/src/Plugin/DataType/EcaCondition.php
 delete mode 100644 modules/agents/src/Plugin/DataType/EcaEvent.php
 rename modules/agents/src/Plugin/DataType/{EcaAction.php => EcaPlugin.php} (52%)
 delete mode 100644 modules/agents/src/TypedData/EcaActionDefinition.php
 delete mode 100644 modules/agents/src/TypedData/EcaConditionDefinition.php
 delete mode 100644 modules/agents/src/TypedData/EcaEventDefinition.php

diff --git a/modules/agents/src/Plugin/DataType/EcaCondition.php b/modules/agents/src/Plugin/DataType/EcaCondition.php
deleted file mode 100644
index 3b6baa0..0000000
--- a/modules/agents/src/Plugin/DataType/EcaCondition.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\ai_eca_agents\Plugin\DataType;
-
-use Drupal\ai_eca_agents\TypedData\EcaConditionDefinition;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\TypedData\Attribute\DataType;
-use Drupal\Core\TypedData\Plugin\DataType\Map;
-
-#[DataType(
-  id: 'eca_condition',
-  label: new TranslatableMarkup('ECA Condition'),
-  definition_class: EcaConditionDefinition::class,
-)]
-class EcaCondition extends Map {
-
-}
diff --git a/modules/agents/src/Plugin/DataType/EcaEvent.php b/modules/agents/src/Plugin/DataType/EcaEvent.php
deleted file mode 100644
index 160a490..0000000
--- a/modules/agents/src/Plugin/DataType/EcaEvent.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\ai_eca_agents\Plugin\DataType;
-
-use Drupal\ai_eca_agents\TypedData\EcaEventDefinition;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\TypedData\Attribute\DataType;
-use Drupal\Core\TypedData\Plugin\DataType\Map;
-
-#[DataType(
-  id: 'eca_event',
-  label: new TranslatableMarkup('ECA Event'),
-  definition_class: EcaEventDefinition::class,
-)]
-class EcaEvent extends Map {
-
-}
diff --git a/modules/agents/src/Plugin/DataType/EcaAction.php b/modules/agents/src/Plugin/DataType/EcaPlugin.php
similarity index 52%
rename from modules/agents/src/Plugin/DataType/EcaAction.php
rename to modules/agents/src/Plugin/DataType/EcaPlugin.php
index 8b2c68b..3e44482 100644
--- a/modules/agents/src/Plugin/DataType/EcaAction.php
+++ b/modules/agents/src/Plugin/DataType/EcaPlugin.php
@@ -2,16 +2,16 @@
 
 namespace Drupal\ai_eca_agents\Plugin\DataType;
 
-use Drupal\ai_eca_agents\TypedData\EcaActionDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaPluginDefinition;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\Attribute\DataType;
 use Drupal\Core\TypedData\Plugin\DataType\Map;
 
 #[DataType(
-  id: 'eca_action',
-  label: new TranslatableMarkup('ECA Action'),
-  definition_class: EcaActionDefinition::class,
+  id: 'eca_plugin',
+  label: new TranslatableMarkup('ECA Plugin'),
+  definition_class: EcaPluginDefinition::class,
 )]
-class EcaAction extends Map {
+class EcaPlugin extends Map {
 
 }
diff --git a/modules/agents/src/TypedData/EcaActionDefinition.php b/modules/agents/src/TypedData/EcaActionDefinition.php
deleted file mode 100644
index e9fec03..0000000
--- a/modules/agents/src/TypedData/EcaActionDefinition.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Drupal\ai_eca_agents\TypedData;
-
-use Drupal\Core\Action\ActionInterface;
-use Drupal\Core\TypedData\DataDefinitionInterface;
-
-class EcaActionDefinition extends EcaPluginDefinition {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_action'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginManagerId(): string {
-    return 'plugin.manager.action';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginInterface(): string {
-    return ActionInterface::class;
-  }
-
-}
diff --git a/modules/agents/src/TypedData/EcaConditionDefinition.php b/modules/agents/src/TypedData/EcaConditionDefinition.php
deleted file mode 100644
index 35f3c05..0000000
--- a/modules/agents/src/TypedData/EcaConditionDefinition.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-namespace Drupal\ai_eca_agents\TypedData;
-
-use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
-
-class EcaConditionDefinition extends EcaPluginDefinition {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_condition'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginManagerId(): string {
-    return 'plugin.manager.eca.condition';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginInterface(): string {
-    return ConditionInterface::class;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getEnabledProperties(): array {
-    return [];
-  }
-
-}
diff --git a/modules/agents/src/TypedData/EcaEventDefinition.php b/modules/agents/src/TypedData/EcaEventDefinition.php
deleted file mode 100644
index efeb545..0000000
--- a/modules/agents/src/TypedData/EcaEventDefinition.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Drupal\ai_eca_agents\TypedData;
-
-use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\eca\Plugin\ECA\Event\EventInterface;
-
-class EcaEventDefinition extends EcaPluginDefinition {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_event'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginManagerId(): string {
-    return 'plugin.manager.eca.event';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getPluginInterface(): string {
-    return EventInterface::class;
-  }
-
-}
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index 7da875e..d7f4e0a 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -35,20 +35,23 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->addConstraint('Length', ['max' => 255])
       ->addConstraint('NotBlank');
 
-    $properties['events'] = ListDataDefinition::create('eca_event')
+    $properties['events'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Events'))
       ->setRequired(TRUE)
+      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_EVENT))
       ->addConstraint('NotNull');
 
-    $properties['conditions'] = ListDataDefinition::create('eca_condition')
-      ->setLabel(new TranslatableMarkup('Conditions'));
+    $properties['conditions'] = ListDataDefinition::create('eca_plugin')
+      ->setLabel(new TranslatableMarkup('Conditions'))
+      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_CONDITION));
 
     $properties['gateways'] = ListDataDefinition::create('eca_gateway')
       ->setLabel(new TranslatableMarkup('Gateways'));
 
-    $properties['actions'] = ListDataDefinition::create('eca_action')
+    $properties['actions'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Actions'))
       ->setRequired(TRUE)
+      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_ACTION))
       ->addConstraint('NotNull');
 
     return $properties;
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index 3055957..98fa2fc 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -2,12 +2,19 @@
 
 namespace Drupal\ai_eca_agents\TypedData;
 
+use Drupal\Core\Action\ActionInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
 use Drupal\Core\TypedData\ListDataDefinition;
+use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
+use Drupal\eca\Plugin\ECA\Event\EventInterface;
 
-abstract class EcaPluginDefinition extends ComplexDataDefinitionBase {
+class EcaPluginDefinition extends ComplexDataDefinitionBase {
+
+  public const DATA_TYPE_EVENT = 'eca_event';
+  public const DATA_TYPE_CONDITION = 'eca_condition';
+  public const DATA_TYPE_ACTION = 'eca_action';
 
   protected const PROP_LABEL = 'label';
 
@@ -53,7 +60,16 @@ abstract class EcaPluginDefinition extends ComplexDataDefinitionBase {
    * @return string
    *   Returns the plugin manager ID.
    */
-  abstract protected function getPluginManagerId(): string;
+  protected function getPluginManagerId(): string {
+    return match($this->getDataType()) {
+      self::DATA_TYPE_ACTION => 'plugin.manager.action',
+      self::DATA_TYPE_EVENT => 'plugin.manager.eca.event',
+      self::DATA_TYPE_CONDITION => 'plugin.manager.eca.condition',
+      default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin manager ID.', [
+        '@type' => $this->getDataType(),
+      ])),
+    };
+  }
 
   /**
    * Get the plugin interface.
@@ -61,7 +77,16 @@ abstract class EcaPluginDefinition extends ComplexDataDefinitionBase {
    * @return string
    *   Returns the plugin interface.
    */
-  abstract protected function getPluginInterface(): string;
+  protected function getPluginInterface(): string {
+    return match($this->getDataType()) {
+      self::DATA_TYPE_ACTION => ActionInterface::class,
+      self::DATA_TYPE_EVENT => EventInterface::class,
+      self::DATA_TYPE_CONDITION => ConditionInterface::class,
+      default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin interface.', [
+        '@type' => $this->getDataType(),
+      ])),
+    };
+  }
 
   /**
    * Get a list of enabled properties.
@@ -70,10 +95,15 @@ abstract class EcaPluginDefinition extends ComplexDataDefinitionBase {
    *   The list of enabled properties.
    */
   protected function getEnabledProperties(): array {
-    return [
+    $default = [
       self::PROP_LABEL,
       self::PROP_SUCCESSORS,
     ];
+
+    return match ($this->getDataType()) {
+      self::DATA_TYPE_CONDITION => [],
+      default => $default,
+    };
   }
 
 }
-- 
GitLab


From 608fda44f8824a9c4f1b864ee50787b9350d99bc Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 18 Dec 2024 13:51:39 +0100
Subject: [PATCH 44/95] #348130 Add ID-property to element definitions

---
 modules/agents/src/TypedData/EcaGatewayDefinition.php | 10 ++++++++++
 modules/agents/src/TypedData/EcaModelDefinition.php   |  2 ++
 modules/agents/src/TypedData/EcaPluginDefinition.php  | 11 ++++++++++-
 .../agents/src/TypedData/EcaSuccessorDefinition.php   |  6 +++++-
 4 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
index 4efd8cc..b5d7a3a 100644
--- a/modules/agents/src/TypedData/EcaGatewayDefinition.php
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -22,6 +22,15 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
    */
   public function getPropertyDefinitions(): array {
     $properties = [];
+
+    $properties['id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID of the element'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.'
+      ]);
+
     $properties['type'] = DataDefinition::create('integer')
       ->setLabel(new TranslatableMarkup('Type'))
       ->setSetting('allowed_values_function', '')
@@ -29,6 +38,7 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
       ->addConstraint('Choice', [
         'choices' => [0]
       ]);
+
     $properties['successors'] = ListDataDefinition::create('eca_successor')
       ->setLabel('Successors');
 
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index d7f4e0a..c276a8a 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -22,6 +22,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
    */
   public function getPropertyDefinitions(): array {
     $properties = [];
+
     $properties['id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('ID'))
       ->setRequired(TRUE)
@@ -29,6 +30,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
         'pattern' => '/^[\w]+$/',
         'message' => 'The %value ID is not valid.'
       ]);
+
     $properties['label'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('Label'))
       ->setRequired(TRUE)
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index 98fa2fc..d19c77c 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -25,6 +25,15 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    */
   public function getPropertyDefinitions(): array {
     $properties = [];
+
+    $properties['id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID of the element'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.'
+      ]);
+
     $properties['plugin'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('Plugin ID'))
       ->setRequired(TRUE)
@@ -44,7 +53,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
         ]);
     }
 
-    $properties['configuration'] = ListDataDefinition::create('any');
+    $properties['configuration'] = DataDefinition::create('any');
 
     if (in_array(self::PROP_SUCCESSORS, $this->getEnabledProperties(), TRUE)) {
       $properties['successors'] = ListDataDefinition::create('eca_successor')
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
index c0f41a0..369e5af 100644
--- a/modules/agents/src/TypedData/EcaSuccessorDefinition.php
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\TypedData;
 
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
 
@@ -12,14 +13,17 @@ class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
    */
   public function getPropertyDefinitions(): array {
     $properties = [];
+
     $properties['id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('The ID of an existing action or gateway.'))
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
         'pattern' => '/^[\w]+$/',
         'message' => 'The %value ID is not valid.',
       ]);
+
     $properties['condition'] = DataDefinition::create('string')
-      ->setRequired(TRUE)
+      ->setLabel(new TranslatableMarkup('The ID of an existing condition.'))
       ->addConstraint('Regex', [
         'pattern' => '/^[\w]+$/',
         'message' => 'The %value ID is not valid.',
-- 
GitLab


From 24ac48c86d3e7eddd92fdeb725fd1d4cd36318b6 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 18 Dec 2024 14:55:26 +0100
Subject: [PATCH 45/95] #348130 Create model mapper service

---
 modules/agents/ai_eca_agents.services.yml     |  5 ++
 .../src/Services/ModelMapper/ModelMapper.php  | 77 +++++++++++++++++++
 .../ModelMapper/ModelMapperInterface.php      | 32 ++++++++
 3 files changed, 114 insertions(+)
 create mode 100644 modules/agents/src/Services/ModelMapper/ModelMapper.php
 create mode 100644 modules/agents/src/Services/ModelMapper/ModelMapperInterface.php

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index c841908..7ed780d 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -14,6 +14,11 @@ services:
       - '@entity_type.manager'
       - '@typed_data_manager'
 
+  ai_eca_agents.services.model_mapper:
+    class: Drupal\ai_eca_agents\Services\ModelMapper\ModelMapper
+    arguments:
+      - '@typed_data_manager'
+
   # Typed data definitions in general can take many forms. This handles final items.
   serializer.normalizer.data_definition.schema_json_ai_eca_agents.json:
     class: Drupal\ai_eca_agents\Normalizer\json\DataDefinitionNormalizer
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
new file mode 100644
index 0000000..5a658aa
--- /dev/null
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\ModelMapper;
+
+use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\eca\Entity\Eca;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * The model mapper.
+ */
+class ModelMapper implements ModelMapperInterface {
+
+  /**
+   * The model mapper.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
+   *   The typed data manager.
+   */
+  public function __construct(
+    protected TypedDataManagerInterface $typedDataManager
+  ) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fromPayload(array $payload): EcaModel {
+    $definition = EcaModelDefinition::create();
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaModel $model */
+    $model = $this->typedDataManager->create($definition, $payload, 'the model');
+
+    /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */
+    $violations = $model->validate();
+    if (count($violations) > 0) {
+      throw new MissingDataException($this->formatValidationMessages($model, $violations));
+    }
+
+    return $model;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fromEntity(Eca $entity): EcaModel {
+    // TODO: Implement fromEntity() method.
+  }
+
+  /**
+   * Format the violation messages in a human-readable way.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
+   *   The typed data object.
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
+   *   The violation list.
+   *
+   * @return string
+   *   Returns the formatted violation messages.
+   */
+  protected function formatValidationMessages(ComplexDataInterface $data, ConstraintViolationListInterface $violations): string {
+    $lines = [
+      sprintf('There were validation errors in %s:', $data->getName()),
+    ];
+    /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
+    foreach ($violations as $violation) {
+      $lines[] = sprintf('- %s: %s', $violation->getPropertyPath(), $violation->getMessage());
+    }
+
+    return implode("\n", $lines);
+  }
+
+}
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
new file mode 100644
index 0000000..72ddbd8
--- /dev/null
+++ b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\ModelMapper;
+
+use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
+use Drupal\eca\Entity\Eca;
+
+interface ModelMapperInterface {
+
+  /**
+   * Map a payload to the ECA Model typed data.
+   *
+   * @param array $payload
+   *   The external payload.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\DataType\EcaModel
+   *   Returns the ECA Model typed data.
+   */
+  public function fromPayload(array $payload): EcaModel;
+
+  /**
+   * Map an ECA-entity to the ECA Model typed data.
+   *
+   * @param \Drupal\eca\Entity\Eca $entity
+   *   The ECA entity.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\DataType\EcaModel
+   *   Returns the ECA Model typed data.
+   */
+  public function fromEntity(Eca $entity): EcaModel;
+
+}
-- 
GitLab


From 96ff3567af0f23bfa38a46f97d8ca6629246a916 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 18 Dec 2024 14:56:18 +0100
Subject: [PATCH 46/95] #348130 Move data-type to setting

---
 .../src/TypedData/EcaModelDefinition.php      | 15 ++++++++++++---
 .../src/TypedData/EcaPluginDefinition.php     | 19 +++++++++++++------
 2 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index c276a8a..5c9b411 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -40,12 +40,18 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
     $properties['events'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Events'))
       ->setRequired(TRUE)
-      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_EVENT))
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_EVENT)
+      )
       ->addConstraint('NotNull');
 
     $properties['conditions'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Conditions'))
-      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_CONDITION));
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_CONDITION)
+      );
 
     $properties['gateways'] = ListDataDefinition::create('eca_gateway')
       ->setLabel(new TranslatableMarkup('Gateways'));
@@ -53,7 +59,10 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
     $properties['actions'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Actions'))
       ->setRequired(TRUE)
-      ->setItemDefinition(EcaPluginDefinition::create(EcaPluginDefinition::DATA_TYPE_ACTION))
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_ACTION)
+      )
       ->addConstraint('NotNull');
 
     return $properties;
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index d19c77c..ad7dfd0 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -6,6 +6,7 @@ use Drupal\Core\Action\ActionInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\Core\TypedData\ListDataDefinition;
 use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
 use Drupal\eca\Plugin\ECA\Event\EventInterface;
@@ -17,9 +18,15 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
   public const DATA_TYPE_ACTION = 'eca_action';
 
   protected const PROP_LABEL = 'label';
-
   protected const PROP_SUCCESSORS = 'successors';
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_plugin'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -70,12 +77,12 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    *   Returns the plugin manager ID.
    */
   protected function getPluginManagerId(): string {
-    return match($this->getDataType()) {
+    return match($this->getSetting('data_type')) {
       self::DATA_TYPE_ACTION => 'plugin.manager.action',
       self::DATA_TYPE_EVENT => 'plugin.manager.eca.event',
       self::DATA_TYPE_CONDITION => 'plugin.manager.eca.condition',
       default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin manager ID.', [
-        '@type' => $this->getDataType(),
+        '@type' => $this->getSetting('data_type'),
       ])),
     };
   }
@@ -87,12 +94,12 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    *   Returns the plugin interface.
    */
   protected function getPluginInterface(): string {
-    return match($this->getDataType()) {
+    return match($this->getSetting('data_type')) {
       self::DATA_TYPE_ACTION => ActionInterface::class,
       self::DATA_TYPE_EVENT => EventInterface::class,
       self::DATA_TYPE_CONDITION => ConditionInterface::class,
       default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin interface.', [
-        '@type' => $this->getDataType(),
+        '@type' => $this->getSetting('data_type'),
       ])),
     };
   }
@@ -109,7 +116,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
       self::PROP_SUCCESSORS,
     ];
 
-    return match ($this->getDataType()) {
+    return match ($this->getSetting('data_type')) {
       self::DATA_TYPE_CONDITION => [],
       default => $default,
     };
-- 
GitLab


From b0f2e25cdd22f201e87aa8a64f1a458eab1ee076 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 18 Dec 2024 14:57:34 +0100
Subject: [PATCH 47/95] #348130 Add basic kernel test for ModelMapper

---
 .../agents/tests/assets/from_payload_0.json   |  91 +++++++++++++
 .../agents/tests/assets/from_payload_1.json   |  89 +++++++++++++
 .../tests/src/Kernel/EcaRepositoryTest.php    |  40 +-----
 .../tests/src/Kernel/ModelMapperTest.php      | 121 ++++++++++++++++++
 tests/src/Kernel/AiEcaKernelTestBase.php      |  53 ++++++++
 5 files changed, 356 insertions(+), 38 deletions(-)
 create mode 100644 modules/agents/tests/assets/from_payload_0.json
 create mode 100644 modules/agents/tests/assets/from_payload_1.json
 create mode 100644 modules/agents/tests/src/Kernel/ModelMapperTest.php
 create mode 100644 tests/src/Kernel/AiEcaKernelTestBase.php

diff --git a/modules/agents/tests/assets/from_payload_0.json b/modules/agents/tests/assets/from_payload_0.json
new file mode 100644
index 0000000..6fdbb61
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_0.json
@@ -0,0 +1,91 @@
+{
+  "id": "process_1",
+  "label": "Create Article on Page Publish",
+  "events": [
+    {
+      "id": "event_1",
+      "plugin": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "id": "action_2"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "id": "action_2",
+      "plugin": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "id": "action_3"
+        }
+      ]
+    },
+    {
+      "id": "action_3",
+      "plugin": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "id": "action_4"
+        }
+      ]
+    },
+    {
+      "id": "action_4",
+      "plugin": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "id": "action_5"
+        }
+      ]
+    },
+    {
+      "id": "action_5",
+      "plugin": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "id": "action_6"
+        }
+      ]
+    },
+    {
+      "id": "action_6",
+      "plugin": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_1.json b/modules/agents/tests/assets/from_payload_1.json
new file mode 100644
index 0000000..dbd1891
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_1.json
@@ -0,0 +1,89 @@
+{
+  "events": [
+    {
+      "id": "event_1",
+      "plugin": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "id": "action_2"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "id": "action_2",
+      "plugin": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "id": "action_3"
+        }
+      ]
+    },
+    {
+      "id": "action_3",
+      "plugin": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "id": "action_4"
+        }
+      ]
+    },
+    {
+      "id": "action_4",
+      "plugin": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "id": "action_5"
+        }
+      ]
+    },
+    {
+      "id": "action_5",
+      "plugin": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "id": "action_6"
+        }
+      ]
+    },
+    {
+      "id": "action_6",
+      "plugin": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index e8f98fc..ffb99e4 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -2,34 +2,16 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
-use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
 use Drupal\TestTools\Random;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
-use Drupal\node\Entity\NodeType;
 
 /**
  * Tests various input data for generating ECA models.
  *
  * @group ai_eca_agents
  */
-class EcaRepositoryTest extends KernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'ai_eca',
-    'ai_eca_agents',
-    'eca',
-    'eca_base',
-    'eca_content',
-    'field',
-    'node',
-    'text',
-    'token',
-    'user',
-    'system',
-  ];
+class EcaRepositoryTest extends AiEcaKernelTestBase {
 
   /**
    * The ECA repository.
@@ -162,24 +144,6 @@ class EcaRepositoryTest extends KernelTestBase {
   protected function setUp(): void {
     parent::setUp();
 
-    $this->installEntitySchema('eca');
-    $this->installEntitySchema('node');
-    $this->installEntitySchema('user');
-
-    $this->installConfig(static::$modules);
-
-    // Create the Page content type with a standard body field.
-    /** @var \Drupal\node\NodeTypeInterface $node_type */
-    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
-    $node_type->save();
-    node_add_body_field($node_type);
-
-    // Create the Article content type with a standard body field.
-    /** @var \Drupal\node\NodeTypeInterface $node_type */
-    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
-    $node_type->save();
-    node_add_body_field($node_type);
-
     $this->ecaRepository = \Drupal::service('ai_eca_agents.services.eca_repository');
   }
 
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
new file mode 100644
index 0000000..a6403be
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\Component\Serialization\Json;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
+
+/**
+ * Tests various input data for generation ECA Model typed data.
+ *
+ * @group ai_eca_agents
+ */
+class ModelMapperTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'eca_user',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'system',
+  ];
+
+  /**
+   * Generate different sets of payloads.
+   *
+   * @return \Generator
+   *   Returns a collection of payloads.
+   */
+  public static function payloadProvider(): \Generator {
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 0,
+        'actions' => 5,
+        'label' => 'Create Article on Page Publish',
+      ]
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
+      [],
+      'id: This value should not be null'
+    ];
+  }
+
+  /**
+   * The model mapper.
+   *
+   * @var \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface|null
+   */
+  protected ?ModelMapperInterface $modelMapper;
+
+  /**
+   * Build an ECA Model type data object with the provided payloads.
+   *
+   * @dataProvider payloadProvider
+   */
+  public function testMappingFromPayload(array $payload, array $assertions, ?string $errorMessage = NULL): void {
+    if (!empty($errorMessage)) {
+      $this->expectExceptionMessage($errorMessage);
+    }
+
+    $model = $this->modelMapper->fromPayload($payload);
+
+    foreach ($assertions as $property => $expected) {
+      switch (TRUE) {
+        case is_int($expected):
+          $this->assertCount($expected, $model->get($property)->getValue());
+          break;
+
+        case is_string($expected):
+          $this->assertEquals($expected, $model->get($property)->getString());
+          break;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    parent::setUp();
+
+    $this->installEntitySchema('eca');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    // Create the Page content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    // Create the Article content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    $this->modelMapper = \Drupal::service('ai_eca_agents.services.model_mapper');
+  }
+
+}
diff --git a/tests/src/Kernel/AiEcaKernelTestBase.php b/tests/src/Kernel/AiEcaKernelTestBase.php
new file mode 100644
index 0000000..a398b6e
--- /dev/null
+++ b/tests/src/Kernel/AiEcaKernelTestBase.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\ai_eca\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+
+abstract class AiEcaKernelTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'eca_user',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('eca');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    // Create the Page content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    // Create the Article content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
+    $node_type->save();
+    node_add_body_field($node_type);
+  }
+
+}
-- 
GitLab


From 3c5d62b0466416d9afa4ec4bf03b50e6c01808ac Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:08:02 +0100
Subject: [PATCH 48/95] #348130 Use label-property for name of datatype plugin

---
 modules/agents/src/Plugin/DataType/EcaModel.php     |  7 +++++++
 modules/agents/src/Plugin/DataType/EcaPlugin.php    | 13 +++++++++++++
 .../agents/src/Services/ModelMapper/ModelMapper.php |  2 +-
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/Plugin/DataType/EcaModel.php b/modules/agents/src/Plugin/DataType/EcaModel.php
index f158c3a..a7f02de 100644
--- a/modules/agents/src/Plugin/DataType/EcaModel.php
+++ b/modules/agents/src/Plugin/DataType/EcaModel.php
@@ -14,4 +14,11 @@ use Drupal\Core\TypedData\Plugin\DataType\Map;
 )]
 class EcaModel extends Map {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getName(): string {
+    return $this->get('label')->getString();
+  }
+
 }
diff --git a/modules/agents/src/Plugin/DataType/EcaPlugin.php b/modules/agents/src/Plugin/DataType/EcaPlugin.php
index 3e44482..ce9d97f 100644
--- a/modules/agents/src/Plugin/DataType/EcaPlugin.php
+++ b/modules/agents/src/Plugin/DataType/EcaPlugin.php
@@ -14,4 +14,17 @@ use Drupal\Core\TypedData\Plugin\DataType\Map;
 )]
 class EcaPlugin extends Map {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getName(): string {
+    try {
+      return $this->get('label')->getString();
+    }
+    catch (\InvalidArgumentException $e) {
+      // The definition of the plugin doesn't specify a label.
+      return parent::getName();
+    }
+  }
+
 }
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index 5a658aa..5604061 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -33,7 +33,7 @@ class ModelMapper implements ModelMapperInterface {
   public function fromPayload(array $payload): EcaModel {
     $definition = EcaModelDefinition::create();
     /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaModel $model */
-    $model = $this->typedDataManager->create($definition, $payload, 'the model');
+    $model = $this->typedDataManager->create($definition, $payload);
 
     /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */
     $violations = $model->validate();
-- 
GitLab


From cf9c375d10caab81a1abc079f1fc9bbce93bd5bb Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:09:14 +0100
Subject: [PATCH 49/95] #348130 Adjust tests for ModelMapper

---
 .../agents/tests/assets/from_payload_2.json   | 76 ++++++++++++++++
 .../agents/tests/assets/from_payload_3.json   | 91 +++++++++++++++++++
 .../tests/src/Kernel/ModelMapperTest.php      | 43 ++++-----
 3 files changed, 186 insertions(+), 24 deletions(-)
 create mode 100644 modules/agents/tests/assets/from_payload_2.json
 create mode 100644 modules/agents/tests/assets/from_payload_3.json

diff --git a/modules/agents/tests/assets/from_payload_2.json b/modules/agents/tests/assets/from_payload_2.json
new file mode 100644
index 0000000..729e3ac
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_2.json
@@ -0,0 +1,76 @@
+{
+  "id": "process_1",
+  "label": "Create Article on Page Publish",
+  "actions": [
+    {
+      "id": "action_2",
+      "plugin": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "id": "action_3"
+        }
+      ]
+    },
+    {
+      "id": "action_3",
+      "plugin": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "id": "action_4"
+        }
+      ]
+    },
+    {
+      "id": "action_4",
+      "plugin": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "id": "action_5"
+        }
+      ]
+    },
+    {
+      "id": "action_5",
+      "plugin": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "id": "action_6"
+        }
+      ]
+    },
+    {
+      "id": "action_6",
+      "plugin": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_3.json b/modules/agents/tests/assets/from_payload_3.json
new file mode 100644
index 0000000..65deddd
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_3.json
@@ -0,0 +1,91 @@
+{
+  "id": "create_article_on_new_page",
+  "label": "Create Article on New Page",
+  "events": [
+    {
+      "id": "start_event",
+      "plugin": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "id": "extract_page_title"
+        }
+      ]
+    }
+  ],
+  "conditions": [
+    {
+      "id": "check_title_for_ai",
+      "plugin": "eca_entity_field_value",
+      "configuration": {
+        "field_name": "title",
+        "expected_value": "AI",
+        "operator": "contains",
+        "type": "value",
+        "case": false,
+        "negate": false,
+        "entity": "entity"
+      }
+    }
+  ],
+  "gateways": [
+    {
+      "id": "title_check_gateway",
+      "type": 0,
+      "successors": [
+        {
+          "id": "create_article_unpublished",
+          "condition": "check_title_for_ai"
+        },
+        {
+          "id": "create_article_published"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "id": "extract_page_title",
+      "plugin": "eca_get_field_value",
+      "label": "Extract Page Title",
+      "configuration": {
+        "field_name": "title",
+        "token_name": "page_title"
+      },
+      "successors": [
+        {
+          "id": "title_check_gateway"
+        }
+      ]
+    },
+    {
+      "id": "create_article_unpublished",
+      "plugin": "eca_new_entity",
+      "label": "Create Unpublished Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article about: {page_title}",
+        "published": false,
+        "owner": "{{session_user.uid}}"
+      }
+    },
+    {
+      "id": "create_article_published",
+      "plugin": "eca_new_entity",
+      "label": "Create Published Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article about: {page_title}",
+        "published": true,
+        "owner": "{{session_user.uid}}"
+      }
+    }
+  ]
+}
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index a6403be..e914cc2 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -4,8 +4,6 @@ namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\Component\Serialization\Json;
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\node\Entity\NodeType;
 use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
 
 /**
@@ -13,7 +11,7 @@ use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
  *
  * @group ai_eca_agents
  */
-class ModelMapperTest extends KernelTestBase {
+class ModelMapperTest extends AiEcaKernelTestBase {
 
   /**
    * {@inheritdoc}
@@ -53,7 +51,24 @@ class ModelMapperTest extends KernelTestBase {
     yield [
       Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
       [],
-      'id: This value should not be null'
+      'id: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
+      [],
+      'events: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+      ]
     ];
   }
 
@@ -95,26 +110,6 @@ class ModelMapperTest extends KernelTestBase {
   protected function setUp(): void {
     parent::setUp();
 
-    parent::setUp();
-
-    $this->installEntitySchema('eca');
-    $this->installEntitySchema('node');
-    $this->installEntitySchema('user');
-
-    $this->installConfig(static::$modules);
-
-    // Create the Page content type with a standard body field.
-    /** @var \Drupal\node\NodeTypeInterface $node_type */
-    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
-    $node_type->save();
-    node_add_body_field($node_type);
-
-    // Create the Article content type with a standard body field.
-    /** @var \Drupal\node\NodeTypeInterface $node_type */
-    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
-    $node_type->save();
-    node_add_body_field($node_type);
-
     $this->modelMapper = \Drupal::service('ai_eca_agents.services.model_mapper');
   }
 
-- 
GitLab


From 33f077ac952b05c6fa7ff2e58f2842271faeec87 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:10:20 +0100
Subject: [PATCH 50/95] #348130 Use the ModelMapper-service for validation and
 building

---
 modules/agents/ai_eca_agents.services.yml     |  1 +
 .../AiAgentValidation/EcaValidation.php       | 34 +++++--
 .../Services/EcaRepository/EcaRepository.php  | 99 ++++++++++++-------
 3 files changed, 92 insertions(+), 42 deletions(-)

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index 7ed780d..f3b6759 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -12,6 +12,7 @@ services:
     class: Drupal\ai_eca_agents\Services\EcaRepository\EcaRepository
     arguments:
       - '@entity_type.manager'
+      - '@ai_eca_agents.services.model_mapper'
       - '@typed_data_manager'
 
   ai_eca_agents.services.model_mapper:
diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index b224bf4..11c87c3 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\Plugin\AiAgentValidation;
 
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -11,6 +12,7 @@ use Drupal\ai_agents\Attribute\AiAgentValidation;
 use Drupal\ai_agents\Exception\AgentRetryableValidationException;
 use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
 use Drupal\ai_agents\PluginInterfaces\AiAgentValidationInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -29,12 +31,20 @@ class EcaValidation extends AiAgentValidationPluginBase implements ContainerFact
    */
   protected PromptJsonDecoderInterface $promptJsonDecoder;
 
+  /**
+   * The model mapper.
+   *
+   * @var \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface
+   */
+  protected ModelMapperInterface $modelMapper;
+
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): AiAgentValidationInterface {
     $instance = new static($configuration, $plugin_id, $plugin_definition);
     $instance->promptJsonDecoder = $container->get('ai.prompt_json_decode');
+    $instance->modelMapper = $container->get('ai_eca_agents.services.model_mapper');
 
     return $instance;
   }
@@ -45,15 +55,25 @@ class EcaValidation extends AiAgentValidationPluginBase implements ContainerFact
   public function defaultValidation(mixed $data): bool {
     $data = $this->decodeData($data);
     if (empty($data)) {
-      throw new AgentRetryableValidationException('The LLM response failed validation: could not decode from JSON.', 0, NULL, 'You MUST only provide a RFC8259 compliant JSON response.');
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: could not decode from JSON.',
+        0,
+        NULL,
+        'You MUST only provide a RFC8259 compliant JSON response.'
+      );
     }
 
-    // Validate that at least 1 part of the response sets the title.
-    $setsTitle = (bool) count(array_filter($data, function($component) {
-      return !empty($component['type']) && $component['type'] === 'set_title';
-    }));
-    if (!$setsTitle) {
-      throw new AgentRetryableValidationException('The LLM response failed validation: no title was provided for the model.', 0, NULL, 'You MUST only provide a title for the model.');
+    // Validate the response against ECA-model schema.
+    try {
+      $this->modelMapper->fromPayload($data);
+    }
+    catch (MissingDataException $e) {
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: the model definition was violated.',
+        0,
+        NULL,
+        sprintf('Fix the following violations: %s.', $e->getMessage())
+      );
     }
 
     return TRUE;
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index 9bdf650..f3e17da 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -4,6 +4,7 @@ namespace Drupal\ai_eca_agents\Services\EcaRepository;
 
 use Drupal\ai_eca_agents\EntityViolationException;
 use Drupal\ai_eca_agents\MissingEventException;
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\TypedData\TypedDataManagerInterface;
@@ -19,11 +20,14 @@ class EcaRepository implements EcaRepositoryInterface {
    *
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
    *   The entity type manager.
+   * @param \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface $modelMapper
+   *   The model mapper.
    * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
    *   The typed data manager.
    */
   public function __construct(
     protected EntityTypeManagerInterface $entityTypeManager,
+    protected ModelMapperInterface $modelMapper,
     protected TypedDataManagerInterface $typedDataManager,
   ) {}
 
@@ -43,49 +47,74 @@ class EcaRepository implements EcaRepositoryInterface {
     $eca = $this->entityTypeManager->getStorage('eca')
       ->create();
 
+    // Convert the given data to the ECA-model.
+    $model = $this->modelMapper->fromPayload($data);
+
+    // Map the model to the entity.
     $random = new Random();
-    $eca->set('id', sprintf('process_%s', $random->name(7)));
-    $eca->set('label', $random->string(16));
+    $eca->set('id', $model->get('id')->getString() ?? sprintf('process_%s', $random->name(7)));
+    $eca->set('label', $model->get('label')->getString());
     $eca->set('modeller', 'fallback');
     $eca->set('version', '0.0.1');
     $eca->setStatus(FALSE);
 
-    foreach ($data as $entry) {
-      // Events, Conditions ("Flows"), Actions and Gateways.
-      if (!empty($entry['id'])) {
-        $successors = $entry['successors'] ?? [];
-        foreach ($successors as &$successor) {
-          $successor['condition'] ??= '';
-        }
-
-        switch (TRUE) {
-          case str_starts_with($entry['id'], 'Event_'):
-            $eca->addEvent($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $successors);
-            break;
-
-          case str_starts_with($entry['id'], 'Flow_'):
-            $eca->addCondition($entry['id'], $entry['plugin'], $entry['config'] ?? []);
-            break;
-
-          case str_starts_with($entry['id'], 'Activity_'):
-            $eca->addAction($entry['id'], $entry['plugin'], $entry['label'], $entry['config'] ?? [], $successors);
-            break;
-
-          case str_starts_with($entry['id'], 'Gateway_'):
-            $eca->addGateway($entry['id'], 0, $successors);
-            break;
-        }
-
-        continue;
+    // Set events.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
+    foreach ($model->get('events') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['condition'] ??= '';
+      }
+
+      $eca->addEvent(
+        $plugin->get('id')->getString(),
+        $plugin->get('plugin')->getString(),
+        $plugin->get('label')->getString(),
+        $plugin->get('configuration')->getValue(),
+        $successors
+      );
+    }
+
+    // Set conditions.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
+    foreach ($model->get('conditions') as $plugin) {
+      $eca->addCondition(
+        $plugin->get('id')->getString(),
+        $plugin->get('plugin')->getString(),
+        $plugin->get('configuration')->getValue(),
+      );
+    }
+
+    // Set gateways.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaGateway $plugin */
+    foreach ($model->get('gateways') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['condition'] ??= '';
       }
 
-      if (!empty($entry['type'])) {
-        switch ($entry['type']) {
-          case 'set_title':
-            $eca->set('label', $entry['value']);
-            break;
-        }
+      $eca->addGateway(
+        $plugin->get('id')->getString(),
+        $plugin->get('type')->getValue(),
+        $successors
+      );
+    }
+
+    // Set actions.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaGateway $plugin */
+    foreach ($model->get('actions') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['condition'] ??= '';
       }
+
+      $eca->addAction(
+        $plugin->get('id')->getString(),
+        $plugin->get('plugin')->getString(),
+        $plugin->get('label')->getString(),
+        $plugin->get('configuration')->getValue() ?? [],
+        $successors
+      );
     }
 
     // Validate the entity.
-- 
GitLab


From 11cbc172a4ded923c24ba914458d6da9a847992c Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:10:51 +0100
Subject: [PATCH 51/95] #348130 Adjust tests for EcaRepository

---
 .../tests/src/Kernel/EcaRepositoryTest.php    | 92 ++++---------------
 1 file changed, 20 insertions(+), 72 deletions(-)

diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index ffb99e4..ca883aa 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
-use Drupal\TestTools\Random;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 
 /**
@@ -27,89 +27,37 @@ class EcaRepositoryTest extends AiEcaKernelTestBase {
    *   Returns a collection of data points.
    */
   public static function dataProvider(): \Generator {
-    $random = Random::getGenerator();
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 0,
+        'actions' => 5,
+        'label' => 'Create Article on Page Publish',
+      ]
+    ];
 
     yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
       [],
-      [],
-      'No events registered.',
+      'id: This value should not be null'
     ];
 
     yield [
-      [
-        [
-          'id' => 'Activity_2f4g6h8',
-          'plugin' => 'eca_new_entity',
-          'label' => 'Create New Article',
-          'config' => [
-            'token_name' => 'new_article',
-            'type' => 'node article',
-            'langcode' => 'en',
-            'label' => '[entity:title] Article',
-            'published' => TRUE,
-            'owner' => '[entity:uid]',
-          ],
-        ],
-      ],
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
       [],
-      'No events registered.',
+      'events: This value should not be null'
     ];
 
-    $label = $random->name();
     yield [
-      [
-        [
-          'id' => 'Event_1a3b5c7',
-          'plugin' => 'content_entity:insert',
-          'label' => 'Insert Page Event',
-          'config' => [
-            'type' => 'node page',
-          ],
-          'successors' => [
-            ['id' => 'Activity_2f4g6h8'],
-          ],
-        ],
-        [
-          'id' => 'Activity_2f4g6h8',
-          'plugin' => 'eca_new_entity',
-          'label' => 'Create New Article',
-          'config' => [
-            'token_name' => 'new_article',
-            'type' => 'node article',
-            'langcode' => 'en',
-            'label' => '[entity:title] Article',
-            'published' => TRUE,
-            'owner' => '[entity:uid]',
-          ],
-          'successors' => [
-            ['id' => 'Activity_3i7j9k0'],
-          ],
-        ],
-        [
-          'id' => 'Activity_3i7j9k0',
-          'plugin' => 'eca_set_field_value',
-          'label' => 'Set Article Body',
-          'config' => [
-            'method' => 'remove',
-            'field_name' => 'body.value',
-            'field_value' => 'Page by [entity:author] - Learn more about the topic discussed in [entity:title].',
-            'strip_tags' => FALSE,
-            'trim' => TRUE,
-            'save_entity' => TRUE,
-          ],
-          'successors' => [],
-        ],
-        [
-          'type' => 'set_title',
-          'value' => $label,
-        ],
-      ],
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
       [
         'events' => 1,
-        'conditions' => 0,
-        'actions' => 2,
-        'label' => $label,
-      ],
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+      ]
     ];
   }
 
-- 
GitLab


From a9dbe2798e0bfe694381cfa384d8aecf3cffb149 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:11:51 +0100
Subject: [PATCH 52/95] #348130 Serialize the schema of the typed data plugins
 of the ECA model

---
 modules/agents/prompts/eca/buildModel.yml | 98 ++++++-----------------
 modules/agents/src/Plugin/AiAgent/Eca.php | 20 +++++
 2 files changed, 43 insertions(+), 75 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 223966d..ee74aa4 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -8,80 +8,28 @@ validation:
 retries: 2
 prompt:
   introduction: |
-    You are a Drupal developer that can generate a configuration file for the Event-Condition-Action module.
+    You are a business process modeling expert. You will be given a textual description of a business process.
+    Generate a JSON model for the process, based on the provided JSON Schema.
 
-    If you can't create the configuration because it's lacking information, just answer with the "no_info" action.
+    Analyze and identify key elements:
+    1. Start events, there should be at least one.
+    2. Tasks and their sequence.
+    3. Gateways (xor) and an array of ”branches” containing tasks. There is a condition for the decision point and each
+    branch has a condition label.
+    4. Loops: involve repeating tasks until a specific condition is met.
 
-    You will receive information about the available plugins and their details, as well as a list of generally available tokens. You can use them how you see fit, but do not generate new tokens.
-    There should be at least one Event-related component.
-  possible_actions:
-    set_title: sets the title of the configuration item
-    no_info: if there's not enough information to create the configuration item, just answer with this action
-  formats:
-    - id: unique random ID of component, it should consist of 7 characters and start with "Event_" for an event, "Activity_" for an activity or "Flow_" for a condition
-      plugin: an existing plugin ID
-      label: human readable label
-      config: optional setup to configure the component
-      successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
-    - id: unique random ID of the Gateway, it should consist of 7 characters and start with "Gateway_"
-      successors: an optional list of successors, each consisting of an ID referring to another Action or Gateway and an optional reference to a Condition.
-    - type: type of action
-      value: value of the action
-  one_shot_learning_examples:
-    - id: Event_0erz1e4
-      plugin: 'user:login'
-      label: 'User Login'
-      successors:
-        - id: Gateway_0hd8858
-          condition: Flow_1o433l9
-    - id: Event_0erz1e4
-      plugin: 'content_entity:insert'
-      label: 'User Register'
-      configuration:
-        type: 'user user'
-      successors:
-        - id: Gateway_0hd8858
-          condition: Flow_1o433l9
-    - id: Flow_
-      plugin: eca_scalar
-      config:
-        case: false
-        left: '[current-page:url:path]'
-        right: /user/reset
-        operator: beginswith
-        type: value
-        negate: true
-    - id: Gateway_0hd8858
-      successors:
-        - id: Activity_0l4w3fc
-          condition: Flow_1hqinah
-        - id: Activity_182vndw
-          condition: Flow_0047zve
-    - id: Activity_0l4w3fc
-      plugin: action_goto_action
-      label: 'Redirect to content overview'
-      config:
-        replace_tokens: false
-        url: /admin/content
-      successors: { }
-    - id: Flow_1hqinah
-      plugin: eca_current_user_role
-      config:
-        negate: false
-        role: content_editor
-    - id: Activity_182vndw
-      plugin: action_goto_action
-      label: 'Redirect to admin overview'
-      config:
-        replace_tokens: false
-        url: /admin
-      successors: { }
-    - id: Flow_0047zve
-      plugin: eca_current_user_role
-      config:
-        negate: false
-        role: administrator
-    - type: set_title
-      value: 'ECA Feature Demo'
-    - type: no_info
-      value: Not enough information to create the configuration item
+    Nested structure: The schema uses nested structures within gateways to represent branching paths.
+    Order matters: The order of elements in the ”process” array defines the execution sequence.
+
+    When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
+    optimization (if it does not contradict the user intended sequence).
+    Use clear names for labels and conditions.
+    Aim for granular detail (e.g., instead of ”Task 1: Action 1 and Action 2”, use ”Task 1:
+    Action 1” and ”Task 2: Action 2”).
+
+    All elements, except gateways, must have a plugin assigned to them and optionally an array configuration parameters.
+    You will given a list of possible plugins and their corresponding configuration structure, you can not deviate from
+    those.
+
+    Sometimes you will be given a previous JSON solution with user instructions to edit.
+  formats: []
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index f4d141b..5cbf753 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\ai_eca_agents\Plugin\AiAgent;
 
+use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
 use Drupal\Component\Render\MarkupInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@@ -17,6 +19,7 @@ use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 use Drupal\eca\Entity\Eca as EcaEntity;
 use Illuminate\Support\Arr;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Serializer\SerializerInterface;
 
 /**
  * The ECA agent.
@@ -55,6 +58,13 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    */
   protected EcaRepositoryInterface $ecaRepository;
 
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected SerializerInterface $serializer;
+
   /**
    * {@inheritdoc}
    */
@@ -62,6 +72,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
     $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
     $instance->ecaRepository = $container->get('ai_eca_agents.services.eca_repository');
+    $instance->serializer = $container->get('serializer');
     $instance->dto = [
       'task_description' => '',
       'feedback' => '',
@@ -327,10 +338,19 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $context = [
       // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
     ];
+    // The schema of the ECA-config that the LLM should follow.
+    $definition = EcaModelDefinition::create();
+    $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
+    $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
+    $context['JSON Schema of the process'] = sprintf("```json\n%s\n```", $schema);
+
+    // Components or plugins that the LLM should use.
     if (Arr::has($this->dto, 'data.0.component_ids')) {
       $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
       $context['The details about the components'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents($componentIds)));
     }
+
+    // Optional feedback that the previous prompt provided.
     if (Arr::has($this->dto, 'data.0.feedback')) {
       $context['Guidelines'] = Arr::get($this->dto, 'data.0.feedback');
     }
-- 
GitLab


From 2dea2f6a7ecd45c5a54f2c310d0dc24e001387af Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:25:43 +0100
Subject: [PATCH 53/95] #348130 Enable schemata_json_schema for kernel tests

---
 modules/agents/ai_eca_agents.info.yml    | 2 +-
 tests/src/Kernel/AiEcaKernelTestBase.php | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
index 3be10c6..d0382da 100644
--- a/modules/agents/ai_eca_agents.info.yml
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -7,5 +7,5 @@ dependencies:
   - ai_eca:ai_eca
   - ai_agents:ai_agents
   - eca:eca_ui
-  - schemata_json_schema:schemata_json_schema
+  - schemata:schemata_json_schema
   - token:token
diff --git a/tests/src/Kernel/AiEcaKernelTestBase.php b/tests/src/Kernel/AiEcaKernelTestBase.php
index a398b6e..14c88b5 100644
--- a/tests/src/Kernel/AiEcaKernelTestBase.php
+++ b/tests/src/Kernel/AiEcaKernelTestBase.php
@@ -22,6 +22,8 @@ abstract class AiEcaKernelTestBase extends KernelTestBase {
     'text',
     'token',
     'user',
+    'schemata',
+    'schemata_json_schema',
     'system',
   ];
 
-- 
GitLab


From 9f0c553da094f80c054b1d02c4ce42bac60a1a24 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:30:38 +0100
Subject: [PATCH 54/95] #3481307 Add support for illuminate/support:^10 as well

---
 composer.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/composer.json b/composer.json
index e82a8f5..184a759 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
         "drupal/core": "^10.3 || ^11",
         "drupal/eca": "^2.0",
         "drupal/token": "^1.15",
-        "illuminate/support": "^11.34"
+        "illuminate/support": "^10.48 || ^11.34"
     },
     "require-dev": {
         "drupal/schemata": "^1.0"
-- 
GitLab


From cfba54fa10e3c10f536bb287b9b55464e86791fb Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:34:56 +0100
Subject: [PATCH 55/95] #3481307 Move Kernel base class and add serialization
 as dependency

---
 .../src/Kernel/AiEcaAgentsKernelTestBase.php  |  8 +++++--
 .../tests/src/Kernel/EcaRepositoryTest.php    |  3 +--
 .../tests/src/Kernel/ModelMapperTest.php      | 21 +------------------
 3 files changed, 8 insertions(+), 24 deletions(-)
 rename tests/src/Kernel/AiEcaKernelTestBase.php => modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php (85%)

diff --git a/tests/src/Kernel/AiEcaKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
similarity index 85%
rename from tests/src/Kernel/AiEcaKernelTestBase.php
rename to modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
index 14c88b5..fe72003 100644
--- a/tests/src/Kernel/AiEcaKernelTestBase.php
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -1,11 +1,14 @@
 <?php
 
-namespace Drupal\Tests\ai_eca\Kernel;
+namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\NodeType;
 
-abstract class AiEcaKernelTestBase extends KernelTestBase {
+/**
+ * Base class for Kernel-tests regarding AI ECA Agents.
+ */
+abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
 
   /**
    * {@inheritdoc}
@@ -22,6 +25,7 @@ abstract class AiEcaKernelTestBase extends KernelTestBase {
     'text',
     'token',
     'user',
+    'serialization',
     'schemata',
     'schemata_json_schema',
     'system',
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index ca883aa..22eaee0 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -3,7 +3,6 @@
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\Component\Serialization\Json;
-use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
 
 /**
@@ -11,7 +10,7 @@ use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
  *
  * @group ai_eca_agents
  */
-class EcaRepositoryTest extends AiEcaKernelTestBase {
+class EcaRepositoryTest extends AiEcaAgentsKernelTestBase {
 
   /**
    * The ECA repository.
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index e914cc2..685ae34 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -4,32 +4,13 @@ namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\Component\Serialization\Json;
-use Drupal\Tests\ai_eca\Kernel\AiEcaKernelTestBase;
 
 /**
  * Tests various input data for generation ECA Model typed data.
  *
  * @group ai_eca_agents
  */
-class ModelMapperTest extends AiEcaKernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'ai_eca',
-    'ai_eca_agents',
-    'eca',
-    'eca_base',
-    'eca_content',
-    'eca_user',
-    'field',
-    'node',
-    'text',
-    'token',
-    'user',
-    'system',
-  ];
+class ModelMapperTest extends AiEcaAgentsKernelTestBase {
 
   /**
    * Generate different sets of payloads.
-- 
GitLab


From 176b7c8383f0f41265ac8049f1b10622f86713f2 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:38:06 +0100
Subject: [PATCH 56/95] #3481307 Move payloadProvider-method to base class

---
 .../src/Kernel/AiEcaAgentsKernelTestBase.php  | 42 +++++++++++++++++
 .../tests/src/Kernel/EcaRepositoryTest.php    | 45 +------------------
 .../tests/src/Kernel/ModelMapperTest.php      | 41 -----------------
 3 files changed, 44 insertions(+), 84 deletions(-)

diff --git a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
index fe72003..558e0fd 100644
--- a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\NodeType;
 
@@ -31,6 +32,47 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
     'system',
   ];
 
+  /**
+   * Generate different sets of data payloads.
+   *
+   * @return \Generator
+   *   Returns a collection of data payloads.
+   */
+  public static function payloadProvider(): \Generator {
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 0,
+        'actions' => 5,
+        'label' => 'Create Article on Page Publish',
+      ],
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
+      [],
+      'id: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
+      [],
+      'events: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+      ],
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index 22eaee0..3edcb24 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
-use Drupal\Component\Serialization\Json;
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
+use Drupal\Component\Serialization\Json;
 
 /**
  * Tests various input data for generating ECA models.
@@ -19,51 +19,10 @@ class EcaRepositoryTest extends AiEcaAgentsKernelTestBase {
    */
   protected ?EcaRepositoryInterface $ecaRepository;
 
-  /**
-   * Generate different sets of data points.
-   *
-   * @return \Generator
-   *   Returns a collection of data points.
-   */
-  public static function dataProvider(): \Generator {
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
-      [
-        'events' => 1,
-        'conditions' => 0,
-        'actions' => 5,
-        'label' => 'Create Article on Page Publish',
-      ]
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
-      [],
-      'id: This value should not be null'
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
-      [],
-      'events: This value should not be null'
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
-      [
-        'events' => 1,
-        'conditions' => 1,
-        'gateways' => 1,
-        'actions' => 3,
-        'label' => 'Create Article on New Page',
-      ]
-    ];
-  }
-
   /**
    * Build an ECA-model with the provided data.
    *
-   * @dataProvider dataProvider
+   * @dataProvider payloadProvider
    */
   public function testBuildModel(array $data, array $assertions, ?string $errorMessage = NULL): void {
     if (!empty($errorMessage)) {
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index 685ae34..ed0ea8d 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -12,47 +12,6 @@ use Drupal\Component\Serialization\Json;
  */
 class ModelMapperTest extends AiEcaAgentsKernelTestBase {
 
-  /**
-   * Generate different sets of payloads.
-   *
-   * @return \Generator
-   *   Returns a collection of payloads.
-   */
-  public static function payloadProvider(): \Generator {
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
-      [
-        'events' => 1,
-        'conditions' => 0,
-        'actions' => 5,
-        'label' => 'Create Article on Page Publish',
-      ]
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
-      [],
-      'id: This value should not be null',
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
-      [],
-      'events: This value should not be null',
-    ];
-
-    yield [
-      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
-      [
-        'events' => 1,
-        'conditions' => 1,
-        'gateways' => 1,
-        'actions' => 3,
-        'label' => 'Create Article on New Page',
-      ]
-    ];
-  }
-
   /**
    * The model mapper.
    *
-- 
GitLab


From 9dc91a1b7ece063f05752a1aef2fb98482ba4777 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:40:22 +0100
Subject: [PATCH 57/95] #3481307 Fix phpcs issues for typed data definitions

---
 .../src/TypedData/EcaGatewayDefinition.php    | 21 ++++++++-------
 .../src/TypedData/EcaModelDefinition.php      | 19 ++++++++------
 .../src/TypedData/EcaPluginDefinition.php     | 26 ++++++++++++-------
 .../src/TypedData/EcaSuccessorDefinition.php  |  3 +++
 4 files changed, 42 insertions(+), 27 deletions(-)

diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
index b5d7a3a..0e42c02 100644
--- a/modules/agents/src/TypedData/EcaGatewayDefinition.php
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -8,15 +8,11 @@ use Drupal\Core\TypedData\DataDefinition;
 use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\Core\TypedData\ListDataDefinition;
 
+/**
+ * Definition of the ECA Gateway data type.
+ */
 class EcaGatewayDefinition extends ComplexDataDefinitionBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_gateway'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -28,7 +24,7 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
         'pattern' => '/^[\w]+$/',
-        'message' => 'The %value ID is not valid.'
+        'message' => 'The %value ID is not valid.',
       ]);
 
     $properties['type'] = DataDefinition::create('integer')
@@ -36,7 +32,7 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
       ->setSetting('allowed_values_function', '')
       ->setSetting('allowed_values', ['0'])
       ->addConstraint('Choice', [
-        'choices' => [0]
+        'choices' => [0],
       ]);
 
     $properties['successors'] = ListDataDefinition::create('eca_successor')
@@ -45,4 +41,11 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_gateway'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
 }
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index 5c9b411..f7f6c58 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -8,15 +8,11 @@ use Drupal\Core\TypedData\DataDefinition;
 use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\Core\TypedData\ListDataDefinition;
 
+/**
+ * Definition of the ECA Model data type.
+ */
 class EcaModelDefinition extends ComplexDataDefinitionBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_model'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -28,7 +24,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
         'pattern' => '/^[\w]+$/',
-        'message' => 'The %value ID is not valid.'
+        'message' => 'The %value ID is not valid.',
       ]);
 
     $properties['label'] = DataDefinition::create('string')
@@ -68,4 +64,11 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_model'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
 }
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index ad7dfd0..8c4fa43 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -11,21 +11,20 @@ use Drupal\Core\TypedData\ListDataDefinition;
 use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
 use Drupal\eca\Plugin\ECA\Event\EventInterface;
 
+/**
+ * Definition of the ECA Plugin data type.
+ */
 class EcaPluginDefinition extends ComplexDataDefinitionBase {
 
   public const DATA_TYPE_EVENT = 'eca_event';
+
   public const DATA_TYPE_CONDITION = 'eca_condition';
+
   public const DATA_TYPE_ACTION = 'eca_action';
 
   protected const PROP_LABEL = 'label';
-  protected const PROP_SUCCESSORS = 'successors';
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create($type = 'eca_plugin'): DataDefinitionInterface {
-    return new self(['type' => $type]);
-  }
+  protected const PROP_SUCCESSORS = 'successors';
 
   /**
    * {@inheritdoc}
@@ -38,7 +37,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
         'pattern' => '/^[\w]+$/',
-        'message' => 'The %value ID is not valid.'
+        'message' => 'The %value ID is not valid.',
       ]);
 
     $properties['plugin'] = DataDefinition::create('string')
@@ -70,6 +69,13 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_plugin'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
   /**
    * Get the plugin manager ID.
    *
@@ -77,7 +83,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    *   Returns the plugin manager ID.
    */
   protected function getPluginManagerId(): string {
-    return match($this->getSetting('data_type')) {
+    return match ($this->getSetting('data_type')) {
       self::DATA_TYPE_ACTION => 'plugin.manager.action',
       self::DATA_TYPE_EVENT => 'plugin.manager.eca.event',
       self::DATA_TYPE_CONDITION => 'plugin.manager.eca.condition',
@@ -94,7 +100,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    *   Returns the plugin interface.
    */
   protected function getPluginInterface(): string {
-    return match($this->getSetting('data_type')) {
+    return match ($this->getSetting('data_type')) {
       self::DATA_TYPE_ACTION => ActionInterface::class,
       self::DATA_TYPE_EVENT => EventInterface::class,
       self::DATA_TYPE_CONDITION => ConditionInterface::class,
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
index 369e5af..8d66bdb 100644
--- a/modules/agents/src/TypedData/EcaSuccessorDefinition.php
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -6,6 +6,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
 
+/**
+ * Definition of the ECA Successor data type.
+ */
 class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
 
   /**
-- 
GitLab


From 71519979102c55b50517a1811576e160f8e8305c Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:47:40 +0100
Subject: [PATCH 58/95] #3481307 Fix various phpcs issues

---
 .../agents/src/Normalizer/json/DataDefinitionNormalizer.php   | 1 +
 modules/agents/src/Plugin/AiAgent/Eca.php                     | 2 ++
 modules/agents/src/Plugin/DataType/EcaGateway.php             | 3 +++
 modules/agents/src/Plugin/DataType/EcaModel.php               | 3 +++
 modules/agents/src/Plugin/DataType/EcaPlugin.php              | 3 +++
 modules/agents/src/Plugin/DataType/EcaSuccessor.php           | 3 +++
 modules/agents/src/Schema/Eca.php                             | 3 +++
 modules/agents/src/Services/ModelMapper/ModelMapper.php       | 4 +---
 .../agents/src/Services/ModelMapper/ModelMapperInterface.php  | 3 +++
 9 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
index afad803..1cdaba3 100644
--- a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
+++ b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
@@ -42,6 +42,7 @@ class DataDefinitionNormalizer extends JsonNormalizerBase {
    * Constructs a DataDefinitionNormalizer object.
    *
    * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The decorated nornalizer.
    */
   public function __construct(NormalizerInterface $normalizer) {
     $this->inner = $normalizer;
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 5cbf753..1f83313 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -260,6 +260,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * Get the data transfer object.
    *
    * @return array
+   *   Returns the data transfer object.
    */
   public function getDto(): array {
     return $this->dto;
@@ -269,6 +270,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    * Set the data transfer object.
    *
    * @param array $dto
+   *   The data transfer object.
    */
   public function setDto(array $dto): void {
     $this->dto = $dto;
diff --git a/modules/agents/src/Plugin/DataType/EcaGateway.php b/modules/agents/src/Plugin/DataType/EcaGateway.php
index 026b97f..8162a97 100644
--- a/modules/agents/src/Plugin/DataType/EcaGateway.php
+++ b/modules/agents/src/Plugin/DataType/EcaGateway.php
@@ -7,6 +7,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\Attribute\DataType;
 use Drupal\Core\TypedData\Plugin\DataType\Map;
 
+/**
+ * Data type plugin of the ECA Gateway data type.
+ */
 #[DataType(
   id: 'eca_gateway',
   label: new TranslatableMarkup('ECA Gateway'),
diff --git a/modules/agents/src/Plugin/DataType/EcaModel.php b/modules/agents/src/Plugin/DataType/EcaModel.php
index a7f02de..63b70b0 100644
--- a/modules/agents/src/Plugin/DataType/EcaModel.php
+++ b/modules/agents/src/Plugin/DataType/EcaModel.php
@@ -7,6 +7,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\Attribute\DataType;
 use Drupal\Core\TypedData\Plugin\DataType\Map;
 
+/**
+ * Data type plugin of the ECA Model data type.
+ */
 #[DataType(
   id: 'eca_model',
   label: new TranslatableMarkup('ECA Model'),
diff --git a/modules/agents/src/Plugin/DataType/EcaPlugin.php b/modules/agents/src/Plugin/DataType/EcaPlugin.php
index ce9d97f..1cd4f57 100644
--- a/modules/agents/src/Plugin/DataType/EcaPlugin.php
+++ b/modules/agents/src/Plugin/DataType/EcaPlugin.php
@@ -7,6 +7,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\Attribute\DataType;
 use Drupal\Core\TypedData\Plugin\DataType\Map;
 
+/**
+ * Data type plugin of the ECA Plugin data type.
+ */
 #[DataType(
   id: 'eca_plugin',
   label: new TranslatableMarkup('ECA Plugin'),
diff --git a/modules/agents/src/Plugin/DataType/EcaSuccessor.php b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
index 539c674..90a65a8 100644
--- a/modules/agents/src/Plugin/DataType/EcaSuccessor.php
+++ b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
@@ -7,6 +7,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\Attribute\DataType;
 use Drupal\Core\TypedData\Plugin\DataType\Map;
 
+/**
+ * Data type plugin of the ECA Successor data type.
+ */
 #[DataType(
   id: 'eca_successor',
   label: new TranslatableMarkup('ECA Successor'),
diff --git a/modules/agents/src/Schema/Eca.php b/modules/agents/src/Schema/Eca.php
index a5fb7c7..7e59476 100644
--- a/modules/agents/src/Schema/Eca.php
+++ b/modules/agents/src/Schema/Eca.php
@@ -6,6 +6,9 @@ use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
 use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\schemata\Schema\SchemaInterface;
 
+/**
+ * Provides support for the ECA Model data type in Schemata.
+ */
 class Eca implements SchemaInterface {
 
   use RefinableCacheableDependencyTrait;
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index 5604061..60bd37f 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -23,9 +23,7 @@ class ModelMapper implements ModelMapperInterface {
    */
   public function __construct(
     protected TypedDataManagerInterface $typedDataManager
-  ) {
-
-  }
+  ) {}
 
   /**
    * {@inheritdoc}
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
index 72ddbd8..da0af27 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
@@ -5,6 +5,9 @@ namespace Drupal\ai_eca_agents\Services\ModelMapper;
 use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
 use Drupal\eca\Entity\Eca;
 
+/**
+ * Interface that describes a model mapper.
+ */
 interface ModelMapperInterface {
 
   /**
-- 
GitLab


From 132b64be44ee7b207f3e90f27c06e8f3b32987bd Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 20:55:35 +0100
Subject: [PATCH 59/95] #3481307 Fix various phpcs issues

---
 modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php | 2 +-
 modules/agents/src/Services/ModelMapper/ModelMapper.php         | 2 +-
 modules/agents/tests/src/Kernel/EcaRepositoryTest.php           | 1 -
 modules/agents/tests/src/Kernel/ModelMapperTest.php             | 1 -
 4 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
index 1cdaba3..72aef2f 100644
--- a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
+++ b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
@@ -42,7 +42,7 @@ class DataDefinitionNormalizer extends JsonNormalizerBase {
    * Constructs a DataDefinitionNormalizer object.
    *
    * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
-   *   The decorated nornalizer.
+   *   The decorated normalizer.
    */
   public function __construct(NormalizerInterface $normalizer) {
     $this->inner = $normalizer;
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index 60bd37f..1a26bf2 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -22,7 +22,7 @@ class ModelMapper implements ModelMapperInterface {
    *   The typed data manager.
    */
   public function __construct(
-    protected TypedDataManagerInterface $typedDataManager
+    protected TypedDataManagerInterface $typedDataManager,
   ) {}
 
   /**
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index 3edcb24..a67f376 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -3,7 +3,6 @@
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
-use Drupal\Component\Serialization\Json;
 
 /**
  * Tests various input data for generating ECA models.
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index ed0ea8d..c63c57b 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -3,7 +3,6 @@
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
 use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
-use Drupal\Component\Serialization\Json;
 
 /**
  * Tests various input data for generation ECA Model typed data.
-- 
GitLab


From 2de2445fbddd5d700e0a224030e75104896e90ac Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 20 Dec 2024 21:11:40 +0100
Subject: [PATCH 60/95] #348130 Add test to validate the schema of the ECA
 Model data type

---
 composer.json                                 |  1 +
 .../tests/src/Kernel/EcaModelSchemaTest.php   | 65 +++++++++++++++++++
 ...caModelSchemaTest.testSchema.approved.json |  1 +
 3 files changed, 67 insertions(+)
 create mode 100644 modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
 create mode 100644 modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json

diff --git a/composer.json b/composer.json
index 184a759..92b8d73 100644
--- a/composer.json
+++ b/composer.json
@@ -16,6 +16,7 @@
         "illuminate/support": "^10.48 || ^11.34"
     },
     "require-dev": {
+        "approvals/approval-tests": "dev-Main",
         "drupal/schemata": "^1.0"
     }
 }
diff --git a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
new file mode 100644
index 0000000..7d8cf3e
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use ApprovalTests\Approvals;
+use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\Component\Serialization\Json;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Kernel test for the ECA Model data type.
+ *
+ * @group ai_eca_agents
+ */
+class EcaModelSchemaTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'serialization',
+    'schemata',
+    'schemata_json_schema',
+    'system',
+    'token',
+    'user',
+  ];
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface|null
+   */
+  protected ?SerializerInterface $serializer;
+
+  /**
+   * Validates the generated schema of the Eca Model data type.
+   */
+  public function testSchema(): void {
+    $definition = EcaModelDefinition::create();
+    $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
+    $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
+
+    Approvals::verifyStringWithFileExtension($schema, 'json');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    $this->serializer = $this->container->get('serializer');
+  }
+
+}
diff --git a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
new file mode 100644
index 0000000..94c32fd
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
@@ -0,0 +1 @@
+{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
-- 
GitLab


From c412efad54444ca61a0a4310541edeb6136eac4a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 21 Dec 2024 16:09:46 +0100
Subject: [PATCH 61/95] #348130 Add description to EcaModel

---
 modules/agents/src/TypedData/EcaModelDefinition.php            | 3 +++
 .../approvals/EcaModelSchemaTest.testSchema.approved.json      | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index f7f6c58..dbea888 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -33,6 +33,9 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->addConstraint('Length', ['max' => 255])
       ->addConstraint('NotBlank');
 
+    $properties['description'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Description'));
+
     $properties['events'] = ListDataDefinition::create('eca_plugin')
       ->setLabel(new TranslatableMarkup('Events'))
       ->setRequired(TRUE)
diff --git a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
index 94c32fd..8137eaf 100644
--- a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
+++ b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
@@ -1 +1 @@
-{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
+{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
-- 
GitLab


From 39f2ac83eaf113275312bfae58024562ed8e4728 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 21 Dec 2024 17:16:30 +0100
Subject: [PATCH 62/95] #348130 Map an ECA-entity to the EcaModel data type

---
 modules/agents/ai_eca_agents.services.yml     |   1 +
 .../src/Services/ModelMapper/ModelMapper.php  | 121 +++++++++++++++++-
 .../src/TypedData/EcaSuccessorDefinition.php  |   8 ++
 3 files changed, 129 insertions(+), 1 deletion(-)

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index f3b6759..bc7a1f3 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -19,6 +19,7 @@ services:
     class: Drupal\ai_eca_agents\Services\ModelMapper\ModelMapper
     arguments:
       - '@typed_data_manager'
+      - '@serializer'
 
   # Typed data definitions in general can take many forms. This handles final items.
   serializer.normalizer.data_definition.schema_json_ai_eca_agents.json:
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index 1a26bf2..90c1c0e 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -3,11 +3,16 @@
 namespace Drupal\ai_eca_agents\Services\ModelMapper;
 
 use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
+use Drupal\ai_eca_agents\TypedData\EcaGatewayDefinition;
 use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaPluginDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaSuccessorDefinition;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\Exception\MissingDataException;
 use Drupal\Core\TypedData\TypedDataManagerInterface;
 use Drupal\eca\Entity\Eca;
+use Drupal\eca\Entity\Objects\EcaEvent;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 use Symfony\Component\Validator\ConstraintViolationListInterface;
 
 /**
@@ -20,9 +25,12 @@ class ModelMapper implements ModelMapperInterface {
    *
    * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
    *   The typed data manager.
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The normalizer.
    */
   public function __construct(
     protected TypedDataManagerInterface $typedDataManager,
+    protected NormalizerInterface $normalizer,
   ) {}
 
   /**
@@ -46,7 +54,93 @@ class ModelMapper implements ModelMapperInterface {
    * {@inheritdoc}
    */
   public function fromEntity(Eca $entity): EcaModel {
-    // TODO: Implement fromEntity() method.
+    $modelDef = EcaModelDefinition::create();
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaModel $model */
+    $model = $this->typedDataManager->create($modelDef);
+
+    // Basic properties.
+    $model->set('id', $entity->id());
+    $model->set('label', $entity->label());
+    if (!empty($entity->getModel()->getDocumentation())) {
+      $model->set('description', $entity->getModel()->getDocumentation());
+    }
+
+    // Events.
+    $plugins = array_reduce($entity->getUsedEvents(), function (array $carry, EcaEvent $event) {
+      $successors = $this->mapSuccessorsToModel($event->getSuccessors());
+
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_EVENT);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('id', $event->getId());
+      $model->set('plugin', $event->getPlugin()->getPluginId());
+      $model->set('label', $event->getLabel());
+      $model->set('configuration', $event->getConfiguration());
+      $model->set('successors', $successors);
+
+      $carry[] = array_filter($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []);
+    $model->set('events', $plugins);
+
+    // Conditions.
+    $conditions = $entity->getConditions();
+    $plugins = array_reduce(array_keys($conditions), function (array $carry, string $conditionId) use ($conditions) {
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_CONDITION);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('id', $conditionId);
+      $model->set('plugin', $conditions[$conditionId]['plugin']);
+      $model->set('configuration', $conditions[$conditionId]['configuration']);
+
+      $carry[] = array_filter($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []);
+    $model->set('conditions', $plugins);
+
+    // Actions.
+    $actions = $entity->getActions();
+    $plugins = array_reduce(array_keys($actions), function (array $carry, string $actionId) use ($actions) {
+      $successors = $this->mapSuccessorsToModel($actions[$actionId]['successors']);
+
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_ACTION);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('id', $actionId);
+      $model->set('plugin', $actions[$actionId]['plugin']);
+      $model->set('label', $actions[$actionId]['label']);
+      $model->set('configuration', $actions[$actionId]['configuration']);
+      $model->set('successors', $successors);
+
+      $carry[] = array_filter($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []);
+    $model->set('actions', $plugins);
+
+    // Gateways.
+    $gateways = $entity->get('gateways');
+    $plugins = array_reduce(array_keys($gateways), function (array $carry, string $gatewayId) use ($gateways) {
+      $successors = $this->mapSuccessorsToModel($gateways[$gatewayId]['successors']);
+
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create(EcaGatewayDefinition::create());
+      $model->set('id', $gatewayId);
+      $model->set('type', $gateways[$gatewayId]['type']);
+      $model->set('successors', $successors);
+
+      $carry[] = array_filter($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []);
+    $model->set('gateways', $plugins);
+
+    return $model;
   }
 
   /**
@@ -72,4 +166,29 @@ class ModelMapper implements ModelMapperInterface {
     return implode("\n", $lines);
   }
 
+  /**
+   * Map an array of successor-data to the EcaSuccessor data type.
+   *
+   * @param array $successors
+   *   The array of successor-data.
+   *
+   * @return array
+   *   Returns a normalized version of the EcaSuccessor data type.
+   *
+   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
+   * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
+   */
+  protected function mapSuccessorsToModel(array $successors): array {
+    return array_reduce($successors, function ($carry, array $successor) {
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaSuccessor $model */
+      $model = $this->typedDataManager->create(EcaSuccessorDefinition::create());
+      $model->set('id', $successor['id']);
+      $model->set('condition', $successor['condition']);
+
+      $carry[] = array_filter($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []);
+  }
+
 }
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
index 8d66bdb..ec07268 100644
--- a/modules/agents/src/TypedData/EcaSuccessorDefinition.php
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -5,6 +5,7 @@ namespace Drupal\ai_eca_agents\TypedData;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
 
 /**
  * Definition of the ECA Successor data type.
@@ -35,4 +36,11 @@ class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_successor'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
 }
-- 
GitLab


From 69ae2533f59a61bcc276fccabe0182eb5cd13442 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 21 Dec 2024 17:18:12 +0100
Subject: [PATCH 63/95] #348130 Map and normalize the ECA-entities in the
 DataProvider-service

---
 modules/agents/ai_eca_agents.services.yml     |  2 ++
 .../Services/DataProvider/DataProvider.php    | 28 ++++++++++---------
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index bc7a1f3..5f6aa0b 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -6,6 +6,8 @@ services:
       - '@eca.service.condition'
       - '@eca.service.action'
       - '@entity_type.manager'
+      - '@ai_eca_agents.services.model_mapper'
+      - '@serializer'
       - '@token.tree_builder'
 
   ai_eca_agents.services.eca_repository:
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index de1d743..e7079cb 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -7,12 +7,15 @@ use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\eca\Attributes\Token;
 use Drupal\eca\Entity\Eca;
 use Drupal\eca\Service\Actions;
 use Drupal\eca\Service\Conditions;
 use Drupal\eca\Service\Modellers;
 use Drupal\token\TreeBuilderInterface;
+use Illuminate\Support\Arr;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 
 /**
  * Class for providing ECA-data.
@@ -37,6 +40,10 @@ class DataProvider implements DataProviderInterface {
    *   The actions.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
    *   The entity type manager.
+   * @param \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface $modelMapper
+   *   The model mapper.
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The normalizer.
    * @param \Drupal\token\TreeBuilderInterface $treeBuilder
    *   The token tree builder.
    */
@@ -45,6 +52,8 @@ class DataProvider implements DataProviderInterface {
     protected Conditions $conditions,
     protected Actions $actions,
     protected EntityTypeManagerInterface $entityTypeManager,
+    protected ModelMapperInterface $modelMapper,
+    protected NormalizerInterface $normalizer,
     protected TreeBuilderInterface $treeBuilder,
   ) {
   }
@@ -125,21 +134,14 @@ class DataProvider implements DataProviderInterface {
     }
 
     return array_reduce($models, function (array $carry, Eca $eca) {
-      $info = [
-        'model_id' => $eca->id(),
-        'label' => $eca->label(),
-      ];
-      if (!empty($eca->getModel()->getDocumentation())) {
-        $info['description'] = $eca->getModel()->getDocumentation();
-      }
+      $model = $this->modelMapper->fromEntity($eca);
+      $data = array_filter($this->normalizer->normalize($model));
 
-      if ($this->viewMode === DataViewModeEnum::Full) {
-        $info['dependencies'] = $eca->getDependencies();
-        $info['events'] = $eca->getEventInfos();
-        $info['conditions'] = $eca->getConditions();
-        $info['actions'] = $eca->getActions();
+      if ($this->viewMode === DataViewModeEnum::Teaser) {
+        $data = Arr::only($data, ['id', 'label', 'description']);
       }
-      $carry[] = $info;
+
+      $carry[] = $data;
 
       return $carry;
     }, []);
-- 
GitLab


From 39c0c3bb233c48ad1d55af7ec3fc7892d5c8e261 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sat, 21 Dec 2024 17:53:12 +0100
Subject: [PATCH 64/95] #348130 Test mapping an ECA-entity to the EcaModel data
 type

---
 .../tests/src/Kernel/ModelMapperTest.php      |  79 +++++
 ...pingFromEntity.approved.eca_test_0001.json | 284 ++++++++++++++++
 ...pingFromEntity.approved.eca_test_0002.json | 179 ++++++++++
 ...pingFromEntity.approved.eca_test_0009.json | 306 ++++++++++++++++++
 4 files changed, 848 insertions(+)
 create mode 100644 modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
 create mode 100644 modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
 create mode 100644 modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json

diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index c63c57b..09a512d 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -2,7 +2,10 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
+use ApprovalTests\Approvals;
 use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 
 /**
  * Tests various input data for generation ECA Model typed data.
@@ -11,6 +14,31 @@ use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
  */
 class ModelMapperTest extends AiEcaAgentsKernelTestBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'eca_user',
+    'eca_test_model_cross_ref',
+    'eca_test_model_entity_basics',
+    'eca_test_model_set_field_value',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'serialization',
+    'schemata',
+    'schemata_json_schema',
+    'system',
+    'taxonomy',
+  ];
+
   /**
    * The model mapper.
    *
@@ -18,6 +46,20 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
    */
   protected ?ModelMapperInterface $modelMapper;
 
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
+   */
+  protected ?EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The normalizer.
+   *
+   * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface|null
+   */
+  protected ?NormalizerInterface $normalizer;
+
   /**
    * Build an ECA Model type data object with the provided payloads.
    *
@@ -43,6 +85,41 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
     }
   }
 
+  /**
+   * Build an ECA Model data type object with the provided entities.
+   *
+   * @dataProvider entityProvider
+   */
+  public function testMappingFromEntity(string $entityId): void {
+    /** @var \Drupal\eca\Entity\Eca $entity */
+    $entity = $this->entityTypeManager->getStorage('eca')
+      ->load($entityId);
+    $model = $this->modelMapper->fromEntity($entity);
+    $data = $this->normalizer->normalize($model);
+
+    Approvals::verifyStringWithFileExtension(json_encode($data, JSON_PRETTY_PRINT), sprintf('%s.json', $entityId));
+  }
+
+  /**
+   * Generate different sets of ECA entity IDs.
+   *
+   * @return \Generator
+   *    Returns a collection of ECA entity IDs.
+   */
+  public static function entityProvider(): \Generator {
+    yield [
+      'eca_test_0001',
+    ];
+
+    yield [
+      'eca_test_0002',
+    ];
+
+    yield [
+      'eca_test_0009',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -50,6 +127,8 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
     parent::setUp();
 
     $this->modelMapper = \Drupal::service('ai_eca_agents.services.model_mapper');
+    $this->entityTypeManager = \Drupal::entityTypeManager();
+    $this->normalizer = \Drupal::service('serializer');
   }
 
 }
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
new file mode 100644
index 0000000..a1068b8
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
@@ -0,0 +1,284 @@
+{
+    "id": "eca_test_0001",
+    "label": "Cross references",
+    "description": "Two different node types are referring each other. If one node gets saved with a reference to another node, the other node gets automatically updated to link back to the first node.",
+    "events": [
+        {
+            "id": "Event_011cx7s",
+            "plugin": "content_entity:insert",
+            "label": "Insert node",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1rlgsjy",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Event_1cfd8ek",
+            "plugin": "content_entity:update",
+            "label": "Update node",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1rlgsjy",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_1cxcwjm",
+                    "condition": null
+                }
+            ]
+        }
+    ],
+    "conditions": [
+        {
+            "id": "Flow_0iztkfs",
+            "plugin": "eca_entity_type_bundle",
+            "configuration": {
+                "negate": false,
+                "type": "node type_1",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_1jqykgu",
+            "plugin": "eca_entity_type_bundle",
+            "configuration": {
+                "negate": false,
+                "type": "node type_2",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0i81v8o",
+            "plugin": "eca_entity_field_value",
+            "configuration": {
+                "case": false,
+                "expected_value": "[entity:nid]",
+                "field_name": "field_other_node",
+                "operator": "equal",
+                "type": "value",
+                "negate": true,
+                "entity": "refentity"
+            }
+        },
+        {
+            "id": "Flow_1tgic5x",
+            "plugin": "eca_entity_field_value_empty",
+            "configuration": {
+                "field_name": "field_other_node",
+                "negate": true,
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0rgzuve",
+            "plugin": "eca_entity_field_value_empty",
+            "configuration": {
+                "negate": false,
+                "field_name": "field_other_node",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0c3s897",
+            "plugin": "eca_entity_field_value_empty",
+            "configuration": {
+                "field_name": "field_other_node",
+                "negate": true,
+                "entity": "originalentity"
+            }
+        }
+    ],
+    "gateways": [
+        {
+            "id": "Gateway_1xl2rvc",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_0k6im8f",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_1oj601y",
+                    "condition": null
+                }
+            ]
+        }
+    ],
+    "actions": [
+        {
+            "id": "Activity_1rlgsjy",
+            "plugin": "eca_token_load_entity",
+            "label": "Load original entity",
+            "configuration": {
+                "token_name": "originalentity",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": true,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "id": "Activity_0r1gs9s",
+                    "condition": "Flow_0iztkfs"
+                },
+                {
+                    "id": "Activity_0r1gs9s",
+                    "condition": "Flow_1jqykgu"
+                }
+            ]
+        },
+        {
+            "id": "Activity_0h8b7vh",
+            "plugin": "eca_token_load_entity_ref",
+            "label": "Load referenced node",
+            "configuration": {
+                "field_name_entity_ref": "field_other_node",
+                "token_name": "refentity",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "id": "Gateway_1xl2rvc",
+                    "condition": "Flow_0i81v8o"
+                }
+            ]
+        },
+        {
+            "id": "Activity_0k6im8f",
+            "plugin": "eca_set_field_value",
+            "label": "Set Cross Ref",
+            "configuration": {
+                "field_name": "field_other_node",
+                "field_value": "[entity:nid]",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": true,
+                "object": "refentity"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_0r1gs9s",
+            "plugin": "eca_void_and_condition",
+            "label": "void",
+            "configuration": null,
+            "successors": [
+                {
+                    "id": "Activity_0h8b7vh",
+                    "condition": "Flow_1tgic5x"
+                },
+                {
+                    "id": "Activity_1ch3wrr",
+                    "condition": "Flow_0rgzuve"
+                }
+            ]
+        },
+        {
+            "id": "Activity_1oj601y",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "Node [entity:title] references [refentity:title]",
+                "replace_tokens": true
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1cxcwjm",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "Node [entity:title] got updated",
+                "replace_tokens": true
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1w7m4sk",
+            "plugin": "eca_token_load_entity_ref",
+            "label": "Load referenced node",
+            "configuration": {
+                "field_name_entity_ref": "field_other_node",
+                "token_name": "originalentityref",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": false,
+                "object": "originalentity"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1bfoheo",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_077d2t8",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Activity_1ch3wrr",
+            "plugin": "eca_void_and_condition",
+            "label": "void",
+            "configuration": null,
+            "successors": [
+                {
+                    "id": "Activity_1w7m4sk",
+                    "condition": "Flow_0c3s897"
+                }
+            ]
+        },
+        {
+            "id": "Activity_1bfoheo",
+            "plugin": "eca_set_field_value",
+            "label": "Empty Cross Ref",
+            "configuration": {
+                "field_name": "field_other_node",
+                "field_value": "",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": true,
+                "object": "originalentityref"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_077d2t8",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "The title of the referenced node is [originalentity:field_other_node:entity:title].",
+                "replace_tokens": true
+            },
+            "successors": []
+        }
+    ]
+}
\ No newline at end of file
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
new file mode 100644
index 0000000..b321c82
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
@@ -0,0 +1,179 @@
+{
+    "id": "eca_test_0002",
+    "label": "Entity Events Part 1",
+    "description": "Triggers custom events in the same model and in another model, see also \"Entity Events Part 2\"",
+    "events": [
+        {
+            "id": "Event_0wm7ta0",
+            "plugin": "content_entity:presave",
+            "label": "Pre-save",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1do22d1",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Event_0sr0xl6",
+            "plugin": "content_entity:custom",
+            "label": "C1",
+            "configuration": {
+                "event_id": "C1"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1sh3bdl",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Event_1l6ov1l",
+            "plugin": "user:set_user",
+            "label": "Set current user",
+            "configuration": null,
+            "successors": [
+                {
+                    "id": "Activity_1p5hvp4",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Event_0n1zpul",
+            "plugin": "eca_base:eca_custom",
+            "label": "Cplain",
+            "configuration": {
+                "event_id": ""
+            },
+            "successors": [
+                {
+                    "id": "Activity_1gguvde",
+                    "condition": null
+                }
+            ]
+        }
+    ],
+    "conditions": [],
+    "gateways": [],
+    "actions": [
+        {
+            "id": "Activity_1do22d1",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message 0: [entity:title]"
+            },
+            "successors": [
+                {
+                    "id": "Activity_03j3ob6",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_1k70gka",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_150pgta",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_00ca469",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Activity_03j3ob6",
+            "plugin": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C1",
+            "configuration": {
+                "event_id": "C1",
+                "tokens": "",
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1k70gka",
+            "plugin": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C2",
+            "configuration": {
+                "event_id": "C2",
+                "tokens": "",
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_150pgta",
+            "plugin": "eca_token_load_user_current",
+            "label": "Load current user",
+            "configuration": {
+                "token_name": "user"
+            },
+            "successors": [
+                {
+                    "id": "Activity_1acmymx",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Activity_1acmymx",
+            "plugin": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C3",
+            "configuration": {
+                "event_id": "C3",
+                "tokens": "",
+                "object": "user"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1sh3bdl",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message 1: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1p5hvp4",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message set current user: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1gguvde",
+            "plugin": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message without event: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_00ca469",
+            "plugin": "eca_trigger_custom_event",
+            "label": "Trigger Cplain",
+            "configuration": {
+                "event_id": "Cplain",
+                "tokens": ""
+            },
+            "successors": []
+        }
+    ]
+}
\ No newline at end of file
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
new file mode 100644
index 0000000..df43949
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
@@ -0,0 +1,306 @@
+{
+    "id": "eca_test_0009",
+    "label": "Set field values",
+    "description": "Set single and multi value fields with values, testing different variations.",
+    "events": [
+        {
+            "id": "Event_056l2f4",
+            "plugin": "content_entity:presave",
+            "label": "Presave Node",
+            "configuration": {
+                "type": "node type_set_field_value"
+            },
+            "successors": [
+                {
+                    "id": "Gateway_113xj72",
+                    "condition": "Flow_0j7r2le"
+                },
+                {
+                    "id": "Gateway_0nagg07",
+                    "condition": "Flow_1eoahw0"
+                },
+                {
+                    "id": "Gateway_1tbbhie",
+                    "condition": "Flow_0e3yjwm"
+                },
+                {
+                    "id": "Gateway_1byzhmc",
+                    "condition": "Flow_0wind58"
+                },
+                {
+                    "id": "Gateway_0aowp4i",
+                    "condition": "Flow_0iwzr0t"
+                }
+            ]
+        }
+    ],
+    "conditions": [
+        {
+            "id": "Flow_0j7r2le",
+            "plugin": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Append",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_1eoahw0",
+            "plugin": "eca_entity_is_new",
+            "configuration": {
+                "negate": false,
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0e3yjwm",
+            "plugin": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Drop First",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0wind58",
+            "plugin": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Reset",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "id": "Flow_0iwzr0t",
+            "plugin": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Drop last",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        }
+    ],
+    "gateways": [
+        {
+            "id": "Gateway_113xj72",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_0na1ecf",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_1on1kw2",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Gateway_1tbbhie",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_03beihz",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Gateway_0nagg07",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_1agtxee",
+                    "condition": null
+                },
+                {
+                    "id": "Activity_13qlvkq",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Gateway_1byzhmc",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_0yt6yuv",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Gateway_0aowp4i",
+            "type": null,
+            "successors": [
+                {
+                    "id": "Activity_036vgtd",
+                    "condition": null
+                }
+            ]
+        }
+    ],
+    "actions": [
+        {
+            "id": "Activity_1agtxee",
+            "plugin": "eca_set_field_value",
+            "label": "Set text line",
+            "configuration": {
+                "field_name": "field_text_line",
+                "field_value": "Title is [entity:title].",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_13qlvkq",
+            "plugin": "eca_set_field_value",
+            "label": "Set lines 1",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 1",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_0na1ecf",
+            "plugin": "eca_set_field_value",
+            "label": "Overwrite text line",
+            "configuration": {
+                "field_name": "field_text_line",
+                "field_value": "The updated text line content.",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_1on1kw2",
+            "plugin": "eca_set_field_value",
+            "label": "Append line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Second line",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "id": "Activity_0aa91q1",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Activity_0aa91q1",
+            "plugin": "eca_set_field_value",
+            "label": "Append another line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 3",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "id": "Activity_0zcaglk",
+                    "condition": null
+                }
+            ]
+        },
+        {
+            "id": "Activity_0zcaglk",
+            "plugin": "eca_set_field_value",
+            "label": "Append another line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 4",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_03beihz",
+            "plugin": "eca_set_field_value",
+            "label": "Append line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 4",
+                "method": "append:drop_first",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_0yt6yuv",
+            "plugin": "eca_set_field_value",
+            "label": "Reset lines",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "This is one line.",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "id": "Activity_036vgtd",
+            "plugin": "eca_set_field_value",
+            "label": "Prepend line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Inserted line",
+                "method": "prepend:drop_last",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        }
+    ]
+}
\ No newline at end of file
-- 
GitLab


From b269874cd517b3cfbaa6668b1450023f7ae08dd9 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Wed, 25 Dec 2024 13:52:40 +0100
Subject: [PATCH 65/95] #348130 Fix type-mapping for Gateways

---
 .../src/Services/ModelMapper/ModelMapper.php  | 15 +++++-----
 .../src/TypedData/EcaGatewayDefinition.php    |  2 +-
 .../tests/src/Kernel/EcaModelSchemaTest.php   |  1 -
 ...pingFromEntity.approved.eca_test_0001.json | 20 ++++++-------
 ...pingFromEntity.approved.eca_test_0002.json | 20 ++++++-------
 ...pingFromEntity.approved.eca_test_0009.json | 28 +++++++++----------
 6 files changed, 43 insertions(+), 43 deletions(-)

diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index 90c1c0e..bc000da 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -12,6 +12,7 @@ use Drupal\Core\TypedData\Exception\MissingDataException;
 use Drupal\Core\TypedData\TypedDataManagerInterface;
 use Drupal\eca\Entity\Eca;
 use Drupal\eca\Entity\Objects\EcaEvent;
+use Illuminate\Support\Arr;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 use Symfony\Component\Validator\ConstraintViolationListInterface;
 
@@ -79,7 +80,7 @@ class ModelMapper implements ModelMapperInterface {
       $model->set('configuration', $event->getConfiguration());
       $model->set('successors', $successors);
 
-      $carry[] = array_filter($this->normalizer->normalize($model));
+      $carry[] = $this->normalizer->normalize($model);
 
       return $carry;
     }, []);
@@ -96,7 +97,7 @@ class ModelMapper implements ModelMapperInterface {
       $model->set('plugin', $conditions[$conditionId]['plugin']);
       $model->set('configuration', $conditions[$conditionId]['configuration']);
 
-      $carry[] = array_filter($this->normalizer->normalize($model));
+      $carry[] = $this->normalizer->normalize($model);
 
       return $carry;
     }, []);
@@ -117,7 +118,7 @@ class ModelMapper implements ModelMapperInterface {
       $model->set('configuration', $actions[$actionId]['configuration']);
       $model->set('successors', $successors);
 
-      $carry[] = array_filter($this->normalizer->normalize($model));
+      $carry[] = $this->normalizer->normalize($model);
 
       return $carry;
     }, []);
@@ -134,7 +135,7 @@ class ModelMapper implements ModelMapperInterface {
       $model->set('type', $gateways[$gatewayId]['type']);
       $model->set('successors', $successors);
 
-      $carry[] = array_filter($this->normalizer->normalize($model));
+      $carry[] = $this->normalizer->normalize($model);
 
       return $carry;
     }, []);
@@ -179,16 +180,16 @@ class ModelMapper implements ModelMapperInterface {
    * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
    */
   protected function mapSuccessorsToModel(array $successors): array {
-    return array_reduce($successors, function ($carry, array $successor) {
+    return array_filter(array_reduce($successors, function ($carry, array $successor) {
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaSuccessor $model */
       $model = $this->typedDataManager->create(EcaSuccessorDefinition::create());
       $model->set('id', $successor['id']);
       $model->set('condition', $successor['condition']);
 
-      $carry[] = array_filter($this->normalizer->normalize($model));
+      $carry[] = Arr::whereNotNull($this->normalizer->normalize($model));
 
       return $carry;
-    }, []);
+    }, []));
   }
 
 }
diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
index 0e42c02..ad6859d 100644
--- a/modules/agents/src/TypedData/EcaGatewayDefinition.php
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -30,7 +30,7 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
     $properties['type'] = DataDefinition::create('integer')
       ->setLabel(new TranslatableMarkup('Type'))
       ->setSetting('allowed_values_function', '')
-      ->setSetting('allowed_values', ['0'])
+      ->setSetting('allowed_values', [0])
       ->addConstraint('Choice', [
         'choices' => [0],
       ]);
diff --git a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
index 7d8cf3e..b96a554 100644
--- a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
+++ b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
@@ -5,7 +5,6 @@ namespace Drupal\Tests\ai_eca_agents\Kernel;
 use ApprovalTests\Approvals;
 use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
 use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
-use Drupal\Component\Serialization\Json;
 use Drupal\KernelTests\KernelTestBase;
 use Symfony\Component\Serializer\SerializerInterface;
 
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
index a1068b8..ddc9abf 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
@@ -13,7 +13,7 @@
             "successors": [
                 {
                     "id": "Activity_1rlgsjy",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -27,11 +27,11 @@
             "successors": [
                 {
                     "id": "Activity_1rlgsjy",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_1cxcwjm",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         }
@@ -99,15 +99,15 @@
     "gateways": [
         {
             "id": "Gateway_1xl2rvc",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_0k6im8f",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_1oj601y",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         }
@@ -183,7 +183,7 @@
             "id": "Activity_0r1gs9s",
             "plugin": "eca_void_and_condition",
             "label": "void",
-            "configuration": null,
+            "configuration": [],
             "successors": [
                 {
                     "id": "Activity_0h8b7vh",
@@ -235,11 +235,11 @@
             "successors": [
                 {
                     "id": "Activity_1bfoheo",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_077d2t8",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -247,7 +247,7 @@
             "id": "Activity_1ch3wrr",
             "plugin": "eca_void_and_condition",
             "label": "void",
-            "configuration": null,
+            "configuration": [],
             "successors": [
                 {
                     "id": "Activity_1w7m4sk",
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
index b321c82..7f995c9 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
@@ -13,7 +13,7 @@
             "successors": [
                 {
                     "id": "Activity_1do22d1",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -27,7 +27,7 @@
             "successors": [
                 {
                     "id": "Activity_1sh3bdl",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -35,11 +35,11 @@
             "id": "Event_1l6ov1l",
             "plugin": "user:set_user",
             "label": "Set current user",
-            "configuration": null,
+            "configuration": [],
             "successors": [
                 {
                     "id": "Activity_1p5hvp4",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -53,7 +53,7 @@
             "successors": [
                 {
                     "id": "Activity_1gguvde",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         }
@@ -72,19 +72,19 @@
             "successors": [
                 {
                     "id": "Activity_03j3ob6",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_1k70gka",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_150pgta",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_00ca469",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -120,7 +120,7 @@
             "successors": [
                 {
                     "id": "Activity_1acmymx",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
index df43949..bad34d2 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
@@ -99,59 +99,59 @@
     "gateways": [
         {
             "id": "Gateway_113xj72",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_0na1ecf",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_1on1kw2",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
         {
             "id": "Gateway_1tbbhie",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_03beihz",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
         {
             "id": "Gateway_0nagg07",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_1agtxee",
-                    "condition": null
+                    "condition": ""
                 },
                 {
                     "id": "Activity_13qlvkq",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
         {
             "id": "Gateway_1byzhmc",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_0yt6yuv",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
         {
             "id": "Gateway_0aowp4i",
-            "type": null,
+            "type": 0,
             "successors": [
                 {
                     "id": "Activity_036vgtd",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         }
@@ -218,7 +218,7 @@
             "successors": [
                 {
                     "id": "Activity_0aa91q1",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
@@ -238,7 +238,7 @@
             "successors": [
                 {
                     "id": "Activity_0zcaglk",
-                    "condition": null
+                    "condition": ""
                 }
             ]
         },
-- 
GitLab


From b76055ab18ac30f8b3474264d9448b5bfbdcf0ad Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 26 Dec 2024 16:39:30 +0100
Subject: [PATCH 66/95] #348130 Enable the repository to alter existing models
 as well

---
 .../agents/src/EntityViolationException.php   | 27 +++++++++----------
 .../agents/src/Plugin/DataType/EcaModel.php   |  2 +-
 .../Services/EcaRepository/EcaRepository.php  | 21 ++++++++-------
 .../EcaRepository/EcaRepositoryInterface.php  |  4 ++-
 .../src/TypedData/EcaModelDefinition.php      |  3 +++
 5 files changed, 31 insertions(+), 26 deletions(-)

diff --git a/modules/agents/src/EntityViolationException.php b/modules/agents/src/EntityViolationException.php
index ed407ab..f765f60 100644
--- a/modules/agents/src/EntityViolationException.php
+++ b/modules/agents/src/EntityViolationException.php
@@ -13,9 +13,9 @@ class EntityViolationException extends ConfigException {
   /**
    * The constraint violations associated with this exception.
    *
-   * @var \Symfony\Component\Validator\ConstraintViolationListInterface
+   * @var \Symfony\Component\Validator\ConstraintViolationListInterface|null
    */
-  protected ConstraintViolationListInterface $violations;
+  protected ?ConstraintViolationListInterface $violations;
 
   /**
    * EntityViolationException constructor.
@@ -26,11 +26,18 @@ class EntityViolationException extends ConfigException {
    *   The Exception code.
    * @param \Throwable|null $previous
    *   The previous throwable used for the exception chaining.
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface|null $violations
+   *   The constraint violations associated with this exception
    */
-  public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = NULL) {
+  public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = NULL, ?ConstraintViolationListInterface $violations = NULL) {
+    $this->violations = $violations;
+
     if (empty($message)) {
       $message = 'Validation of the entity failed.';
     }
+    if (!empty($violations)) {
+      $message = sprintf('%s Violations: %s.', $message, (string) $violations);
+    }
 
     parent::__construct($message, $code, $previous);
   }
@@ -38,21 +45,11 @@ class EntityViolationException extends ConfigException {
   /**
    * Gets the constraint violations associated with this exception.
    *
-   * @return \Symfony\Component\Validator\ConstraintViolationListInterface
+   * @return \Symfony\Component\Validator\ConstraintViolationListInterface|null
    *   The constraint violations.
    */
-  public function getViolations(): ConstraintViolationListInterface {
+  public function getViolations(): ?ConstraintViolationListInterface {
     return $this->violations;
   }
 
-  /**
-   * Sets the constraint violations associated with this exception.
-   *
-   * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
-   *   The constraint violations.
-   */
-  public function setViolations(ConstraintViolationListInterface $violations): void {
-    $this->violations = $violations;
-  }
-
 }
diff --git a/modules/agents/src/Plugin/DataType/EcaModel.php b/modules/agents/src/Plugin/DataType/EcaModel.php
index 63b70b0..fb622d0 100644
--- a/modules/agents/src/Plugin/DataType/EcaModel.php
+++ b/modules/agents/src/Plugin/DataType/EcaModel.php
@@ -21,7 +21,7 @@ class EcaModel extends Map {
    * {@inheritdoc}
    */
   public function getName(): string {
-    return $this->get('label')->getString();
+    return !empty($this->get('label')->getString()) ? $this->get('label')->getString() : 'the model';
   }
 
 }
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index f3e17da..efa790f 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -42,21 +42,27 @@ class EcaRepository implements EcaRepositoryInterface {
   /**
    * {@inheritdoc}
    */
-  public function build(array $data, bool $save = TRUE): Eca {
+  public function build(array $data, bool $save = TRUE, ?string $id = NULL): Eca {
+    /** @var \Drupal\eca\Entity\EcaStorage $storage */
+    $storage = $this->entityTypeManager->getStorage('eca');
     /** @var \Drupal\eca\Entity\Eca $eca */
-    $eca = $this->entityTypeManager->getStorage('eca')
-      ->create();
+    $eca = $storage->create();
+    if (!empty($id)) {
+      $eca = $storage->load($id);
+    }
 
     // Convert the given data to the ECA-model.
     $model = $this->modelMapper->fromPayload($data);
 
     // Map the model to the entity.
     $random = new Random();
-    $eca->set('id', $model->get('id')->getString() ?? sprintf('process_%s', $random->name(7)));
+    $idFallback = $model->get('id')?->getString() ?? sprintf('process_%s', $random->name(7));
+    $eca->set('id', $id ?? $idFallback);
     $eca->set('label', $model->get('label')->getString());
     $eca->set('modeller', 'fallback');
-    $eca->set('version', '0.0.1');
+    $eca->set('version', $eca->get('version') ?? '0.0.1');
     $eca->setStatus(FALSE);
+    $eca->resetComponents();
 
     // Set events.
     /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
@@ -122,10 +128,7 @@ class EcaRepository implements EcaRepositoryInterface {
     $violations = $this->typedDataManager->create($definition, $eca)
       ->validate();
     if ($violations->count()) {
-      $exception = new EntityViolationException();
-      $exception->setViolations($violations);
-
-      throw $exception;
+      throw new EntityViolationException('', 0, NULL, $violations);
     }
     if (empty($eca->getUsedEvents())) {
       throw new MissingEventException('No events registered.');
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
index 3a9c64f..f1f571e 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
@@ -27,10 +27,12 @@ interface EcaRepositoryInterface {
    *   The data to use.
    * @param bool $save
    *   Toggle to save the model.
+   * @param string|null $id
+   *   The ID of an existing model to load.
    *
    * @return \Drupal\eca\Entity\Eca
    *   Returns the ECA-model.
    */
-  public function build(array $data, bool $save = TRUE): Eca;
+  public function build(array $data, bool $save = TRUE, ?string $id = NULL): Eca;
 
 }
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index dbea888..ab905a2 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -33,6 +33,9 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->addConstraint('Length', ['max' => 255])
       ->addConstraint('NotBlank');
 
+    $properties['version'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Version'));
+
     $properties['description'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('Description'));
 
-- 
GitLab


From 1e2d1c7d1341ba22db7af07cae86b6edd4ca6aa9 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 26 Dec 2024 16:39:55 +0100
Subject: [PATCH 67/95] #348130 Adjust existing tests for model updates

---
 modules/agents/tests/assets/from_payload_3.json  |  1 +
 .../src/Kernel/AiEcaAgentsKernelTestBase.php     | 16 ++++++++++++++++
 .../tests/src/Kernel/EcaRepositoryTest.php       |  4 ++--
 .../agents/tests/src/Kernel/ModelMapperTest.php  |  2 +-
 .../EcaModelSchemaTest.testSchema.approved.json  |  2 +-
 ...MappingFromEntity.approved.eca_test_0001.json |  1 +
 ...MappingFromEntity.approved.eca_test_0002.json |  1 +
 ...MappingFromEntity.approved.eca_test_0009.json |  1 +
 8 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/modules/agents/tests/assets/from_payload_3.json b/modules/agents/tests/assets/from_payload_3.json
index 65deddd..a7999e5 100644
--- a/modules/agents/tests/assets/from_payload_3.json
+++ b/modules/agents/tests/assets/from_payload_3.json
@@ -1,6 +1,7 @@
 {
   "id": "create_article_on_new_page",
   "label": "Create Article on New Page",
+  "version": "v1",
   "events": [
     {
       "id": "start_event",
diff --git a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
index 558e0fd..e3e68a0 100644
--- a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -20,6 +20,7 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
     'eca',
     'eca_base',
     'eca_content',
+    'eca_test_model_cross_ref',
     'eca_user',
     'field',
     'node',
@@ -52,12 +53,14 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
     yield [
       Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
       [],
+      NULL,
       'id: This value should not be null',
     ];
 
     yield [
       Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
       [],
+      NULL,
       'events: This value should not be null',
     ];
 
@@ -71,6 +74,19 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
         'label' => 'Create Article on New Page',
       ],
     ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+        'version' => 'v1',
+      ],
+      'eca_test_0001',
+    ];
   }
 
   /**
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
index a67f376..a82ca3a 100644
--- a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -23,12 +23,12 @@ class EcaRepositoryTest extends AiEcaAgentsKernelTestBase {
    *
    * @dataProvider payloadProvider
    */
-  public function testBuildModel(array $data, array $assertions, ?string $errorMessage = NULL): void {
+  public function testBuildModel(array $data, array $assertions, ?string $id = NULL, ?string $errorMessage = NULL): void {
     if (!empty($errorMessage)) {
       $this->expectExceptionMessage($errorMessage);
     }
 
-    $eca = $this->ecaRepository->build($data);
+    $eca = $this->ecaRepository->build($data, TRUE, $id);
 
     foreach ($assertions as $property => $expected) {
       switch (TRUE) {
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index 09a512d..9645bcb 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -65,7 +65,7 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
    *
    * @dataProvider payloadProvider
    */
-  public function testMappingFromPayload(array $payload, array $assertions, ?string $errorMessage = NULL): void {
+  public function testMappingFromPayload(array $payload, array $assertions, ?string $id = NULL, ?string $errorMessage = NULL): void {
     if (!empty($errorMessage)) {
       $this->expectExceptionMessage($errorMessage);
     }
diff --git a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
index 8137eaf..f2c66b4 100644
--- a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
+++ b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
@@ -1 +1 @@
-{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
+{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"version":{"type":"string","title":"Version"},"description":{"type":"string","title":"Description"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
index ddc9abf..e803eb1 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
@@ -1,6 +1,7 @@
 {
     "id": "eca_test_0001",
     "label": "Cross references",
+    "version": null,
     "description": "Two different node types are referring each other. If one node gets saved with a reference to another node, the other node gets automatically updated to link back to the first node.",
     "events": [
         {
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
index 7f995c9..5a3473c 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
@@ -1,6 +1,7 @@
 {
     "id": "eca_test_0002",
     "label": "Entity Events Part 1",
+    "version": null,
     "description": "Triggers custom events in the same model and in another model, see also \"Entity Events Part 2\"",
     "events": [
         {
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
index bad34d2..d254ceb 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
+++ b/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
@@ -1,6 +1,7 @@
 {
     "id": "eca_test_0009",
     "label": "Set field values",
+    "version": null,
     "description": "Set single and multi value fields with values, testing different variations.",
     "events": [
         {
-- 
GitLab


From b1a5ae8f1fa4f98c2743560f0c4ea94706741af7 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 26 Dec 2024 16:40:38 +0100
Subject: [PATCH 68/95] #348130 Provide the model to the AI that needs to be
 altered

---
 modules/agents/prompts/eca/buildModel.yml |  8 +++-----
 modules/agents/src/Plugin/AiAgent/Eca.php | 13 ++++++++-----
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index ee74aa4..532b685 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -24,12 +24,10 @@ prompt:
     When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
     optimization (if it does not contradict the user intended sequence).
     Use clear names for labels and conditions.
-    Aim for granular detail (e.g., instead of ”Task 1: Action 1 and Action 2”, use ”Task 1:
-    Action 1” and ”Task 2: Action 2”).
 
-    All elements, except gateways, must have a plugin assigned to them and optionally an array configuration parameters.
-    You will given a list of possible plugins and their corresponding configuration structure, you can not deviate from
-    those.
+    All elements, except gateways, must have a plugin assigned to them and optionally an array of configuration
+    parameters. You will given a list of possible plugins and their corresponding configuration structure, you can not
+    deviate from those.
 
     Sometimes you will be given a previous JSON solution with user instructions to edit.
   formats: []
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 1f83313..3276fba 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -174,11 +174,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     switch (Arr::get($this->dto, 'data.0.action')) {
       case 'create':
-        $this->createConfig();
-        break;
-
       case 'edit':
-        $this->dto['logs'][] = 'edit';
+        $this->buildModel();
         break;
 
       case 'info':
@@ -333,7 +330,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
   /**
    * Create a configuration item for ECA.
    */
-  protected function createConfig(): void {
+  protected function buildModel(): void {
     $this->dataProvider->setViewMode(DataViewModeEnum::Full);
 
     // Prepare the prompt.
@@ -346,6 +343,12 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
     $context['JSON Schema of the process'] = sprintf("```json\n%s\n```", $schema);
 
+    // The model that should be edited.
+    if (!empty($this->model)) {
+      $models = $this->dataProvider->getModels([$this->model->id()]);
+      $context['The model to edit'] = sprintf("```json\n%s\n```", reset($models));
+    }
+
     // Components or plugins that the LLM should use.
     if (Arr::has($this->dto, 'data.0.component_ids')) {
       $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
-- 
GitLab


From b034dd10d326651edb074a7d3c2cc348ed007580 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Thu, 26 Dec 2024 16:57:02 +0100
Subject: [PATCH 69/95] #3481307 Fix phpcs issues

---
 .cspell-project-words.txt                           | 4 ----
 modules/agents/src/EntityViolationException.php     | 2 +-
 modules/agents/tests/src/Kernel/ModelMapperTest.php | 2 +-
 3 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index c67d11d..e69de29 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -1,4 +0,0 @@
-beginswith
-hqinah
-Retryable
-vndw
diff --git a/modules/agents/src/EntityViolationException.php b/modules/agents/src/EntityViolationException.php
index f765f60..d498b2d 100644
--- a/modules/agents/src/EntityViolationException.php
+++ b/modules/agents/src/EntityViolationException.php
@@ -27,7 +27,7 @@ class EntityViolationException extends ConfigException {
    * @param \Throwable|null $previous
    *   The previous throwable used for the exception chaining.
    * @param \Symfony\Component\Validator\ConstraintViolationListInterface|null $violations
-   *   The constraint violations associated with this exception
+   *   The constraint violations associated with this exception.
    */
   public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = NULL, ?ConstraintViolationListInterface $violations = NULL) {
     $this->violations = $violations;
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index 9645bcb..0103c86 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -104,7 +104,7 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
    * Generate different sets of ECA entity IDs.
    *
    * @return \Generator
-   *    Returns a collection of ECA entity IDs.
+   *   Returns a collection of ECA entity IDs.
    */
   public static function entityProvider(): \Generator {
     yield [
-- 
GitLab


From 7ab5e31295244fea612da8427e8eeaaee9b6a908 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:19:18 +0100
Subject: [PATCH 70/95] #3481307 Replace public constant with enum

---
 modules/agents/src/EcaElementType.php         | 45 +++++++++++++++++++
 .../Services/DataProvider/DataProvider.php    |  9 ++--
 .../src/Services/ModelMapper/ModelMapper.php  |  7 +--
 .../src/TypedData/EcaModelDefinition.php      |  7 +--
 .../src/TypedData/EcaPluginDefinition.php     | 21 ++++-----
 5 files changed, 67 insertions(+), 22 deletions(-)
 create mode 100644 modules/agents/src/EcaElementType.php

diff --git a/modules/agents/src/EcaElementType.php b/modules/agents/src/EcaElementType.php
new file mode 100644
index 0000000..9582e7e
--- /dev/null
+++ b/modules/agents/src/EcaElementType.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+/**
+ * Enum of the element types.
+ */
+enum EcaElementType: string {
+
+  case Event = 'event';
+  case Condition = 'condition';
+  case Action = 'action';
+  case Gateway = 'gateway';
+
+  /**
+   * Get the plural form of a type.
+   *
+   * @return string
+   *   Returns the plural form of a type.
+   */
+  public function getPlural(): string {
+    return match ($this) {
+      self::Event => 'events',
+      self::Condition => 'conditions',
+      self::Action => 'actions',
+      self::Gateway => 'gateways',
+    };
+  }
+
+  /**
+   * Returns a list of types, keyed by their plural form.
+   *
+   * @return \Drupal\ai_eca_agents\EcaElementType[]
+   *   The list of types, keyed by their plural form.
+   */
+  public static function getPluralMap(): array {
+    return [
+      self::Event->getPlural() => self::Event,
+      self::Condition->getPlural() => self::Condition,
+      self::Action->getPlural() => self::Action,
+      self::Gateway->getPlural() => self::Gateway,
+    ];
+  }
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index e7079cb..4dcb74e 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\Services\DataProvider;
 
+use Drupal\ai_eca_agents\EcaElementType;
 use Drupal\Component\Plugin\ConfigurableInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -112,10 +113,12 @@ class DataProvider implements DataProviderInterface {
    * {@inheritdoc}
    */
   public function getComponents(array $filterIds = []): array {
+    $filterFunction = fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE);
+
     return [
-      'events' => empty($filterIds) ? $this->getEvents() : array_filter($this->getEvents(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
-      'conditions' => empty($filterIds) ? $this->getConditions() : array_filter($this->getConditions(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
-      'actions' => empty($filterIds) ? $this->getActions() : array_filter($this->getActions(), fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE)),
+      EcaElementType::Event->getPlural() => empty($filterIds) ? $this->getEvents() : array_values(array_filter($this->getEvents(), $filterFunction)),
+      EcaElementType::Condition->getPlural() => empty($filterIds) ? $this->getConditions() : array_values(array_filter($this->getConditions(), $filterFunction)),
+      EcaElementType::Action->getPlural() => empty($filterIds) ? $this->getActions() : array_values(array_filter($this->getActions(), $filterFunction)),
     ];
   }
 
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index bc000da..d8619e1 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\Services\ModelMapper;
 
+use Drupal\ai_eca_agents\EcaElementType;
 use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
 use Drupal\ai_eca_agents\TypedData\EcaGatewayDefinition;
 use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
@@ -71,7 +72,7 @@ class ModelMapper implements ModelMapperInterface {
       $successors = $this->mapSuccessorsToModel($event->getSuccessors());
 
       $def = EcaPluginDefinition::create();
-      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_EVENT);
+      $def->setSetting('data_type', EcaElementType::Event);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
       $model->set('id', $event->getId());
@@ -90,7 +91,7 @@ class ModelMapper implements ModelMapperInterface {
     $conditions = $entity->getConditions();
     $plugins = array_reduce(array_keys($conditions), function (array $carry, string $conditionId) use ($conditions) {
       $def = EcaPluginDefinition::create();
-      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_CONDITION);
+      $def->setSetting('data_type', EcaElementType::Condition);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
       $model->set('id', $conditionId);
@@ -109,7 +110,7 @@ class ModelMapper implements ModelMapperInterface {
       $successors = $this->mapSuccessorsToModel($actions[$actionId]['successors']);
 
       $def = EcaPluginDefinition::create();
-      $def->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_ACTION);
+      $def->setSetting('data_type', EcaElementType::Action);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
       $model->set('id', $actionId);
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index ab905a2..79b0301 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\TypedData;
 
+use Drupal\ai_eca_agents\EcaElementType;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
 use Drupal\Core\TypedData\DataDefinition;
@@ -44,7 +45,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->setRequired(TRUE)
       ->setItemDefinition(
         EcaPluginDefinition::create()
-          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_EVENT)
+          ->setSetting('data_type', EcaElementType::Event)
       )
       ->addConstraint('NotNull');
 
@@ -52,7 +53,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->setLabel(new TranslatableMarkup('Conditions'))
       ->setItemDefinition(
         EcaPluginDefinition::create()
-          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_CONDITION)
+          ->setSetting('data_type', EcaElementType::Condition)
       );
 
     $properties['gateways'] = ListDataDefinition::create('eca_gateway')
@@ -63,7 +64,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
       ->setRequired(TRUE)
       ->setItemDefinition(
         EcaPluginDefinition::create()
-          ->setSetting('data_type', EcaPluginDefinition::DATA_TYPE_ACTION)
+          ->setSetting('data_type', EcaElementType::Action)
       )
       ->addConstraint('NotNull');
 
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index 8c4fa43..02e432d 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\ai_eca_agents\TypedData;
 
+use Drupal\ai_eca_agents\EcaElementType;
 use Drupal\Core\Action\ActionInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataDefinitionBase;
@@ -16,12 +17,6 @@ use Drupal\eca\Plugin\ECA\Event\EventInterface;
  */
 class EcaPluginDefinition extends ComplexDataDefinitionBase {
 
-  public const DATA_TYPE_EVENT = 'eca_event';
-
-  public const DATA_TYPE_CONDITION = 'eca_condition';
-
-  public const DATA_TYPE_ACTION = 'eca_action';
-
   protected const PROP_LABEL = 'label';
 
   protected const PROP_SUCCESSORS = 'successors';
@@ -84,9 +79,9 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    */
   protected function getPluginManagerId(): string {
     return match ($this->getSetting('data_type')) {
-      self::DATA_TYPE_ACTION => 'plugin.manager.action',
-      self::DATA_TYPE_EVENT => 'plugin.manager.eca.event',
-      self::DATA_TYPE_CONDITION => 'plugin.manager.eca.condition',
+      EcaElementType::Action => 'plugin.manager.action',
+      EcaElementType::Event => 'plugin.manager.eca.event',
+      EcaElementType::Condition => 'plugin.manager.eca.condition',
       default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin manager ID.', [
         '@type' => $this->getSetting('data_type'),
       ])),
@@ -101,9 +96,9 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
    */
   protected function getPluginInterface(): string {
     return match ($this->getSetting('data_type')) {
-      self::DATA_TYPE_ACTION => ActionInterface::class,
-      self::DATA_TYPE_EVENT => EventInterface::class,
-      self::DATA_TYPE_CONDITION => ConditionInterface::class,
+      EcaElementType::Action => ActionInterface::class,
+      EcaElementType::Event => EventInterface::class,
+      EcaElementType::Condition => ConditionInterface::class,
       default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin interface.', [
         '@type' => $this->getSetting('data_type'),
       ])),
@@ -123,7 +118,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
     ];
 
     return match ($this->getSetting('data_type')) {
-      self::DATA_TYPE_CONDITION => [],
+      EcaElementType::Condition => [],
       default => $default,
     };
   }
-- 
GitLab


From 10cc230c9bb9df1df4bbeb7579a031d7b00be060 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:24:18 +0100
Subject: [PATCH 71/95] #3481307 Create and use SuccessorsAreValid-constraint

---
 .../SuccessorAreValidConstraint.php           | 46 +++++++++
 .../SuccessorsAreValidConstraintValidator.php | 98 +++++++++++++++++++
 .../src/Services/ModelMapper/ModelMapper.php  |  6 +-
 .../src/TypedData/EcaModelDefinition.php      |  9 ++
 4 files changed, 158 insertions(+), 1 deletion(-)
 create mode 100644 modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php
 create mode 100644 modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php

diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php
new file mode 100644
index 0000000..aec9285
--- /dev/null
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Validation\Constraint;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Validation\Attribute\Constraint;
+use Symfony\Component\Validator\Constraint as SymfonyConstraint;
+
+/**
+ * Checks if all successors are valid.
+ */
+#[Constraint(
+  id: 'SuccessorsAreValid',
+  label: new TranslatableMarkup('Successor are valid')
+)]
+class SuccessorAreValidConstraint extends SymfonyConstraint {
+
+  /**
+   * The error message if the successor is invalid.
+   *
+   * @var string
+   */
+  public string $invalidSuccessorMessage = "Invalid successor ID '@successorId' for @type '@elementId'. Must be a gateway or action.";
+
+  /**
+   * The error message if the condition is invalid.
+   *
+   * @var string
+   */
+  public string $invalidConditionMessage = "Invalid condition '@conditionId' for successor of @type '@elementId'. Must be in the list of conditions.";
+
+  /**
+   * The error message if the event is succeeded by another event.
+   *
+   * @var string
+   */
+  public string $disallowedSuccessorEvent = "Event '@elementId' cannot be succeeded by another event.";
+
+  /**
+   * The error message if a non-event element is succeeded by an event.
+   *
+   * @var string
+   */
+  public string $disallowedSuccessor = "@type '@elementId' cannot be succeeded by an event.";
+
+}
diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
new file mode 100644
index 0000000..2a7406d
--- /dev/null
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Validation\Constraint;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validator of the SuccessorsAreValid-constraint.
+ */
+class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint): void {
+    assert($constraint instanceof SuccessorAreValidConstraint);
+
+    $lookup = [
+      EcaElementType::Event->getPlural() => array_flip(array_column($value['events'] ?? [], 'id')),
+      EcaElementType::Condition->getPlural() => array_flip(array_column($value['conditions'] ?? [], 'id')),
+      EcaElementType::Action->getPlural() => array_flip(array_column($value['actions'] ?? [], 'id')),
+      EcaElementType::Gateway->getPlural() => array_flip(array_column($value['gateways'] ?? [], 'id')),
+    ];
+
+    foreach (EcaElementType::getPluralMap() as $plural => $type) {
+      if (empty($value[$plural])) {
+        continue;
+      }
+
+      foreach ($value[$plural] as $element) {
+        $this->validateSuccessor($constraint, $element, $type, $lookup);
+      }
+    }
+  }
+
+  /**
+   * Validate the successors of an element.
+   *
+   * @param \Drupal\ai_eca_agents\Plugin\Validation\Constraint\SuccessorAreValidConstraint $constraint
+   *   The constraint.
+   * @param array $element
+   *   The element to validate.
+   * @param \Drupal\ai_eca_agents\EcaElementType $type
+   *   The type of the element.
+   * @param array $lookup
+   *   The lookup-array containing all the referencable IDs.
+   */
+  protected function validateSuccessor(SuccessorAreValidConstraint $constraint, array $element, EcaElementType $type, array $lookup): void {
+    if (empty($element['successors'])) {
+      return;
+    }
+
+    foreach ($element['successors'] as $successor) {
+      $successorId = $successor['id'];
+      $conditionId = $successor['condition'] ?? NULL;
+
+      // Check if the successor ID is valid (must be a gateway or an action).
+      if (
+        !isset($lookup[EcaElementType::Action->getPlural()][$successorId])
+        && !isset($lookup[EcaElementType::Gateway->getPlural()][$successorId])
+      ) {
+        $this->context->addViolation($constraint->invalidSuccessorMessage, [
+          '@successorId' => $successorId,
+          '@type' => $type->name,
+          '@elementId' => $element['id'],
+        ]);
+      }
+
+      // Check if the condition ID is valid.
+      if ($conditionId && !isset($lookup[EcaElementType::Condition->getPlural()][$conditionId])) {
+        $this->context->addViolation($constraint->invalidConditionMessage, [
+          '@conditionId' => $conditionId,
+          '@type' => $type->name,
+          '@elementId' => $element['id'],
+        ]);
+      }
+
+      // Check for disallowed successor relationships.
+      if (
+        ($type === EcaElementType::Action || $type === EcaElementType::Gateway)
+        && isset($lookup[EcaElementType::Event->getPlural()][$successorId])
+      ) {
+        $this->context->addViolation($constraint->disallowedSuccessor, [
+          '@type' => $type->name,
+          '@elementId' => $element['id'],
+        ]);
+      }
+      if ($type === EcaElementType::Event && isset($lookup[EcaElementType::Event->getPlural()][$successorId])) {
+        $this->context->addViolation($constraint->disallowedSuccessorEvent, [
+          '@elementId' => $element['id'],
+        ]);
+      }
+    }
+  }
+
+}
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index d8619e1..fd96b26 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -162,7 +162,11 @@ class ModelMapper implements ModelMapperInterface {
     ];
     /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
     foreach ($violations as $violation) {
-      $lines[] = sprintf('- %s: %s', $violation->getPropertyPath(), $violation->getMessage());
+      $message = sprintf('- %s', $violation->getMessage());
+      if (!empty($violation->getPropertyPath())) {
+        $message = sprintf('- %s: %s', $violation->getPropertyPath(), $violation->getMessage());
+      }
+      $lines[] = $message;
     }
 
     return implode("\n", $lines);
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index 79b0301..88d0de2 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -14,6 +14,15 @@ use Drupal\Core\TypedData\ListDataDefinition;
  */
 class EcaModelDefinition extends ComplexDataDefinitionBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values = []) {
+    parent::__construct($values);
+
+    $this->addConstraint('SuccessorsAreValid');
+  }
+
   /**
    * {@inheritdoc}
    */
-- 
GitLab


From 794e8639968697ef547c425adfc79802100024c1 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:24:40 +0100
Subject: [PATCH 72/95] #3481307 Add test for SuccessorsAreValid-constraint

---
 .../agents/tests/assets/from_payload_4.json   | 105 ++++++++++++++++++
 .../src/Kernel/AiEcaAgentsKernelTestBase.php  |   7 ++
 2 files changed, 112 insertions(+)
 create mode 100644 modules/agents/tests/assets/from_payload_4.json

diff --git a/modules/agents/tests/assets/from_payload_4.json b/modules/agents/tests/assets/from_payload_4.json
new file mode 100644
index 0000000..0331b54
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_4.json
@@ -0,0 +1,105 @@
+{
+  "id": "create_article_from_page",
+  "label": "Create Article From Page Publication",
+  "events": [
+    {
+      "id": "event_page_published",
+      "plugin": "content_entity:insert",
+      "label": "Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "id": "condition_check_title",
+          "condition": ""
+        }
+      ]
+    }
+  ],
+  "conditions": [
+    {
+      "id": "condition_check_title",
+      "plugin": "eca_entity_field_value",
+      "configuration": {
+        "field_name": "title",
+        "expected_value": "AI",
+        "operator": "contains",
+        "type": "value",
+        "case": false,
+        "negate": false,
+        "entity": "event.entity"
+      }
+    }
+  ],
+  "gateways": [
+    {
+      "id": "gateway_title_check",
+      "type": 0,
+      "successors": [
+        {
+          "id": "action_create_article_offline",
+          "condition": "condition_check_title"
+        },
+        {
+          "id": "action_create_article",
+          "condition": ""
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "id": "action_create_article",
+      "plugin": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article for {{event.entity.title}} by {{event.entity.owner}}",
+        "published": true,
+        "owner": "{{event.entity.owner}}"
+      },
+      "successors": [
+        {
+          "id": "action_set_article_field",
+          "condition": ""
+        }
+      ]
+    },
+    {
+      "id": "action_create_article_offline",
+      "plugin": "eca_new_entity",
+      "label": "Create New Article Offline",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article for {{event.entity.title}} by {{event.entity.owner}}",
+        "published": false,
+        "owner": "{{event.entity.owner}}"
+      },
+      "successors": [
+        {
+          "id": "action_set_article_field",
+          "condition": ""
+        }
+      ]
+    },
+    {
+      "id": "action_set_article_field",
+      "plugin": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "This article was auto-generated from the page titled '{{event.entity.title}}', authored by {{event.entity.owner}}. Additional static information about the author can be included here.",
+        "method": "set:clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
index e3e68a0..024fa59 100644
--- a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -87,6 +87,13 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
       ],
       'eca_test_0001',
     ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_4.json', __DIR__))),
+      [],
+      NULL,
+      "Invalid successor ID 'condition_check_title' for Event 'event_page_published'. Must be a gateway or action.",
+    ];
   }
 
   /**
-- 
GitLab


From 867b350ce6a2a5b3e05fba582abf2707bd953435 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:26:05 +0100
Subject: [PATCH 73/95] #3481307 Use Json from the Symfony-package for
 (de)encoding-functions

---
 modules/agents/src/Form/AskAiForm.php     |  2 +-
 modules/agents/src/Plugin/AiAgent/Eca.php | 17 +++++++++--------
 2 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index 15bbc69..e392a37 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -53,7 +53,7 @@ class AskAiForm extends FormBase {
   public function submitForm(array &$form, FormStateInterface $form_state): void {
     $batch = new BatchBuilder();
     $batch->setTitle($this->t('Solving ECA question with AI.'));
-    $batch->setInitMessage($this->t('Contacting the AI...'));
+    $batch->setInitMessage($this->t('Asking AI which task to perform...'));
     $batch->setErrorMessage($this->t('An error occurred during processing.'));
     $batch->setFinishCallback([self::class, 'batchFinished']);
 
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 3276fba..be40d0e 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -5,6 +5,7 @@ namespace Drupal\ai_eca_agents\Plugin\AiAgent;
 use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
 use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
 use Drupal\Component\Render\MarkupInterface;
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -199,10 +200,10 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     $context = [];
     if (!empty($this->model)) {
-      $context['The information of the model, in JSON format'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getModels([$this->model->id()])));
+      $context['The information of the model, in JSON format'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels([$this->model->id()])));
     }
     elseif (!empty($this->data[0]['component_ids'])) {
-      $context['The details about the components'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+      $context['The details about the components'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
     }
 
     if (empty($context)) {
@@ -294,8 +295,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     // Prepare and run the prompt by fetching all the relevant info.
     $data = $this->agentHelper->runSubAgent('determineTask', [
       'Task description and if available comments description' => $context,
-      'The list of existing models, in JSON format' => sprintf("```json\n%s\n```", json_encode($this->dataProvider->getModels())),
-      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents())),
+      'The list of existing models, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels())),
+      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents())),
     ]);
 
     // Quit early if the returned response isn't what we expected.
@@ -335,24 +336,24 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
 
     // Prepare the prompt.
     $context = [
-      // 'The available tokens' => sprintf("```json\n%s", json_encode($this->dataProvider->getTokens())),
+      // 'The available tokens' => sprintf("```json\n%s", Json::encode($this->dataProvider->getTokens())),
     ];
     // The schema of the ECA-config that the LLM should follow.
     $definition = EcaModelDefinition::create();
     $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
     $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
-    $context['JSON Schema of the process'] = sprintf("```json\n%s\n```", $schema);
+    $context['JSON Schema of the model'] = sprintf("```json\n%s\n```", $schema);
 
     // The model that should be edited.
     if (!empty($this->model)) {
       $models = $this->dataProvider->getModels([$this->model->id()]);
-      $context['The model to edit'] = sprintf("```json\n%s\n```", reset($models));
+      $context['The model to edit'] = sprintf("```json\n%s\n```", Json::encode(reset($models)));
     }
 
     // Components or plugins that the LLM should use.
     if (Arr::has($this->dto, 'data.0.component_ids')) {
       $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
-      $context['The details about the components'] = sprintf("```json\n%s\n```", json_encode($this->dataProvider->getComponents($componentIds)));
+      $context['The details about the components'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents($componentIds)));
     }
 
     // Optional feedback that the previous prompt provided.
-- 
GitLab


From 0fef276873dd4cbb5c608cf2d703302722b2f2b8 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:26:19 +0100
Subject: [PATCH 74/95] #3481307 Register a generic logger

---
 ai_eca.services.yml | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 ai_eca.services.yml

diff --git a/ai_eca.services.yml b/ai_eca.services.yml
new file mode 100644
index 0000000..cc0b53f
--- /dev/null
+++ b/ai_eca.services.yml
@@ -0,0 +1,5 @@
+services:
+  logger.channel.ai_eca:
+    parent: logger.channel_base
+    arguments:
+      - 'ai_eca'
-- 
GitLab


From 014f6afbf7e4f10add5b7d4859445a02a58a0c2e Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:26:31 +0100
Subject: [PATCH 75/95] #3481307 Adjust the prompt for building the model

---
 modules/agents/prompts/eca/buildModel.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 532b685..09471e9 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -8,7 +8,7 @@ validation:
 retries: 2
 prompt:
   introduction: |
-    You are a business process modeling expert. You will be given a textual description of a business process.
+    You are a business process modelling expert. You will be given a textual description of a business process.
     Generate a JSON model for the process, based on the provided JSON Schema.
 
     Analyze and identify key elements:
@@ -24,6 +24,8 @@ prompt:
     When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
     optimization (if it does not contradict the user intended sequence).
     Use clear names for labels and conditions.
+    Aim for detail when naming the elements and the model (e.g., instead of "Event 1" or "Action 2", use "User login"
+    or "Create node").
 
     All elements, except gateways, must have a plugin assigned to them and optionally an array of configuration
     parameters. You will given a list of possible plugins and their corresponding configuration structure, you can not
-- 
GitLab


From 527c4018f3cb1c02d4f7d99fd7cfa23bf36e2843 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:26:53 +0100
Subject: [PATCH 76/95] #3481307 Give the existing model ID to the repository
 when building

---
 modules/agents/src/Plugin/AiAgent/Eca.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index be40d0e..2390a52 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -368,7 +368,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
       throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
     }
 
-    $eca = $this->ecaRepository->build($data);
+    $eca = $this->ecaRepository->build($data, TRUE, $this->model?->id());
     $this->dto['logs'][] = $this->t("The model '@name' has been created. You can find it here: %link.", [
       '@name' => $eca->label(),
       '%link' => Url::fromRoute('entity.eca.collection')->toString(),
-- 
GitLab


From dbb3267b17636554abccef191cd57875a595a2be Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:32:09 +0100
Subject: [PATCH 77/95] #3481307 Fix phpcs issues

---
 .cspell-project-words.txt                 | 29 +++++++++++++++++++++++
 modules/agents/src/Plugin/AiAgent/Eca.php |  5 ++--
 2 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index e69de29..53db739 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -0,0 +1,29 @@
+acmymx
+agtxee
+aowp
+beihz
+bfoheo
+byzhmc
+Cplain
+cxcwjm
+eoahw
+gguvde
+iwzr
+iztkfs
+jqykgu
+nagg
+originalentity
+originalentityref
+pgta
+qlvkq
+refentity
+referencable
+Retryable
+rgzuve
+rlgsjy
+tbbhie
+tgic
+vgtd
+yjwm
+zcaglk
+zpul
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 2390a52..902405c 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -335,9 +335,8 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     $this->dataProvider->setViewMode(DataViewModeEnum::Full);
 
     // Prepare the prompt.
-    $context = [
-      // 'The available tokens' => sprintf("```json\n%s", Json::encode($this->dataProvider->getTokens())),
-    ];
+    $context = [];
+
     // The schema of the ECA-config that the LLM should follow.
     $definition = EcaModelDefinition::create();
     $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
-- 
GitLab


From 66f8e4ecf37fe9ff88b620f54d5fca3a2454b9cc Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 14:33:19 +0100
Subject: [PATCH 78/95] #3481307 Fix constraint name

---
 ...ValidConstraint.php => SuccessorsAreValidConstraint.php} | 2 +-
 .../Constraint/SuccessorsAreValidConstraintValidator.php    | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)
 rename modules/agents/src/Plugin/Validation/Constraint/{SuccessorAreValidConstraint.php => SuccessorsAreValidConstraint.php} (95%)

diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
similarity index 95%
rename from modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php
rename to modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
index aec9285..88f800f 100644
--- a/modules/agents/src/Plugin/Validation/Constraint/SuccessorAreValidConstraint.php
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
@@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraint as SymfonyConstraint;
   id: 'SuccessorsAreValid',
   label: new TranslatableMarkup('Successor are valid')
 )]
-class SuccessorAreValidConstraint extends SymfonyConstraint {
+class SuccessorsAreValidConstraint extends SymfonyConstraint {
 
   /**
    * The error message if the successor is invalid.
diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
index 2a7406d..804a3c9 100644
--- a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
@@ -15,7 +15,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate(mixed $value, Constraint $constraint): void {
-    assert($constraint instanceof SuccessorAreValidConstraint);
+    assert($constraint instanceof SuccessorsAreValidConstraint);
 
     $lookup = [
       EcaElementType::Event->getPlural() => array_flip(array_column($value['events'] ?? [], 'id')),
@@ -38,7 +38,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
   /**
    * Validate the successors of an element.
    *
-   * @param \Drupal\ai_eca_agents\Plugin\Validation\Constraint\SuccessorAreValidConstraint $constraint
+   * @param \Drupal\ai_eca_agents\Plugin\Validation\Constraint\SuccessorsAreValidConstraint $constraint
    *   The constraint.
    * @param array $element
    *   The element to validate.
@@ -47,7 +47,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
    * @param array $lookup
    *   The lookup-array containing all the referencable IDs.
    */
-  protected function validateSuccessor(SuccessorAreValidConstraint $constraint, array $element, EcaElementType $type, array $lookup): void {
+  protected function validateSuccessor(SuccessorsAreValidConstraint $constraint, array $element, EcaElementType $type, array $lookup): void {
     if (empty($element['successors'])) {
       return;
     }
-- 
GitLab


From 59d916b596afcfd67f512123a5861bea7cdfb516 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 15:05:07 +0100
Subject: [PATCH 79/95] #3481307 Replace approvals/approval-tests with
 spatie/phpunit-snapshot-assertions

---
 composer.json                                 |   4 +-
 .../tests/src/Kernel/EcaModelSchemaTest.php   |   6 +-
 .../tests/src/Kernel/ModelMapperTest.php      |   6 +-
 .../EcaModelSchemaTest__testSchema__1.json    | 198 ++++++++++++++++++
 ...MappingFromEntity with data set 0__1.json} |   2 +-
 ...MappingFromEntity with data set 1__1.json} |   2 +-
 ...MappingFromEntity with data set 2__1.json} |   2 +-
 ...caModelSchemaTest.testSchema.approved.json |   1 -
 8 files changed, 211 insertions(+), 10 deletions(-)
 create mode 100644 modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
 rename modules/agents/tests/src/Kernel/{approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json => __snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json} (99%)
 rename modules/agents/tests/src/Kernel/{approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json => __snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json} (99%)
 rename modules/agents/tests/src/Kernel/{approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json => __snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json} (99%)
 delete mode 100644 modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json

diff --git a/composer.json b/composer.json
index 92b8d73..2f83c3c 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,7 @@
         "illuminate/support": "^10.48 || ^11.34"
     },
     "require-dev": {
-        "approvals/approval-tests": "dev-Main",
-        "drupal/schemata": "^1.0"
+        "drupal/schemata": "^1.0",
+        "spatie/phpunit-snapshot-assertions": "^5.1"
     }
 }
diff --git a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
index b96a554..95ca0cd 100644
--- a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
+++ b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
-use ApprovalTests\Approvals;
 use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
 use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
 use Drupal\KernelTests\KernelTestBase;
+use Spatie\Snapshots\MatchesSnapshots;
 use Symfony\Component\Serializer\SerializerInterface;
 
 /**
@@ -15,6 +15,8 @@ use Symfony\Component\Serializer\SerializerInterface;
  */
 class EcaModelSchemaTest extends KernelTestBase {
 
+  use MatchesSnapshots;
+
   /**
    * {@inheritdoc}
    */
@@ -45,7 +47,7 @@ class EcaModelSchemaTest extends KernelTestBase {
     $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
     $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
 
-    Approvals::verifyStringWithFileExtension($schema, 'json');
+    $this->assertMatchesJsonSnapshot($schema);
   }
 
   /**
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
index 0103c86..3f13f69 100644
--- a/modules/agents/tests/src/Kernel/ModelMapperTest.php
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\Tests\ai_eca_agents\Kernel;
 
-use ApprovalTests\Approvals;
 use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Spatie\Snapshots\MatchesSnapshots;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 
 /**
@@ -14,6 +14,8 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  */
 class ModelMapperTest extends AiEcaAgentsKernelTestBase {
 
+  use MatchesSnapshots;
+
   /**
    * {@inheritdoc}
    */
@@ -97,7 +99,7 @@ class ModelMapperTest extends AiEcaAgentsKernelTestBase {
     $model = $this->modelMapper->fromEntity($entity);
     $data = $this->normalizer->normalize($model);
 
-    Approvals::verifyStringWithFileExtension(json_encode($data, JSON_PRETTY_PRINT), sprintf('%s.json', $entityId));
+    $this->assertMatchesJsonSnapshot(json_encode($data, JSON_PRETTY_PRINT));
   }
 
   /**
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
new file mode 100644
index 0000000..69fadb5
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
@@ -0,0 +1,198 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "id": "http://localhost/schemata/eca_model?_format=schema_json&_describes=json",
+    "type": "object",
+    "title": "ECA Model Schema",
+    "description": "The schema describing the properties of an ECA model.",
+    "properties": {
+        "id": {
+            "type": "string",
+            "title": "ID"
+        },
+        "label": {
+            "type": "string",
+            "title": "Label"
+        },
+        "version": {
+            "type": "string",
+            "title": "Version"
+        },
+        "description": {
+            "type": "string",
+            "title": "Description"
+        },
+        "events": {
+            "type": "array",
+            "title": "Events",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "label": {
+                        "type": "string",
+                        "title": "Label"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "id",
+                    "plugin",
+                    "label"
+                ]
+            },
+            "minItems": 1
+        },
+        "conditions": {
+            "type": "array",
+            "title": "Conditions",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    }
+                },
+                "required": [
+                    "id",
+                    "plugin"
+                ]
+            }
+        },
+        "gateways": {
+            "type": "array",
+            "title": "Gateways",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "type": {
+                        "type": "integer",
+                        "title": "Type",
+                        "enum": [
+                            0
+                        ]
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "id"
+                ]
+            }
+        },
+        "actions": {
+            "type": "array",
+            "title": "Actions",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "label": {
+                        "type": "string",
+                        "title": "Label"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "id",
+                    "plugin",
+                    "label"
+                ]
+            },
+            "minItems": 1
+        }
+    },
+    "required": [
+        "id",
+        "label",
+        "events",
+        "actions"
+    ]
+}
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json
similarity index 99%
rename from modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
rename to modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json
index e803eb1..37ca359 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0001.json
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json	
@@ -282,4 +282,4 @@
             "successors": []
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json
similarity index 99%
rename from modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
rename to modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json
index 5a3473c..98afd15 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0002.json
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json	
@@ -177,4 +177,4 @@
             "successors": []
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json
similarity index 99%
rename from modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
rename to modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json
index d254ceb..797191c 100644
--- a/modules/agents/tests/src/Kernel/approvals/ModelMapperTest.testMappingFromEntity.approved.eca_test_0009.json
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json	
@@ -304,4 +304,4 @@
             "successors": []
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json b/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
deleted file mode 100644
index f2c66b4..0000000
--- a/modules/agents/tests/src/Kernel/approvals/EcaModelSchemaTest.testSchema.approved.json
+++ /dev/null
@@ -1 +0,0 @@
-{"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","id":"http:\/\/localhost\/schemata\/eca_model?_format=schema_json\u0026_describes=json","type":"object","title":"ECA Model Schema","description":"The schema describing the properties of an ECA model.","properties":{"id":{"type":"string","title":"ID"},"label":{"type":"string","title":"Label"},"version":{"type":"string","title":"Version"},"description":{"type":"string","title":"Description"},"events":{"type":"array","title":"Events","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1},"conditions":{"type":"array","title":"Conditions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"configuration":{"type":"any"}},"required":["id","plugin"]}},"gateways":{"type":"array","title":"Gateways","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"type":{"type":"integer","title":"Type","enum":[0]},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id"]}},"actions":{"type":"array","title":"Actions","items":{"type":"object","properties":{"id":{"type":"string","title":"ID of the element"},"plugin":{"type":"string","title":"Plugin ID"},"label":{"type":"string","title":"Label"},"configuration":{"type":"any"},"successors":{"type":"array","title":"Successors","items":{"type":"object","properties":{"id":{"type":"string","title":"The ID of an existing action or gateway."},"condition":{"type":"string","title":"The ID of an existing condition."}},"required":["id"]}}},"required":["id","plugin","label"]},"minItems":1}},"required":["id","label","events","actions"]}
\ No newline at end of file
-- 
GitLab


From 052b014ca4ace19361be2acd43512511aa9876c1 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 27 Dec 2024 15:17:47 +0100
Subject: [PATCH 80/95] #3481307 Allow v4 of spatie/phpunit-snapshot-assertions
 as well

---
 .cspell-project-words.txt | 1 +
 composer.json             | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index 53db739..4b223cc 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -21,6 +21,7 @@ referencable
 Retryable
 rgzuve
 rlgsjy
+Spatie
 tbbhie
 tgic
 vgtd
diff --git a/composer.json b/composer.json
index 2f83c3c..9576e17 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,6 @@
     },
     "require-dev": {
         "drupal/schemata": "^1.0",
-        "spatie/phpunit-snapshot-assertions": "^5.1"
+        "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1"
     }
 }
-- 
GitLab


From 29c85537246ed04bc8e2e9c9f7dbc5737000e690 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 31 Dec 2024 11:23:17 +0100
Subject: [PATCH 81/95] #3481307 Add 'Ask AI'-button on model edit form

---
 modules/agents/ai_eca_agents.module       | 17 +++++++
 modules/agents/ai_eca_agents.services.yml |  4 ++
 modules/agents/src/Form/AskAiForm.php     |  2 +-
 modules/agents/src/Hook/FormHooks.php     | 54 +++++++++++++++++++++++
 4 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 modules/agents/ai_eca_agents.module
 create mode 100644 modules/agents/src/Hook/FormHooks.php

diff --git a/modules/agents/ai_eca_agents.module b/modules/agents/ai_eca_agents.module
new file mode 100644
index 0000000..3cf22ee
--- /dev/null
+++ b/modules/agents/ai_eca_agents.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * AI ECA Agents module file.
+ */
+
+use Drupal\ai_eca_agents\Hook\FormHooks;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Implements hook_FORM_ID_alter.
+ */
+#[LegacyHook]
+function ai_eca_agents_form_bpmn_io_modeller_alter(&$form, FormStateInterface $form_state): void {
+  Drupal::service(FormHooks::class)->formModellerAlter($form, $form_state);
+}
diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
index 5f6aa0b..feaf93f 100644
--- a/modules/agents/ai_eca_agents.services.yml
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -29,3 +29,7 @@ services:
     decorates: serializer.normalizer.data_definition.schema_json.json
     arguments:
       - '@serializer.normalizer.data_definition.schema_json_ai_eca_agents.json.inner'
+
+  Drupal\ai_eca_agents\Hook\FormHooks:
+    class: Drupal\ai_eca_agents\Hook\FormHooks
+    autowire: true
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index e392a37..883cae1 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -52,7 +52,7 @@ class AskAiForm extends FormBase {
    */
   public function submitForm(array &$form, FormStateInterface $form_state): void {
     $batch = new BatchBuilder();
-    $batch->setTitle($this->t('Solving ECA question with AI.'));
+    $batch->setTitle($this->t('Solving question.'));
     $batch->setInitMessage($this->t('Asking AI which task to perform...'));
     $batch->setErrorMessage($this->t('An error occurred during processing.'));
     $batch->setFinishCallback([self::class, 'batchFinished']);
diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
new file mode 100644
index 0000000..3dd6a29
--- /dev/null
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Hook;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drush\Attributes\Hook;
+
+/**
+ * Provides hook implementations for form alterations.
+ */
+class FormHooks {
+
+  use StringTranslationTrait;
+
+  /**
+   * Alters the ECA Modeller form.
+   *
+   * @param array $form
+   *   The form.
+   * @param \Drupal\Core\Form\FormStateInterface $formState
+   *   The form state.
+   *
+   * @return void
+   */
+  #[Hook('form_bpmn_io_modeller_alter')]
+  public function formModellerAlter(array &$form, FormStateInterface $formState): void {
+    // Take the options from the export-link.
+    /** @var \Drupal\Core\Url $exportLink */
+    $exportLink = $form['actions']['export_archive']['#url'];
+
+    $form['actions']['ask_ai'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Ask AI'),
+      '#url' => Url::fromRoute('ai_eca_agents.ask_ai', [], $exportLink->getOptions()),
+      '#attributes' => [
+        'class' => ['button', 'ai-eca-ask-ai', 'use-ajax'],
+        'data-dialog-type' => 'modal',
+        'data-dialog-options' => Json::encode([
+          'width' => 1000,
+          'dialogClass' => 'ui-dialog-off-canvas',
+        ]),
+      ],
+      '#attached' => [
+        'library' => [
+          'core/drupal.dialog.ajax',
+        ],
+      ],
+    ];
+  }
+
+}
-- 
GitLab


From c03271de25a557c4a543f153ef453119aa82f472 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sun, 5 Jan 2025 18:08:53 +0100
Subject: [PATCH 82/95] #3481307 Specify destination and model_id when invoking
 Agent via form

---
 modules/agents/ai_eca_agents.routing.yml  |  4 +-
 modules/agents/src/Form/AskAiForm.php     | 46 +++++++++++++++++++++--
 modules/agents/src/Hook/FormHooks.php     |  4 +-
 modules/agents/src/Plugin/AiAgent/Eca.php | 14 ++++---
 4 files changed, 56 insertions(+), 12 deletions(-)

diff --git a/modules/agents/ai_eca_agents.routing.yml b/modules/agents/ai_eca_agents.routing.yml
index 4e11c91..d9e58a4 100644
--- a/modules/agents/ai_eca_agents.routing.yml
+++ b/modules/agents/ai_eca_agents.routing.yml
@@ -1,7 +1,9 @@
 ai_eca_agents.ask_ai:
-  path: '/admin/config/workflow/eca/ask-ai'
+  path: '/api/ai-eca-agents/ask-ai'
   defaults:
     _title: 'Ask AI'
     _form: '\Drupal\ai_eca_agents\Form\AskAiForm'
+  options:
+    _admin_route: true
   requirements:
     _permission: 'administer eca'
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index 883cae1..142425a 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -34,6 +34,20 @@ class AskAiForm extends FormBase {
       '#required' => TRUE,
     ];
 
+    // Determine the destination for when the batch process is finished.
+    $destination = \Drupal::request()->query->get('destination', Url::fromRoute('entity.eca.collection')->toString());
+    $form['destination'] = [
+      '#type' => 'value',
+      '#value' => $destination,
+    ];
+
+    // Determine if an existing model is the subject of the prompt.
+    $modelId = \Drupal::request()->query->get('model-id');
+    $form['model_id'] = [
+      '#type' => 'value',
+      '#value' => $modelId,
+    ];
+
     $form['actions'] = [
       '#type' => 'actions',
     ];
@@ -57,26 +71,44 @@ class AskAiForm extends FormBase {
     $batch->setErrorMessage($this->t('An error occurred during processing.'));
     $batch->setFinishCallback([self::class, 'batchFinished']);
 
-    $batch->addOperation([self::class, 'determineTask'], [$form_state->getValue('question')]);
+    $batch->addOperation([self::class, 'initProcess'], [$form_state->getValue('destination')]);
+    $batch->addOperation([self::class, 'determineTask'], [$form_state->getValue('question'), $form_state->getValue('model_id')]);
     $batch->addOperation([self::class, 'executeTask']);
 
     batch_set($batch->toArray());
   }
 
+  /**
+   * Batch operation for initializing the process.
+   *
+   * @param string $destination
+   *   The destination for when the process is finished.
+   * @param array $context
+   *   The context.
+   */
+  public static function initProcess(string $destination, array &$context): void {
+    $context['message'] = t('Initializing...');
+
+    $context['results']['destination'] = $destination;
+  }
+
   /**
    * Batch operation for determining the task to execute.
    *
    * @param string $question
    *   The question of the user.
+   * @param string|null $modelId
+   *   The ECA model ID.
    * @param array $context
    *   The context.
    *
    * @throws \Drupal\Component\Plugin\Exception\PluginException
    */
-  public static function determineTask(string $question, array &$context): void {
+  public static function determineTask(string $question, ?string $modelId, array &$context): void {
     $context['message'] = t('Task determined. Executing...');
 
     $context['sandbox']['question'] = $question;
+    $context['sandbox']['model_id'] = $modelId;
     $agent = self::initAgent($context);
 
     // Let the agent decide how it can answer the question.
@@ -124,7 +156,7 @@ class AskAiForm extends FormBase {
       \Drupal::messenger()->addStatus($results['response']);
     }
 
-    return new RedirectResponse(Url::fromRoute('entity.eca.collection')->toString());
+    return new RedirectResponse($results['destination']);
   }
 
   /**
@@ -143,11 +175,17 @@ class AskAiForm extends FormBase {
     /** @var \Drupal\ai_agents\PluginInterfaces\AiAgentInterface $agent */
     $agent = \Drupal::service('plugin.manager.ai_agents')
       ->createInstance('eca');
+    assert($agent instanceof Eca);
 
     $dto = [];
+    if (!empty($context['sandbox']['model_id'])) {
+      $dto['model_id'] = $context['sandbox']['model_id'];
+    }
     if (!empty($context['results']['dto']) && is_array($context['results']['dto'])) {
-      $dto = $context['results']['dto'];
+      $dto = array_merge($dto, $context['results']['dto']);
       $dto['setup_agent'] = TRUE;
+    }
+    if (!empty($dto)) {
       $agent->setDto($dto);
     }
 
diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
index 3dd6a29..75d7660 100644
--- a/modules/agents/src/Hook/FormHooks.php
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -30,11 +30,13 @@ class FormHooks {
     // Take the options from the export-link.
     /** @var \Drupal\Core\Url $exportLink */
     $exportLink = $form['actions']['export_archive']['#url'];
+    $options = $exportLink->getOptions();
+    $options['query']['model-id'] = \Drupal::routeMatch()->getParameter('eca');
 
     $form['actions']['ask_ai'] = [
       '#type' => 'link',
       '#title' => $this->t('Ask AI'),
-      '#url' => Url::fromRoute('ai_eca_agents.ask_ai', [], $exportLink->getOptions()),
+      '#url' => Url::fromRoute('ai_eca_agents.ask_ai', [], $options),
       '#attributes' => [
         'class' => ['button', 'ai-eca-ask-ai', 'use-ajax'],
         'data-dialog-type' => 'modal',
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 902405c..de42eff 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -288,16 +288,18 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    */
   protected function determineTypeOfTask(): string {
     $context = $this->getFullContextOfTask($this->task);
+    $userContext = [
+      'A summary of the existing models' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels())),
+      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents())),
+    ];
     if (!empty($this->model)) {
-      $context .= "\n\nA model already exists, so creation is not possible.";
+      $context .= "A model already exists, so creation is not possible.";
+      $userContext['A summary of the existing models'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels([$this->model->id()])));
     }
+    $userContext['Task description and if available comments description'] = $context;
 
     // Prepare and run the prompt by fetching all the relevant info.
-    $data = $this->agentHelper->runSubAgent('determineTask', [
-      'Task description and if available comments description' => $context,
-      'The list of existing models, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels())),
-      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents())),
-    ]);
+    $data = $this->agentHelper->runSubAgent('determineTask', $userContext);
 
     // Quit early if the returned response isn't what we expected.
     if (empty($data[0]['action'])) {
-- 
GitLab


From 9654ff6bea3c07fd9455e55092a5d9924b35950a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 6 Jan 2025 21:55:03 +0100
Subject: [PATCH 83/95] ##3481307 Specify actions for buildModel-subagent

---
 modules/agents/prompts/eca/buildModel.yml     | 15 +++++++++++++-
 modules/agents/src/Form/AskAiForm.php         | 11 +++++++++-
 modules/agents/src/Plugin/AiAgent/Eca.php     | 20 ++++++++++---------
 .../AiAgentValidation/EcaValidation.php       | 12 ++++++++++-
 4 files changed, 46 insertions(+), 12 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 09471e9..aa1ed0d 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -32,4 +32,17 @@ prompt:
     deviate from those.
 
     Sometimes you will be given a previous JSON solution with user instructions to edit.
-  formats: []
+  possible_actions:
+    build: The generated JSON model that should be imported.
+    fail: You are unable to create or alter the model.
+  formats:
+    - action: Action ID from the list
+      model: The JSON model of the process
+      message: A small summary of the model and how you came to the provided solution or feedback why you were not able to generate a model.
+  one_shot_learning_examples:
+    - action: build
+      model: |
+        {"id":"process_1","label":"Create Article on Page Publish","events":[{"id":"event_1","plugin":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"id":"action_2"}]}],"actions":[{"id":"action_2","plugin":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"id":"action_3"}]},{"id":"action_3","plugin":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"id":"action_4"}]},{"id":"action_4","plugin":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"id":"action_5"}]},{"id":"action_5","plugin":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"id":"action_6"}]},{"id":"action_6","plugin":"eca_save_entity","label":"Save Article","successors":[]}]}
+      message: The model "Create Article on Page Publish" creates an article about a new published page, contain the label and the author of that new page.
+    - action: fail
+      message: I was unable to analyze the description.
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index 142425a..2d183e7 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -113,12 +113,21 @@ class AskAiForm extends FormBase {
 
     // Let the agent decide how it can answer the question.
     $solvability = $agent->determineSolvability();
+
     if ($solvability === AiAgentInterface::JOB_NOT_SOLVABLE) {
-      $context['results']['error'] = t('The AI agent could not solve the task');
+      $context['results']['error'] = t('The AI agent could not solve the task.');
 
       return;
     }
 
+    // Redirect to the convert-endpoint of the BPMN.io-module.
+    $bpmnIoIsAvailable = \Drupal::moduleHandler()->moduleExists('bpmn_io');
+    if ($solvability === AiAgentInterface::JOB_SOLVABLE && $bpmnIoIsAvailable && !empty($modelId)) {
+      $context['results']['destination'] = Url::fromRoute('bpmn_io.convert', [
+        'eca' => $modelId,
+      ])->toString();
+    }
+
     $context['results']['dto'] = $agent->getDto();
   }
 
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index de42eff..0ae1fd0 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -43,7 +43,7 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
    *
    * @var \Drupal\eca\Entity\Eca|null
    */
-  protected ?EcaEntity $model;
+  protected ?EcaEntity $model = NULL;
 
   /**
    * The ECA data provider.
@@ -365,16 +365,18 @@ class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
     // Execute it.
     $data = $this->agentHelper->runSubAgent('buildModel', $context);
 
-    if (empty($data) || (!empty($data[0]['type']) && $data[0]['type'] === 'no_info')) {
-      throw new \Exception(!empty($data[0]['value']) ? $data[0]['value'] : 'Could not create ECA config.');
+    $message = Arr::get($data, '0.message', 'Could not create ECA config.');
+    if (
+      Arr::get($data, '0.action', 'fail') !== 'build'
+      || !Arr::has($data, '0.model')
+    ) {
+      throw new \Exception($message);
     }
 
-    $eca = $this->ecaRepository->build($data, TRUE, $this->model?->id());
-    $this->dto['logs'][] = $this->t("The model '@name' has been created. You can find it here: %link.", [
-      '@name' => $eca->label(),
-      '%link' => Url::fromRoute('entity.eca.collection')->toString(),
-    ]);
-    $this->dto['logs'][] = $this->t('Note that the model is not enabled by default and that you have to change that manually.');
+    $eca = $this->ecaRepository->build(Json::decode(Arr::get($data, '0.model')), TRUE, $this->model?->id());
+
+    $this->dto['logs'][] = $this->model ? $this->t('Model "@model" was altered.', ['@model' => $this->model->label()]) : $this->t('A new model was created: "@model".', ['@model' => $eca->label()]);
+    $this->dto['logs'][] = $message;
   }
 
 }
diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index 11c87c3..4ecb3d4 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -13,6 +13,7 @@ use Drupal\ai_agents\Exception\AgentRetryableValidationException;
 use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
 use Drupal\ai_agents\PluginInterfaces\AiAgentValidationInterface;
 use Drupal\Core\TypedData\Exception\MissingDataException;
+use Illuminate\Support\Arr;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -63,9 +64,18 @@ class EcaValidation extends AiAgentValidationPluginBase implements ContainerFact
       );
     }
 
+    if (!Arr::has($data, '0.message')) {
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: feedback was not provided.',
+        0,
+        NULL,
+        'You MUST provide a summary of your action or give feedback why you failed.'
+      );
+    }
+
     // Validate the response against ECA-model schema.
     try {
-      $this->modelMapper->fromPayload($data);
+      $this->modelMapper->fromPayload(Json::decode(Arr::get($data, '0.model')));
     }
     catch (MissingDataException $e) {
       throw new AgentRetryableValidationException(
-- 
GitLab


From a6f0d11d792ff4cc2063ce0c658281a3fc029b29 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Mon, 6 Jan 2025 22:13:42 +0100
Subject: [PATCH 84/95] #3481307 Ensure that model ID is passed as query param

---
 modules/agents/src/Hook/FormHooks.php | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
index 75d7660..0a780e2 100644
--- a/modules/agents/src/Hook/FormHooks.php
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -6,6 +6,7 @@ use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
+use Drupal\eca\Entity\Eca;
 use Drush\Attributes\Hook;
 
 /**
@@ -31,7 +32,12 @@ class FormHooks {
     /** @var \Drupal\Core\Url $exportLink */
     $exportLink = $form['actions']['export_archive']['#url'];
     $options = $exportLink->getOptions();
-    $options['query']['model-id'] = \Drupal::routeMatch()->getParameter('eca');
+
+    $eca = \Drupal::routeMatch()->getParameter('eca');
+    if ($eca instanceof Eca) {
+      $eca = $eca->id();
+    }
+    $options['query']['model-id'] = $eca;
 
     $form['actions']['ask_ai'] = [
       '#type' => 'link',
-- 
GitLab


From 263d521fa44c9b86e4cb133aca7513462d6d0da2 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sun, 12 Jan 2025 14:38:37 +0100
Subject: [PATCH 85/95] #3481307 Adjust properties of ECA Model

---
 .../SuccessorsAreValidConstraint.php          |   2 +-
 .../SuccessorsAreValidConstraintValidator.php |  19 ++--
 .../Services/EcaRepository/EcaRepository.php  |  22 ++--
 .../src/Services/ModelMapper/ModelMapper.php  |  18 ++--
 .../src/TypedData/EcaGatewayDefinition.php    |   2 +-
 .../src/TypedData/EcaModelDefinition.php      |   2 +-
 .../src/TypedData/EcaPluginDefinition.php     |   4 +-
 .../src/TypedData/EcaSuccessorDefinition.php  |   2 +-
 .../agents/tests/assets/from_payload_0.json   |  36 +++----
 .../agents/tests/assets/from_payload_1.json   |  34 +++---
 .../agents/tests/assets/from_payload_2.json   |  30 +++---
 .../agents/tests/assets/from_payload_3.json   |  32 +++---
 .../agents/tests/assets/from_payload_4.json   |  34 +++---
 .../src/Kernel/AiEcaAgentsKernelTestBase.php  |   4 +-
 .../EcaModelSchemaTest__testSchema__1.json    |  44 ++++----
 ...tMappingFromEntity with data set 0__1.json | 102 +++++++++---------
 ...tMappingFromEntity with data set 1__1.json |  72 ++++++-------
 ...tMappingFromEntity with data set 2__1.json | 100 ++++++++---------
 18 files changed, 283 insertions(+), 276 deletions(-)

diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
index 88f800f..d064a38 100644
--- a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
@@ -20,7 +20,7 @@ class SuccessorsAreValidConstraint extends SymfonyConstraint {
    *
    * @var string
    */
-  public string $invalidSuccessorMessage = "Invalid successor ID '@successorId' for @type '@elementId'. Must be a gateway or action.";
+  public string $invalidSuccessorMessage = "Invalid successor ID '@successorId' for @type '@elementId'. Must reference a gateway or action.";
 
   /**
    * The error message if the condition is invalid.
diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
index 804a3c9..41d8968 100644
--- a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
@@ -18,10 +18,10 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
     assert($constraint instanceof SuccessorsAreValidConstraint);
 
     $lookup = [
-      EcaElementType::Event->getPlural() => array_flip(array_column($value['events'] ?? [], 'id')),
-      EcaElementType::Condition->getPlural() => array_flip(array_column($value['conditions'] ?? [], 'id')),
-      EcaElementType::Action->getPlural() => array_flip(array_column($value['actions'] ?? [], 'id')),
-      EcaElementType::Gateway->getPlural() => array_flip(array_column($value['gateways'] ?? [], 'id')),
+      EcaElementType::Event->getPlural() => array_flip(array_column($value['events'] ?? [], 'element_id')),
+      EcaElementType::Condition->getPlural() => array_flip(array_column($value['conditions'] ?? [], 'element_id')),
+      EcaElementType::Action->getPlural() => array_flip(array_column($value['actions'] ?? [], 'element_id')),
+      EcaElementType::Gateway->getPlural() => array_flip(array_column($value['gateways'] ?? [], 'gateway_id')),
     ];
 
     foreach (EcaElementType::getPluralMap() as $plural => $type) {
@@ -53,7 +53,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
     }
 
     foreach ($element['successors'] as $successor) {
-      $successorId = $successor['id'];
+      $successorId = $successor['element_id'] ?? $element['gateway_id'];
       $conditionId = $successor['condition'] ?? NULL;
 
       // Check if the successor ID is valid (must be a gateway or an action).
@@ -64,7 +64,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
         $this->context->addViolation($constraint->invalidSuccessorMessage, [
           '@successorId' => $successorId,
           '@type' => $type->name,
-          '@elementId' => $element['id'],
+          '@elementId' => $element['element_id'],
         ]);
       }
 
@@ -73,7 +73,7 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
         $this->context->addViolation($constraint->invalidConditionMessage, [
           '@conditionId' => $conditionId,
           '@type' => $type->name,
-          '@elementId' => $element['id'],
+          '@elementId' => $element['element_id'],
         ]);
       }
 
@@ -82,14 +82,15 @@ class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
         ($type === EcaElementType::Action || $type === EcaElementType::Gateway)
         && isset($lookup[EcaElementType::Event->getPlural()][$successorId])
       ) {
+        $elementId = $element['element_id'] ?? $element['gateway_id'];
         $this->context->addViolation($constraint->disallowedSuccessor, [
           '@type' => $type->name,
-          '@elementId' => $element['id'],
+          '@elementId' => $elementId,
         ]);
       }
       if ($type === EcaElementType::Event && isset($lookup[EcaElementType::Event->getPlural()][$successorId])) {
         $this->context->addViolation($constraint->disallowedSuccessorEvent, [
-          '@elementId' => $element['id'],
+          '@elementId' => $element['element_id'],
         ]);
       }
     }
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
index efa790f..bb39e14 100644
--- a/modules/agents/src/Services/EcaRepository/EcaRepository.php
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -56,7 +56,7 @@ class EcaRepository implements EcaRepositoryInterface {
 
     // Map the model to the entity.
     $random = new Random();
-    $idFallback = $model->get('id')?->getString() ?? sprintf('process_%s', $random->name(7));
+    $idFallback = $model->get('model_id')?->getString() ?? sprintf('process_%s', $random->name(7));
     $eca->set('id', $id ?? $idFallback);
     $eca->set('label', $model->get('label')->getString());
     $eca->set('modeller', 'fallback');
@@ -69,12 +69,14 @@ class EcaRepository implements EcaRepositoryInterface {
     foreach ($model->get('events') as $plugin) {
       $successors = $plugin->get('successors')->getValue() ?? [];
       foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
         $successor['condition'] ??= '';
+        unset($successor['element_id']);
       }
 
       $eca->addEvent(
-        $plugin->get('id')->getString(),
-        $plugin->get('plugin')->getString(),
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
         $plugin->get('label')->getString(),
         $plugin->get('configuration')->getValue(),
         $successors
@@ -85,8 +87,8 @@ class EcaRepository implements EcaRepositoryInterface {
     /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
     foreach ($model->get('conditions') as $plugin) {
       $eca->addCondition(
-        $plugin->get('id')->getString(),
-        $plugin->get('plugin')->getString(),
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
         $plugin->get('configuration')->getValue(),
       );
     }
@@ -96,11 +98,13 @@ class EcaRepository implements EcaRepositoryInterface {
     foreach ($model->get('gateways') as $plugin) {
       $successors = $plugin->get('successors')->getValue() ?? [];
       foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
         $successor['condition'] ??= '';
+        unset($successor['element_id']);
       }
 
       $eca->addGateway(
-        $plugin->get('id')->getString(),
+        $plugin->get('gateway_id')->getString(),
         $plugin->get('type')->getValue(),
         $successors
       );
@@ -111,12 +115,14 @@ class EcaRepository implements EcaRepositoryInterface {
     foreach ($model->get('actions') as $plugin) {
       $successors = $plugin->get('successors')->getValue() ?? [];
       foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
         $successor['condition'] ??= '';
+        unset($successor['element_id']);
       }
 
       $eca->addAction(
-        $plugin->get('id')->getString(),
-        $plugin->get('plugin')->getString(),
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
         $plugin->get('label')->getString(),
         $plugin->get('configuration')->getValue() ?? [],
         $successors
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
index fd96b26..28f9ef7 100644
--- a/modules/agents/src/Services/ModelMapper/ModelMapper.php
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -61,7 +61,7 @@ class ModelMapper implements ModelMapperInterface {
     $model = $this->typedDataManager->create($modelDef);
 
     // Basic properties.
-    $model->set('id', $entity->id());
+    $model->set('model_id', $entity->id());
     $model->set('label', $entity->label());
     if (!empty($entity->getModel()->getDocumentation())) {
       $model->set('description', $entity->getModel()->getDocumentation());
@@ -75,8 +75,8 @@ class ModelMapper implements ModelMapperInterface {
       $def->setSetting('data_type', EcaElementType::Event);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
-      $model->set('id', $event->getId());
-      $model->set('plugin', $event->getPlugin()->getPluginId());
+      $model->set('element_id', $event->getId());
+      $model->set('plugin_id', $event->getPlugin()->getPluginId());
       $model->set('label', $event->getLabel());
       $model->set('configuration', $event->getConfiguration());
       $model->set('successors', $successors);
@@ -94,8 +94,8 @@ class ModelMapper implements ModelMapperInterface {
       $def->setSetting('data_type', EcaElementType::Condition);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
-      $model->set('id', $conditionId);
-      $model->set('plugin', $conditions[$conditionId]['plugin']);
+      $model->set('element_id', $conditionId);
+      $model->set('plugin_id', $conditions[$conditionId]['plugin']);
       $model->set('configuration', $conditions[$conditionId]['configuration']);
 
       $carry[] = $this->normalizer->normalize($model);
@@ -113,8 +113,8 @@ class ModelMapper implements ModelMapperInterface {
       $def->setSetting('data_type', EcaElementType::Action);
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create($def);
-      $model->set('id', $actionId);
-      $model->set('plugin', $actions[$actionId]['plugin']);
+      $model->set('element_id', $actionId);
+      $model->set('plugin_id', $actions[$actionId]['plugin']);
       $model->set('label', $actions[$actionId]['label']);
       $model->set('configuration', $actions[$actionId]['configuration']);
       $model->set('successors', $successors);
@@ -132,7 +132,7 @@ class ModelMapper implements ModelMapperInterface {
 
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
       $model = $this->typedDataManager->create(EcaGatewayDefinition::create());
-      $model->set('id', $gatewayId);
+      $model->set('gateway_id', $gatewayId);
       $model->set('type', $gateways[$gatewayId]['type']);
       $model->set('successors', $successors);
 
@@ -188,7 +188,7 @@ class ModelMapper implements ModelMapperInterface {
     return array_filter(array_reduce($successors, function ($carry, array $successor) {
       /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaSuccessor $model */
       $model = $this->typedDataManager->create(EcaSuccessorDefinition::create());
-      $model->set('id', $successor['id']);
+      $model->set('element_id', $successor['id']);
       $model->set('condition', $successor['condition']);
 
       $carry[] = Arr::whereNotNull($this->normalizer->normalize($model));
diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
index ad6859d..4ba9629 100644
--- a/modules/agents/src/TypedData/EcaGatewayDefinition.php
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -19,7 +19,7 @@ class EcaGatewayDefinition extends ComplexDataDefinitionBase {
   public function getPropertyDefinitions(): array {
     $properties = [];
 
-    $properties['id'] = DataDefinition::create('string')
+    $properties['gateway_id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('ID of the element'))
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
index 88d0de2..4a40b5b 100644
--- a/modules/agents/src/TypedData/EcaModelDefinition.php
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -29,7 +29,7 @@ class EcaModelDefinition extends ComplexDataDefinitionBase {
   public function getPropertyDefinitions(): array {
     $properties = [];
 
-    $properties['id'] = DataDefinition::create('string')
+    $properties['model_id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('ID'))
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
index 02e432d..364e936 100644
--- a/modules/agents/src/TypedData/EcaPluginDefinition.php
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -27,7 +27,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
   public function getPropertyDefinitions(): array {
     $properties = [];
 
-    $properties['id'] = DataDefinition::create('string')
+    $properties['element_id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('ID of the element'))
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
@@ -35,7 +35,7 @@ class EcaPluginDefinition extends ComplexDataDefinitionBase {
         'message' => 'The %value ID is not valid.',
       ]);
 
-    $properties['plugin'] = DataDefinition::create('string')
+    $properties['plugin_id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('Plugin ID'))
       ->setRequired(TRUE)
       ->addConstraint('PluginExists', [
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
index ec07268..b797cf4 100644
--- a/modules/agents/src/TypedData/EcaSuccessorDefinition.php
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -18,7 +18,7 @@ class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
   public function getPropertyDefinitions(): array {
     $properties = [];
 
-    $properties['id'] = DataDefinition::create('string')
+    $properties['element_id'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('The ID of an existing action or gateway.'))
       ->setRequired(TRUE)
       ->addConstraint('Regex', [
diff --git a/modules/agents/tests/assets/from_payload_0.json b/modules/agents/tests/assets/from_payload_0.json
index 6fdbb61..3425c79 100644
--- a/modules/agents/tests/assets/from_payload_0.json
+++ b/modules/agents/tests/assets/from_payload_0.json
@@ -1,25 +1,25 @@
 {
-  "id": "process_1",
+  "model_id": "process_1",
   "label": "Create Article on Page Publish",
   "events": [
     {
-      "id": "event_1",
-      "plugin": "content_entity:insert",
+      "element_id": "event_1",
+      "plugin_id": "content_entity:insert",
       "label": "New Page Published",
       "configuration": {
         "type": "node page"
       },
       "successors": [
         {
-          "id": "action_2"
+          "element_id": "action_2"
         }
       ]
     }
   ],
   "actions": [
     {
-      "id": "action_2",
-      "plugin": "eca_token_set_value",
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
       "label": "Set Page Title Token",
       "configuration": {
         "token_name": "page_title",
@@ -28,26 +28,26 @@
       },
       "successors": [
         {
-          "id": "action_3"
+          "element_id": "action_3"
         }
       ]
     },
     {
-      "id": "action_3",
-      "plugin": "eca_token_load_user_current",
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
       "label": "Load Author Info",
       "configuration": {
         "token_name": "author_info"
       },
       "successors": [
         {
-          "id": "action_4"
+          "element_id": "action_4"
         }
       ]
     },
     {
-      "id": "action_4",
-      "plugin": "eca_new_entity",
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
       "label": "Create New Article",
       "configuration": {
         "token_name": "new_article",
@@ -59,13 +59,13 @@
       },
       "successors": [
         {
-          "id": "action_5"
+          "element_id": "action_5"
         }
       ]
     },
     {
-      "id": "action_5",
-      "plugin": "eca_set_field_value",
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
       "label": "Set Article Body",
       "configuration": {
         "field_name": "body.value",
@@ -77,13 +77,13 @@
       },
       "successors": [
         {
-          "id": "action_6"
+          "element_id": "action_6"
         }
       ]
     },
     {
-      "id": "action_6",
-      "plugin": "eca_save_entity",
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
       "label": "Save Article",
       "successors": []
     }
diff --git a/modules/agents/tests/assets/from_payload_1.json b/modules/agents/tests/assets/from_payload_1.json
index dbd1891..fb33b80 100644
--- a/modules/agents/tests/assets/from_payload_1.json
+++ b/modules/agents/tests/assets/from_payload_1.json
@@ -1,23 +1,23 @@
 {
   "events": [
     {
-      "id": "event_1",
-      "plugin": "content_entity:insert",
+      "element_id": "event_1",
+      "plugin_id": "content_entity:insert",
       "label": "New Page Published",
       "configuration": {
         "type": "node page"
       },
       "successors": [
         {
-          "id": "action_2"
+          "element_id": "action_2"
         }
       ]
     }
   ],
   "actions": [
     {
-      "id": "action_2",
-      "plugin": "eca_token_set_value",
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
       "label": "Set Page Title Token",
       "configuration": {
         "token_name": "page_title",
@@ -26,26 +26,26 @@
       },
       "successors": [
         {
-          "id": "action_3"
+          "element_id": "action_3"
         }
       ]
     },
     {
-      "id": "action_3",
-      "plugin": "eca_token_load_user_current",
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
       "label": "Load Author Info",
       "configuration": {
         "token_name": "author_info"
       },
       "successors": [
         {
-          "id": "action_4"
+          "element_id": "action_4"
         }
       ]
     },
     {
-      "id": "action_4",
-      "plugin": "eca_new_entity",
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
       "label": "Create New Article",
       "configuration": {
         "token_name": "new_article",
@@ -57,13 +57,13 @@
       },
       "successors": [
         {
-          "id": "action_5"
+          "element_id": "action_5"
         }
       ]
     },
     {
-      "id": "action_5",
-      "plugin": "eca_set_field_value",
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
       "label": "Set Article Body",
       "configuration": {
         "field_name": "body.value",
@@ -75,13 +75,13 @@
       },
       "successors": [
         {
-          "id": "action_6"
+          "element_id": "action_6"
         }
       ]
     },
     {
-      "id": "action_6",
-      "plugin": "eca_save_entity",
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
       "label": "Save Article",
       "successors": []
     }
diff --git a/modules/agents/tests/assets/from_payload_2.json b/modules/agents/tests/assets/from_payload_2.json
index 729e3ac..98bbba1 100644
--- a/modules/agents/tests/assets/from_payload_2.json
+++ b/modules/agents/tests/assets/from_payload_2.json
@@ -1,10 +1,10 @@
 {
-  "id": "process_1",
+  "model_id": "process_1",
   "label": "Create Article on Page Publish",
   "actions": [
     {
-      "id": "action_2",
-      "plugin": "eca_token_set_value",
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
       "label": "Set Page Title Token",
       "configuration": {
         "token_name": "page_title",
@@ -13,26 +13,26 @@
       },
       "successors": [
         {
-          "id": "action_3"
+          "element_id": "action_3"
         }
       ]
     },
     {
-      "id": "action_3",
-      "plugin": "eca_token_load_user_current",
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
       "label": "Load Author Info",
       "configuration": {
         "token_name": "author_info"
       },
       "successors": [
         {
-          "id": "action_4"
+          "element_id": "action_4"
         }
       ]
     },
     {
-      "id": "action_4",
-      "plugin": "eca_new_entity",
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
       "label": "Create New Article",
       "configuration": {
         "token_name": "new_article",
@@ -44,13 +44,13 @@
       },
       "successors": [
         {
-          "id": "action_5"
+          "element_id": "action_5"
         }
       ]
     },
     {
-      "id": "action_5",
-      "plugin": "eca_set_field_value",
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
       "label": "Set Article Body",
       "configuration": {
         "field_name": "body.value",
@@ -62,13 +62,13 @@
       },
       "successors": [
         {
-          "id": "action_6"
+          "element_id": "action_6"
         }
       ]
     },
     {
-      "id": "action_6",
-      "plugin": "eca_save_entity",
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
       "label": "Save Article",
       "successors": []
     }
diff --git a/modules/agents/tests/assets/from_payload_3.json b/modules/agents/tests/assets/from_payload_3.json
index a7999e5..c564472 100644
--- a/modules/agents/tests/assets/from_payload_3.json
+++ b/modules/agents/tests/assets/from_payload_3.json
@@ -1,26 +1,26 @@
 {
-  "id": "create_article_on_new_page",
+  "model_id": "create_article_on_new_page",
   "label": "Create Article on New Page",
   "version": "v1",
   "events": [
     {
-      "id": "start_event",
-      "plugin": "content_entity:insert",
+      "element_id": "start_event",
+      "plugin_id": "content_entity:insert",
       "label": "New Page Published",
       "configuration": {
         "type": "node page"
       },
       "successors": [
         {
-          "id": "extract_page_title"
+          "element_id": "extract_page_title"
         }
       ]
     }
   ],
   "conditions": [
     {
-      "id": "check_title_for_ai",
-      "plugin": "eca_entity_field_value",
+      "element_id": "check_title_for_ai",
+      "plugin_id": "eca_entity_field_value",
       "configuration": {
         "field_name": "title",
         "expected_value": "AI",
@@ -34,23 +34,23 @@
   ],
   "gateways": [
     {
-      "id": "title_check_gateway",
+      "gateway_id": "title_check_gateway",
       "type": 0,
       "successors": [
         {
-          "id": "create_article_unpublished",
+          "element_id": "create_article_unpublished",
           "condition": "check_title_for_ai"
         },
         {
-          "id": "create_article_published"
+          "element_id": "create_article_published"
         }
       ]
     }
   ],
   "actions": [
     {
-      "id": "extract_page_title",
-      "plugin": "eca_get_field_value",
+      "element_id": "extract_page_title",
+      "plugin_id": "eca_get_field_value",
       "label": "Extract Page Title",
       "configuration": {
         "field_name": "title",
@@ -58,13 +58,13 @@
       },
       "successors": [
         {
-          "id": "title_check_gateway"
+          "element_id": "title_check_gateway"
         }
       ]
     },
     {
-      "id": "create_article_unpublished",
-      "plugin": "eca_new_entity",
+      "element_id": "create_article_unpublished",
+      "plugin_id": "eca_new_entity",
       "label": "Create Unpublished Article",
       "configuration": {
         "token_name": "new_article",
@@ -76,8 +76,8 @@
       }
     },
     {
-      "id": "create_article_published",
-      "plugin": "eca_new_entity",
+      "element_id": "create_article_published",
+      "plugin_id": "eca_new_entity",
       "label": "Create Published Article",
       "configuration": {
         "token_name": "new_article",
diff --git a/modules/agents/tests/assets/from_payload_4.json b/modules/agents/tests/assets/from_payload_4.json
index 0331b54..5eca026 100644
--- a/modules/agents/tests/assets/from_payload_4.json
+++ b/modules/agents/tests/assets/from_payload_4.json
@@ -1,17 +1,17 @@
 {
-  "id": "create_article_from_page",
+  "model_id": "create_article_from_page",
   "label": "Create Article From Page Publication",
   "events": [
     {
-      "id": "event_page_published",
-      "plugin": "content_entity:insert",
+      "element_id": "event_page_published",
+      "plugin_id": "content_entity:insert",
       "label": "Page Published",
       "configuration": {
         "type": "node page"
       },
       "successors": [
         {
-          "id": "condition_check_title",
+          "element_id": "condition_check_title",
           "condition": ""
         }
       ]
@@ -19,8 +19,8 @@
   ],
   "conditions": [
     {
-      "id": "condition_check_title",
-      "plugin": "eca_entity_field_value",
+      "element_id": "condition_check_title",
+      "plugin_id": "eca_entity_field_value",
       "configuration": {
         "field_name": "title",
         "expected_value": "AI",
@@ -34,15 +34,15 @@
   ],
   "gateways": [
     {
-      "id": "gateway_title_check",
+      "gateway_id": "gateway_title_check",
       "type": 0,
       "successors": [
         {
-          "id": "action_create_article_offline",
+          "element_id": "action_create_article_offline",
           "condition": "condition_check_title"
         },
         {
-          "id": "action_create_article",
+          "element_id": "action_create_article",
           "condition": ""
         }
       ]
@@ -50,8 +50,8 @@
   ],
   "actions": [
     {
-      "id": "action_create_article",
-      "plugin": "eca_new_entity",
+      "element_id": "action_create_article",
+      "plugin_id": "eca_new_entity",
       "label": "Create New Article",
       "configuration": {
         "token_name": "new_article",
@@ -63,14 +63,14 @@
       },
       "successors": [
         {
-          "id": "action_set_article_field",
+          "element_id": "action_set_article_field",
           "condition": ""
         }
       ]
     },
     {
-      "id": "action_create_article_offline",
-      "plugin": "eca_new_entity",
+      "element_id": "action_create_article_offline",
+      "plugin_id": "eca_new_entity",
       "label": "Create New Article Offline",
       "configuration": {
         "token_name": "new_article",
@@ -82,14 +82,14 @@
       },
       "successors": [
         {
-          "id": "action_set_article_field",
+          "element_id": "action_set_article_field",
           "condition": ""
         }
       ]
     },
     {
-      "id": "action_set_article_field",
-      "plugin": "eca_set_field_value",
+      "element_id": "action_set_article_field",
+      "plugin_id": "eca_set_field_value",
       "label": "Set Article Body",
       "configuration": {
         "field_name": "body.value",
diff --git a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
index 024fa59..94fa524 100644
--- a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -54,7 +54,7 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
       Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
       [],
       NULL,
-      'id: This value should not be null',
+      'model_id: This value should not be null',
     ];
 
     yield [
@@ -92,7 +92,7 @@ abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
       Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_4.json', __DIR__))),
       [],
       NULL,
-      "Invalid successor ID 'condition_check_title' for Event 'event_page_published'. Must be a gateway or action.",
+      "Invalid successor ID 'condition_check_title' for Event 'event_page_published'. Must reference a gateway or action.",
     ];
   }
 
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
index 69fadb5..f7eab86 100644
--- a/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
+++ b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
@@ -5,7 +5,7 @@
     "title": "ECA Model Schema",
     "description": "The schema describing the properties of an ECA model.",
     "properties": {
-        "id": {
+        "model_id": {
             "type": "string",
             "title": "ID"
         },
@@ -27,11 +27,11 @@
             "items": {
                 "type": "object",
                 "properties": {
-                    "id": {
+                    "element_id": {
                         "type": "string",
                         "title": "ID of the element"
                     },
-                    "plugin": {
+                    "plugin_id": {
                         "type": "string",
                         "title": "Plugin ID"
                     },
@@ -48,7 +48,7 @@
                         "items": {
                             "type": "object",
                             "properties": {
-                                "id": {
+                                "element_id": {
                                     "type": "string",
                                     "title": "The ID of an existing action or gateway."
                                 },
@@ -58,14 +58,14 @@
                                 }
                             },
                             "required": [
-                                "id"
+                                "element_id"
                             ]
                         }
                     }
                 },
                 "required": [
-                    "id",
-                    "plugin",
+                    "element_id",
+                    "plugin_id",
                     "label"
                 ]
             },
@@ -77,11 +77,11 @@
             "items": {
                 "type": "object",
                 "properties": {
-                    "id": {
+                    "element_id": {
                         "type": "string",
                         "title": "ID of the element"
                     },
-                    "plugin": {
+                    "plugin_id": {
                         "type": "string",
                         "title": "Plugin ID"
                     },
@@ -90,8 +90,8 @@
                     }
                 },
                 "required": [
-                    "id",
-                    "plugin"
+                    "element_id",
+                    "plugin_id"
                 ]
             }
         },
@@ -101,7 +101,7 @@
             "items": {
                 "type": "object",
                 "properties": {
-                    "id": {
+                    "gateway_id": {
                         "type": "string",
                         "title": "ID of the element"
                     },
@@ -118,7 +118,7 @@
                         "items": {
                             "type": "object",
                             "properties": {
-                                "id": {
+                                "element_id": {
                                     "type": "string",
                                     "title": "The ID of an existing action or gateway."
                                 },
@@ -128,13 +128,13 @@
                                 }
                             },
                             "required": [
-                                "id"
+                                "element_id"
                             ]
                         }
                     }
                 },
                 "required": [
-                    "id"
+                    "gateway_id"
                 ]
             }
         },
@@ -144,11 +144,11 @@
             "items": {
                 "type": "object",
                 "properties": {
-                    "id": {
+                    "element_id": {
                         "type": "string",
                         "title": "ID of the element"
                     },
-                    "plugin": {
+                    "plugin_id": {
                         "type": "string",
                         "title": "Plugin ID"
                     },
@@ -165,7 +165,7 @@
                         "items": {
                             "type": "object",
                             "properties": {
-                                "id": {
+                                "element_id": {
                                     "type": "string",
                                     "title": "The ID of an existing action or gateway."
                                 },
@@ -175,14 +175,14 @@
                                 }
                             },
                             "required": [
-                                "id"
+                                "element_id"
                             ]
                         }
                     }
                 },
                 "required": [
-                    "id",
-                    "plugin",
+                    "element_id",
+                    "plugin_id",
                     "label"
                 ]
             },
@@ -190,7 +190,7 @@
         }
     },
     "required": [
-        "id",
+        "model_id",
         "label",
         "events",
         "actions"
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json
index 37ca359..4280d51 100644
--- a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json	
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json	
@@ -1,37 +1,37 @@
 {
-    "id": "eca_test_0001",
+    "model_id": "eca_test_0001",
     "label": "Cross references",
     "version": null,
     "description": "Two different node types are referring each other. If one node gets saved with a reference to another node, the other node gets automatically updated to link back to the first node.",
     "events": [
         {
-            "id": "Event_011cx7s",
-            "plugin": "content_entity:insert",
+            "element_id": "Event_011cx7s",
+            "plugin_id": "content_entity:insert",
             "label": "Insert node",
             "configuration": {
                 "type": "node _all"
             },
             "successors": [
                 {
-                    "id": "Activity_1rlgsjy",
+                    "element_id": "Activity_1rlgsjy",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Event_1cfd8ek",
-            "plugin": "content_entity:update",
+            "element_id": "Event_1cfd8ek",
+            "plugin_id": "content_entity:update",
             "label": "Update node",
             "configuration": {
                 "type": "node _all"
             },
             "successors": [
                 {
-                    "id": "Activity_1rlgsjy",
+                    "element_id": "Activity_1rlgsjy",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_1cxcwjm",
+                    "element_id": "Activity_1cxcwjm",
                     "condition": ""
                 }
             ]
@@ -39,8 +39,8 @@
     ],
     "conditions": [
         {
-            "id": "Flow_0iztkfs",
-            "plugin": "eca_entity_type_bundle",
+            "element_id": "Flow_0iztkfs",
+            "plugin_id": "eca_entity_type_bundle",
             "configuration": {
                 "negate": false,
                 "type": "node type_1",
@@ -48,8 +48,8 @@
             }
         },
         {
-            "id": "Flow_1jqykgu",
-            "plugin": "eca_entity_type_bundle",
+            "element_id": "Flow_1jqykgu",
+            "plugin_id": "eca_entity_type_bundle",
             "configuration": {
                 "negate": false,
                 "type": "node type_2",
@@ -57,8 +57,8 @@
             }
         },
         {
-            "id": "Flow_0i81v8o",
-            "plugin": "eca_entity_field_value",
+            "element_id": "Flow_0i81v8o",
+            "plugin_id": "eca_entity_field_value",
             "configuration": {
                 "case": false,
                 "expected_value": "[entity:nid]",
@@ -70,8 +70,8 @@
             }
         },
         {
-            "id": "Flow_1tgic5x",
-            "plugin": "eca_entity_field_value_empty",
+            "element_id": "Flow_1tgic5x",
+            "plugin_id": "eca_entity_field_value_empty",
             "configuration": {
                 "field_name": "field_other_node",
                 "negate": true,
@@ -79,8 +79,8 @@
             }
         },
         {
-            "id": "Flow_0rgzuve",
-            "plugin": "eca_entity_field_value_empty",
+            "element_id": "Flow_0rgzuve",
+            "plugin_id": "eca_entity_field_value_empty",
             "configuration": {
                 "negate": false,
                 "field_name": "field_other_node",
@@ -88,8 +88,8 @@
             }
         },
         {
-            "id": "Flow_0c3s897",
-            "plugin": "eca_entity_field_value_empty",
+            "element_id": "Flow_0c3s897",
+            "plugin_id": "eca_entity_field_value_empty",
             "configuration": {
                 "field_name": "field_other_node",
                 "negate": true,
@@ -99,15 +99,15 @@
     ],
     "gateways": [
         {
-            "id": "Gateway_1xl2rvc",
+            "gateway_id": "Gateway_1xl2rvc",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_0k6im8f",
+                    "element_id": "Activity_0k6im8f",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_1oj601y",
+                    "element_id": "Activity_1oj601y",
                     "condition": ""
                 }
             ]
@@ -115,8 +115,8 @@
     ],
     "actions": [
         {
-            "id": "Activity_1rlgsjy",
-            "plugin": "eca_token_load_entity",
+            "element_id": "Activity_1rlgsjy",
+            "plugin_id": "eca_token_load_entity",
             "label": "Load original entity",
             "configuration": {
                 "token_name": "originalentity",
@@ -132,18 +132,18 @@
             },
             "successors": [
                 {
-                    "id": "Activity_0r1gs9s",
+                    "element_id": "Activity_0r1gs9s",
                     "condition": "Flow_0iztkfs"
                 },
                 {
-                    "id": "Activity_0r1gs9s",
+                    "element_id": "Activity_0r1gs9s",
                     "condition": "Flow_1jqykgu"
                 }
             ]
         },
         {
-            "id": "Activity_0h8b7vh",
-            "plugin": "eca_token_load_entity_ref",
+            "element_id": "Activity_0h8b7vh",
+            "plugin_id": "eca_token_load_entity_ref",
             "label": "Load referenced node",
             "configuration": {
                 "field_name_entity_ref": "field_other_node",
@@ -160,14 +160,14 @@
             },
             "successors": [
                 {
-                    "id": "Gateway_1xl2rvc",
+                    "element_id": "Gateway_1xl2rvc",
                     "condition": "Flow_0i81v8o"
                 }
             ]
         },
         {
-            "id": "Activity_0k6im8f",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_0k6im8f",
+            "plugin_id": "eca_set_field_value",
             "label": "Set Cross Ref",
             "configuration": {
                 "field_name": "field_other_node",
@@ -181,24 +181,24 @@
             "successors": []
         },
         {
-            "id": "Activity_0r1gs9s",
-            "plugin": "eca_void_and_condition",
+            "element_id": "Activity_0r1gs9s",
+            "plugin_id": "eca_void_and_condition",
             "label": "void",
             "configuration": [],
             "successors": [
                 {
-                    "id": "Activity_0h8b7vh",
+                    "element_id": "Activity_0h8b7vh",
                     "condition": "Flow_1tgic5x"
                 },
                 {
-                    "id": "Activity_1ch3wrr",
+                    "element_id": "Activity_1ch3wrr",
                     "condition": "Flow_0rgzuve"
                 }
             ]
         },
         {
-            "id": "Activity_1oj601y",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1oj601y",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "message": "Node [entity:title] references [refentity:title]",
@@ -207,8 +207,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1cxcwjm",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1cxcwjm",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "message": "Node [entity:title] got updated",
@@ -217,8 +217,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1w7m4sk",
-            "plugin": "eca_token_load_entity_ref",
+            "element_id": "Activity_1w7m4sk",
+            "plugin_id": "eca_token_load_entity_ref",
             "label": "Load referenced node",
             "configuration": {
                 "field_name_entity_ref": "field_other_node",
@@ -235,30 +235,30 @@
             },
             "successors": [
                 {
-                    "id": "Activity_1bfoheo",
+                    "element_id": "Activity_1bfoheo",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_077d2t8",
+                    "element_id": "Activity_077d2t8",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Activity_1ch3wrr",
-            "plugin": "eca_void_and_condition",
+            "element_id": "Activity_1ch3wrr",
+            "plugin_id": "eca_void_and_condition",
             "label": "void",
             "configuration": [],
             "successors": [
                 {
-                    "id": "Activity_1w7m4sk",
+                    "element_id": "Activity_1w7m4sk",
                     "condition": "Flow_0c3s897"
                 }
             ]
         },
         {
-            "id": "Activity_1bfoheo",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_1bfoheo",
+            "plugin_id": "eca_set_field_value",
             "label": "Empty Cross Ref",
             "configuration": {
                 "field_name": "field_other_node",
@@ -272,8 +272,8 @@
             "successors": []
         },
         {
-            "id": "Activity_077d2t8",
-            "plugin": "action_message_action",
+            "element_id": "Activity_077d2t8",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "message": "The title of the referenced node is [originalentity:field_other_node:entity:title].",
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json
index 98afd15..8ba24aa 100644
--- a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json	
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json	
@@ -1,59 +1,59 @@
 {
-    "id": "eca_test_0002",
+    "model_id": "eca_test_0002",
     "label": "Entity Events Part 1",
     "version": null,
     "description": "Triggers custom events in the same model and in another model, see also \"Entity Events Part 2\"",
     "events": [
         {
-            "id": "Event_0wm7ta0",
-            "plugin": "content_entity:presave",
+            "element_id": "Event_0wm7ta0",
+            "plugin_id": "content_entity:presave",
             "label": "Pre-save",
             "configuration": {
                 "type": "node _all"
             },
             "successors": [
                 {
-                    "id": "Activity_1do22d1",
+                    "element_id": "Activity_1do22d1",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Event_0sr0xl6",
-            "plugin": "content_entity:custom",
+            "element_id": "Event_0sr0xl6",
+            "plugin_id": "content_entity:custom",
             "label": "C1",
             "configuration": {
                 "event_id": "C1"
             },
             "successors": [
                 {
-                    "id": "Activity_1sh3bdl",
+                    "element_id": "Activity_1sh3bdl",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Event_1l6ov1l",
-            "plugin": "user:set_user",
+            "element_id": "Event_1l6ov1l",
+            "plugin_id": "user:set_user",
             "label": "Set current user",
             "configuration": [],
             "successors": [
                 {
-                    "id": "Activity_1p5hvp4",
+                    "element_id": "Activity_1p5hvp4",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Event_0n1zpul",
-            "plugin": "eca_base:eca_custom",
+            "element_id": "Event_0n1zpul",
+            "plugin_id": "eca_base:eca_custom",
             "label": "Cplain",
             "configuration": {
                 "event_id": ""
             },
             "successors": [
                 {
-                    "id": "Activity_1gguvde",
+                    "element_id": "Activity_1gguvde",
                     "condition": ""
                 }
             ]
@@ -63,8 +63,8 @@
     "gateways": [],
     "actions": [
         {
-            "id": "Activity_1do22d1",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1do22d1",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "replace_tokens": false,
@@ -72,26 +72,26 @@
             },
             "successors": [
                 {
-                    "id": "Activity_03j3ob6",
+                    "element_id": "Activity_03j3ob6",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_1k70gka",
+                    "element_id": "Activity_1k70gka",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_150pgta",
+                    "element_id": "Activity_150pgta",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_00ca469",
+                    "element_id": "Activity_00ca469",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Activity_03j3ob6",
-            "plugin": "eca_trigger_content_entity_custom_event",
+            "element_id": "Activity_03j3ob6",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
             "label": "Trigger C1",
             "configuration": {
                 "event_id": "C1",
@@ -101,8 +101,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1k70gka",
-            "plugin": "eca_trigger_content_entity_custom_event",
+            "element_id": "Activity_1k70gka",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
             "label": "Trigger C2",
             "configuration": {
                 "event_id": "C2",
@@ -112,22 +112,22 @@
             "successors": []
         },
         {
-            "id": "Activity_150pgta",
-            "plugin": "eca_token_load_user_current",
+            "element_id": "Activity_150pgta",
+            "plugin_id": "eca_token_load_user_current",
             "label": "Load current user",
             "configuration": {
                 "token_name": "user"
             },
             "successors": [
                 {
-                    "id": "Activity_1acmymx",
+                    "element_id": "Activity_1acmymx",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Activity_1acmymx",
-            "plugin": "eca_trigger_content_entity_custom_event",
+            "element_id": "Activity_1acmymx",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
             "label": "Trigger C3",
             "configuration": {
                 "event_id": "C3",
@@ -137,8 +137,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1sh3bdl",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1sh3bdl",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "replace_tokens": false,
@@ -147,8 +147,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1p5hvp4",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1p5hvp4",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "replace_tokens": false,
@@ -157,8 +157,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1gguvde",
-            "plugin": "action_message_action",
+            "element_id": "Activity_1gguvde",
+            "plugin_id": "action_message_action",
             "label": "Msg",
             "configuration": {
                 "replace_tokens": false,
@@ -167,8 +167,8 @@
             "successors": []
         },
         {
-            "id": "Activity_00ca469",
-            "plugin": "eca_trigger_custom_event",
+            "element_id": "Activity_00ca469",
+            "plugin_id": "eca_trigger_custom_event",
             "label": "Trigger Cplain",
             "configuration": {
                 "event_id": "Cplain",
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json
index 797191c..e05ea77 100644
--- a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json	
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json	
@@ -1,35 +1,35 @@
 {
-    "id": "eca_test_0009",
+    "model_id": "eca_test_0009",
     "label": "Set field values",
     "version": null,
     "description": "Set single and multi value fields with values, testing different variations.",
     "events": [
         {
-            "id": "Event_056l2f4",
-            "plugin": "content_entity:presave",
+            "element_id": "Event_056l2f4",
+            "plugin_id": "content_entity:presave",
             "label": "Presave Node",
             "configuration": {
                 "type": "node type_set_field_value"
             },
             "successors": [
                 {
-                    "id": "Gateway_113xj72",
+                    "element_id": "Gateway_113xj72",
                     "condition": "Flow_0j7r2le"
                 },
                 {
-                    "id": "Gateway_0nagg07",
+                    "element_id": "Gateway_0nagg07",
                     "condition": "Flow_1eoahw0"
                 },
                 {
-                    "id": "Gateway_1tbbhie",
+                    "element_id": "Gateway_1tbbhie",
                     "condition": "Flow_0e3yjwm"
                 },
                 {
-                    "id": "Gateway_1byzhmc",
+                    "element_id": "Gateway_1byzhmc",
                     "condition": "Flow_0wind58"
                 },
                 {
-                    "id": "Gateway_0aowp4i",
+                    "element_id": "Gateway_0aowp4i",
                     "condition": "Flow_0iwzr0t"
                 }
             ]
@@ -37,8 +37,8 @@
     ],
     "conditions": [
         {
-            "id": "Flow_0j7r2le",
-            "plugin": "eca_entity_field_value",
+            "element_id": "Flow_0j7r2le",
+            "plugin_id": "eca_entity_field_value",
             "configuration": {
                 "negate": false,
                 "case": false,
@@ -50,16 +50,16 @@
             }
         },
         {
-            "id": "Flow_1eoahw0",
-            "plugin": "eca_entity_is_new",
+            "element_id": "Flow_1eoahw0",
+            "plugin_id": "eca_entity_is_new",
             "configuration": {
                 "negate": false,
                 "entity": ""
             }
         },
         {
-            "id": "Flow_0e3yjwm",
-            "plugin": "eca_entity_field_value",
+            "element_id": "Flow_0e3yjwm",
+            "plugin_id": "eca_entity_field_value",
             "configuration": {
                 "negate": false,
                 "case": false,
@@ -71,8 +71,8 @@
             }
         },
         {
-            "id": "Flow_0wind58",
-            "plugin": "eca_entity_field_value",
+            "element_id": "Flow_0wind58",
+            "plugin_id": "eca_entity_field_value",
             "configuration": {
                 "negate": false,
                 "case": false,
@@ -84,8 +84,8 @@
             }
         },
         {
-            "id": "Flow_0iwzr0t",
-            "plugin": "eca_entity_field_value",
+            "element_id": "Flow_0iwzr0t",
+            "plugin_id": "eca_entity_field_value",
             "configuration": {
                 "negate": false,
                 "case": false,
@@ -99,59 +99,59 @@
     ],
     "gateways": [
         {
-            "id": "Gateway_113xj72",
+            "gateway_id": "Gateway_113xj72",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_0na1ecf",
+                    "element_id": "Activity_0na1ecf",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_1on1kw2",
+                    "element_id": "Activity_1on1kw2",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Gateway_1tbbhie",
+            "gateway_id": "Gateway_1tbbhie",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_03beihz",
+                    "element_id": "Activity_03beihz",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Gateway_0nagg07",
+            "gateway_id": "Gateway_0nagg07",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_1agtxee",
+                    "element_id": "Activity_1agtxee",
                     "condition": ""
                 },
                 {
-                    "id": "Activity_13qlvkq",
+                    "element_id": "Activity_13qlvkq",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Gateway_1byzhmc",
+            "gateway_id": "Gateway_1byzhmc",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_0yt6yuv",
+                    "element_id": "Activity_0yt6yuv",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Gateway_0aowp4i",
+            "gateway_id": "Gateway_0aowp4i",
             "type": 0,
             "successors": [
                 {
-                    "id": "Activity_036vgtd",
+                    "element_id": "Activity_036vgtd",
                     "condition": ""
                 }
             ]
@@ -159,8 +159,8 @@
     ],
     "actions": [
         {
-            "id": "Activity_1agtxee",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_1agtxee",
+            "plugin_id": "eca_set_field_value",
             "label": "Set text line",
             "configuration": {
                 "field_name": "field_text_line",
@@ -174,8 +174,8 @@
             "successors": []
         },
         {
-            "id": "Activity_13qlvkq",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_13qlvkq",
+            "plugin_id": "eca_set_field_value",
             "label": "Set lines 1",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -189,8 +189,8 @@
             "successors": []
         },
         {
-            "id": "Activity_0na1ecf",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_0na1ecf",
+            "plugin_id": "eca_set_field_value",
             "label": "Overwrite text line",
             "configuration": {
                 "field_name": "field_text_line",
@@ -204,8 +204,8 @@
             "successors": []
         },
         {
-            "id": "Activity_1on1kw2",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_1on1kw2",
+            "plugin_id": "eca_set_field_value",
             "label": "Append line",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -218,14 +218,14 @@
             },
             "successors": [
                 {
-                    "id": "Activity_0aa91q1",
+                    "element_id": "Activity_0aa91q1",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Activity_0aa91q1",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_0aa91q1",
+            "plugin_id": "eca_set_field_value",
             "label": "Append another line",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -238,14 +238,14 @@
             },
             "successors": [
                 {
-                    "id": "Activity_0zcaglk",
+                    "element_id": "Activity_0zcaglk",
                     "condition": ""
                 }
             ]
         },
         {
-            "id": "Activity_0zcaglk",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_0zcaglk",
+            "plugin_id": "eca_set_field_value",
             "label": "Append another line",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -259,8 +259,8 @@
             "successors": []
         },
         {
-            "id": "Activity_03beihz",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_03beihz",
+            "plugin_id": "eca_set_field_value",
             "label": "Append line",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -274,8 +274,8 @@
             "successors": []
         },
         {
-            "id": "Activity_0yt6yuv",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_0yt6yuv",
+            "plugin_id": "eca_set_field_value",
             "label": "Reset lines",
             "configuration": {
                 "field_name": "field_text_lines",
@@ -289,8 +289,8 @@
             "successors": []
         },
         {
-            "id": "Activity_036vgtd",
-            "plugin": "eca_set_field_value",
+            "element_id": "Activity_036vgtd",
+            "plugin_id": "eca_set_field_value",
             "label": "Prepend line",
             "configuration": {
                 "field_name": "field_text_lines",
-- 
GitLab


From b4c5d4579874fb9242e0a3d7aa019ea060ae1e6c Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sun, 12 Jan 2025 14:44:14 +0100
Subject: [PATCH 86/95] #3481307 Fix typo

---
 modules/agents/prompts/eca/buildModel.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index aa1ed0d..00d1b03 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -42,7 +42,7 @@ prompt:
   one_shot_learning_examples:
     - action: build
       model: |
-        {"id":"process_1","label":"Create Article on Page Publish","events":[{"id":"event_1","plugin":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"id":"action_2"}]}],"actions":[{"id":"action_2","plugin":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"id":"action_3"}]},{"id":"action_3","plugin":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"id":"action_4"}]},{"id":"action_4","plugin":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"id":"action_5"}]},{"id":"action_5","plugin":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"id":"action_6"}]},{"id":"action_6","plugin":"eca_save_entity","label":"Save Article","successors":[]}]}
-      message: The model "Create Article on Page Publish" creates an article about a new published page, contain the label and the author of that new page.
+        {"model_id":"process_1","label":"Create Article on Page Publish","events":[{"element_id":"event_1","plugin_id":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"element_id":"action_2"}]}],"actions":[{"element_id":"action_2","plugin_id":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"element_id":"action_3"}]},{"element_id":"action_3","plugin_id":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"element_id":"action_4"}]},{"element_id":"action_4","plugin_id":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"element_id":"action_5"}]},{"element_id":"action_5","plugin_id":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"element_id":"action_6"}]},{"element_id":"action_6","plugin_id":"eca_save_entity","label":"Save Article","successors":[]}]}
+      message: The model "Create Article on Page Publish" creates an article about a new published page, it contains the label and the author of that new page.
     - action: fail
       message: I was unable to analyze the description.
-- 
GitLab


From 68a32510b28eaf11706f6fdba94a277f668d4f10 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sun, 12 Jan 2025 14:45:52 +0100
Subject: [PATCH 87/95] #3481307 Fix phpcs warnings

---
 modules/agents/src/Form/AskAiForm.php     | 5 ++++-
 modules/agents/src/Hook/FormHooks.php     | 2 --
 modules/agents/src/Plugin/AiAgent/Eca.php | 1 -
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index 2d183e7..a9e53e5 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -72,7 +72,10 @@ class AskAiForm extends FormBase {
     $batch->setFinishCallback([self::class, 'batchFinished']);
 
     $batch->addOperation([self::class, 'initProcess'], [$form_state->getValue('destination')]);
-    $batch->addOperation([self::class, 'determineTask'], [$form_state->getValue('question'), $form_state->getValue('model_id')]);
+    $batch->addOperation([self::class, 'determineTask'], [
+      $form_state->getValue('question'),
+      $form_state->getValue('model_id'),
+    ]);
     $batch->addOperation([self::class, 'executeTask']);
 
     batch_set($batch->toArray());
diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
index 0a780e2..9611eb2 100644
--- a/modules/agents/src/Hook/FormHooks.php
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -23,8 +23,6 @@ class FormHooks {
    *   The form.
    * @param \Drupal\Core\Form\FormStateInterface $formState
    *   The form state.
-   *
-   * @return void
    */
   #[Hook('form_bpmn_io_modeller_alter')]
   public function formModellerAlter(array &$form, FormStateInterface $formState): void {
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
index 0ae1fd0..a0fe9b2 100644
--- a/modules/agents/src/Plugin/AiAgent/Eca.php
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -9,7 +9,6 @@ use Drupal\Component\Serialization\Json;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\Url;
 use Drupal\ai_agents\Attribute\AiAgent;
 use Drupal\ai_agents\PluginBase\AiAgentBase;
 use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
-- 
GitLab


From f85f21cd742b68ce5b95bbbb20ea2d3cc198c7e0 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Sun, 12 Jan 2025 14:50:42 +0100
Subject: [PATCH 88/95] #3481307 Solve PHPStan issues

---
 modules/agents/ai_eca_agents.module   |  1 +
 modules/agents/src/Form/AskAiForm.php |  4 ++--
 modules/agents/src/Hook/FormHooks.php | 16 ++++++++++++++--
 3 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/modules/agents/ai_eca_agents.module b/modules/agents/ai_eca_agents.module
index 3cf22ee..5071496 100644
--- a/modules/agents/ai_eca_agents.module
+++ b/modules/agents/ai_eca_agents.module
@@ -7,6 +7,7 @@
 
 use Drupal\ai_eca_agents\Hook\FormHooks;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\LegacyHook;
 
 /**
  * Implements hook_FORM_ID_alter.
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
index a9e53e5..ce4e865 100644
--- a/modules/agents/src/Form/AskAiForm.php
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -35,14 +35,14 @@ class AskAiForm extends FormBase {
     ];
 
     // Determine the destination for when the batch process is finished.
-    $destination = \Drupal::request()->query->get('destination', Url::fromRoute('entity.eca.collection')->toString());
+    $destination = $this->getRequest()->query->get('destination', Url::fromRoute('entity.eca.collection')->toString());
     $form['destination'] = [
       '#type' => 'value',
       '#value' => $destination,
     ];
 
     // Determine if an existing model is the subject of the prompt.
-    $modelId = \Drupal::request()->query->get('model-id');
+    $modelId = $this->getRequest()->query->get('model-id');
     $form['model_id'] = [
       '#type' => 'value',
       '#value' => $modelId,
diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
index 9611eb2..0494334 100644
--- a/modules/agents/src/Hook/FormHooks.php
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -4,10 +4,11 @@ namespace Drupal\ai_eca_agents\Hook;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
 use Drupal\eca\Entity\Eca;
-use Drush\Attributes\Hook;
 
 /**
  * Provides hook implementations for form alterations.
@@ -16,6 +17,17 @@ class FormHooks {
 
   use StringTranslationTrait;
 
+  /**
+   * Constructs a FormHooks instance.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
+   *   The route match.
+   */
+  public function __construct(
+    protected RouteMatchInterface $routeMatch,
+  ) {
+  }
+
   /**
    * Alters the ECA Modeller form.
    *
@@ -31,7 +43,7 @@ class FormHooks {
     $exportLink = $form['actions']['export_archive']['#url'];
     $options = $exportLink->getOptions();
 
-    $eca = \Drupal::routeMatch()->getParameter('eca');
+    $eca = $this->routeMatch->getParameter('eca');
     if ($eca instanceof Eca) {
       $eca = $eca->id();
     }
-- 
GitLab


From 9cd0e5256a51df037fa04eb3bc44b9d9e617a5f3 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 14 Jan 2025 11:32:57 +0100
Subject: [PATCH 89/95] #3481307 Stop validation if the LLM failed to respond

---
 .../agents/src/Plugin/AiAgentValidation/EcaValidation.php    | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
index 4ecb3d4..3b25949 100644
--- a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -73,6 +73,11 @@ class EcaValidation extends AiAgentValidationPluginBase implements ContainerFact
       );
     }
 
+    // Stop validation if the LLM failed to return a response.
+    if (Arr::get($data, '0.action', 'fail')) {
+      return TRUE;
+    }
+
     // Validate the response against ECA-model schema.
     try {
       $this->modelMapper->fromPayload(Json::decode(Arr::get($data, '0.model')));
-- 
GitLab


From fa5553fc06d8c6b119254930a3c489b9859cac9a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Tue, 14 Jan 2025 11:33:20 +0100
Subject: [PATCH 90/95] #3481307 Ensure that the correct property is filtered

---
 modules/agents/src/Services/DataProvider/DataProvider.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
index 4dcb74e..76dce05 100644
--- a/modules/agents/src/Services/DataProvider/DataProvider.php
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -141,7 +141,7 @@ class DataProvider implements DataProviderInterface {
       $data = array_filter($this->normalizer->normalize($model));
 
       if ($this->viewMode === DataViewModeEnum::Teaser) {
-        $data = Arr::only($data, ['id', 'label', 'description']);
+        $data = Arr::only($data, ['model_id', 'label', 'description']);
       }
 
       $carry[] = $data;
-- 
GitLab


From 4a3f86ede489188368e807bab3574ff08779083a Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 24 Jan 2025 09:15:44 +0100
Subject: [PATCH 91/95] #3481307 Adjust prompt for model building

---
 modules/agents/prompts/eca/buildModel.yml | 34 +++++++++++------------
 1 file changed, 16 insertions(+), 18 deletions(-)

diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
index 00d1b03..a2fde9d 100644
--- a/modules/agents/prompts/eca/buildModel.yml
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -8,30 +8,28 @@ validation:
 retries: 2
 prompt:
   introduction: |
-    You are a business process modelling expert. You will be given a textual description of a business process.
-    Generate a JSON model for the process, based on the provided JSON Schema.
+    You are a business process modelling expert, following the BPMN 2.0 standard. The prompts will contain a textual
+    description of a business process or updates that you need to execute.
+    Generate a JSON model for the process, complying to the provided JSON Schema. YOU CAN NOT DEVIATE FROM THAT.
 
     Analyze and identify key elements:
-    1. Start events, there should be at least one.
-    2. Tasks and their sequence.
+    1. Start events, there should be at least one;
+    2. Tasks and their sequence;
     3. Gateways (xor) and an array of ”branches” containing tasks. There is a condition for the decision point and each
-    branch has a condition label.
+    branch has a condition label;
     4. Loops: involve repeating tasks until a specific condition is met.
 
-    Nested structure: The schema uses nested structures within gateways to represent branching paths.
-    Order matters: The order of elements in the ”process” array defines the execution sequence.
-
-    When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
-    optimization (if it does not contradict the user intended sequence).
-    Use clear names for labels and conditions.
-    Aim for detail when naming the elements and the model (e.g., instead of "Event 1" or "Action 2", use "User login"
-    or "Create node").
-
-    All elements, except gateways, must have a plugin assigned to them and optionally an array of configuration
+    Other requirements:
+    * Nested structure: the schema uses nested structures within gateways to represent branching paths;
+    * When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
+    optimization (if it does not contradict the user intended sequence);
+    * Use clear names for labels and conditions: e.g., instead of "Event 1" or "Action 2", use "User login" or
+    "Create node");
+    * All elements, except gateways, must have a plugin assigned to them and optionally an array of configuration
     parameters. You will given a list of possible plugins and their corresponding configuration structure, you can not
-    deviate from those.
+    deviate from those;
+    * When given an existing model, you are allowed to modify any part of it.
 
-    Sometimes you will be given a previous JSON solution with user instructions to edit.
   possible_actions:
     build: The generated JSON model that should be imported.
     fail: You are unable to create or alter the model.
@@ -42,7 +40,7 @@ prompt:
   one_shot_learning_examples:
     - action: build
       model: |
-        {"model_id":"process_1","label":"Create Article on Page Publish","events":[{"element_id":"event_1","plugin_id":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"element_id":"action_2"}]}],"actions":[{"element_id":"action_2","plugin_id":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"element_id":"action_3"}]},{"element_id":"action_3","plugin_id":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"element_id":"action_4"}]},{"element_id":"action_4","plugin_id":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"element_id":"action_5"}]},{"element_id":"action_5","plugin_id":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"element_id":"action_6"}]},{"element_id":"action_6","plugin_id":"eca_save_entity","label":"Save Article","successors":[]}]}
+        {"model_id":"create_article","label":"Create Article on Page Publish","events":[{"element_id":"event_1","plugin_id":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"element_id":"action_2"}]}],"actions":[{"element_id":"action_2","plugin_id":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"element_id":"action_3"}]},{"element_id":"action_3","plugin_id":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"element_id":"action_4"}]},{"element_id":"action_4","plugin_id":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"element_id":"action_5"}]},{"element_id":"action_5","plugin_id":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"element_id":"action_6"}]},{"element_id":"action_6","plugin_id":"eca_save_entity","label":"Save Article","successors":[]}]}
       message: The model "Create Article on Page Publish" creates an article about a new published page, it contains the label and the author of that new page.
     - action: fail
       message: I was unable to analyze the description.
-- 
GitLab


From 6c350dbcf75fb7dbec1b7fc2d6b6a58c351fb0b8 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 24 Jan 2025 10:17:32 +0100
Subject: [PATCH 92/95] #3481307 Add docs

---
 README.md                    | 26 ++++++++++++++++++++++++++
 docs/index.md                | 27 +++++++++++++++++++++++++++
 docs/modules/agents/index.md | 30 ++++++++++++++++++++++++++++++
 docs/usage.md                |  3 +++
 mkdocs.yml                   | 10 ++++++++++
 5 files changed, 96 insertions(+)
 create mode 100644 docs/index.md
 create mode 100644 docs/modules/agents/index.md
 create mode 100644 docs/usage.md
 create mode 100644 mkdocs.yml

diff --git a/README.md b/README.md
index 314942f..5992059 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,27 @@
 # AI - ECA
+
+Artificial Intelligence integration for Event-Condition-Action module, combining the unified framework
+and the power of Drupal to perform various AI-related operations.
+
+## Dependencies
+
+- Drupal ^10.3 || ^11
+- [AI module](https://drupal.org/project/ai)
+- [ECA module](https://drupal.org/project/eca)
+
+## Getting started
+
+1. Enable the module
+2. Open an ECA model
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeec` etc.) as action
+
+## Documentation
+
+This project uses MkDocs for documentation: you can see the current
+documentation at [https://project.pages.drupalcode.org/ai/](https://project.pages.drupalcode.org/ai/).
+The documentation source files are located in the `docs/` directory, and to
+build your own local version of the documentation please follow these steps:
+
+1. Install MkDocs: `pip install mkdocs mkdocs-material`
+2. Run `mkdocs serve` in the project root
+3. Open `http://localhost:8000` in your browser
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..5992059
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,27 @@
+# AI - ECA
+
+Artificial Intelligence integration for Event-Condition-Action module, combining the unified framework
+and the power of Drupal to perform various AI-related operations.
+
+## Dependencies
+
+- Drupal ^10.3 || ^11
+- [AI module](https://drupal.org/project/ai)
+- [ECA module](https://drupal.org/project/eca)
+
+## Getting started
+
+1. Enable the module
+2. Open an ECA model
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeec` etc.) as action
+
+## Documentation
+
+This project uses MkDocs for documentation: you can see the current
+documentation at [https://project.pages.drupalcode.org/ai/](https://project.pages.drupalcode.org/ai/).
+The documentation source files are located in the `docs/` directory, and to
+build your own local version of the documentation please follow these steps:
+
+1. Install MkDocs: `pip install mkdocs mkdocs-material`
+2. Run `mkdocs serve` in the project root
+3. Open `http://localhost:8000` in your browser
diff --git a/docs/modules/agents/index.md b/docs/modules/agents/index.md
new file mode 100644
index 0000000..d1f90e3
--- /dev/null
+++ b/docs/modules/agents/index.md
@@ -0,0 +1,30 @@
+# AI ECA - Agents
+
+## Scope
+
+- Answer questions about existing models
+- Answer questions about specific components, like events, conditions or actions
+- Create new models
+- Edit existing models
+
+## Usage
+
+- Go to `/admin/config/workflow/eca`
+- Click on `Ask AI` and write down your question
+  - The same button is available on the edit-screen of a model
+
+## Roadmap
+
+This has no specific order or prio, just things that I can think of right now
+
+- Refactor the custom Typed Data model once config validation is in core
+  - https://www.drupal.org/project/eca/issues/3446331
+- Create a separate sub-agent that can interpret an image and provide a prompt for the main one
+  - See Webform Agent
+
+## Resources
+
+- https://webthesis.biblio.polito.it/31175/1/tesi.pdf
+- https://github.com/RobJenks/gpt-codegen
+- https://github.com/jtlicardo/bpmn-assistant
+- https://ceur-ws.org/Vol-3758/paper-15.pdf
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..cb5d184
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,3 @@
+# Usage
+
+@todo describe which operation type is exposed as an action
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..66e686f
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,10 @@
+site_name: Documentation for AI - ECA module for Drupal
+theme:
+  name: material
+nav:
+  - Home: index.md
+  - Usage: usage.md
+  - Modules:
+    -  AI ECA Agents: modules/agents/index.md
+docs_dir: docs
+site_dir: site
-- 
GitLab


From 7e47427856609187ab1b17efe80643856b701c52 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 24 Jan 2025 10:24:12 +0100
Subject: [PATCH 93/95] #3481307 Fix cspell warnings

---
 .cspell-project-words.txt    | 2 ++
 README.md                    | 2 +-
 docs/index.md                | 2 +-
 docs/modules/agents/index.md | 2 +-
 mkdocs.yml                   | 2 +-
 5 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index 0dd7c1a..b2fda9f 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -15,6 +15,7 @@ gguvde
 iwzr
 iztkfs
 jqykgu
+mkdocs
 nagg
 originalentity
 originalentityref
@@ -29,6 +30,7 @@ Spatie
 tbbhie
 tgic
 vgtd
+webform
 yjwm
 zcaglk
 zpul
diff --git a/README.md b/README.md
index 5992059..26fd1d4 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ and the power of Drupal to perform various AI-related operations.
 
 1. Enable the module
 2. Open an ECA model
-3. Use an operation type (`Chat`, `Moderation`, `TextToSpeec` etc.) as action
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeech` etc.) as action
 
 ## Documentation
 
diff --git a/docs/index.md b/docs/index.md
index 5992059..26fd1d4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,7 +13,7 @@ and the power of Drupal to perform various AI-related operations.
 
 1. Enable the module
 2. Open an ECA model
-3. Use an operation type (`Chat`, `Moderation`, `TextToSpeec` etc.) as action
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeech` etc.) as action
 
 ## Documentation
 
diff --git a/docs/modules/agents/index.md b/docs/modules/agents/index.md
index d1f90e3..e151376 100644
--- a/docs/modules/agents/index.md
+++ b/docs/modules/agents/index.md
@@ -15,7 +15,7 @@
 
 ## Roadmap
 
-This has no specific order or prio, just things that I can think of right now
+This has no specific order or priority, just things that I can think of right now
 
 - Refactor the custom Typed Data model once config validation is in core
   - https://www.drupal.org/project/eca/issues/3446331
diff --git a/mkdocs.yml b/mkdocs.yml
index 66e686f..0bc9694 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,6 +5,6 @@ nav:
   - Home: index.md
   - Usage: usage.md
   - Modules:
-    -  AI ECA Agents: modules/agents/index.md
+    - AI ECA Agents: modules/agents/index.md
 docs_dir: docs
 site_dir: site
-- 
GitLab


From a6c9f8bd186545147d41baf73bb7c3aad6f9c031 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 24 Jan 2025 10:28:02 +0100
Subject: [PATCH 94/95] #3481307 Add warning about Agentic approach

---
 docs/modules/agents/index.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/docs/modules/agents/index.md b/docs/modules/agents/index.md
index e151376..4e9e660 100644
--- a/docs/modules/agents/index.md
+++ b/docs/modules/agents/index.md
@@ -1,5 +1,10 @@
 # AI ECA - Agents
 
+WARNING: this agent has the capability of creating and editing ECA models, without "realising" that the result may be
+destructive. Meaning that, if you ask it to create a model that removes all nodes when a user logs in, nothing is
+holding it back.
+USE AT YOUR OWN RISK!
+
 ## Scope
 
 - Answer questions about existing models
@@ -17,6 +22,7 @@
 
 This has no specific order or priority, just things that I can think of right now
 
+- Make the prompt more "deterministic": there's an issue where the LLM edits an existing model and doesn't follow the provided JSON schema.
 - Refactor the custom Typed Data model once config validation is in core
   - https://www.drupal.org/project/eca/issues/3446331
 - Create a separate sub-agent that can interpret an image and provide a prompt for the main one
-- 
GitLab


From 8ca945ba5e837115613f6a0a27b16114d30a49f1 Mon Sep 17 00:00:00 2001
From: Jasper Lammens <jasper.lammens@dropsolid.com>
Date: Fri, 24 Jan 2025 10:31:57 +0100
Subject: [PATCH 95/95] #3481307 Fix cspell warnings

---
 docs/modules/agents/index.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/modules/agents/index.md b/docs/modules/agents/index.md
index 4e9e660..b9c949c 100644
--- a/docs/modules/agents/index.md
+++ b/docs/modules/agents/index.md
@@ -1,6 +1,6 @@
 # AI ECA - Agents
 
-WARNING: this agent has the capability of creating and editing ECA models, without "realising" that the result may be
+WARNING: this agent has the capability of creating and editing ECA models, without "realizing" that the result may be
 destructive. Meaning that, if you ask it to create a model that removes all nodes when a user logs in, nothing is
 holding it back.
 USE AT YOUR OWN RISK!
-- 
GitLab