Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
26 changes: 23 additions & 3 deletions lib/agents/class-abstract-llm-agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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;
}
}
24 changes: 24 additions & 0 deletions lib/memory/class-abstract-memory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Wp_Agents\Memory;

use Wp_Agents\System\Message;
use Wp_Agents\System\Message_Stack;

abstract class Abstract_Memory {

protected string $agent;

protected string $session_id;

public function __construct( string $agent, string $session_id ) {
$this->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;
}
78 changes: 78 additions & 0 deletions lib/memory/class-database-memory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Wp_Agents\Memory;

use Wp_Agents\System\Memorized_Message;
use Wp_Agents\System\Message;
use Wp_Agents\System\Message_Stack;

class Database_Memory extends Abstract_Memory {

protected \wpdb $db;

protected string $table;

public function __construct( string $agent, string $session_id ) {
parent::__construct( $agent, $session_id );
global $wpdb;
$this->db = $wpdb;
$this->table = $wpdb->prefix . 'agents_memory_messages';
}

public function remember( Message $message ): void {
$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 {
$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,
array(
'agent' => $this->agent,
'session_id' => $this->session_id,
)
);
}
}
89 changes: 54 additions & 35 deletions lib/providers/class-open-ai-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
);

Expand All @@ -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 );

Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion lib/providers/class-provider-interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion lib/services/class-agent-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 19 additions & 1 deletion lib/system/class-agent-runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
8 changes: 8 additions & 0 deletions lib/system/class-memorized-message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Wp_Agents\System;

class Memorized_Message extends Message {

protected bool $memorized = true;
}
Loading