> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enneo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK

> Enneo SDK - Documentation

The Enneo SDK provides a comprehensive collection of tools that facilitate the development of specific code for custom solutions in the field of AI-powered customer support. The SDK supports the implementation of rule-based logic for AI agents, event-based integrations, custom webhooks, and user-defined functions. The SDK is loaded automatically and features are immediately available.

### Integrating the SDK in Code

To incorporate the Enneo SDK into your code, the following code snippet can be used:

<CodeGroup>
  ```php PHP theme={null}
  <?php

  require(getenv()['SDK'] ?? 'sdk.php');
  ```

  ```python Python theme={null}
  file_path = os.getenv('SDK', 'sdk.py')
  spec = importlib.util.spec_from_file_location('sdk', file_path)
  sdk = importlib.util.module_from_spec(spec)
  spec.loader.exec_module(sdk)
  ```
</CodeGroup>

### Enneo API Wrapper

The Enneo SDK provides wrappers for the Enneo API endpoints, allowing convenient access to the API:

* Load contract data by contract ID
* Load ticket data by ticket ID
* Execution of [user-defined functions](/en/system-integration/user-defined-functions-tools/user-defined-functions-in-enneo)
* Call of any Enneo API endpoints (GET | POST | PATCH | PUT | DELETE)

  <CodeGroup>
    ```php PHP theme={null}
    <?php

    use EnneoSDK\ApiEnneo;

    require(getenv()['SDK'] ?? 'sdk.php');

    /**
     * Loads contract data for the contract with ID 123
     */
    $contract = ApiEnneo::getContract(contractId: 123);

    /**
     * Loads ticket data for the ticket with ID 456
     */
    $ticket = ApiEnneo::getTicket(ticketId: 456);

    /**
     * Executes user-defined function 'user-defined-function'.
     * (See documentation of the user-defined functions)
     * Entered parameters are passed to the user-defined function
     * and can be loaded there using \EnneoSDK\Input::load().
     */
    $response = ApiEnneo::executeUdf(
        name: 'user-defined-function',
        parameters: [
            'method' => 'POST',
            'api' => 'redirect',
            'params' => $payload
        ]
    );

    ##########################################################################
    ## The methods get | post | patch | put | delete can be used,
    ## to call any Enneo API endpoints
     ## (see API documentation)
    ##########################################################################

    /**
     * Load profile of the currently logged in user
     */
    $profile = ApiEnneo::get(endpoint: '/api/mind/profile');

    /**
     * OCR meter reading detection for predetermined ticket ID and URL to ticket attachment
     */
    $response = ApiEnneo::post(
        endpoint: '/api/cortex/ocrMeter',
        body: [
            'ticketId' => $ticketId,
            'fileUrl' => $_ENV['ENNEO_API_URL'] . $attachment->url,
        ]
    );

    /**
     * Close ticket
     */
    $response = ApiEnneo::patch(
        endpoint: '/api/mind/ticket/' . $ticketId,
        body: ['status' => 'closed']
    );
    ```

    ```python Python theme={null}
    tbd
    ```
  </CodeGroup>

### Invocation of Custom Functions

The invocation of custom functions in the code is done using **executeUdf**:

```php theme={null}
<?php

// Load Enneo SDK
require(getenv()['SDK']);

// Prepare payload for the API call of the third-party system
$payload = [
    'ticket-id' => 123,
    'key' => 'value'
];

// Invocation of the custom function "third-party-api-call"
// this function receives as input parameter an object:
// [
//      'method' => 'POST',
//      'api' => 'item',
//      'params' => [
//          'ticket-id' => 123,
//           'key' => 'value'
//      ]
// ]
EnneoSDK\ApiEnneo::executeUdf(
    'third-party-api-call',
    ['method' => 'POST', 'api' => 'item', 'params' => $payload]
);

```

### SDK Code

The SDKs are available at:

* [PHP SDK](https://demo.enneo.ai/api/codeExecutor/sdk/php82.php)
* [Python SDK](https://demo.enneo.ai/api/codeExecutor/sdk/python311.py)
* [Node.JS SDK](https://demo.enneo.ai/api/codeExecutor/sdk/node20.js)

The above URLs work without authorization, i.e., they can be directly included in your scripts.

<CodeGroup>
  ```php PHP theme={null}
  <?php

  // ----------------------
  // ----- Enneo SDK ------
  // ----------------------

  namespace EnneoSDK;

  use DateTime;
  use Exception;
  use stdClass;

  define("ENNEO_SDK_VERSION", "0.2.3");

  // Parametrisation (can be overriden by client)
  $SDK_RETRY_ON_RATE_LIMIT = false;
  $SDK_API_TIMEOUT = 60;


  /**
  * This is the input data that is passed to the business logic code.
  * Added for convenience here.
  */
  $_ENV = getenv();

  // Aliases for input data functions
  $in = Input::load(true);

  class Input
  {
  private static object $in; // Cache input parameters to allow multiple calls to load()

  /**
   * Loads the input parameters for the AI function that enneo was able to extract from the ticket/contract
   *
   * This function first checks if the input parameters are passed through STDIN.
   * If not, it checks if the input parameters are passed through a 'input.json' file in the working directory.
   * If the input parameters are passed through STDIN or the 'input.json' file, it returns the loaded input data as a dictionary.
   * The input.json file can be overriden with the environment variable INPUT_JSON
   *
   * @return object The input parameters extracted from Enneo from the ticket as key-value object
   */
  public static function load(bool $includeMetadata = false): object
  {
      if (isset(self::$in)) {
          return self::$in;
      }

      $inputStr = null;

      # Check if we have input from STDIN
      $stdin = fopen('php://stdin', 'r');
      stream_set_blocking($stdin, 0);
      $inputStr = stream_get_contents($stdin);
      fclose($stdin);

      # If not, check if we have input from a file
      if (!$inputStr && file_exists($_ENV['INPUT_JSON'] ?? 'input.json')) {
          $inputStr = file_get_contents($_ENV['INPUT_JSON'] ?? 'input.json');
      } elseif (!$inputStr) {
          echo "No input data found. Please provide input data as input.json or through stdin.\n";
          exit(1);
      }

      # Decode the input data from JSON to a PHP object
      self::$in = json_decode($inputStr);
      if (json_last_error() !== JSON_ERROR_NONE) {
          echo "Input parameters were not correctly loaded: " . json_last_error_msg() . "\n";
          echo "Your input was: " . $inputStr . "\n";
          exit(1);
      }

      # Some input values might be strings that are actually JSON objects
      # We try to decode them here
      foreach (self::$in as $key => $value) {
          if (is_string($value) && !is_numeric($value)) {
              $decoded = json_decode($value);
              if (json_last_error() === JSON_ERROR_NONE) {
                  self::$in->$key = $decoded;
              }
          }
      }

      # Strip the metadata
      if (!$includeMetadata) {
          unset(self::$in->_metadata);
      }

      # Return the input data
      return self::$in;
  }
  }


  /**
  * Creates a new intent info (=message box visible to the agent in the Enneo UI).
  */
  class IntentInfo
  {
  /**
   * @param string      $type      The type of the intent. Must be one of 'success', 'neutral', 'warning', 'danger'.
   * @param string      $message   The message to be shown to the agent, e.g. "Reading is plausible".
   * @param string|null $extraInfo Optional supplemental information about the intent, e.g. "Expected reading was 421 kWh. Plausbible because difference to 317 kWh is below threshold of 200 kWh"
   * @param string|null $code      Optional internal code associated with the info. Not used by enneo. Defaults to None.
   */
  public function __construct(
      public string $type,
      public string $message,
      public ?string $extraInfo = null,
      public ?string $code = null,
  ) {
  }
  }

  /**
  * Creates a new intent option (=button visible to the agent in the Enneo UI).
  */
  class IntentOption
  {
  /**
   * @param string $type The type of the intent. Must be unique for each option. Usually accompanied by a matching output case
   * @param string $name The name of the option, e.g. "Enter into system".
   * @param string $icon The icon to be shown next to the option. Defaults to 'check'. Options: check, cancel
   * @param bool $recommended True for default action. An Intent can only have one default action. Defaults to False.
   * @param int $order Sorting index order. Frontend sorts in ascending order
   * @param string|null $handler Which microservice should handle this action, Options: cortex, mind or fe. ONLY USE IF YOU KNOW WHAT YOU ARE DOING. Defaults to None.
   */
  public function __construct(
      public string $type,
      public string $name,
      public string $icon = 'check',
      public bool $recommended = false,
      public int $order = 0,
      public string|null $handler = '',
  ) {
  }
  }

  /**
  * Represents a field in form, displayed in user UI
  */
  class FormField
  {
  public function __construct(
      public string $id,
      public string $type,
      public string $valueRef,
      public string $label,
      public bool $readonly = false,
      public bool $hidden = false,
      public array $options = [],
  ) {
  }
  }

  /**
  * Represents a form (as a set of form fields), displayed in user UI
  */
  class Form
  {
  public array $fields = [];

  public static function fromInput(stdClass $in): Form
  {
      $result = new Form();
      foreach ($in->_metadata->inputParameters as $param) {
          $field = new FormField(
              id: $param->key,
              type: match ($param->type) {
                  'date',
                  'datetime' => 'date',
                  'int' => 'integer',
                  'float' => 'number',
                  'bool' => 'checkbox',
                  'enum' => 'select',
                  default => 'text',
              },
              valueRef: 'data.' . $param->key,
              label: $param->description,
              readonly: $param->visibility === 'readonly',
              hidden: $param->visibility === 'hidden',
              options: []
          );
          $optionsCounter = 1;
          foreach ($param->options as $option) {
              $field->options[] = [
                  'label' => $option->label,
                  'value' => $option->value,
                  'id' => 'option_' . $optionsCounter++,
              ];
          }
          $result->fields[] = $field;
      }

      return $result;
  }

  public function getFormField(string $id): ?FormField
  {
      foreach ($this->fields as $field) {
          if ($field->id === $id) {
              return $field;
          }
      }

      return null;
  }
  }

  /**
  * Creates a new interaction object.
  * This is an enneo-specific object that defines the form shown to the agent in the Enneo UI.
  * The standard AI function returns an json-encoded Interaction object to STDOUT
  */
  class Interaction
  {
  /**
   * @param object $data      The input data for the AI function, i.e. form values for the input variables. It is recommended to pre-fill this with the input data coming from load_input_data().
   * @param array  $options   A list of intent options (=buttons visible to the agent in the Enneo UI). Defaults to [].
   * @param array  $infos     A list of intent infos (=message boxes visible to the agent in the Enneo UI). Defaults to [].
   * @param object|null $form Optional dictionary to override the forms to be shown to the agent. Usually not needed. Defaults to None.
   */
  public function __construct(
      public object $data,
      public array $options = [],
      public array $infos = [],
      public ?object $form = null,
  ) {
      unset($this->data->_metadata);
  }
  }

  /**
  * These are wrappers for the Enneo API endpoints.
  * You can use them to conveniently access the Enneo API.
  */
  class ApiEnneo
  {
  /**
   * Get a contract by its ID.
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param int|string $contractId The contract ID
   * @return object The contract as object
   * @throws Exception
   */
  public static function getContract(int|string $contractId): object
  {
      return self::get('/api/mind/contract/' . $contractId . '?includeRawData=true');
  }

  /**
   * Get a ticket by its ID.
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param int $ticketId The ticket ID
   * @return object The ticket as object
   */
  public static function getTicket(int $ticketId): object
  {
      return self::get('/api/mind/ticket/' . $ticketId, authorizeAs: 'serviceWorker');
  }

  /**
   * Do an API GET call to an Enneo API endpoint
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param string $endpoint The endpoint, e.g. /api/mind/contract/123456
   * @param array $params The HTTP GET parameters, e.g. ['contractId' => 123456]. Can also be passed through the URL, e.g. /api/mind/ticket/123?includeIntents=true
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.
   *
   * @return object|array The response body as object. All Enneo APIs return an object as response.
   * @throws Exception
   */
  public static function get(string $endpoint, array $params = [], string $authorizeAs = 'user'): object|array
  {
      $headers = self::buildAuthorizationHeader($authorizeAs);
      return Api::call("GET", self::getEnneoUrl($endpoint), $headers);
  }

  /**
   * Do an API POST call to an Enneo API endpoint
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param string $endpoint The endpoint, e.g. /api/mind/ticket
   * @param object|array $body The HTTP body payload as object or array. Will be JSON-encoded before sending.
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.     *
   * @return object|array The response body as object
   * @throws Exception
   */
  public static function post(string $endpoint, object|array $body, string $authorizeAs = 'user'): object|array
  {
      $headers = self::buildAuthorizationHeader($authorizeAs);
      return Api::call("POST", self::getEnneoUrl($endpoint), $headers, $body);
  }

  /**
   * Do an API PATCH call to an Enneo API endpoint
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param string $endpoint The endpoint, e.g. /api/mind/ticket
   * @param object|array $body The HTTP body payload as object or array. Will be JSON-encoded before sending.
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.
   *
   * @return object|array The response body as object
   * @throws Exception
   */
  public static function patch(string $endpoint, object|array $body, string $authorizeAs = 'user'): object|array
  {
      $headers = self::buildAuthorizationHeader($authorizeAs);
      return Api::call("PATCH", self::getEnneoUrl($endpoint), $headers, $body);
  }

  /**
   * Do an API PUT call to an Enneo API endpoint
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param string $endpoint The endpoint, e.g. /api/mind/ticket
   * @param object|array $body The HTTP body payload as object or array. Will be JSON-encoded before sending.
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.
   *
   * @return object|array The response body as object
   * @throws Exception
   */
  public static function put(string $endpoint, object|array $body, string $authorizeAs = 'user'): object|array
  {
      $headers = self::buildAuthorizationHeader($authorizeAs);
      return Api::call("PUT", self::getEnneoUrl($endpoint), $headers, $body);
  }

  /**
   * Do an API DELETE call to an Enneo API endpoint
   * See https://main.enneo.dev/api/mind/docs for documentation.
   *
   * @param string $endpoint The endpoint, e.g. /api/mind/ticket
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.
   *
   * @return object|array The response body as object
   * @throws Exception
   */
  public static function delete(string $endpoint, string $authorizeAs = 'user'): object|array
  {
      $headers = self::buildAuthorizationHeader($authorizeAs);
      return Api::call("DELETE", self::getEnneoUrl($endpoint), $headers);
  }

  /**
   * Execute a helper function.
   * See https://main.enneo.dev/api/mind/docs for documentation.
   * @param string $name The name of the helper function, e.g. 'getContract'
   * @param object|array $parameters The parameters to pass to the user defined function
   * @param string $authorizeAs If 'user', any API requests from the script inherit the permissions of the user who started the script. If 'serviceWorker', the script runs with the permissions of the service worker. Defaults to 'user'.
   *
   * @return object|array
   * @throws Exception
   */
  public static function executeUdf(string $name, object|array $parameters, string $authorizeAs = 'user'): object|array
  {
      return ApiEnneo::post("/api/mind/executor/execute/$name", ["parameters" => $parameters], $authorizeAs);
  }

  /**
   * Internal helper function to build the authorization header.
   */
  private static function buildAuthorizationHeader(string $authorizeAs): array
  {
      if ($authorizeAs == 'user' && ($_ENV['ENNEO_USER_AUTH_HEADER'] ?? false)) {
          return [$_ENV['ENNEO_USER_AUTH_HEADER']];
      } else {
          return ['Authorization: Bearer ' . $_ENV['ENNEO_SESSION_TOKEN']];
      }
  }

  /**
   * Ensure we address the corect enneo microservice port when running inside Kubernetes
   */
  private static function getEnneoUrl(string $endpoint): string
  {
      if (str_starts_with($endpoint, '/api/mind')) {
          $enneoUrl = $_ENV['ENNEO_API_URL'];
      } elseif (str_starts_with($endpoint, '/api/cortex')) {
          $enneoUrl = str_replace(':8005', ':8006', $_ENV['ENNEO_API_URL']);
      } elseif (str_starts_with($endpoint, '/api/auth')) {
          $enneoUrl = str_replace(':8005', ':8002', $_ENV['ENNEO_API_URL']);
      } else {
          throw new Exception("Unknown Enneo API endpoint $endpoint. They should start with /api/mind or /api/cortex or /api/auth");
      }

      return $enneoUrl . $endpoint;
  }
  }

  class Api
  {
  // -------------------------------------
  // --- Shared REST service endpoints ---
  // -------------------------------------

  /**
   * HTTP call to REST-endpoint
   *
   * @param string $method  HTTP method: GET, POST, PUT, DELETE or PATCH
   * @param string $url   URL to call, e.g. https://my-api.com/api/v1/endpoint
   * @param array $headers Array of HTTP headers, e.g. ['Content-Type: application/json']
   * @param array|object|string|bool $params Body of the HTTP request, e.g. ['param1' => 'value1', 'param2' => 'value2']
   * @return array|object The response body as object
   * @throws Exception If the call was not successful, an exception is thrown
   */
  public static function call(string $method, string $url, array $headers = [], array|object|string|false $params = false): array|object
  {
      $curl = curl_init($url);

      // Log::info( "API $method call to $url about to start");
      if ($method == 'GET') {
          // No curl parameters necessary
      } elseif ($method == 'POST') {
          curl_setopt($curl, CURLOPT_POST, 1);
      } elseif ($method == 'PUT' || $method == 'DELETE' || $method == 'PATCH') {
          curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
      } else {
          throw new Exception("Invalid Curl request method $method");
      }

      $payload = null;

      if ($params && $params !== 'false' && $params !== '0') {
          if (is_object($params) || is_array($params)) {
              $payload = json_encode($params);
          } else {
              $payload = $params;
          }
          curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
      }

      // Set curl timeout to 10 seconds
      curl_setopt($curl, CURLOPT_TIMEOUT, $GLOBALS['SDK_API_TIMEOUT']);
      curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($curl, CURLOPT_HEADER, 1);
      $headers[] = 'Content-Type: application/json';
      $headers[] = 'User-Agent: enneo/1.0.0';
      curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
      curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // Needed to avoid buggy powercloud SSL certificate
      curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // Needed to avoid buggy HTTP2 implementation in Freshdesk
      $url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);

      // Execute call
      $curl_response_with_header = curl_exec($curl);
      if ($curl_response_with_header === false) {
          throw new \Exception("API Call to " . parse_url($url)['host'] . "... failed: " . curl_error($curl), 1);
      }

      $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
      $headers = substr($curl_response_with_header, 0, $header_size);
      $curl_response = substr($curl_response_with_header, $header_size);
      $curl_reponse_statuscode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
      curl_close($curl);

      // Extract headers
      $headers = explode("\n", $headers);
      foreach ($headers as $header) {
          if (str_contains($header, ':')) {
              list($key, $value) = explode(':', $header, 2);
              $headers[trim(strtolower($key))] = trim($value);
              if (preg_match_all('/charset[\:|=]\s{0,1}(\S*)/i', $value, $matches)) {
                  $encoding = $matches[1][0];
              } elseif (stristr($key, 'blacklisted')) {
                  throw new \Exception(parse_url($url, PHP_URL_HOST) . ' responded that your IP is blacklisted');
              }
          }
      }

      // Do we have a rate limit? if yes wait and retry
      if ($GLOBALS['SDK_RETRY_ON_RATE_LIMIT'] && $curl_reponse_statuscode == 429) {
          if ((int) ($headers['x-ratelimit-reset'] ?? 0)) {
              $delay = (int) $headers['x-ratelimit-reset'] - time();
          } elseif ((int) ($headers['retry-after'] ?? 0)) {
              $delay = (int) $headers['retry-after'] + 1;
          } else {
              $delay = 60;
          }

          // Rate limit requests retry after $delay seconds. Sleeping..."
          sleep($delay);

          return self::call($method, $url, $headers, $params);
      } elseif ($curl_reponse_statuscode < 200 || $curl_reponse_statuscode >= 400) {
          if (str_starts_with($url, $_ENV['ENNEO_API_URL'])) {
              // For internal Enneo API calls, we show the original error message directly
              throw new Exception($curl_response);
          } else {
              throw new Exception("Api call to $url failed with code $curl_reponse_statuscode and response: $curl_response");
          }
      }

      // Is the string a JSON?
      if (str_starts_with($curl_response, '{') || str_starts_with($curl_response, '[')) {
          // Convert into UTF8 used by PHP
          if (isset($encoding) && $encoding != 'utf-8') {
              $curl_response = preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function ($match) {
                  return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
              }, $curl_response);
          }

          // Parse data
          $res = json_decode($curl_response);
          if (is_null($res)) {
              throw new \Exception("Could not decode JSON String $curl_response returned from $url, Error Code " . json_last_error(), 1);
          }

          return $res;
      } else {
          // Special case when a REST API does not respond with a json-encoded response. Should not happen in modern times.
          return (object) ['response' => $curl_response, 'code' => $curl_reponse_statuscode];
      }
  }
  }

  class Setting
  {
  /**
   * Helper class for getting settings from Enneo through the API.
   *
   * @param string $settingName The name of the setting, e.g. 'powercloudApiUrl'
   * @return mixed The setting value
   * @throws Exception
   */
  public static function get(string $settingName): mixed
  {
      return ApiEnneo::get('/api/mind/settings/compact?showSecrets=true&filterByName=' . $settingName, authorizeAs: 'serviceWorker')->$settingName ?? null;
  }

  /**
   * @param string $settingName
   * @param mixed $value
   * @return void
   * @throws Exception
   */
  public static function set(string $settingName, mixed $value): void
  {
      $setting = new stdClass();
      $setting->$settingName = $value;
      ApiEnneo::post('/api/mind/settings', $setting);
  }
  }

  /**
  * Helper class for calling the Powercloud API.
  * Authorization is handled by this SDK.
  */
  class ApiPowercloud
  {
  /**
   * @throws Exception
   */
  public static function getCall(string $endpoint): object|array
  {
      $url = Setting::get('powercloudApiUrl') . $endpoint;
      $headers = ['Authorization: Basic ' . Setting::get('powercloudApiAuth')];

      $result = Api::call("GET", $url, $headers);

      return $result;
  }

  /**
   * @throws Exception
   */
  public static function postCall(string $endpoint, array|object $params): object|array
  {
      $url = Setting::get('powercloudApiUrl') . $endpoint;
      $headers = ['Authorization: Basic ' . Setting::get('powercloudApiAuth')];

      $result = Api::call("POST", $url, $headers, $params);

      return $result;
  }

  /**
   * Extract the error message from a Powercloud API response.
   *
   * @param object|string $result The powercloud API response as object or string
   * @return string A human-readable error message
   */
  public static function extractError(object|string $result): string
  {
      $message = 'Powercloud-Fehler: ';
      if (!is_object($result)) {
          $message .= $result;
      } elseif (sizeof($result->errors ?? []) > 0) {
          foreach ($result->errors as $error) {
              $message .= $error->messageLocalized . '; ';
          }
      } elseif (isset($result->response) && is_string($result->response)) {
          $message .= $result->response;
      } elseif (isset($result->messageLocalized)) {
          $message .= $result->messageLocalized;
      } else {
          $message .= print_r($result, 1);
      }

      return $message;
  }
  }

  class Helpers
  {
  /**
   * Format a date for display in german or english.
   *
   * @param string|DateTime $date   The date to format
   * @param string          $format The language code of the format. Either 'de' or 'en'
   *
   * @return string The date in a country-formatted style, e.g. 01.01.2021 or 2021-01-01
   *
   * @throws Exception
   */
  public static function formatDate(string|DateTime $date, string $format = 'de'): string
  {
      // $date must be formatted as mysql timestamp (yyyy-mm-dd)
      if (is_string($date)) {
          $d = DateTime::createFromFormat('Y-m-d', $date);
          if (!$d) {
              $d = DateTime::createFromFormat('Y-m-d H:i:s', $date);
          }
          if (!$d && $date) {
              return $date;
          }
          if (!$d) {
              if ($date === '') {
                  throw new Exception('No date to format');
              } else {
                  throw new Exception("Date '$date' invalid");
              }
          }
      } else {
          $d = $date;
      }

      if ($format == 'en') {
          return $d->format('Y-m-d');
      } elseif ($format == 'de') {
          return $d->format('d.m.Y');
      } else {
          throw new Exception("Unknown language $format");
      }
  }

  /**
   * Parse a textual date into yyyy-mm-dd format.
   *
   * @param string $textualDate The textual date, e.g. 2021-01-01 or 01.01.2021 or 1.1.2021 or 1.1.21
   * @return ?string The date in yyyy-mm-dd format or null if the date format is invalid
   */
  public static function parseDateToYMD(string $str): ?string
  {
      $parsedDate = date_parse($str);

      // Check if the parsing was successful
      if ($parsedDate['error_count'] == 0 && $parsedDate['warning_count'] == 0 && $parsedDate['year'] > 0 && $parsedDate['month'] > 0 && $parsedDate['day'] > 0) {
          // Format and return the date as yyyy-mm-dd
          return sprintf('%04d-%02d-%02d', $parsedDate['year'], $parsedDate['month'], $parsedDate['day']);
      } else {
          $yearOfText = date('Y');
          $str .= ' '; // Needed to capture dates and the end of the subject

          // Get format dd.mm.yyyy
          if (preg_match_all('/\s+(3[01]|[12][0-9]|0?[1-9])\.(1[012]|0?[1-9])\.((?:19|20)?(?:1|2)\d)[^\d]/', $str, $m)) {
              foreach ($m[0] as $key => $val) {
                  $day = str_pad($m[1][$key], 2, '0', STR_PAD_LEFT);
                  $month = str_pad($m[2][$key], 2, '0', STR_PAD_LEFT);
                  $year = (strlen($m[3][$key]) == 2) ? '20' . $m[3][$key] : $m[3][$key];
                  $date = new DateTime($year . '-' . $month . '-' . $day);
                  // $date = DateTime::createFromFormat('Y-m-d', $date)
                  if ($date) {
                      return $date->format('Y-m-d');
                  }
              }
          }

          // Get format dd.mm. and dd.mm
          if (preg_match_all('/\s+(3[01]|[12][0-9]|0?[1-9])\.(1[012]|0?[1-9])(?:\.|\s)[^\d]/', $str, $m)) {
              foreach ($m[0] as $key => $val) {
                  $day = str_pad($m[1][$key], 2, '0', STR_PAD_LEFT);
                  $month = str_pad($m[2][$key], 2, '0', STR_PAD_LEFT);
                  $year = $yearOfText;
                  $date = new DateTime($year . '-' . $month . '-' . $day);
                  if ($date) {
                      return $date->format('Y-m-d');
                  }
              }
          }

          return null;
      }
  }

  /**
   * Convert a string to a boolean value.
   *
   * @param mixed $val The value to convert to a boolean
   * @param bool|null $default The default value to return if the input value is null or an empty string
   * @return bool The converted boolean value
   */

  public static function boolval(mixed $val, ?bool $default = null): bool
  {
      if ($val === 'false' || $val === '0') {
          return false;
      } elseif ($val === 'true' || $val === '1') {
          return true;
      } elseif (!is_null($default) && (is_null($val) || $val === '')) {
          return $default;
      } else {
          $boolval = (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : (bool) $val);
      }

      return (bool) $boolval;
  }
  }

  ```

  ```python Python theme={null}
  # ----------------------
  # ----- Enneo SDK ------
  # ----------------------

  import os
  import json
  import requests
  import sys
  import warnings
  from datetime import datetime
  from pydantic import BaseModel, Field
  from typing import Union, Optional, List, Dict
  from enum import Enum

  ENNEO_SDK_VERSION = "0.2.2"

  def get_sdk_version() -> str:
  """
  Returns the version of the Enneo SDK.

  Returns:
      str: The version of the Enneo SDK.
  """
  return ENNEO_SDK_VERSION

  def load_input_data(include_metadata: bool = False) -> Dict:
  """
  Loads the input parameters for the AI function that enneo was able to extract from the ticket/contract

  This function first checks if the input parameters are passed through STDIN.
  If not, it checks if the input parameters are passed through a 'input.json' file in the working directory.
  If the input parameters are passed through STDIN or the 'input.json' file, it returns the loaded input data as a dictionary.

  Returns:
      Dict: The loaded input data as a dictionary.

  Raises:
      FileNotFoundError: If the 'input.json' file is not found in the working directory.
      json.JSONDecodeError: If the input parameters are not valid JSON.
      Exception: If any other unexpected error occurs.
  """
  input_data = None
  try:
      # Option 1: Parameters are passed through STDIN
      if not sys.stdin.isatty():
          input_data = sys.stdin.read()
      if not input_data:
          # Option 2: Parameters are passed through a file
          input_json_path = os.getenv('INPUT_JSON', 'input.json')
          with open(input_json_path, 'r') as file:
              input_data = json.load(file)
      else:
          input_data = json.loads(input_data)

      # Some input values might be strings that are actually JSON objects
      # We try to decode them here
      for key, value in input_data.items():
          try:
              # Try converting the string value to a dict
              decoded_value = json.loads(value)
              input_data[key] = decoded_value
          except (TypeError, json.JSONDecodeError):
              pass

      # Strip any metadata unless requested
      if include_metadata == False:
          if "_metadata" in input_data:
              del input_data["_metadata"]

      return input_data
  except FileNotFoundError:
      print("Error: Input parameters must either be passed through STDIN or through a 'input.json' file in the working directory.")
  except json.JSONDecodeError:
      print("Error: Input parameters must be valid JSON.")
      print("Your input was: ", input_data)
  except Exception as e:
      print(f"An unexpected error occurred: {e}")
  exit(1)

  class IntentType(str, Enum):
  success = 'success'
  neutral = 'neutral'
  warning = 'warning'
  danger = 'danger'

  class IntentInfo(BaseModel):
  """
  Creates a new intent info (=message box visible to the agent in the Enneo UI).

  Attributes:
      type (IntentType): The type of the intent. Must be one of 'success', 'neutral', 'warning', 'danger'.
      message (str): The message to be shown to the agent, e.g. "Reading is plausible".
      extra_info (Optional[str], optional): Optional supplemental information about the intent, e.g. "Expected reading was 421 kWh. Plausbible because difference to 317 kWh is below threshold of 200 kWh"
      code (Optional[str], optional): Optional internal code associated with the info. Not used by enneo. Defaults to None.
  """
  type: IntentType
  message: str
  extra_info: Optional[str] = None
  code: Optional[str] = None

  class IntentOption(BaseModel):
  """
  Creates a new intent option (=button visible to the agent in the Enneo UI).

  Attributes:
      type (str): The type of the intent. Must be unique for each option. Usually accompanied by a matching output case
      name (str): The name of the option, e.g. "Enter into system".
      icon (Optional[str], optional): The icon to be shown next to the option. Defaults to 'check'. Options: check, cancel
      recommended (bool, optional): True for default action. An Intent can only have one default action. Defaults to False.
      order (int, optional): Sorting index order. Frontend sorts in ascending order
      handler (Optional[str], optional): Which microservice should handle this action, Options: cortex, mind or fe. ONLY USE IF YOU KNOW WHAT YOU ARE DOING. Defaults to None.
  """
  type: str
  name: str
  icon: str = 'check'
  recommended: bool = False
  order: int = 0
  handler: Optional[str] = None

  class Interaction(BaseModel):
  """
  Creates a new interaction object.
  This is an enneo-specific object that defines the form shown to the agent in the Enneo UI.
  The standard AI function returns an json-encoded Interaction object to STDOUT

  Attributes:
      data (dict): The input data for the AI function, i.e. form values for the input variables. It is recommended to pre-fill this with the input data coming from load_input_data().
      options (List[IntentOption], optional): A list of intent options (=buttons visible to the agent in the Enneo UI). Defaults to [].
      infos (List[IntentInfo], optional): A list of intent infos (=message boxes visible to the agent in the Enneo UI). Defaults to [].
      form (Optional[dict], optional): Optional dictionary to override the forms to be shown to the agent. Usually not needed. Defaults to None.
  """
  data: dict
  options: List[IntentOption] = []
  infos: List[IntentInfo] = []
  form: Optional[dict] = None

  class ApiEnneo:
  """
  Helper class for calling the Enneo API.
  """
  @staticmethod
  def get_contract(contract_id: Union[int, str]) -> dict:
      """
      Get a contract by its ID.
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      return ApiEnneo.get(f'/api/mind/contract/{contract_id}?includeRawData=true')

  @staticmethod
  def get_ticket(ticket_id: int) -> dict:
      """
      Get a ticket by its ID.
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      return ApiEnneo.get(f'/api/mind/ticket/{ticket_id}')

  @staticmethod
  def get(endpoint: str, params: dict = {}, authorizeAs: str = 'user') -> dict:
      """
      Do an API GET call to an Enneo API endpoint
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      url = ApiEnneo.__get_enneo_url(endpoint)
      headers = ApiEnneo.__build_authorization_header(authorizeAs)
      return Api.call("GET", url, headers, params)

  @staticmethod
  def post(endpoint: str, body: dict, authorizeAs: str = 'user') -> dict:
      """
      Do an API POST call to an Enneo API endpoint
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      url = ApiEnneo.__get_enneo_url(endpoint)
      headers = ApiEnneo.__build_authorization_header(authorizeAs)
      return Api.call("POST", url, headers, body)

  @staticmethod
  def patch(endpoint: str, body: dict, authorizeAs: str = 'user') -> dict:
      """
      Do an API PATCH call to an Enneo API endpoint
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      url = ApiEnneo.__get_enneo_url(endpoint)
      headers = ApiEnneo.__build_authorization_header(authorizeAs)
      return Api.call("PATCH", url, headers, body)

  @staticmethod
  def put(endpoint: str, body: dict, authorizeAs: str = 'user') -> dict:
      """
      Do an API PUT call to an Enneo API endpoint
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      url = ApiEnneo.__get_enneo_url(endpoint)
      headers = ApiEnneo.__build_authorization_header(authorizeAs)
      return Api.call("PUT", url, headers, body)

  @staticmethod
  def delete(endpoint: str, authorizeAs: str = 'user') -> dict:
      """
      Do an API DELETE call to an Enneo API endpoint
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      url = ApiEnneo.__get_enneo_url(endpoint)
      headers = ApiEnneo.__build_authorization_header(authorizeAs)
      return Api.call("DELETE", url, headers)

  @staticmethod
  def executeUdf(name: str, params: dict, authorizeAs: str = 'user') -> dict:
      """
      Execute a UDF (User Defined Function) in Enneo.
      See https://main.enneo.dev/api/mind/docs for documentation.
      """
      return ApiEnneo.post('/api/mind/executor/execute/' + name, {'parameters': params}, authorizeAs)

  @staticmethod
  def __build_authorization_header(authorize_as: str) -> dict:
      if authorize_as == 'user' and os.getenv('ENNEO_USER_AUTH_HEADER'):
          header_split = os.environ["ENNEO_USER_AUTH_HEADER"].split(': ')
          return {header_split[0]: header_split[1]}
      else:
          return {'Authorization': f'Bearer {os.environ["ENNEO_SESSION_TOKEN"]}'}

  @staticmethod
  def __get_enneo_url(endpoint: str) -> str:
      enneo_url = os.getenv('ENNEO_API_URL','')
      if endpoint.startswith('/api/mind'):
          pass
      elif endpoint.startswith('/api/cortex'):
          enneo_url = enneo_url.replace(':8005', ':8006')
      elif endpoint.startswith('/api/auth'):
          enneo_url = enneo_url.replace(':8005', ':8002')
      else:
          raise Exception(f"Unknown Enneo API endpoint {endpoint}. They should start with /api/mind or /api/cortex or /api/auth")

      return enneo_url + endpoint

  class Api:
  """
  Helper class for calling APIs.
  """
  @staticmethod
  def call(method: str, url: str, headers: dict = {}, params: Union[dict, object, str, bool] = False, verify: bool = True) -> dict:
      """
      Do an API call to an API endpoint

      Args:
          method (str): HTTP method: GET, POST, PUT, DELETE or PATCH
          url (str): URL to call, e.g. https://my-api.com/api/v1/endpoint
          headers (dict, optional): The headers to send with the request, e.g. {'Authorization': 'Bearer xxx'}. Defaults to {}.
          params (Union[dict, object, str, bool], optional): The body to send with the request if request is POST/PUT/DELETE. Defaults to False.
      """

      if method == "GET":
          response = requests.get(url, headers=headers, verify=verify)
      elif method == "POST":
          response = requests.post(url, headers=headers, json=params, verify=verify)
      elif method == "PUT":
          response = requests.put(url, headers=headers, json=params, verify=verify)
      elif method == "DELETE":
          response = requests.delete(url, headers=headers, json=params, verify=verify)
      elif method == "PATCH":
          response = requests.patch(url, headers=headers, json=params, verify=verify)
      else:
          raise Exception(f"Unknown method {method}")
      if response.status_code == 200:
          return response.json()
      else:
          if url.startswith(os.environ['ENNEO_API_URL']):
              # For internal Enneo API calls, we show the original error message directly
              raise Exception(response.content)
          else:
              raise Exception(f"Api call to {url} failed with code {response.status_code} and response: {response.content}")

  class Setting:
  """
  Helper class for getting settings from Enneo through the API.
  """
  @staticmethod
  def get(setting_name: str) -> Union[object, None, str]:
      return ApiEnneo.get(f'/api/mind/settings/compact?showSecrets=true&filterByName={setting_name}', authorizeAs='serviceWorker').get(setting_name, None)

  class ApiPowercloud:
  """
  Helper class for calling the Powercloud API.
  Authorization is handled by this SDK.

  Note: As Powercloud unfortunately has frequent issues with invalid SSL certificates, we disable SSL verification for all calls to the Powercloud API.
  """
  @staticmethod
  def get_call(endpoint: str) -> dict:
      warnings.filterwarnings("ignore", message="Unverified HTTPS request")
      url = str(Setting.get('powercloudApiUrl')) + endpoint
      headers = {'Authorization': f'Basic {Setting.get("powercloudApiAuth")}'}
      return Api.call("GET", url, headers, verify=False)

  @staticmethod
  def post_call(endpoint: str, params: Union[dict, object]) -> dict:
      warnings.filterwarnings("ignore", message="Unverified HTTPS request")
      url = str(Setting.get('powercloudApiUrl')) + endpoint
      headers = {'Authorization': f'Basic {Setting.get("powercloudApiAuth")}'}
      return Api.call("POST", url, headers, params, verify=False)

  @staticmethod
  def extract_error(result: Union[object, str]) -> str:
      message = 'Powercloud-Fehler: '
      if not isinstance(result, dict):
          message += str(result)
      elif len(result.get('errors', [])) > 0:
          for error in result['errors']:
              message += f"{error['messageLocalized']}; "
      elif 'response' in result and isinstance(result['response'], str):
          message += result['response']
      elif 'messageLocalized' in result:
          message += result['messageLocalized']
      else:
          message += str(result)
      return message

  class Helpers:
  @staticmethod
  def format_date(date: Union[str, datetime], format: str = 'de') -> str:
      """
      Format a date string or datetime object to a specific format.
      Needed to convert date strings coming from the input data (yyyy-mm-dd) to a human-readable format (dd.mm.yyyy).

      Args:
          date (Union[str, datetime]): The date to format.
          format (str, optional): The format to use. Must be one of 'en' or 'de'. Defaults to 'de'.
      """
      if isinstance(date, str):
          try:
              d = datetime.strptime(date, '%Y-%m-%d')
          except ValueError:
              d = datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
          if not d:
              if date == '':
                  raise Exception('No date to format')
              else:
                  raise Exception(f"Date '{date}' invalid")
          else:
              d = datetime.strptime(date, '%Y-%m-%d')
              if format == 'en':
                  return d.strftime('%Y-%m-%d')
              elif format == 'de':
                  return d.strftime('%d.%m.%Y')
              else:
                  raise Exception(f"Unknown language {format}")
      else:
          raise Exception("Invalid date format")

  ```
</CodeGroup>
