From bffbed8c7b79fa223b7b61596bb01b36123e5158 Mon Sep 17 00:00:00 2001
From: soustruh <martin@struzsky.cz>
Date: Fri, 7 Mar 2025 11:24:20 +0100
Subject: [PATCH 1/3] =?UTF-8?q?ignore=20flake8's=20E203=20warning=20(space?=
 =?UTF-8?q?=20before=20colon)=20+=20.venv=20folder=20=E2=9D=84=EF=B8=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Funnily enough, Flake8's E203 is not PEP8 compliant and it's even stated in Flake8's official configuration example. 😏

Sources:
https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices
https://flake8.pycqa.org/en/latest/user/configuration.html
https://stackoverflow.com/questions/75614339/black-and-flake8-hooks-conflicts
---
 flake8.cfg | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/flake8.cfg b/flake8.cfg
index b577af2..56d457d 100644
--- a/flake8.cfg
+++ b/flake8.cfg
@@ -1,6 +1,8 @@
 [flake8]
+extend-ignore = E203
 exclude =
     .git,
+    .venv,
     __pycache__,
     tests,
     example

From bec13f21d82096f1056203479e34b4aa72523901 Mon Sep 17 00:00:00 2001
From: soustruh <martin@struzsky.cz>
Date: Fri, 7 Mar 2025 11:24:28 +0100
Subject: [PATCH 2/3] sync action for the `run` method

---
 src/component.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/src/component.py b/src/component.py
index 1fd940c..6db7214 100644
--- a/src/component.py
+++ b/src/component.py
@@ -8,7 +8,7 @@
 from os import mkdir, path
 
 from keboola.component.base import ComponentBase, sync_action
-from keboola.component.dao import SupportedDataTypes, BaseType, ColumnDefinition
+from keboola.component.dao import BaseType, ColumnDefinition, SupportedDataTypes
 from keboola.component.exceptions import UserException
 from keboola.component.sync_actions import MessageType, SelectElement, ValidationResult
 from keboola.utils.header_normalizer import NormalizerStrategy, get_normalizer
@@ -86,7 +86,7 @@ class Component(ComponentBase):
     def __init__(self):
         super().__init__()
 
-    def run(self):
+    def run(self, return_data=False):
         self.validate_configuration_parameters(REQUIRED_PARAMETERS)
         self.validate_image_parameters(REQUIRED_IMAGE_PARS)
 
@@ -130,6 +130,9 @@ def run(self):
         logging.debug([result for result in results])
         logging.info(f"Downloaded {total_records} records in total")
 
+        if return_data:
+            return results
+
         # remove headers and get columns
         output_columns = self._fix_header_from_csv(results)
         output_columns = self.normalize_column_names(output_columns)
@@ -419,9 +422,9 @@ def _get_login_method(self) -> LoginType:
     @staticmethod
     def process_salesforce_domain(url):
         if url.startswith("http://"):
-            url = url[len("http://"):]
+            url = url[len("http://") :]
         if url.startswith("https://"):
-            url = url[len("https://"):]
+            url = url[len("https://") :]
         if url.endswith(".salesforce.com"):
             url = url[: -len(".salesforce.com")]
 
@@ -619,6 +622,13 @@ def load_possible_primary_keys(self) -> list[SelectElement]:
         else:
             raise UserException(f"Invalid {KEY_QUERY_TYPE}")
 
+    # @sync_action("runComponent")
+    # def sync_run_component(self):
+    #     """
+    #     Run the component as a sync action
+    #     """
+    #     return self.run(return_data=True)
+
     def _get_object_name_from_custom_query(self) -> str:
         params = self.configuration.parameters
         salesforce_client = self.get_salesforce_client(params)

From 158261a8bbfa6214fffe3918045c93b4378b2c43 Mon Sep 17 00:00:00 2001
From: soustruh <martin@struzsky.cz>
Date: Tue, 11 Mar 2025 08:01:09 +0100
Subject: [PATCH 3/3] =?UTF-8?q?listSkills=20sync=20action=20defining=20the?=
 =?UTF-8?q?=20available=20AI=20agent=20skills=20=F0=9F=A4=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

eventually the response of this sync action shall be generated dynamically based on component configuration
---
 .github/workflows/push.yml |   2 +-
 src/component.py           | 147 +++++++++++++++++++++++++++++++++++--
 2 files changed, 140 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index a986c0d..75449cb 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -67,7 +67,7 @@ jobs:
           TAG="${GITHUB_REF##*/}"
           IS_SEMANTIC_TAG=$(echo "$TAG" | grep -q '^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$' && echo true || echo false)
           echo "is_semantic_tag=$IS_SEMANTIC_TAG" | tee -a $GITHUB_OUTPUT
-          echo "app_image_tag=$TAG" | tee -a $GITHUB_OUTPUT
+          echo "app_image_tag=$TAG-${{ github.run_id }}" | tee -a $GITHUB_OUTPUT
 
       - name: Deploy-Ready check
         id: deploy_ready
diff --git a/src/component.py b/src/component.py
index 6db7214..90597a0 100644
--- a/src/component.py
+++ b/src/component.py
@@ -1,4 +1,5 @@
 import csv
+import json
 import logging
 import os
 import shutil
@@ -60,6 +61,115 @@
 
 DEFAULT_LOGIN_METHOD = "security_token"
 
+# the listSkills response shall be generated based on user config in a very near future,
+# so I just hardcoded it here for now
+LIST_SKILLS_STATIC_RESPONSE = [
+    {
+        "name": "Get Salesforce contacts",
+        "description": "Returns list of Salesforce contacts.",
+        "parameters": [
+            {
+                "name": "action",
+                "type": "string",
+                "required": True,
+                "enum": ["runSkill"],
+            },
+            {
+                "name": "parameters",
+                "type": "object",
+                "required": True,
+                "parameters": {
+                    "name": "configData",
+                    "type": "object",
+                    "required": True,
+                    "parameters": [
+                        {
+                            "name": "object",
+                            "type": "string",
+                            "required": True,
+                            "enum": ["Contact"],
+                            "description": "String identifer of the agent skill to run.",
+                        },
+                    ],
+                },
+            },
+        ],
+        "response": {
+            "type": "object",
+            "properties": {
+                "status": {
+                    "title": "Response status",
+                    "type": "string",
+                    "enum": ["ok", "error"],
+                },
+                "number_of_records": {
+                    "title": "Number of records in the response",
+                    "type": "integer",
+                },
+                "records": {
+                    "title": "Salesforce records",
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                    },
+                },
+            },
+        },
+    },
+    {
+        "name": "Get Salesforce reports",
+        "description": "Returns list of Salesforce reports.",
+        "parameters": [
+            {
+                "name": "action",
+                "type": "string",
+                "required": True,
+                "enum": ["runSkill"],
+            },
+            {
+                "name": "parameters",
+                "type": "object",
+                "required": True,
+                "parameters": {
+                    "name": "configData",
+                    "type": "object",
+                    "required": True,
+                    "parameters": [
+                        {
+                            "name": "object",
+                            "type": "string",
+                            "required": True,
+                            "enum": ["Report"],
+                            "description": "String identifer of the agent skill to run.",
+                        },
+                    ],
+                },
+            },
+        ],
+        "response": {
+            "type": "object",
+            "properties": {
+                "status": {
+                    "title": "Response status",
+                    "type": "string",
+                    "enum": ["ok", "error"],
+                },
+                "number_of_records": {
+                    "title": "Number of records in the response",
+                    "type": "integer",
+                },
+                "records": {
+                    "title": "Salesforce records",
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                    },
+                },
+            },
+        },
+    },
+]
+
 
 class LoginType(str, Enum):
     SECURITY_TOKEN_LOGIN = "security_token"
@@ -86,13 +196,16 @@ class Component(ComponentBase):
     def __init__(self):
         super().__init__()
 
-    def run(self, return_data=False):
+    def run(self, return_json=False):
         self.validate_configuration_parameters(REQUIRED_PARAMETERS)
         self.validate_image_parameters(REQUIRED_IMAGE_PARS)
 
         start_run_time = str(datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"))
 
         params = self.configuration.parameters
+
+        logging.info("%s", json.dumps(params, indent=4))
+
         loading_options = params.get(KEY_LOADING_OPTIONS, {})
 
         bucket_name = params.get(KEY_BUCKET_NAME, self.get_bucket_name())
@@ -130,7 +243,15 @@ def run(self, return_data=False):
         logging.debug([result for result in results])
         logging.info(f"Downloaded {total_records} records in total")
 
-        if return_data:
+        if return_json:
+            for result in results:
+                if "file" not in result:
+                    result["status"] = "fileError"
+                    continue
+                result["records"] = list(self._csv_result_to_dict(result.pop("file")))
+                result["status"] = "ok"
+            with open("response.json", "w") as f:
+                json.dump(results, f)
             return results
 
         # remove headers and get columns
@@ -180,6 +301,12 @@ def _fix_header_from_csv(results: list[dict]) -> list[str]:
             os.replace(temp_file_path, result_file_path)
         return expected_header
 
+    def _csv_result_to_dict(self, filename: str):
+        with open(filename) as f:
+            reader = csv.DictReader(f)
+            for row in reader:
+                yield row
+
     def set_proxy(self) -> None:
         """Sets proxy if defined"""
         proxy_config = self.configuration.parameters.get(KEY_PROXY, {})
@@ -622,12 +749,16 @@ def load_possible_primary_keys(self) -> list[SelectElement]:
         else:
             raise UserException(f"Invalid {KEY_QUERY_TYPE}")
 
-    # @sync_action("runComponent")
-    # def sync_run_component(self):
-    #     """
-    #     Run the component as a sync action
-    #     """
-    #     return self.run(return_data=True)
+    @sync_action("runSkill")
+    def sync_run_component(self):
+        """
+        Run the component as a skill for an AI agent
+        """
+        return self.run(return_json=True)
+
+    @sync_action("listSkills")
+    def list_skills(self):
+        return LIST_SKILLS_STATIC_RESPONSE
 
     def _get_object_name_from_custom_query(self) -> str:
         params = self.configuration.parameters