/** Datei: plugins/agents/library/ARUNOX/Plugins/Agents/API/v1/Routes.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Agents\API\v1; use ARUNOX\Core\Hooks; use ARUNOX\Core\Router; use ARUNOX\Plugins\REST; class Routes { public static function register() : void { /* ### MAIN ROUTES ############################## # */ Router::addAll( Controller::class ); /* ### MENU ROUTES ############################## # */ Hooks::add('router::get->api.v1.rest.routes', function( $routes ) { $routes[] = [ "path" => "/agents", "label" => "Assistenten", "icon" => "bi bi-person-gear", "menu" => "content", ]; $routes[] = [ "path" => "/agents/{id}", "label" => "Bearbeiten", ]; return $routes; }); } } /** Datei: plugins/agents/library/ARUNOX/Plugins/Agents/API/v1/Controller.php **/ * @copyright 2025 ARUNOX * @version 1.1.0 * * @since 1.0.0 File is created */ namespace ARUNOX\Plugins\Agents\API\v1; use ARUNOX\HTTP; use ARUNOX\Core\Hooks; use ARUNOX\API\OpenAI; use ARUNOX\Plugins\REST; use ARUNOX\Plugins\Users; use ARUNOX\Plugins\Agents; use ARUNOX\API\BaseController; use ARUNOX\Database\BaseEntity; class Controller extends BaseController { /* ### VARIABLES ################################################ # */ protected static array $NOTIFY = [ "notfound" => "Assistent nicht gefunden", "saved" => "Assistent wurde erfolgreich gespeichert", "deleted" => "Assistent wurde erfolgreich gelöscht", "created" => "Assistent wurde erfolgreich erstellt", "statuschanged.0" => "Assistent wurde erfolgreich deaktiviert", "statuschanged.1" => "Assistent wurde erfolgreich aktiviert", "notcreated" => "Assistent konnte nicht erstellt werden", "notsaved" => "Assistent konnte nicht gespeichert werden", ]; /* ### PUBLIC GET FUNCTIONS ##################################### # */ /** * @api * @method GET * * @route.path /agents * @route.description Listet alle Agents */ public static function getAll() : array { return Agents\Model::getAll(); } /** * @api * @method GET * * @route.path /agents/{hashid} * @route.description Gibt einen einzelnen Agent anhand der ID zurück */ public static function get( int|string $hashid ) : BaseEntity | HTTP\Response { if( $result = Agents\Model::get( $hashid ) ) return $result; return REST\Response::autoResponse( 404, static::$NOTIFY["notfound"] ); } /* ### PUBLIC SAVE FUNCTIONS #################################### # */ /** * @api * @method PUT * * @route.path /agents/{hashid} * @route.description Speichert Änderungen an einem bestehenden Agent */ public static function save( int|string $hashid, array $data ) : HTTP\Response { if( $result = Agents\Model::get( $hashid ) ) { if( $result->save( $data ) ) { return REST\Response::autoResponse( 200, static::$NOTIFY["saved"] ); } return REST\Response::autoResponse( 500, static::$NOTIFY["notsaved"] ); } return REST\Response::autoResponse( 404, static::$NOTIFY["notfound"] ); } /** * @api * @method POST * * @route.path /agents * @route.description Erstellt einen neuen Agent */ public static function create( array $data ) : HTTP\Response { if( $hashid = Agents\Model::save( $data ) ) { $result = Agents\Model::get( $hashid ); return REST\Response::autoResponse( 200, static::$NOTIFY["created"], [ "data" => $result ] ); } return REST\Response::autoResponse( 500, static::$NOTIFY["notfound"] ); } /** * @api * @method DELETE * * @route.path /agents/{hashid} * @route.description Löscht einen Agent anhand der ID */ public static function delete( int|string $hashid ) : HTTP\Response { if( Agents\Model::delete( $hashid ) ) { return REST\Response::autoResponse( 200, static::$NOTIFY["deleted"] ); } return REST\Response::autoResponse( 500, static::$NOTIFY["notfound"] ); } /** * @api * @method PATCH * * @route.path /agents/{hashid}/status * @route.description Wechselt den Status (aktiv/inaktiv) eines Agenten */ public static function toggleStatus( int|string $hashid ) : HTTP\Response { if( $result = Agents\Model::get( $hashid ) ) { $result->toggleStatus( TRUE ); return REST\Response::autoResponse( 200, static::$NOTIFY["statuschanged.{$result->status->getValue()}"], [ "data" => $result ] ); } return REST\Response::autoResponse( 404, static::$NOTIFY["notfound"] ); } /* ### PUBLIC VALIDATE FUNCTIONS ################################ # */ /* ### PUBLIC REST FUNCTIONS #################################### # */ /* ### PUBLIC REST RESPONSE FUNCTIONS ########################### # */ /** * @internal rest * @method PUT * * @route.path /agents/{hashid} * @route.description REST-Antwort nach dem Speichern eines Agenten */ public static function restResponseSave( int|string $hashid, array $data ) : HTTP\Response { $result = static::save( $hashid, $data ); if( $result->isStatus( 200 ) ) { $data = Agents\Model::get( $hashid ); REST\Response::closeModals(); REST\Response::addUpdater( [ "data/table/row", $hashid ], $data ); } return static::restResponse( $result ); } /** * @internal rest * @method POST * * @route.path /agents * @route.description REST-Antwort nach dem Erstellen eines Agenten (inkl. Notifikation, Redirect etc.) */ public static function restResponseCreate( array $data ) : HTTP\Response { $result = static::create( $data ); if( $result->isStatus( 200 ) ) { REST\Response::closeModals(); REST\Response::redirect( "/agents/{$result->getBody("data", "hashid")}" ); } return static::restResponse( $result ); } /** * @internal rest * @method DELETE * * @route.path /agents/{hashid} * @route.description REST-Antwort nach dem Löschen eines Agenten */ public static function restResponseDelete( int|string $hashid ) : HTTP\Response { REST\Response::closeModals(); $result = static::delete( $hashid ); if( $result->isStatus( 200 ) ) { REST\Response::redirect( "/agents" ); } return static::restResponse( $result ); } /** * @internal rest * @method PATCH * * @route.path /agents/{hashid}/status * @route.description REST-Antwort zur Status-Umschaltung eines Agenten (z. B. für Notifications) */ public static function restResponseToggleStatus( int|string $hashid ) : HTTP\Response { $result = static::toggleStatus( $hashid ); if( $result->isStatus( 200 ) ) { $data = $result->getBody("data"); REST\Response::addUpdater( [ "data/table/row", $hashid ], $data ); } return static::restResponse( $result ); } /* ### PUBLIC REST CONFIG FUNCTIONS ############################# # */ /** * @internal rest * @method GET * * @route.path /agents * @route.description REST-Konfiguration für Agenten-Tabelle */ public static function restConfigOverview() : REST\Generator\Base\BaseGenerator { return REST\Response::addWrapper( "overview", "Assistenten" ) ->get() ->setApi( "/agents" ) ->setAction( "/agents/{hashid}" ) ->setOrder( "name", "asc" ) ->setSearch( TRUE ) ->addButton(...Hooks::filter( "rest.generator::button.name->create", [], "agents" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [], "name", TRUE, TRUE, TRUE, "agents" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [], "voice", TRUE, TRUE, TRUE, "agents" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->status", [], "status", [], "agents", "hashid" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->text", [], "phone", TRUE, FALSE, TRUE, "agents" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->datetime", [], "date_edit", FALSE, FALSE, TRUE, "agents" )) ->addColumn(...Hooks::filter( "rest.generator::column.type->actions", [], [], FALSE, "agents" ))->get() ->addButton(...Hooks::filter( "rest.generator::action.name->edit", [], "agents", "hashid" )) ->addButton(...Hooks::filter( "rest.generator::action.name->delete", [], "agents", "hashid" )); } /** * @internal rest * @method GET * * @route.path /agents/{hashid} * @route.alternative.0 /agents/{hashid}#kontext * @route.alternative.1 /agents/{hashid}#chat * @route.alternative.2 /agents/{hashid}#telefon * @route.description REST-Konfiguration für Detailansicht eines Agenten */ public static function restConfigDetails( int|string $hashid ) : REST\Generator\Base\BaseGenerator { $data = Agents\Model::get( $hashid ); return REST\Response::addWrapper( "details", "Assistent bearbeiten" ) ->setBacklink( "/agents" ) ->get() ->setApi( "/agents/{$hashid}" ) ->setAction( "/agents/{$hashid}", "PUT" ) ->addTab( "Assistent" )->get() ->addHeadline( "Assistent", 2 ) ->addText( "Mit diesen Informationen wird das Verhalten des Assistenten gesteuert." ) ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "name", TRUE, "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->text", [ "readonly" => TRUE, ], "uuid", FALSE, "agents" )) ->parent() ->addTab( "Kontext" )->get() ->addHeadline( "Kontext", 2 ) ->addText( "Kontext der sowohl für Chat als auch Telefon gültig ist. Zur besseren Struktur kann Markdown genutzt werden." ) ->addHeadline( "Persönlichkeit", 3 ) ->addText( "Informationen über den Assistenten sowie welche Persönlichkeit der Assistent haben soll." ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Persönlichkeit" ], "prompt_personality", FALSE, "agents" )) ->addHeadline( "Allgemeine Informationen", 3 ) ->addText( "Allgemeine Informationen für den Assistenten, z.B. was er machen soll." ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Allgemeine Informationen" ], "prompt_tasks", FALSE, "agents" )) ->addHeadline( "Verhalten bei Unklarheiten", 3 ) ->addText( "Falls du das Anliegen des Anrufers nicht beantworten kannst:" ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Verhalten bei Unklarheiten" ], "prompt_uncertainties", FALSE, "agents" )) ->addHeadline( "Sensible Themen", 3 ) ->addText( "Falls der Kunde über folgende Themen spricht, sage, dass du nicht weiterhelfen kannst: " ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Sensible Themen" ], "prompt_sensitives", FALSE, "agents" )) ->addHeadline( "Gesprächsablauf", 3 ) ->addText( "Ein grober Ablauf eines Gespräches, damit der Assistent weiß wie er das Gespräch führen soll." ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Gesprächsablauf" ], "prompt_conversation_flow", FALSE, "agents" )) ->addHeadline( "Häufige Fragen", 3 ) ->addText( "Hier eine Übersicht über häufig gestellte Fragen und deren Antworten, die als Orientierungshilfe dienen soll:" ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Häufige Fragen" ], "prompt_faq", FALSE, "agents" )) ->addHeadline( "Wissensdatenbank", 3 ) ->addText( "Hier eine generelle Auflistung an Informationen, die bei der Gesprächsführung helfen sollen:" ) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Wissensdatenbank" ], "prompt_knowledge_base", FALSE, "agents" )) ->parent() ->addTab( "Chat" )->get() ->addHeadline( "Einstellung für Chat-KI", 2 ) ->addField(...Hooks::filter( "rest.generator::control.type->code", [ "label" => "Einbettungscode", "value" => implode( "\n", [ "", "domain . "/api/v1/agents/chat/{$data->uuid}/styles\">", ]), "readonly" => TRUE, ], "embed_code", FALSE, "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [], "start_message_chat", FALSE, "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Zusätzlicher Kontext", ], "prompt_chat", FALSE, "agents" )) ->parent() ->addTab( "Telefon" )->get() ->addHeadline( "Einstellung für Telefon-KI", 2 ) ->addGroup() ->addField(...Hooks::filter( "rest.generator::control.type->select", [], "phone_number", FALSE, FALSE, "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->select", [], "phone_voice", FALSE, FALSE, "agents" )) ->parent() ->addGroup() ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [], "start_message_phone", FALSE, "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->textarea", [ "label" => "Zusätzlicher Kontext", ], "prompt_phone", FALSE, "agents" )) ->parent( 2 ) ->addTab( "Tools" )->get() ->addText( "Tools sind Werkzeuge die der Assistent während des Anrufs verwenden kann." ) ->addContent( Agents\Tools\API\v1\Controller::restConfigTable( $hashid ) ) ->parent() ->addTab( "Nachverarbeitung" )->get() ->addText( "Nachverarbeitungen des Assistenten werden nach dem Anruf parallel ausgeführt." ) ->addContent( Agents\Processing\API\v1\Controller::restConfigTable() ) ->parent() ->addTab( "Speichern" )->get() ->setColor( REST\Generator\Config\Color::Primary ) ->setIcon( "bi bi-save" ) ->setType( "submit" ); } /** * @internal rest * @method GET * * @route.path /agents/create * @route.description REST-Konfiguration für das Erstellen eines Agenten (Formular) */ public static function restConfigCreate() : REST\Generator\Base\BaseGenerator { return REST\Response::addModal( "details", "Assistent erstellen" ) ->get() ->setAction( "/agents", "POST" ) ->addButton(...Hooks::filter( "rest.generator::button.type->submit", [], "Erstellen", "agents" )) ->addField(...Hooks::filter( "rest.generator::control.type->text", [], "name", TRUE, "agents" )); } /** * @internal rest * @method GET * * @route.path /agents/{hashid}/delete * @route.description REST-Konfiguration für die Lösch-Bestätigung eines Agenten */ public static function restConfigDelete( int|string $hashid ) : REST\Generator\Base\BaseGenerator { return REST\Response::addModal( "details", "Assistent Löschen" ) ->setSize( REST\Wrapper\Modal\Size::sm ) ->get() ->setAction( "/agents/{$hashid}", "DELETE" ) ->addText( "Möchtest du wirklich den Datensatz (#{$hashid}) löschen? " ) ->addButton(...Hooks::filter( "rest.generator::button.type->cancel", [], NULL, "agents" )) ->addButton(...Hooks::filter( "rest.generator::button.type->delete", [], NULL, "agents" )); } /* ### PUBLIC CHAT FUNCTIONS #################################### # */ /** * @api * @method GET * * @route.path /agents/chat/{uuid}/reset * @route.description Resetten den Chat des Users zum aktuellen Agenten */ public static function resetChat( string $uuid ) : HTTP\Response { $agent = Agents\Model::getByColumn( "uuid", $uuid ); $user = Users\Model::get(); $hashid = hashid( "chat", $agent->hashid, $user->id ); \ARUNOX\API::setAuthToken( ARUNOX_API_KEY ); \ARUNOX\API::setApiUrl( "https://" . SYSTEM->domain ."/api/v1/agents/{$agent->hashid}" ); $prompt = \ARUNOX\API::get( "?columns=prompt" ); $prompt = $prompt->prompt; $tools = \ARUNOX\API::get( "/tools?status=1&columns=id,type,description,url,defaults,header,method,parameters" ); $tools = array_map( fn( $item ) => (object)array_merge( (array)$item, [ "name" => "applyTool-{$item->id}", "openai" => [ "type" => "function", "function" => (object)[ "name" => "applyTool-{$item->id}", "description" => $item->description, "parameters" => (object)json_decode( $item->parameters ?? "{}" ), ], ], ]), $tools ); $tools = array_column( $tools, NULL, "name" ); // show( $tools ); $config = [ "model" => "gpt-4o-mini", // "instructions" => $prompt, "tools" => array_values( array_column( $tools, "openai" ) ), "tool_choice" => "auto", "user" => $hashid, "store" => FALSE, ]; // show( $config ); $calls = [ [ "role" => OpenAI\Messages\Role::Developer, "content" => $prompt, ], [ "role" => OpenAI\Messages\Role::User, "content" => "Ich möchte ein Tool für Agent sam erstellen", ], ]; function runOpenAiResponseLoop( array $config, array &$calls, array $tools, array &$logging ) { $response = OpenAI\Responses::create(array_merge( $config, [ "input" => $calls ])); $toolCall = FALSE; if( OpenAI::hasError() ) show( OpenAI::getError(), "STOP" ); // show( $response, FALSE ); $logging[] = $response; // show( $response ); foreach( $response->output as $output ) { if( $output->type == Agents\Calls\Type::ToolCall ) { $output->role = OpenAI\Messages\Role::Tool; $calls[] = [ "call_id" => $output->call_id, "arguments" => $output->arguments, "name" => $output->name, "type" => $output->type, "id" => $output->id, "status" => $output->status, ]; if( ! $tool = $tools[ $output->name ] ?? NULL ) continue; $toolCall = TRUE; if( $tool->type == 3 ) { $defaults = json_decode( $tool->defaults ?? "[]", TRUE ); $arguments = array_replace( $defaults, json_decode( $output->arguments ?? "[]", TRUE ) ); $toolUrl = preg_replace_callback('/\{(\w+)\}/', function( $m ) use( $arguments ) { return trim( $arguments[ $m[1] ] ?? $m[0], "/" ); }, $tool->url ); $result = \ARUNOX\API::{$tool->method}( $toolUrl, $arguments , $tool->header ); if( !is_scalar( $result ) ) { $result = json_encode( $result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } $calls[] = [ "call_id" => $output->call_id, "output" => $result, "type" => Agents\Calls\Type::ToolResult, "id" => $output->id, "status" => "completed", ]; } // show( $output, $calls ); } if( $output->type == Agents\Calls\Type::Message ) { $calls[] = [ "content" => $output->content[0]->text, "id" => $output->id, "role" => $output->role, "status" => $output->status, "type" => $output->type, ]; } } if( $toolCall ) return runOpenAiResponseLoop( $config, $calls, $tools, $logging ); return $response; } function runOpenAiChatLoop( array $config, array &$calls, array $tools, array &$logging ) { $response = OpenAI\Chat\Completions::create(array_merge( $config, [ "messages" => $calls ])); if( OpenAI::hasError() ) show( OpenAI::getError(), "STOP" ); $logging[] = $response; $choice = $response->choices[0]; $message = $choice->message; $calls[] = (array)$message; // show( $response ); if( $choice->finish_reason == "tool_calls" ) { foreach( $message->tool_calls as $tool_call ) { if( ! $tool = $tools[ $tool_call->function->name ] ?? NULL ) continue; if( $tool->type == 3 ) { $defaults = json_decode( $tool->defaults ?? "[]", TRUE ); $arguments = array_replace( $defaults, json_decode( $tool_call->function->arguments ?? "[]", TRUE ) ); $toolUrl = preg_replace_callback('/\{(\w+)\}/', function( $m ) use( $arguments ) { return trim( $arguments[ $m[1] ] ?? $m[0], "/" ); }, $tool->url ); $result = \ARUNOX\API::{$tool->method}( $toolUrl, $arguments , $tool->header ); if( !is_scalar( $result ) ) { $result = json_encode( $result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } $calls[] = [ "tool_call_id" => $tool_call->id, "content" => $result, "role" => OpenAI\Messages\Role::Tool, ]; } } return runOpenAiChatLoop( $config, $calls, $tools, $logging ); } return $response; } $logging = []; // $response = runOpenAiResponseLoop( $config, $calls, $tools, $logging ); $response = runOpenAiChatLoop( $config, $calls, $tools, $logging ); show( $response->choices[0]->message->content, $calls, $logging, "STOP" ); $data = [ "origin_id" => $agent->hashid, "chat_hashid" => $response->user, "status" => Agents\Calls\Status::getByConstant( $response->output[0]->status ), "role" => $response->output[0]->role, "type" => $response->output[0]->type, "msg_id" => $response->output[0]->id, "model" => $response->model, "content" => $response->output[0]->content[0]->text, "tool_name" => $response->output[0]->name, "tool_args" => $response->output[0]->arguments, "tool_call_id" => $response->output[0]->call_id, "usage_prompt" => $response->usage->prompt_tokens, "usage_completion" => $response->usage->completion_tokens, "usage_total" => $response->usage->total_tokens, // "cost_usd" => $response->user, "source" => Agents\Calls\Source::Webchat, // "hashid" => $response->user, ]; show( $response, $data, "STOP" ); $call_id = \ARUNOX\API::post( "/calls/{$hashid}", [ "content" => $agent->start_message_chat, "role" => OpenAI\Messages\Role::Assistant, "type" => Agents\Calls\Type::Message, "status" => Agents\Calls\Status::Complete, ]); show( $call_id, "X"); } /** * @api * @method GET * * @route.path /agents/chat/{uuid}/data * @route.description Lädt den aktuellen Chat-Verlauf für einen Agenten */ public static function getChatData( string $uuid, array $data ) : BaseEntity | bool | array { return Agents\Model::getByColumn( "uuid", $uuid ); } /** * @api * @method GET * * @route.path /agents/chat/{uuid}/script * @route.description Liefert das benötigte JavaScript für den Chat */ public static function includeChatScript( string $uuid, array $data ) : never { $agent = Agents\Model::getByColumn( "uuid", $uuid ); $user = Users\Model::get(); $icons = glob( PLUGINS_DIR . "/agents/assets/icons/*.svg" ); $icons = array_map( fn( $icon ) => [ "file" => file_get_contents( $icon ), "name" => preg_replace_callback('/-([a-z])/', fn( $m ) => strtoupper( $m[1] ), basename( $icon, ".svg" ) ), ], $icons ); $icons = array_column( $icons, "file", "name" ); $openai = json_decode( $user->openai_chats ?? "{}", TRUE ); $hashid = hashid( "chat", $agent->hashid, $user->id ); if( !in_array( $hashid, $openai ) ) { $openai[] = $hashid; $user->save([ "openai_chats" => $openai ]); } // show( $icons ); header("Content-Type: text/javascript "); // echo "const chatAI_MD = '" . base64_encode(file_get_contents( PLUGINS_DIR . "/agents/assets/demo.md" )) . "';"; echo "const chatAI_NAME = '{$agent->name}';"; echo "const chatAI_API = 'https://" . SYSTEM->domain ."/api/v1/agents/{$agent->hashid}';\n"; echo "const chatAI_WS_URL = 'wss://ws.arunox.dev';"; echo "const chatAI_WS_Port = '2347';"; // echo "const chatAI_WS_Token = 'ws_" . session_id(). "';"; echo "const chatAI_minWidth = '350px';"; echo "const chatAI_minHeight = '250px';"; echo "const chatAI_maxWidth = '33vw';"; echo "const chatAI_maxHeight = '66vh';"; echo "const chatAI_icons = " . json_encode( $icons, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) .";"; echo "const chatAI_CHAT_ID = '{$hashid}';"; echo file_get_contents( PLUGINS_DIR . "/agents/assets/js/marked.min.js" ); echo file_get_contents( PLUGINS_DIR . "/agents/assets/js/chatai.js" ); echo file_get_contents( PLUGINS_DIR . "/agents/assets/js/chatai_ws.js" ); exit; } /** * @api * @method GET * * @route.path /agents/chat/{id}/styles * @route.description Liefert die benötigten Stylesheets für den Chat */ public static function includeChatStyles( string $uuid, array $data ) : never { header("Content-Type: text/css"); include PLUGINS_DIR . "/agents/assets/css/chatai.css"; exit; } }