# WPAI - WordPress AI Module

AI capability layer for the WXKN WordPress child theme. Supports OpenAI, Anthropic, and Gemini providers through a unified interface, with multi-round Agent (tool calling) and WordPress admin configuration.

## Architecture

```
functions.php
  -> require bootstrap.php          # PSR-4 autoloader
  -> Admin\Settings::init()        # WP admin page (admin only)

Agent (multi-round loop)
  -> ClientFactory::create()       # Factory: pick provider by name
    -> IClient::chat(Request)      # Interface contract
      -> OpenAIClient              # SSE streaming, native format
      -> AnthropicClient           # SSE + message/tool conversion
      -> GeminiClient              # SSE + function calling conversion
```

**Key design decision:** SSE is an implementation detail inside each Provider. The `chat()` method is synchronous -- it streams internally to avoid timeout, then returns a complete `Response`. Callers (including Agent) never see chunks.

## File Structure

| File | Responsibility |
|------|---------------|
| `bootstrap.php` | PSR-4 autoloader for `Infility\WPAI\` |
| `Request.php` | Unified request DTO (messages, tools, temperature, etc.) |
| `Response.php` | Unified response DTO (content, toolCalls, usage, error) |
| `Interface/IClient.php` | Single-method contract: `chat(Request): Response` |
| `Provider/OpenAIClient.php` | OpenAI API adapter (no format conversion needed) |
| `Provider/AnthropicClient.php` | Anthropic adapter (message + tool format conversion) |
| `Provider/GeminiClient.php` | Gemini adapter (function calling conversion, most complex) |
| `Agent.php` | Multi-round tool-calling loop with event callbacks |
| `ClientFactory.php` | Factory: create IClient by provider name or from WP options |
| `Admin/Settings.php` | WordPress Settings > General integration + AJAX endpoints |
| `assets/wpai-admin.js` | Test connection button handler |

## Quick Start

### Basic chat (single turn)

```php
use Infility\WPAI\ClientFactory;
use Infility\WPAI\Request;

$client = ClientFactory::create('gemini', [
    'api_key'  => 'your-key',
    'base_url' => 'https://your-proxy.com',
]);

$request = new Request('gemini-2.5-pro', [
    ['role' => 'user', 'content' => 'Hello!'],
]);

$response = $client->chat($request);

if ($response->success) {
    echo $response->content;        // string
} else {
    echo $response->error;          // error message
}
```

### Agent with tools (multi-turn)

```php
use Infility\WPAI\Agent;
use Infility\WPAI\Request;
use Infility\WPAI\ClientFactory;

$client = ClientFactory::createFromOptions(); // reads wp_option('wpai_settings')
$request = new Request('gpt-4o', [
    ['role' => 'user', 'content' => 'Read home.html and summarize it.'],
]);

$agent = new Agent($client, $request);

$agent->setTools([
    'read_file' => [
        'schema' => [
            'type'     => 'function',
            'function' => [
                'name'        => 'read_file',
                'description' => 'Read a file and return its contents',
                'parameters'  => [
                    'type'       => 'object',
                    'properties' => [
                        'path' => ['type' => 'string', 'description' => 'File path'],
                    ],
                    'required'   => ['path'],
                ],
            ],
        ],
        'execute' => function (Request $req, array $args): string {
            return file_get_contents($args['path']);
        },
    ],
])
->maxRounds(10)
->onEvent(function (string $type, array $data) {
    // Log or push events to frontend via SSE
    error_log("[wpai:{$type}] " . json_encode($data));
});

$response = $agent->run();
echo $response->content;
```

## Request DTO

```php
$request = new Request(
    model: 'gpt-4o',                          // required
    messages: [                                 // required, OpenAI format
        ['role' => 'system', 'content' => '...'],
        ['role' => 'user', 'content' => '...'],
    ],
    systemInstruction: null,                    // optional, used by Gemini
    tools: null,                                // optional, auto-set by Agent::setTools()
    temperature: 0.7,                           // default 0.7
    maxTokens: 4096,                            // default 4096
);
$request->extra['responseMimeType'] = 'application/json'; // provider-specific passthrough
```

### Helper methods

```php
$request->addUserMessage('next question');                    // append user msg
$request->addAssistantMessage('previous answer');             // append assistant msg
$request->addToolResult('call_123', 'calc', '{"result":42}');// append tool result
$request->addAssistantMessageWithToolCalls([...]);            // append assistant+tool_calls
```

## Response DTO

```php
$response->success;      // bool
$response->content;      // string (final text when no tool calls)
$response->toolCalls;    // [{id, name, arguments(JSON)}, ...]
$response->finishReason; // 'stop' | 'tool_calls' | 'length'
$response->usage;        // [promptTokens, completionTokens, totalTokens] or null
$response->raw;          // full raw SSE response JSON
$response->error;        // string or null
$response->httpCode;     // int
```

Static factories:

```php
Response::ok('text')                                    // success with content
Response::withToolCalls([{id,name,arguments}])         // success with tool calls
Response::error('something failed', 502)               // failure
```

## Supported Providers

### OpenAI

| Item | Value |
|------|-------|
| Endpoint | `{base_url}/chat/completions` |
| Auth | `Authorization: Bearer {api_key}` |
| Format | Native OpenAI (no conversion) |
| Default base URL | `https://api.openai.com/v1` |

### Anthropic

| Item | Value |
|------|-------|
| Endpoint | `{base_url}/v1/messages` |
| Auth | `x-api-key:` header + `anthropic-version: 2023-06-01` |
| Conversion | system messages -> top-level `system`; tool calls -> `tool_use` blocks; tool results -> `tool_result` blocks |
| Default base URL | `https://api.anthropic.com` |

### Gemini

| Item | Value |
|------|-------|
| Endpoint | `{base_url}/v1beta/models/{model}:streamGenerateContent?alt=sse&key={api_key}` |
| Auth | URL query parameter `key=` |
| Conversion | assistant -> role=`model`; tool results -> `functionResponse`; tools -> `functionDeclarations` |
| Default base URL | `https://generativelanguage.googleapis.com` |

Gemini has the most complex conversion because its function calling format differs significantly from OpenAI.

## ClientFactory

```php
// Explicit config
$client = ClientFactory::create('openai', [
    'api_key'  => 'sk-...',
    'base_url' => 'https://api.openai.com/v1',
]);

// From WordPress options (reads wp_option('wpai_settings'))
$client = ClientFactory::createFromOptions();

// From explicit config array
$client = ClientFactory::createFromOptions([
    'provider' => 'anthropic',
    'api_key'  => 'sk-ant-...',
]);
```

Config priority: explicit `$config` param > WordPress option > default values.

## Agent Event System

Register a callback to observe the agent's execution flow:

```php
$agent->onEvent(function (string $type, array $data): void {
    switch ($type) {
        case 'thinking':
            // $data = ['round' => int]
            break;
        case 'tool_call':
            // $data = ['name' => string, 'args' => array]
            break;
        case 'tool_result':
            // $data = ['name' => string, 'result' => string]
            break;
        case 'content':
            // $data = ['text' => string]
            break;
        case 'error':
            // $data = ['message' => string]
            break;
        case 'done':
            // $data = ['usage' => array, 'rounds' => int]
            break;
    }
});
```

This is designed for future Layer 1 SSE (Server -> Browser) integration. Push these events over an SSE connection to show real-time progress to the user.

## Tool Registration Format

```php
$agent->setTools([
    // Full form: schema (sent to AI) + execute (your code)
    'tool_name' => [
        'schema' => [
            'type'     => 'function',
            'function' => [
                'name'        => 'tool_name',
                'description' => 'What this tool does',
                'parameters'  => [
                    'type'       => 'object',
                    'properties' => [
                        'param1' => ['type' => 'string', 'description' => '...'],
                    ],
                    'required'   => ['param1'],
                ],
            ],
        ],
        'execute' => function (Request $request, array $args): string {
            // $request: current Request object (read messages, context)
            // $args: decoded parameters from AI
            return 'result as string';
        },
    ],

    // Minimal form: just a callable (auto-generates minimal schema)
    'simple_tool' => function (Request $request, array $args): string {
        return 'result';
    },
]);
```

Handler return value must be `string`. Non-string returns are JSON-encoded automatically.

## Admin Settings

Navigate to **WordPress Dashboard > Settings > General**, scroll to bottom for **AI Configuration** section.

| Field | Description |
|-------|-------------|
| Provider | Dropdown: OpenAI / Anthropic / Gemini |
| API Key | Password field (masked after save) |
| Base URL | Optional, defaults per provider |
| Model | Optional, e.g. gpt-4o / claude-sonnet-4-20250514 / gemini-2.5-pro |
| Test Connection | Button to verify API connectivity |

### AJAX Endpoints

All require `POST` with `nonce` (from `wpaiAdmin.nonce`) and `manage_options` capability.

| Action | Method | Purpose |
|--------|--------|---------|
| `wpai_get_settings` | GET | Read settings (API key masked) |
| `wpai_save_settings` | POST | Save settings |
| `wpai_test_connection` | POST | Send test message, return result |

## CLI Testing

```bash
cd /path/to/theme
php wpai/test.php
```

Runs two tests against Gemini:
1. **Basic chat** -- single turn, verifies connectivity and text response
2. **Agent with tools** -- multi-turn calculator tool call, verifies tool calling loop

Edit `test.php` to change provider/config before running.

## Constraints

- Zero Composer dependencies (native PHP only)
- curl-based HTTP (no Guzzle, no SDKs)
- PHP 8.3+
- No emoji in output or UI
