Skip to content
New site — Virtual Media Folders now has its own home. Visit vmf.soderlind.no →

AI Provider Architecture

This document describes how each AI provider is accessed internally, with code examples and flow diagrams.

  1. Overview Architecture
  2. Provider Selection Flow
  3. Error Handling & Fallback
  4. Individual Providers

The plugin uses a centralized orchestration pattern where all AI suggestion requests flow through a single service class.

flowchart TB
    subgraph WordPress
        Editor[Block Editor]
        REST[REST API<br>/content-poll/v1/suggest]
    end

    subgraph "ContentPoll Core"
        SC[SuggestionController]
        AIS[AISuggestionService]
        SP[SettingsPage]
    end

    subgraph "AI Providers"
        H[Heuristic]
        PF[PocketFlow<br>OpenAI/Azure]
        AN[Anthropic]
        GE[Gemini]
        OL[Ollama]
        GR[Grok]
        EX[Exo]
    end

    subgraph "External APIs"
        OAPI[api.openai.com]
        AAPI[api.anthropic.com]
        GAPI[generativelanguage.googleapis.com]
        XAPI[api.x.ai]
        AZAPI[Azure OpenAI]
        LOCAL1[localhost:11434<br>Ollama]
        LOCAL2[localhost:8000<br>Exo Cluster]
    end

    Editor -->|"postId"| REST
    REST --> SC
    SC -->|"post_content"| AIS
    AIS -->|"get_ai_provider()"| SP
    
    AIS --> H
    AIS --> PF
    AIS --> AN
    AIS --> GE
    AIS --> OL
    AIS --> GR
    AIS --> EX

    PF --> OAPI
    PF --> AZAPI
    AN --> AAPI
    GE --> GAPI
    GR --> XAPI
    OL --> LOCAL1
    EX --> LOCAL2
FilePurpose
src/php/REST/SuggestionController.phpREST endpoint handler
src/php/Services/AISuggestionService.phpProvider orchestration
src/php/Admin/SettingsPage.phpConfiguration resolution
src/php/AI/LLMClient.phpOpenAI/Azure HTTP client
src/php/AI/PocketFlow/*.phpMulti-step flow nodes

When a suggestion is requested, the service reads the configured provider and dispatches to the appropriate method.

flowchart TD
    Start([suggest called]) --> Strip[Strip HTML tags]
    Strip --> Limit[Limit to 1000 chars]
    Limit --> GetProvider[Get provider from SettingsPage]
    
    GetProvider --> Switch{Provider?}
    
    Switch -->|openai| PF[suggest_pocketflow]
    Switch -->|anthropic| AN[suggest_anthropic]
    Switch -->|gemini| GE[suggest_gemini]
    Switch -->|ollama| OL[suggest_ollama]
    Switch -->|grok| GR[suggest_grok]
    Switch -->|exo| EX[suggest_exo]
    Switch -->|default| Empty[Return empty array]
    
    PF --> Check{Result empty?}
    AN --> Check
    GE --> Check
    OL --> Check
    GR --> Check
    EX --> Check
    Empty --> Check
    
    Check -->|Yes| Fallback[suggest_heuristic]
    Check -->|No| Normalize[normalize_suggestion]
    Fallback --> Normalize
    
    Normalize --> Return([Return result])

From src/php/Services/AISuggestionService.php:

public function suggest( string $content ): array {
$provider = SettingsPage::get_ai_provider();
$text = strip_tags( $content );
$text = mb_substr( $text, 0, 1000 );
switch ( $provider ) {
case 'openai':
$result = $this->suggest_pocketflow( $text );
break;
case 'anthropic':
$result = $this->suggest_anthropic( $text );
break;
case 'gemini':
$result = $this->suggest_gemini( $text );
break;
case 'ollama':
$result = $this->suggest_ollama( $text );
break;
case 'grok':
$result = $this->suggest_grok( $text );
break;
case 'exo':
$result = $this->suggest_exo( $text );
break;
default:
$result = [];
}
// Fallback to heuristic if AI fails
if ( empty( $result ) ) {
$result = $this->suggest_heuristic( $content );
}
return $this->normalize_suggestion( $result, $content );
}

All providers implement the same error handling pattern: log errors and return empty array to trigger heuristic fallback.

flowchart TD
    Request[API Request] --> Check{wp_error?}
    Check -->|Yes| LogNet[error_log network error]
    Check -->|No| Parse[Parse JSON response]
    
    Parse --> APIErr{API error<br>in response?}
    APIErr -->|Yes| LogAPI[error_log API error]
    APIErr -->|No| Extract[Extract content]
    
    LogNet --> Empty([Return empty array])
    LogAPI --> Transient[Set transient for admin notice]
    Transient --> Empty
    
    Extract --> ParseJSON[parse_poll_json]
    ParseJSON --> Valid{Valid structure?}
    Valid -->|Yes| Return([Return parsed data])
    Valid -->|No| Empty
    
    Empty --> Heuristic[Heuristic Fallback]
if ( is_wp_error( $response ) ) {
error_log( 'ContentPoll AI Error: ' . $response->get_error_message() );
return [];
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $data['error'] ) ) {
$error_message = $data['error']['message'] ?? 'Unknown error';
error_log( 'ContentPoll AI API Error: ' . $error_message );
if ( current_user_can( 'manage_options' ) ) {
set_transient( 'content_poll_ai_error', $error_message, 300 );
}
return [];
}

Local keyword extraction with no external API calls. Always succeeds.

sequenceDiagram
    participant AIS as AISuggestionService
    participant H as suggest_heuristic()
    
    AIS->>H: content text
    H->>H: Strip HTML tags
    H->>H: Tokenize (min 4 chars)
    H->>H: Count word frequency
    H->>H: Sort by frequency
    H->>H: Take top 6 words
    H->>H: Build question from top word
    H-->>AIS: {question, options[]}
private function suggest_heuristic( string $content ): array {
$text = strip_tags( $content );
$tokens = preg_split( '/[^\p{L}\p{N}]+/u', mb_strtolower( $text ) );
$freq = [];
foreach ( $tokens as $t ) {
if ( $t === '' || mb_strlen( $t ) < 4 ) {
continue;
}
$freq[ $t ] = ( $freq[ $t ] ?? 0 ) + 1;
}
arsort( $freq );
$top = array_slice( array_keys( $freq ), 0, 6 );
if ( empty( $top ) ) {
return [
'question' => __( 'What is your opinion of this content?', 'content-poll' ),
'options' => [ 'Great', 'Informative', 'Neutral', 'Confusing' ],
];
}
$stem = $top[0];
$question = sprintf( __( 'Your view on "%s"?', 'content-poll' ), ucfirst( $stem ) );
$options = array_map( 'ucfirst', $top );
return [ 'question' => $question, 'options' => $options ];
}

Multi-step flow using LLMClient for OpenAI-compatible APIs.

sequenceDiagram
    participant AIS as AISuggestionService
    participant PF as suggest_pocketflow()
    participant Factory as PollGenerationFlowFactory
    participant Flow as Flow
    participant EKN as ExtractKeywordsNode
    participant GPN as GeneratePollNode
    participant VPN as ValidatePollNode
    participant LLM as LLMClient
    participant API as OpenAI/Azure API

    AIS->>PF: text content
    PF->>LLM: new LLMClient()
    PF->>Factory: create(client)
    Factory->>EKN: new ExtractKeywordsNode(client)
    Factory->>GPN: new GeneratePollNode(client)
    Factory->>VPN: new ValidatePollNode()
    Factory->>Flow: new Flow(extract)
    Factory-->>PF: flow
    
    PF->>Flow: run(shared)
    Flow->>EKN: run(shared)
    EKN->>LLM: chat(messages)
    LLM->>API: POST /v1/chat/completions
    API-->>LLM: {"topics": [...]}
    LLM-->>EKN: topics JSON
    EKN->>EKN: shared.topics = parsed
    EKN-->>Flow: nextNode
    
    Flow->>GPN: run(shared)
    GPN->>LLM: chat(messages with topics)
    LLM->>API: POST /v1/chat/completions
    API-->>LLM: poll JSON
    LLM-->>GPN: raw response
    GPN->>GPN: shared.raw_poll_response = raw
    GPN-->>Flow: nextNode
    
    Flow->>VPN: run(shared)
    VPN->>VPN: parse_poll_json(raw)
    VPN->>VPN: shared.final_poll = parsed
    VPN-->>Flow: null (end)
    
    Flow-->>PF: shared.final_poll
    PF-->>AIS: {question, options[]}
src/php/AI/LLMClient.php
public function chat( array $messages, array $options = [] ): string {
$type = SettingsPage::get_openai_type();
$model = $options['model'] ?? SettingsPage::get_openai_model();
$key = SettingsPage::get_openai_key();
$body = [
'messages' => $messages,
'temperature' => $options['temperature'] ?? 0.7,
'max_tokens' => $options['max_tokens'] ?? 200,
];
if ( $type === 'azure' ) {
$endpoint = SettingsPage::get_azure_endpoint();
$api_version = SettingsPage::get_azure_api_version();
$url = rtrim( $endpoint, '/' ) . '/openai/deployments/'
. $model . '/chat/completions?api-version=' . $api_version;
$response = wp_remote_post( $url, [
'headers' => [
'Content-Type' => 'application/json',
'api-key' => $key,
],
'body' => wp_json_encode( $body ),
'timeout' => 10,
] );
} else {
$body['model'] = $model;
$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $key,
],
'body' => wp_json_encode( $body ),
'timeout' => 10,
] );
}
// ... error handling and response parsing
return $data['choices'][0]['message']['content'];
}
POST https://api.openai.com/v1/chat/completions
Authorization: Bearer sk-...
Content-Type: application/json
{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "system", "content": "You generate poll questions..."},
{"role": "user", "content": "Analyze content..."}
],
"temperature": 0.7,
"max_tokens": 200
}
POST https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version=2024-02-15-preview
api-key: your-azure-key
Content-Type: application/json
{
"messages": [...],
"temperature": 0.7,
"max_tokens": 200
}

Direct API call to Anthropic’s Messages API.

sequenceDiagram
    participant AIS as AISuggestionService
    participant AN as suggest_anthropic()
    participant SP as SettingsPage
    participant WP as wp_remote_post
    participant API as api.anthropic.com

    AIS->>AN: text content
    AN->>SP: get_anthropic_key()
    AN->>SP: get_anthropic_model()
    
    AN->>AN: Build prompt from template
    
    AN->>WP: POST /v1/messages
    WP->>API: Request
    
    Note over API: Headers:<br>x-api-key: sk-ant-...<br>anthropic-version: 2023-06-01

    API-->>WP: Response
    WP-->>AN: Response body
    
    AN->>AN: Extract content[0].text
    AN->>AN: parse_poll_json()
    AN-->>AIS: {question, options[]}
private function suggest_anthropic( string $text ): array {
$api_key = SettingsPage::get_anthropic_key();
$model = SettingsPage::get_anthropic_model();
if ( empty( $api_key ) || empty( $model ) ) {
return [];
}
$prompt = sprintf( self::PROMPT_TEMPLATE, $text );
$response = wp_remote_post( 'https://api.anthropic.com/v1/messages', [
'headers' => [
'Content-Type' => 'application/json',
'x-api-key' => $api_key,
'anthropic-version' => '2023-06-01',
],
'body' => wp_json_encode( [
'model' => $model,
'max_tokens' => 1024,
'messages' => [
[ 'role' => 'user', 'content' => $prompt ],
],
] ),
'timeout' => 10,
] );
// ... error handling ...
$content_text = $data['content'][0]['text'];
return $this->parse_poll_json( $content_text );
}
POST https://api.anthropic.com/v1/messages
x-api-key: sk-ant-...
anthropic-version: 2023-06-01
Content-Type: application/json
{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "You are creating a poll..."}
]
}
{
"content": [
{
"type": "text",
"text": "{\"question\": \"...\", \"options\": [...]}"
}
]
}

API key passed in URL query parameter.

sequenceDiagram
    participant AIS as AISuggestionService
    participant GE as suggest_gemini()
    participant SP as SettingsPage
    participant WP as wp_remote_post
    participant API as generativelanguage.googleapis.com

    AIS->>GE: text content
    GE->>SP: get_gemini_key()
    GE->>SP: get_gemini_model()
    
    GE->>GE: Build URL with API key
    GE->>GE: Build prompt from template
    
    GE->>WP: POST /v1beta/models/{model}:generateContent
    WP->>API: Request
    
    Note over API: API key in URL:<br>?key=your-api-key

    API-->>WP: Response
    WP-->>GE: Response body
    
    GE->>GE: Extract candidates[0].content.parts[0].text
    GE->>GE: parse_poll_json()
    GE-->>AIS: {question, options[]}
private function suggest_gemini( string $text ): array {
$api_key = SettingsPage::get_gemini_key();
$model = SettingsPage::get_gemini_model();
if ( empty( $api_key ) || empty( $model ) ) {
return [];
}
$prompt = sprintf( self::PROMPT_TEMPLATE, $text );
$url = 'https://generativelanguage.googleapis.com/v1beta/models/'
. $model . ':generateContent?key=' . $api_key;
$response = wp_remote_post( $url, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [
'contents' => [
[
'parts' => [
[ 'text' => $prompt ],
],
],
],
] ),
'timeout' => 10,
] );
// ... error handling ...
$content_text = $data['candidates'][0]['content']['parts'][0]['text'];
return $this->parse_poll_json( $content_text );
}
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=AI...
Content-Type: application/json
{
"contents": [
{
"parts": [
{"text": "You are creating a poll..."}
]
}
]
}
{
"candidates": [
{
"content": {
"parts": [
{"text": "{\"question\": \"...\", \"options\": [...]}"}
]
}
}
]
}

Local inference with no authentication required.

sequenceDiagram
    participant AIS as AISuggestionService
    participant OL as suggest_ollama()
    participant SP as SettingsPage
    participant WP as wp_remote_post
    participant API as localhost:11434

    AIS->>OL: text content
    OL->>SP: get_ollama_endpoint()
    OL->>SP: get_ollama_model()
    
    OL->>OL: Build prompt from template
    
    OL->>WP: POST /api/generate
    
    Note over WP,API: Timeout: 30s<br>(local models slower)
    
    WP->>API: Request (no auth)
    API-->>WP: Response
    WP-->>OL: Response body
    
    OL->>OL: Extract response field
    OL->>OL: parse_poll_json()
    OL-->>AIS: {question, options[]}
private function suggest_ollama( string $text ): array {
$endpoint = SettingsPage::get_ollama_endpoint();
$model = SettingsPage::get_ollama_model();
if ( empty( $endpoint ) || empty( $model ) ) {
return [];
}
$prompt = sprintf( self::PROMPT_TEMPLATE, $text );
$url = rtrim( $endpoint, '/' ) . '/api/generate';
$response = wp_remote_post( $url, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [
'model' => $model,
'prompt' => $prompt,
'stream' => false,
] ),
'timeout' => 30, // Ollama might be slower
] );
// ... error handling ...
$content_text = $data['response'];
return $this->parse_poll_json( $content_text );
}
POST http://localhost:11434/api/generate
Content-Type: application/json
{
"model": "llama3.2",
"prompt": "You are creating a poll...",
"stream": false
}
{
"model": "llama3.2",
"response": "{\"question\": \"...\", \"options\": [...]}",
"done": true
}

OpenAI-compatible API from xAI.

sequenceDiagram
    participant AIS as AISuggestionService
    participant GR as suggest_grok()
    participant SP as SettingsPage
    participant WP as wp_remote_post
    participant API as api.x.ai

    AIS->>GR: text content
    GR->>SP: get_grok_key()
    GR->>SP: get_grok_model()
    
    GR->>GR: Build prompt from template
    
    GR->>WP: POST /v1/chat/completions
    WP->>API: Request
    
    Note over API: Headers:<br>Authorization: Bearer xai-...

    API-->>WP: Response
    WP-->>GR: Response body
    
    GR->>GR: Extract choices[0].message.content
    GR->>GR: parse_poll_json()
    GR-->>AIS: {question, options[]}
private function suggest_grok( string $text ): array {
$api_key = SettingsPage::get_grok_key();
$model = SettingsPage::get_grok_model();
if ( empty( $api_key ) || empty( $model ) ) {
return [];
}
$prompt = sprintf( self::PROMPT_TEMPLATE, $text );
$response = wp_remote_post( 'https://api.x.ai/v1/chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => wp_json_encode( [
'model' => $model,
'messages' => [
[ 'role' => 'system', 'content' => 'You generate poll questions...' ],
[ 'role' => 'user', 'content' => $prompt ],
],
'temperature' => 0.7,
'max_tokens' => 200,
] ),
'timeout' => 10,
] );
// ... error handling ...
$content_text = $data['choices'][0]['message']['content'];
return $this->parse_poll_json( $content_text );
}
POST https://api.x.ai/v1/chat/completions
Authorization: Bearer xai-...
Content-Type: application/json
{
"model": "grok-2",
"messages": [
{"role": "system", "content": "You generate poll questions..."},
{"role": "user", "content": "You are creating a poll..."}
],
"temperature": 0.7,
"max_tokens": 200
}
{
"choices": [
{
"message": {
"role": "assistant",
"content": "{\"question\": \"...\", \"options\": [...]}"
}
}
]
}

OpenAI-compatible API with SSE streaming response handling.

sequenceDiagram
    participant AIS as AISuggestionService
    participant EX as suggest_exo()
    participant SP as SettingsPage
    participant WP as wp_remote_post
    participant API as localhost:8000

    AIS->>EX: text content
    EX->>SP: get_exo_endpoint()
    EX->>SP: get_exo_model()
    
    EX->>EX: Build prompt from template
    
    EX->>WP: POST /v1/chat/completions
    
    Note over WP,API: Timeout: 60s<br>(local inference)
    
    WP->>API: Request (no auth)
    API-->>WP: SSE streaming response
    WP-->>EX: Raw body with SSE chunks
    
    EX->>EX: parse_exo_sse_response()
    
    Note over EX: Parse each "data: {...}" line<br>Concatenate delta.content<br>Handle [DONE] marker

    alt SSE parsing fails
        EX->>EX: Try standard JSON parse
    end
    
    EX->>EX: parse_poll_json()
    EX-->>AIS: {question, options[]}
private function suggest_exo( string $text ): array {
$endpoint = SettingsPage::get_exo_endpoint();
$model = SettingsPage::get_exo_model();
if ( empty( $endpoint ) || empty( $model ) ) {
return [];
}
$prompt = sprintf( self::PROMPT_TEMPLATE, $text );
$url = rtrim( $endpoint, '/' ) . '/v1/chat/completions';
$response = wp_remote_post( $url, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [
'model' => $model,
'messages' => [
[ 'role' => 'system', 'content' => 'You generate poll questions...' ],
[ 'role' => 'user', 'content' => $prompt ],
],
'temperature' => 0.7,
'max_tokens' => 500,
] ),
'timeout' => 60, // Local models can be slower
] );
// ... error handling ...
$body = wp_remote_retrieve_body( $response );
// Exo returns SSE streaming format
$content_text = $this->parse_exo_sse_response( $body );
if ( empty( $content_text ) ) {
// Try standard JSON (non-streaming fallback)
$data = json_decode( $body, true );
if ( isset( $data['choices'][0]['message']['content'] ) ) {
$content_text = $data['choices'][0]['message']['content'];
}
}
return $this->parse_poll_json( $content_text );
}
private function parse_exo_sse_response( string $body ): string {
$content = '';
$lines = explode( "\n", $body );
foreach ( $lines as $line ) {
$line = trim( $line );
// Skip empty lines and comments
if ( empty( $line ) || strpos( $line, ':' ) === 0 ) {
continue;
}
// Parse "data: {...}" lines
if ( strpos( $line, 'data: ' ) === 0 ) {
$json_str = substr( $line, 6 );
// Skip [DONE] marker
if ( $json_str === '[DONE]' ) {
continue;
}
$data = json_decode( $json_str, true );
if ( ! is_array( $data ) ) {
continue;
}
// Extract delta content (streaming)
if ( isset( $data['choices'][0]['delta']['content'] ) ) {
$content .= $data['choices'][0]['delta']['content'];
}
// Handle full message format (non-streaming)
if ( isset( $data['choices'][0]['message']['content'] ) ) {
$content .= $data['choices'][0]['message']['content'];
}
}
}
return $content;
}
POST http://localhost:8000/v1/chat/completions
Content-Type: application/json
{
"model": "llama-3.2-3b",
"messages": [
{"role": "system", "content": "You generate poll questions..."},
{"role": "user", "content": "You are creating a poll..."}
],
"temperature": 0.7,
"max_tokens": 500
}
data: {"choices":[{"delta":{"role":"assistant"}}]}
data: {"choices":[{"delta":{"content":"{"}}]}
data: {"choices":[{"delta":{"content":"\"question\""}}]}
data: {"choices":[{"delta":{"content":": \"What..."}}]}
...
data: [DONE]

The plugin includes additional REST endpoints for Exo configuration:

sequenceDiagram
    participant Admin as Settings Page
    participant REST as ExoController
    participant Exo as Exo Cluster

    Admin->>REST: POST /exo-health {endpoint}
    REST->>Exo: GET /v1/models
    Exo-->>REST: 200 OK
    REST-->>Admin: {status: "ok"}

    Admin->>REST: POST /exo-models {endpoint}
    REST->>Exo: GET /v1/models
    Exo-->>REST: Model list
    REST->>Exo: GET /state
    Exo-->>REST: Running instances
    REST->>REST: Cross-reference to find running models
    REST-->>Admin: {models: [{id, name}]}

All providers use a common JSON parser that handles various response formats.

flowchart TD
    Input["Raw AI response"] --> Direct{"Direct JSON decode?"}
    Direct -->|Valid| Check1{"Has question and options?"}
    Check1 -->|Yes| Sanitize1["Sanitize fields"]
    Check1 -->|No| FindBraces
    Direct -->|Invalid| FindBraces
    
    FindBraces["Find first and last braces"] --> Extract["Extract substring"]
    Extract --> Decode2{"JSON decode?"}
    Decode2 -->|Valid| Check2{"Has question and options?"}
    Check2 -->|Yes| Sanitize2["Sanitize fields"]
    Check2 -->|No| Regex
    Decode2 -->|Invalid| Regex
    
    Regex["Regex: find all objects"] --> Loop{"Try each match"}
    Loop -->|Found valid| Sanitize3["Sanitize fields"]
    Loop -->|None valid| Empty(["Return empty"])
    
    Sanitize1 --> Return(["Return result"])
    Sanitize2 --> Return
    Sanitize3 --> Return
private function parse_poll_json( string $raw ): array {
$raw = trim( $raw );
// 1. Try direct decode
$direct = json_decode( $raw, true );
if ( is_array( $direct ) && isset( $direct['question'], $direct['options'] ) ) {
return [
'question' => sanitize_text_field( $direct['question'] ),
'options' => array_map( 'sanitize_text_field', $direct['options'] ),
];
}
// 2. Find first '{' and last '}'
$start = strpos( $raw, '{' );
$end = strrpos( $raw, '}' );
if ( $start !== false && $end !== false && $end > $start ) {
$candidate = substr( $raw, $start, $end - $start + 1 );
$decoded = json_decode( $candidate, true );
if ( is_array( $decoded ) && isset( $decoded['question'], $decoded['options'] ) ) {
return [
'question' => sanitize_text_field( $decoded['question'] ),
'options' => array_map( 'sanitize_text_field', $decoded['options'] ),
];
}
}
// 3. Fallback: regex for simple objects
if ( preg_match_all( '/\{[^{}]*\}/', $raw, $all ) ) {
foreach ( $all[0] as $fragment ) {
$decoded = json_decode( $fragment, true );
if ( is_array( $decoded ) && isset( $decoded['question'], $decoded['options'] ) ) {
return [
'question' => sanitize_text_field( $decoded['question'] ),
'options' => array_map( 'sanitize_text_field', $decoded['options'] ),
];
}
}
}
return [];
}