$value) { if (is_string($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)->$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; } }