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