From 1a0a4de45f6198142691a6782b84b7127465e19e Mon Sep 17 00:00:00 2001 From: Francis Santerre Date: Fri, 24 Oct 2025 03:58:25 -0400 Subject: [PATCH 1/4] Add database memory --- lib/agents/class-abstract-llm-agent.php | 26 ++++++- lib/memory/class-abstract-memory.php | 25 ++++++ lib/memory/class-database-memory.php | 69 +++++++++++++++++ lib/providers/class-open-ai-provider.php | 89 +++++++++++++--------- lib/providers/class-provider-interface.php | 2 +- lib/services/class-agent-manager.php | 2 +- lib/system/class-agent-runner.php | 20 ++++- lib/system/class-memorized-message.php | 9 +++ lib/system/class-message-stack.php | 29 ++++--- lib/system/class-message.php | 47 +++++------- lib/system/class-rest.php | 3 +- lib/system/class-schema.php | 38 +++++++++ wp-agents.php | 28 +++++-- 13 files changed, 301 insertions(+), 86 deletions(-) create mode 100644 lib/memory/class-abstract-memory.php create mode 100644 lib/memory/class-database-memory.php create mode 100644 lib/system/class-memorized-message.php create mode 100644 lib/system/class-schema.php diff --git a/lib/agents/class-abstract-llm-agent.php b/lib/agents/class-abstract-llm-agent.php index e84e961..7c93881 100644 --- a/lib/agents/class-abstract-llm-agent.php +++ b/lib/agents/class-abstract-llm-agent.php @@ -2,8 +2,7 @@ namespace Wp_Agents\Agents; -use Wp_Agents\Providers\Provider_Interface; -use Wp_Agents\Services\Provider_Manager; +use Wp_Agents\Memory\Abstract_Memory; use Wp_Agents\System\Agent_Runner; abstract class Abstract_Llm_Agent { @@ -18,6 +17,16 @@ abstract class Abstract_Llm_Agent { protected array $tools = array(); + protected ?string $memory = null; + + protected ?int $memory_limit = null; + + protected string $name = ''; + + public function __construct( string $name ) { + $this->name = $name; + } + abstract public function instructions(): string; public function filters(): array { @@ -31,6 +40,10 @@ public function prompt( mixed $input ): Agent_Runner { ); } + public function name(): string { + return $this->name; + } + public function get_model(): string { return $this->model; } @@ -47,6 +60,13 @@ public function tools(): array { return $this->tools; } - public function handle_response( mixed $answer, array $args = array() ): void { + public function get_memory( string $session_id ): ?Abstract_Memory { + if ( $this->memory ) { + $memory_class = $this->memory; + + return new $memory_class( $this->name(), $session_id ); + } + + return null; } } diff --git a/lib/memory/class-abstract-memory.php b/lib/memory/class-abstract-memory.php new file mode 100644 index 0000000..369d385 --- /dev/null +++ b/lib/memory/class-abstract-memory.php @@ -0,0 +1,25 @@ +agent = $agent; + $this->session_id = $session_id; + } + + abstract public function remember( Message $message ): void; + + abstract public function load( ?int $limit = null ): Message_Stack; + + abstract public function forget_all(): void; + +} diff --git a/lib/memory/class-database-memory.php b/lib/memory/class-database-memory.php new file mode 100644 index 0000000..82788eb --- /dev/null +++ b/lib/memory/class-database-memory.php @@ -0,0 +1,69 @@ +db = $wpdb; + $this->table = $wpdb->prefix . 'agents_memory_messages'; + } + + public function remember( Message $message ): void { + $this->db->insert( $this->table, [ + 'agent' => $this->agent, + 'session_id' => $this->session_id, + 'author' => $message->get_author(), + 'message' => $message->get_message(), + 'metadata' => $message->get_metadata() + ? wp_json_encode( $message->get_metadata() ) + : null, + ] ); + } + + public function load( ?int $limit = null ): Message_Stack { + $sql = "SELECT author, message, metadata + FROM {$this->table} + WHERE agent = %s AND session_id = %s + ORDER BY id ASC"; + + $params = array( $this->agent, $this->session_id ); + + if ( null !== $limit ) { + $sql .= " LIMIT %d"; + $params[] = $limit; + } + + $results = $this->db->get_results( + $this->db->prepare( $sql, ...$params ), + ARRAY_A + ); + + $messages = array_map( function ( $row ) { + return new Memorized_Message( + $row['author'], + $row['message'], + json_decode( $row['metadata'] ?? '[]', true ) + ); + }, $results ); + + return new Message_Stack( $messages ); + } + + public function forget_all(): void { + $this->db->delete( $this->table, [ + 'agent' => $this->agent, + 'session_id' => $this->session_id, + ] ); + } +} diff --git a/lib/providers/class-open-ai-provider.php b/lib/providers/class-open-ai-provider.php index a0c7e4a..873c271 100644 --- a/lib/providers/class-open-ai-provider.php +++ b/lib/providers/class-open-ai-provider.php @@ -15,20 +15,22 @@ public function __construct( string $api_key ) { $this->api_key = $api_key; } - public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack { + public function chat( Message_Stack $message_stack, Abstract_Llm_Agent $agent ): void { $client = \OpenAI::client( $this->api_key ); $tools = $agent->tools(); - $messages = new Message_Stack( - array( - new Message( 'system', $agent->instructions() ), - new Message( 'user', $input ), - ) - ); + $message_stack->unshift( new Message( 'system', $agent->instructions() ) ); + + $flatten = function ( Message $message ) { + return array( + 'role' => $message->get_author(), + 'content' => $message->get_message(), + ) + $message->get_metadata(); + }; $parameters = array( 'model' => $agent->get_model(), - 'messages' => $messages->to_raw_array(), + 'messages' => $message_stack->map( $flatten ), 'response_format' => array( 'type' => $agent->json() ? 'json_object' : 'text' ), ); @@ -37,21 +39,16 @@ public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack $parameters['tools'] = $tool_registry->definitions(); } - $response = $client->chat()->create( $parameters ); - - $message = $response->choices[0]->message; - $messages->add( - new Message( - $message->role, - $message->content, - $message->toArray() - ) - ); + $response = $client->chat()->create( $parameters ); + $openai_message = $response->choices[0]->message; // phpcs:ignore WordPress.NamingConventions.ValidVariableName - if ( ! empty( $message->toolCalls ) ) { + if ( ! empty( $openai_message->toolCalls ) ) { + $tool_calls = array(); + $tool_messages = array(); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName - foreach ( $message->toolCalls as $call ) { + foreach ( $openai_message->toolCalls as $call ) { $function = $call->function; $args = json_decode( $function->arguments ?? '{}', true ); @@ -60,35 +57,57 @@ public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack } $result = $tool_registry->execute( $function->name, $args ); - $messages->add( - new Message( - 'tool', - wp_json_encode( $result ), - array( - 'tool_call_id' => $call->id, - ) + $tool_calls[] = array( + 'id' => $call->id, + 'type' => $call->type ?? 'function', + 'function' => array( + 'name' => $call->function->name, + 'arguments' => $call->function->arguments, + ), + ); + + $tool_messages[] = new Message( + 'tool', + wp_json_encode( $result ), + array( + 'tool_call_id' => $call->id, ) ); } + $message_stack->add( + new Message( + $openai_message->role, + $openai_message->content, + array( + 'tool_calls' => $tool_calls + ) + ) + ); + $message_stack->add( $tool_messages ); + $follow = $client->chat()->create( array( 'model' => $agent->get_model(), - 'messages' => $messages->to_raw_array(), + 'messages' => $message_stack->map( $flatten ), 'response_format' => array( 'type' => $agent->json() ? 'json_object' : 'text' ), ) ); - $message = $follow->choices[0]->message; - $messages->add( + $openai_message = $follow->choices[0]->message; + $message_stack->add( new Message( - $message->role, - $message->content, - $message->toArray(), + $openai_message->role, + $openai_message->content + ) + ); + } else { + $message_stack->add( + new Message( + $openai_message->role, + $openai_message->content, ) ); } - - return $messages; } } diff --git a/lib/providers/class-provider-interface.php b/lib/providers/class-provider-interface.php index f942f21..11033d5 100644 --- a/lib/providers/class-provider-interface.php +++ b/lib/providers/class-provider-interface.php @@ -7,5 +7,5 @@ interface Provider_Interface { - public function chat( string $input, Abstract_Llm_Agent $agent ): Message_Stack; + public function chat( Message_Stack $message_stack, Abstract_Llm_Agent $agent ): void; } diff --git a/lib/services/class-agent-manager.php b/lib/services/class-agent-manager.php index 72acf15..179f220 100644 --- a/lib/services/class-agent-manager.php +++ b/lib/services/class-agent-manager.php @@ -10,7 +10,7 @@ class Agent_Manager { protected static array $agents = array(); public static function register( string $name, string $agent_class ): void { - self::$agents[ $name ] = new $agent_class(); + self::$agents[ $name ] = new $agent_class( $name ); } public static function get( string $name ): Abstract_Llm_Agent { diff --git a/lib/system/class-agent-runner.php b/lib/system/class-agent-runner.php index 7bbe6a2..c9c39a2 100644 --- a/lib/system/class-agent-runner.php +++ b/lib/system/class-agent-runner.php @@ -14,14 +14,32 @@ class Agent_Runner { protected Abstract_Llm_Agent $agent; + protected ?string $session_id = null; + public function __construct( string $input, Abstract_Llm_Agent $agent ) { $this->input = $input; $this->agent = $agent; $this->provider = Provider_Manager::get( $agent->get_provider() ); } + public function with_session( ?string $session_id ): static { + $this->session_id = $session_id; + + return $this; + } + public function chat(): Message { - $message_stack = $this->provider->chat( $this->input, $this->agent ); + $memory = $this->session_id ? $this->agent->get_memory( $this->session_id ) : null; + $message_stack = $memory ? $memory->load() : new Message_Stack(); + $message_stack->add( new Message( 'user', $this->input ) ); + + $this->provider->chat( $message_stack, $this->agent ); + + foreach ( $message_stack->all() as $message ) { + if ( ! $message->memorized() && 'system' !== $message->get_author() ) { + $memory?->remember( $message ); + } + } return $message_stack->last(); } diff --git a/lib/system/class-memorized-message.php b/lib/system/class-memorized-message.php new file mode 100644 index 0000000..7d197bc --- /dev/null +++ b/lib/system/class-memorized-message.php @@ -0,0 +1,9 @@ +messages = $messages; } @@ -30,16 +30,25 @@ public function last(): ?Message { return $count ? $this->messages[ $count - 1 ] : null; } - public function add( Message $message ) { - $this->messages[] = $message; + public function unshift( Message $message ) { + array_unshift( $this->messages, $message ); } - public function to_raw_array(): array { - return array_map( - function ( Message $message ) { - return $message->get_raw_response(); - }, - $this->messages - ); + public function add( array|Message $messages ) { + $messages = $messages instanceof Message ? [ $messages ] : $messages; + + foreach ( $messages as $message ) { + if ( $message instanceof Message ) { + $this->messages[] = $message; + } + } + } + + public function all(): array { + return $this->messages; + } + + public function map( callable $callback ): array { + return array_map( $callback, $this->all() ); } } diff --git a/lib/system/class-message.php b/lib/system/class-message.php index ba21196..1886f75 100644 --- a/lib/system/class-message.php +++ b/lib/system/class-message.php @@ -4,46 +4,41 @@ class Message { - protected string $role; + protected string $author; - protected ?string $content; + protected ?string $message; - protected array $raw_response = array(); + protected array $metadata = array(); - public function __construct( string $role, ?string $content, array $raw_response = array() ) { - $this->role = $role; - $this->content = $content; + protected bool $memorized = false; - if ( empty( $raw_response ) ) { - $this->raw_response = array( - 'role' => $role, - 'content' => $content, - ); - } else { - $this->raw_response = $raw_response; - } + public function __construct( string $author, ?string $message, array $metadata = array() ) { + $this->author = $author; + $this->message = $message; + $this->metadata = $metadata; } - public function get_role(): string { - return $this->role; + public function get_author(): string { + return $this->author; } - public function get_content(): string { - return $this->content; + public function get_message(): string { + return $this->message; } - public function get_raw_response(): array { - return array( - 'role' => $this->role, - 'content' => $this->content, - ) + $this->raw_response; + public function memorized(): bool { + return $this->memorized; + } + + public function get_metadata( ?string $key = null, mixed $default = null ): array { + return $key ? ( $this->metadata[ $key ] ?? $default ) : $this->metadata; } public function to_array(): array { return array( - 'role' => $this->role, - 'content' => $this->content, - 'raw_response' => $this->raw_response, + 'author' => $this->author, + 'message' => $this->message, + 'metadata' => $this->metadata, ); } } diff --git a/lib/system/class-rest.php b/lib/system/class-rest.php index cdfbbd5..7b9b395 100644 --- a/lib/system/class-rest.php +++ b/lib/system/class-rest.php @@ -24,13 +24,14 @@ public static function handle( \WP_REST_Request $request ) { $message = $request->get_param( 'message' ); $response = $agent ->prompt( $message ) + ->with_session( $request->get_param( 'session_id' ) ) ->chat(); return rest_ensure_response( array( 'agent' => $request->get_param( 'agent' ), 'message' => $message, - 'response' => $response->get_raw_response(), + 'response' => $response->to_array(), ) ); } diff --git a/lib/system/class-schema.php b/lib/system/class-schema.php new file mode 100644 index 0000000..1df8b3c --- /dev/null +++ b/lib/system/class-schema.php @@ -0,0 +1,38 @@ +db = $wpdb; + } + + public function install(): void { + $installed = get_option( 'wp_agents_installed' ); + if ( ! $installed ) { + $table = $this->db->prefix . 'agents_memory_messages'; + $charset = $this->db->get_charset_collate(); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + dbDelta( "CREATE TABLE $table ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + agent VARCHAR(150) NOT NULL, + session_id VARCHAR(150) NOT NULL, + author VARCHAR(150) NOT NULL, + message LONGTEXT NOT NULL, + metadata JSON NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX (agent), + INDEX (session_id) + ) $charset;" ); + + update_option( 'wp_agents_installed', true ); + } + } + +} diff --git a/wp-agents.php b/wp-agents.php index c125249..1255361 100644 --- a/wp-agents.php +++ b/wp-agents.php @@ -3,7 +3,7 @@ * Plugin Name: WP Agents * Plugin URI: https://santerref.com/ * Description: Build autonomous, hook-driven agents for WordPress — automate tasks and add LLM intelligence with clean, developer-first architecture. - * Version: 0.0.1 + * Version: 0.3.0 * Requires at least: 6.8 * Requires PHP: 8.4 * Author: Francis Santerre @@ -13,12 +13,23 @@ * Domain Path: /languages */ -add_action( - 'plugins_loaded', - function () { - if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) { - require_once __DIR__ . '/vendor/autoload.php'; +if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) { + require_once __DIR__ . '/vendor/autoload.php'; + if ( ! function_exists( 'wp_agents_install' ) ) { + + function wp_agents_install() { + $schema = new \Wp_Agents\System\Schema(); + $schema->install(); + } + + register_activation_hook( __FILE__, 'wp_agents_install' ); + + } + + add_action( + 'plugins_loaded', + function () { if ( ! function_exists( 'wp_agents_logger' ) ) { function wp_agents_logger() { @@ -63,5 +74,6 @@ function wp_agents_get( string $name ) { add_action( 'init', array( \Wp_Agents\Services\Agent_Manager::class, 'boot' ) ); add_action( 'rest_api_init', array( \Wp_Agents\System\Rest::class, 'register' ) ); } - } -); + ); + +} From c63ab6fb04d2369e34cbb9262e80e9af79cca124 Mon Sep 17 00:00:00 2001 From: Francis Santerre Date: Fri, 24 Oct 2025 03:58:53 -0400 Subject: [PATCH 2/4] Fix phpcs errors --- lib/memory/class-abstract-memory.php | 1 - lib/memory/class-database-memory.php | 51 ++++++++++++++---------- lib/providers/class-open-ai-provider.php | 8 ++-- lib/system/class-memorized-message.php | 1 - lib/system/class-message-stack.php | 2 +- lib/system/class-schema.php | 7 ++-- 6 files changed, 39 insertions(+), 31 deletions(-) diff --git a/lib/memory/class-abstract-memory.php b/lib/memory/class-abstract-memory.php index 369d385..28c8aa0 100644 --- a/lib/memory/class-abstract-memory.php +++ b/lib/memory/class-abstract-memory.php @@ -21,5 +21,4 @@ abstract public function remember( Message $message ): void; abstract public function load( ?int $limit = null ): Message_Stack; abstract public function forget_all(): void; - } diff --git a/lib/memory/class-database-memory.php b/lib/memory/class-database-memory.php index 82788eb..5a03fec 100644 --- a/lib/memory/class-database-memory.php +++ b/lib/memory/class-database-memory.php @@ -20,15 +20,18 @@ public function __construct( string $agent, string $session_id ) { } public function remember( Message $message ): void { - $this->db->insert( $this->table, [ - 'agent' => $this->agent, - 'session_id' => $this->session_id, - 'author' => $message->get_author(), - 'message' => $message->get_message(), - 'metadata' => $message->get_metadata() - ? wp_json_encode( $message->get_metadata() ) - : null, - ] ); + $this->db->insert( + $this->table, + array( + 'agent' => $this->agent, + 'session_id' => $this->session_id, + 'author' => $message->get_author(), + 'message' => $message->get_message(), + 'metadata' => $message->get_metadata() + ? wp_json_encode( $message->get_metadata() ) + : null, + ) + ); } public function load( ?int $limit = null ): Message_Stack { @@ -40,7 +43,7 @@ public function load( ?int $limit = null ): Message_Stack { $params = array( $this->agent, $this->session_id ); if ( null !== $limit ) { - $sql .= " LIMIT %d"; + $sql .= ' LIMIT %d'; $params[] = $limit; } @@ -49,21 +52,27 @@ public function load( ?int $limit = null ): Message_Stack { ARRAY_A ); - $messages = array_map( function ( $row ) { - return new Memorized_Message( - $row['author'], - $row['message'], - json_decode( $row['metadata'] ?? '[]', true ) - ); - }, $results ); + $messages = array_map( + function ( $row ) { + return new Memorized_Message( + $row['author'], + $row['message'], + json_decode( $row['metadata'] ?? '[]', true ) + ); + }, + $results + ); return new Message_Stack( $messages ); } public function forget_all(): void { - $this->db->delete( $this->table, [ - 'agent' => $this->agent, - 'session_id' => $this->session_id, - ] ); + $this->db->delete( + $this->table, + array( + 'agent' => $this->agent, + 'session_id' => $this->session_id, + ) + ); } } diff --git a/lib/providers/class-open-ai-provider.php b/lib/providers/class-open-ai-provider.php index 873c271..1305497 100644 --- a/lib/providers/class-open-ai-provider.php +++ b/lib/providers/class-open-ai-provider.php @@ -23,9 +23,9 @@ public function chat( Message_Stack $message_stack, Abstract_Llm_Agent $agent ): $flatten = function ( Message $message ) { return array( - 'role' => $message->get_author(), - 'content' => $message->get_message(), - ) + $message->get_metadata(); + 'role' => $message->get_author(), + 'content' => $message->get_message(), + ) + $message->get_metadata(); }; $parameters = array( @@ -80,7 +80,7 @@ public function chat( Message_Stack $message_stack, Abstract_Llm_Agent $agent ): $openai_message->role, $openai_message->content, array( - 'tool_calls' => $tool_calls + 'tool_calls' => $tool_calls, ) ) ); diff --git a/lib/system/class-memorized-message.php b/lib/system/class-memorized-message.php index 7d197bc..e3f60a2 100644 --- a/lib/system/class-memorized-message.php +++ b/lib/system/class-memorized-message.php @@ -5,5 +5,4 @@ class Memorized_Message extends Message { protected bool $memorized = true; - } diff --git a/lib/system/class-message-stack.php b/lib/system/class-message-stack.php index b66378c..d41e5b0 100644 --- a/lib/system/class-message-stack.php +++ b/lib/system/class-message-stack.php @@ -35,7 +35,7 @@ public function unshift( Message $message ) { } public function add( array|Message $messages ) { - $messages = $messages instanceof Message ? [ $messages ] : $messages; + $messages = $messages instanceof Message ? array( $messages ) : $messages; foreach ( $messages as $message ) { if ( $message instanceof Message ) { diff --git a/lib/system/class-schema.php b/lib/system/class-schema.php index 1df8b3c..661c68f 100644 --- a/lib/system/class-schema.php +++ b/lib/system/class-schema.php @@ -19,7 +19,8 @@ public function install(): void { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - dbDelta( "CREATE TABLE $table ( + dbDelta( + "CREATE TABLE $table ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, agent VARCHAR(150) NOT NULL, session_id VARCHAR(150) NOT NULL, @@ -29,10 +30,10 @@ public function install(): void { created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX (agent), INDEX (session_id) - ) $charset;" ); + ) $charset;" + ); update_option( 'wp_agents_installed', true ); } } - } From 65ec01ec8442003fcb0bebd9209ec4a3a6aa0ee4 Mon Sep 17 00:00:00 2001 From: Francis Santerre Date: Fri, 24 Oct 2025 04:01:27 -0400 Subject: [PATCH 3/4] Add README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 02c46b2..ebefdb1 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Build autonomous, hook-driven agents for WordPress — automate tasks and add LL - **Providers:** Define and manage LLM providers (e.g., OpenAI, Anthropic) through a unified API for completions and structured outputs. - **Tools:** Register callable functions that agents can dynamically execute during reasoning or task completion. -- **REST API:** Add REST API endpoints to enable external chat and interaction with agents. -- **Input:** Capture and preprocess data from actions and filters to generate structured prompts for LLMs. +- **REST API:** Provides endpoints that allow external applications to interact with agents and send or receive chat messages programmatically. +- **Memory:** Supports persistent conversation history, enabling agents to retain context across sessions and handle long-running or multi-turn interactions. - **Agents:** Create modular agents that interact with WordPress using a consistent architecture. - **Tests:** Basic Pest testing structure in place — full coverage and assertions to be implemented. @@ -70,7 +70,6 @@ https://github.com/santerref/wp-agents-demo ## Roadmap - **Workflows:** Introduce workflows to connect and orchestrate multiple agents for complex, multi-step tasks. -- **Memory:** Implement persistent memory to maintain context across conversations and enable long-running sessions. - **Tests:** Expand and complete Pest test coverage for agents, tools, and provider logic. - **Providers:** Add more LLM providers and improve compatibility with third-party APIs. - **RAG:** Integrate Retrieval-Augmented Generation to allow agents to query custom datasets, documents, or WordPress content for more context-aware responses. From 10875b725cd2d38bbe3df95826602d85229e5c50 Mon Sep 17 00:00:00 2001 From: Francis Santerre Date: Fri, 24 Oct 2025 04:05:26 -0400 Subject: [PATCH 4/4] Remove reserved parameter --- lib/system/class-message.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/system/class-message.php b/lib/system/class-message.php index 1886f75..82afcfb 100644 --- a/lib/system/class-message.php +++ b/lib/system/class-message.php @@ -30,8 +30,8 @@ public function memorized(): bool { return $this->memorized; } - public function get_metadata( ?string $key = null, mixed $default = null ): array { - return $key ? ( $this->metadata[ $key ] ?? $default ) : $this->metadata; + public function get_metadata( ?string $key = null, mixed $default_value = null ): array { + return $key ? ( $this->metadata[ $key ] ?? $default_value ) : $this->metadata; } public function to_array(): array {