diff --git a/README.md b/README.md index 666b760..429906f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,42 @@ cd wp-agents && composer install wp plugin activate wp-agents ``` +## REST API + +Each agent can be accessed programmatically through the WordPress REST API. + +This allows external systems or front-end applications to send a message to any registered agent and receive the model’s response in JSON format. + +**Endpoint** + +```bash +POST /wp-json/wp-agents/v1/chat +``` + +**Example** + +```bash +curl --location 'https://example.com/wp-json/wp-agents/v1/chat' \ +--header 'Content-Type: application/json' \ +--data '{ + "agent": "taxonomy_agent", + "message": "Hello" +}' +``` + +**Example response** + +```json +{ + "agent": "taxonomy_agent", + "message": "Hello", + "response": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } +} +``` + ## Demo https://github.com/santerref/wp-agents-demo diff --git a/inc/inputs.php b/inc/inputs.php deleted file mode 100644 index a5a6805..0000000 --- a/inc/inputs.php +++ /dev/null @@ -1,7 +0,0 @@ -actions; - } - public function filters(): array { return $this->filters; } - public function run( mixed $input, Provider_Interface $provider ): mixed { - return $provider->complete( $input, $this ); + public function prompt( mixed $input ): Agent_Runner { + return new Agent_Runner( + $input, + $this + ); } public function get_model(): string { diff --git a/lib/exceptions/class-agent-not-found-exception.php b/lib/exceptions/class-agent-not-found-exception.php new file mode 100644 index 0000000..3aba256 --- /dev/null +++ b/lib/exceptions/class-agent-not-found-exception.php @@ -0,0 +1,7 @@ +priority; - } - - public function get_accepted_args(): int { - return $this->accepted_args; - } -} diff --git a/lib/inputs/class-post-updated-input.php b/lib/inputs/class-post-updated-input.php deleted file mode 100644 index e3004a5..0000000 --- a/lib/inputs/class-post-updated-input.php +++ /dev/null @@ -1,32 +0,0 @@ -post_type ) { - throw new Skip_Agent_Exception(); - } - - $categories = wp_get_post_categories( $post->ID, array( 'fields' => 'names' ) ); - $tags = wp_get_post_tags( $post->ID, array( 'fields' => 'names' ) ); - - $category_list = empty( $categories ) ? 'none' : implode( ', ', $categories ); - $tag_list = empty( $tags ) ? 'none' : implode( ', ', $tags ); - - return <<post_title} - -Post content: {$post->post_content} - -Existing category: {$category_list} -Existing tags: {$tag_list} -PROMPT; - } -} diff --git a/lib/providers/class-open-ai-provider.php b/lib/providers/class-open-ai-provider.php index f9feac9..a0c7e4a 100644 --- a/lib/providers/class-open-ai-provider.php +++ b/lib/providers/class-open-ai-provider.php @@ -3,6 +3,8 @@ namespace Wp_Agents\Providers; use Wp_Agents\Agents\Abstract_Llm_Agent; +use Wp_Agents\System\Message; +use Wp_Agents\System\Message_Stack; use Wp_Agents\Tools\Tool_Registry; class Open_Ai_Provider implements Provider_Interface { @@ -13,23 +15,20 @@ public function __construct( string $api_key ) { $this->api_key = $api_key; } - public function complete( string $prompt, Abstract_Llm_Agent $agent ): string { + public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack { $client = \OpenAI::client( $this->api_key ); $tools = $agent->tools(); - $messages = array( + $messages = new Message_Stack( array( - 'role' => 'system', - 'content' => $agent->instructions(), - ), - array( - 'role' => 'user', - 'content' => $prompt, - ), + new Message( 'system', $agent->instructions() ), + new Message( 'user', $input ), + ) ); + $parameters = array( 'model' => $agent->get_model(), - 'messages' => $messages, + 'messages' => $messages->to_raw_array(), 'response_format' => array( 'type' => $agent->json() ? 'json_object' : 'text' ), ); @@ -40,8 +39,14 @@ public function complete( string $prompt, Abstract_Llm_Agent $agent ): string { $response = $client->chat()->create( $parameters ); - $message = $response->choices[0]->message; - $messages[] = $message->toArray(); + $message = $response->choices[0]->message; + $messages->add( + new Message( + $message->role, + $message->content, + $message->toArray() + ) + ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName if ( ! empty( $message->toolCalls ) ) { @@ -55,24 +60,35 @@ public function complete( string $prompt, Abstract_Llm_Agent $agent ): string { } $result = $tool_registry->execute( $function->name, $args ); - $messages[] = array( - 'role' => 'tool', - 'tool_call_id' => $call->id, - 'content' => wp_json_encode( $result ), + $messages->add( + new Message( + 'tool', + wp_json_encode( $result ), + array( + 'tool_call_id' => $call->id, + ) + ) ); } $follow = $client->chat()->create( array( 'model' => $agent->get_model(), - 'messages' => $messages, + 'messages' => $messages->to_raw_array(), 'response_format' => array( 'type' => $agent->json() ? 'json_object' : 'text' ), ) ); - return $follow->choices[0]->message->content ?? ''; + $message = $follow->choices[0]->message; + $messages->add( + new Message( + $message->role, + $message->content, + $message->toArray(), + ) + ); } - return $response->choices[0]->message->content ?? ''; + return $messages; } } diff --git a/lib/providers/class-provider-interface.php b/lib/providers/class-provider-interface.php index 880df5c..f942f21 100644 --- a/lib/providers/class-provider-interface.php +++ b/lib/providers/class-provider-interface.php @@ -3,8 +3,9 @@ namespace Wp_Agents\Providers; use Wp_Agents\Agents\Abstract_Llm_Agent; +use Wp_Agents\System\Message_Stack; interface Provider_Interface { - public function complete( string $prompt, Abstract_Llm_Agent $agent ): string; + public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack; } diff --git a/lib/services/class-agent-manager.php b/lib/services/class-agent-manager.php index ead679d..72acf15 100644 --- a/lib/services/class-agent-manager.php +++ b/lib/services/class-agent-manager.php @@ -3,42 +3,22 @@ namespace Wp_Agents\Services; use Wp_Agents\Agents\Abstract_Llm_Agent; -use Wp_Agents\Exceptions\Skip_Agent_Exception; +use Wp_Agents\Exceptions\Agent_Not_Found_Exception; class Agent_Manager { protected static array $agents = array(); - public static function register( string $name, Abstract_Llm_Agent $agent ): void { - self::$agents[ $name ] = $agent; - - foreach ( $agent->actions() as $action ) { - $input = Input_Manager::make( $action ); - - add_action( - $action, - function ( ...$args ) use ( $agent, $input, $name, $action ) { - Recursion_Guard::run( - "{$name}:{$action}", - function () use ( $input, $args, $agent ) { - try { - $built_input = $input->build( ...$args ); - if ( ! empty( $built_input ) ) { - $provider = Provider_Manager::get( $agent->get_provider() ); - $answer = $agent->run( $built_input, $provider ); - $agent->handle_response( $answer, $args ); - } - } catch ( Skip_Agent_Exception $e ) { - wp_agents_logger()->error( $e->getMessage() ); - } - } - ); - }, - $input->get_priority(), - $input->get_accepted_args() - ); + public static function register( string $name, string $agent_class ): void { + self::$agents[ $name ] = new $agent_class(); + } + public static function get( string $name ): Abstract_Llm_Agent { + if ( ! isset( self::$agents[ $name ] ) ) { + throw new Agent_Not_Found_Exception(); } + + return self::$agents[ $name ]; } public static function boot(): void { diff --git a/lib/services/class-input-manager.php b/lib/services/class-input-manager.php deleted file mode 100644 index 9914df8..0000000 --- a/lib/services/class-input-manager.php +++ /dev/null @@ -1,27 +0,0 @@ -input = $input; + $this->agent = $agent; + $this->provider = Provider_Manager::get( $agent->get_provider() ); + } + + public function chat(): Message { + $message_stack = $this->provider->chat( $this->input, $this->agent ); + + return $message_stack->last(); + } +} diff --git a/lib/system/class-message-stack.php b/lib/system/class-message-stack.php new file mode 100644 index 0000000..255915c --- /dev/null +++ b/lib/system/class-message-stack.php @@ -0,0 +1,45 @@ +messages = $messages; + } + + public function getIterator(): Traversable { + return new \ArrayIterator( $this->messages ); + } + + public function count(): int { + return count( $this->messages ); + } + + public function first(): ?Message { + return $this->messages[0] ?? null; + } + + public function last(): ?Message { + $count = count( $this->messages ); + + return $count ? $this->messages[ $count - 1 ] : null; + } + + public function add( Message $message ) { + $this->messages[] = $message; + } + + public function to_raw_array(): array { + return array_map( + function ( Message $message ) { + return $message->get_raw_response(); + }, + $this->messages + ); + } +} diff --git a/lib/system/class-message.php b/lib/system/class-message.php new file mode 100644 index 0000000..ba21196 --- /dev/null +++ b/lib/system/class-message.php @@ -0,0 +1,49 @@ +role = $role; + $this->content = $content; + + if ( empty( $raw_response ) ) { + $this->raw_response = array( + 'role' => $role, + 'content' => $content, + ); + } else { + $this->raw_response = $raw_response; + } + } + + public function get_role(): string { + return $this->role; + } + + public function get_content(): string { + return $this->content; + } + + public function get_raw_response(): array { + return array( + 'role' => $this->role, + 'content' => $this->content, + ) + $this->raw_response; + } + + public function to_array(): array { + return array( + 'role' => $this->role, + 'content' => $this->content, + 'raw_response' => $this->raw_response, + ); + } +} diff --git a/lib/system/class-rest.php b/lib/system/class-rest.php new file mode 100644 index 0000000..cdfbbd5 --- /dev/null +++ b/lib/system/class-rest.php @@ -0,0 +1,37 @@ + 'POST', + 'callback' => array( self::class, 'handle' ), + 'permission_callback' => '__return_true', + ) + ); + } + + public static function handle( \WP_REST_Request $request ) { + $agent = Agent_Manager::get( $request->get_param( 'agent' ) ); + + $message = $request->get_param( 'message' ); + $response = $agent + ->prompt( $message ) + ->chat(); + + return rest_ensure_response( + array( + 'agent' => $request->get_param( 'agent' ), + 'message' => $message, + 'response' => $response->get_raw_response(), + ) + ); + } +} diff --git a/wp-agents.php b/wp-agents.php index 1d1355f..c125249 100644 --- a/wp-agents.php +++ b/wp-agents.php @@ -36,34 +36,32 @@ function wp_agents_logger() { if ( ! function_exists( 'wp_agents_register' ) ) { function wp_agents_register( string $name, string $agent_class ) { - \Wp_Agents\Services\Agent_Manager::register( $name, new $agent_class() ); + \Wp_Agents\Services\Agent_Manager::register( $name, $agent_class ); } } - if ( ! function_exists( 'wp_agents_register_input' ) ) { + if ( ! function_exists( 'wp_agents_register_provider' ) ) { - function wp_agents_register_input( string $name, string $input_class ) { - \Wp_Agents\Services\Input_Manager::register( $name, $input_class ); + function wp_agents_register_provider( string $name, callable $callback ) { + \Wp_Agents\Services\Provider_Manager::register( $name, $callback ); } - require_once __DIR__ . '/inc/inputs.php'; + require_once __DIR__ . '/inc/providers.php'; } - if ( ! function_exists( 'wp_agents_register_provider' ) ) { + if ( ! function_exists( 'wp_agents_get' ) ) { - function wp_agents_register_provider( string $name, callable $callback ) { - \Wp_Agents\Services\Provider_Manager::register( $name, $callback ); + function wp_agents_get( string $name ) { + return \Wp_Agents\Services\Agent_Manager::get( $name ); } - require_once __DIR__ . '/inc/providers.php'; - } add_action( 'init', array( \Wp_Agents\Services\Provider_Manager::class, 'boot' ) ); - add_action( 'init', array( \Wp_Agents\Services\Input_Manager::class, 'boot' ) ); add_action( 'init', array( \Wp_Agents\Services\Agent_Manager::class, 'boot' ) ); + add_action( 'rest_api_init', array( \Wp_Agents\System\Rest::class, 'register' ) ); } } );