first commit all files
This commit is contained in:
931
recommender-impl-brief.md
Normal file
931
recommender-impl-brief.md
Normal file
@@ -0,0 +1,931 @@
|
||||
# WP Agentic Writer: Model Recommender Implementation Brief
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The **Model Recommender** is an interactive guided experience in the plugin settings that asks users 4–5 questions, then **automatically generates and applies** a personalized OpenRouter model configuration matching their budget and use case.
|
||||
|
||||
**Why it exists:** Users arrive with widely varying budgets (totally free → premium agencies). The 3 presets (Budget/Balanced/Premium) cover 80% of use cases, but the Recommender handles the remaining 20%: ultra-low-budget users, niche workflows, or custom combinations.
|
||||
|
||||
**Where it lives:** Settings page → "AI Model Configuration" card → Button: "Open Model Recommender"
|
||||
|
||||
**Output:** A filled-in model configuration that users can save or customize.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [User Flow](#user-flow)
|
||||
2. [System Prompt (Agent Logic)](#system-prompt-agent-logic)
|
||||
3. [Question Schema](#question-schema)
|
||||
4. [Response JSON Format](#response-json-format)
|
||||
5. [Backend Implementation](#backend-implementation)
|
||||
6. [Frontend UI Component](#frontend-ui-component)
|
||||
7. [Conversation Examples](#conversation-examples)
|
||||
8. [Edge Cases & Fallbacks](#edge-cases--fallbacks)
|
||||
9. [Development Checklist](#development-checklist)
|
||||
|
||||
---
|
||||
|
||||
## User Flow
|
||||
|
||||
```
|
||||
User clicks: [Open Model Recommender] button on settings page
|
||||
↓
|
||||
Modal opens with welcoming intro text
|
||||
↓
|
||||
Question 1: "What's your main goal?"
|
||||
├─ Dev blog, agency client content, hobby writing, etc.
|
||||
↓
|
||||
Question 2: "What's your budget per article?"
|
||||
├─ <$0.01, $0.01–0.05, $0.05–0.20, >$0.20
|
||||
↓
|
||||
Question 3: "How many articles per month?"
|
||||
├─ 1–3, 4–8, 9–15, 15+
|
||||
↓
|
||||
Question 4: "Do you need AI-generated images?"
|
||||
├─ Yes, No, Optional (depends on image quality)
|
||||
↓
|
||||
(Optional) Question 5: "Any specific provider preference?"
|
||||
├─ No preference, Google Gemini, OpenAI, Anthropic
|
||||
↓
|
||||
Agent processes answers → generates recommended config JSON
|
||||
↓
|
||||
Modal displays: "Recommended config: [Name]"
|
||||
├─ Chat: [Model]
|
||||
├─ Writing: [Model]
|
||||
├─ etc.
|
||||
├─ Estimated cost: $X/article
|
||||
↓
|
||||
User: [Apply Config] or [Customize Manually] or [Cancel]
|
||||
↓
|
||||
Config saved to plugin options / UI refreshed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Prompt (Agent Logic)
|
||||
|
||||
This prompt runs **server-side** to convert user answers into a model configuration.
|
||||
|
||||
```
|
||||
System Prompt for Model Recommender Agent
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
You are a Model Configuration Recommender for WP Agentic Writer.
|
||||
|
||||
Your job: Convert user answers (goal, budget, volume, preferences)
|
||||
into an optimal OpenRouter model preset for 6 tasks:
|
||||
- chat, clarity, planning, writing, refinement, image
|
||||
|
||||
You MUST:
|
||||
1. Respect budget constraints strictly
|
||||
2. Optimize cost:quality tradeoff for use case
|
||||
3. Prefer Gemini Flash for planning/chat (best value)
|
||||
4. Prefer Claude Sonnet for writing (industry standard)
|
||||
5. Disable images if budget <$0.01/article
|
||||
6. Return ONLY valid JSON (no extra text)
|
||||
|
||||
Budget tiers (for reference):
|
||||
- Ultra (<$0.01/article): Gemini Flash only, no images
|
||||
- Budget ($0.01–0.05): DeepSeek + Flash, FLUX.2 klein images
|
||||
- Balanced ($0.05–0.20): Gemini Flash + Claude Sonnet, Riverflow images
|
||||
- Premium (>$0.20): GPT-5.2/Opus + Flash, FLUX.2 max images
|
||||
|
||||
User answers will come as:
|
||||
{
|
||||
"goal": "string",
|
||||
"budget_per_article": "string ($0.01–0.05, etc.)",
|
||||
"articles_per_month": "number",
|
||||
"images_needed": "yes/no/optional",
|
||||
"provider_preference": "string or null"
|
||||
}
|
||||
|
||||
Return a JSON object with this schema:
|
||||
{
|
||||
"preset_name": "string (e.g., 'Budget: Gemini Flash + DeepSeek')",
|
||||
"rationale": "string (brief explanation of why this config)",
|
||||
"estimated_cost_per_article": number (USD, including 5.5% fee),
|
||||
"models": {
|
||||
"chat": "model_slug_string",
|
||||
"clarity": "model_slug_string",
|
||||
"planning": "model_slug_string",
|
||||
"writing": "model_slug_string",
|
||||
"refinement": "model_slug_string",
|
||||
"image": "model_slug_string or null"
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "string",
|
||||
"workflow_expectation": "string"
|
||||
}
|
||||
}
|
||||
|
||||
Examples of model slugs (valid on OpenRouter):
|
||||
- deepseek-v3
|
||||
- google/gemini-3-flash-preview
|
||||
- anthropic/claude-3.5-sonnet
|
||||
- anthropic/claude-sonnet-4
|
||||
- mistral/mistral-small
|
||||
- openai/gpt-5.2
|
||||
- black-forest-labs/flux.2-klein
|
||||
- black-forest-labs/flux.2-max
|
||||
- sourceful/riverflow-v2-max
|
||||
|
||||
If budget <$0.01/article:
|
||||
- Recommend: Gemini Flash for ALL tasks
|
||||
- Image: null (disabled)
|
||||
- Estimated cost: ~$0.008–0.012/article
|
||||
|
||||
If budget $0.01–0.05/article:
|
||||
- Chat/Planning: Gemini Flash
|
||||
- Writing/Refinement: Mistral Small or DeepSeek
|
||||
- Images: FLUX.2 klein or null
|
||||
- Estimated cost: $0.03–0.05/article
|
||||
|
||||
If budget $0.05–0.20/article:
|
||||
- Chat/Clarity/Planning: Gemini Flash
|
||||
- Writing/Refinement: Claude Sonnet
|
||||
- Images: Riverflow V2 Max or FLUX.2 Pro
|
||||
- Estimated cost: $0.10–0.18/article
|
||||
|
||||
If budget >$0.20/article:
|
||||
- Chat/Planning: Gemini Flash (cost doesn't improve)
|
||||
- Clarity: Claude Sonnet 4 (nuanced feedback)
|
||||
- Writing/Refinement: GPT-5.2 or Claude Opus
|
||||
- Images: FLUX.2 max
|
||||
- Estimated cost: $0.25–0.50+/article
|
||||
|
||||
Return valid JSON only. No markdown, no explanations outside the JSON.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Question Schema
|
||||
|
||||
### Frontend: Questions Array (React/JS)
|
||||
|
||||
```javascript
|
||||
const recommenderQuestions = [
|
||||
{
|
||||
id: "goal",
|
||||
question: "What's your main goal for this plugin?",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ value: "dev_blog", label: "Dev blog / technical tutorials" },
|
||||
{ value: "agency_content", label: "Agency client content" },
|
||||
{ value: "hobby_writing", label: "Hobby writing / personal blog" },
|
||||
{ value: "marketing", label: "Marketing / sales content" },
|
||||
{ value: "research", label: "Research papers / long-form" }
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: "budget_per_article",
|
||||
question: "What's your budget limit per article?",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ value: "<0.01", label: "Ultra-budget (< $0.01 / article)" },
|
||||
{ value: "0.01-0.05", label: "Budget ($0.01–$0.05 / article)" },
|
||||
{ value: "0.05-0.20", label: "Balanced ($0.05–$0.20 / article)" },
|
||||
{ value: ">0.20", label: "Premium (> $0.20 / article)" },
|
||||
{ value: "no_limit", label: "No budget limit" }
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: "articles_per_month",
|
||||
question: "How many articles per month do you plan to generate?",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ value: "1-3", label: "1–3 articles/month" },
|
||||
{ value: "4-8", label: "4–8 articles/month" },
|
||||
{ value: "9-15", label: "9–15 articles/month" },
|
||||
{ value: "15+", label: "15+ articles/month" }
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: "images_needed",
|
||||
question: "Do you need AI-generated images?",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ value: "yes", label: "Yes, high-quality hero images" },
|
||||
{ value: "optional", label: "Optional / nice-to-have" },
|
||||
{ value: "no", label: "No, I'll upload my own" }
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: "provider_preference",
|
||||
question: "(Optional) Any provider preference?",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ value: null, label: "No preference (use what's best)" },
|
||||
{ value: "google", label: "Google (Gemini)" },
|
||||
{ value: "openai", label: "OpenAI (GPT)" },
|
||||
{ value: "anthropic", label: "Anthropic (Claude)" }
|
||||
],
|
||||
required: false
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response JSON Format
|
||||
|
||||
### Backend: Agent Response
|
||||
|
||||
```json
|
||||
{
|
||||
"preset_name": "Balanced: Gemini Flash + Claude Sonnet + Riverflow",
|
||||
"rationale": "Your budget ($0.10–0.20/article) and moderate volume (8 articles/month) fit the Balanced preset perfectly. Gemini Flash handles chat/planning efficiently; Claude Sonnet is the industry standard for long-form writing. Riverflow images provide high quality at flat $0.03/image.",
|
||||
"estimated_cost_per_article": 0.1359,
|
||||
"models": {
|
||||
"chat": "google/gemini-3-flash-preview",
|
||||
"clarity": "google/gemini-3-flash-preview",
|
||||
"planning": "google/gemini-3-flash-preview",
|
||||
"writing": "anthropic/claude-3.5-sonnet",
|
||||
"refinement": "anthropic/claude-3.5-sonnet",
|
||||
"image": "sourceful/riverflow-v2-max"
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "Regular blogging (4–10 articles/month), mixed content types, publishing to professional blog or portfolio.",
|
||||
"workflow_expectation": "User does one refinement cycle; publishes with high confidence."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### 1. PHP Endpoint (WordPress REST API)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// File: includes/class-model-recommender.php
|
||||
|
||||
class Agentic_Writer_Model_Recommender {
|
||||
|
||||
/**
|
||||
* Register REST endpoint
|
||||
*/
|
||||
public static function register_endpoints() {
|
||||
register_rest_route(
|
||||
'agentic-writer/v1',
|
||||
'/recommend-models',
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [ self::class, 'get_recommendation' ],
|
||||
'permission_callback' => [ self::class, 'check_permissions' ],
|
||||
'args' => [
|
||||
'goal' => ['required' => true],
|
||||
'budget_per_article' => ['required' => true],
|
||||
'articles_per_month' => ['required' => true],
|
||||
'images_needed' => ['required' => true],
|
||||
'provider_preference' => ['required' => false],
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model recommendation from Claude/LLM
|
||||
*/
|
||||
public static function get_recommendation( $request ) {
|
||||
$params = $request->get_json_params();
|
||||
|
||||
// Build prompt with user answers
|
||||
$user_input = self::format_user_input( $params );
|
||||
|
||||
// Call OpenRouter API (using Claude 3.5 Sonnet for reasoning)
|
||||
$recommendation = self::call_recommender_agent( $user_input );
|
||||
|
||||
if ( is_wp_error( $recommendation ) ) {
|
||||
return new WP_REST_Response(
|
||||
['error' => $recommendation->get_error_message()],
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $recommendation, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format user answers into structured prompt
|
||||
*/
|
||||
private static function format_user_input( $params ) {
|
||||
return json_encode([
|
||||
'goal' => $params['goal'] ?? null,
|
||||
'budget_per_article' => $params['budget_per_article'] ?? null,
|
||||
'articles_per_month' => (int) $params['articles_per_month'] ?? null,
|
||||
'images_needed' => $params['images_needed'] ?? 'no',
|
||||
'provider_preference' => $params['provider_preference'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call OpenRouter API with system prompt + user input
|
||||
*/
|
||||
private static function call_recommender_agent( $user_input ) {
|
||||
$api_key = get_option( 'agentic_writer_openrouter_api_key' );
|
||||
|
||||
if ( !$api_key ) {
|
||||
return new WP_Error(
|
||||
'no_api_key',
|
||||
'OpenRouter API key not configured.'
|
||||
);
|
||||
}
|
||||
|
||||
$system_prompt = self::get_system_prompt();
|
||||
|
||||
$response = wp_remote_post(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => json_encode([
|
||||
'model' => 'anthropic/claude-3.5-sonnet',
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => $system_prompt,
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => "User answers: {$user_input}\n\nBased on these answers, generate a recommended model configuration.",
|
||||
]
|
||||
],
|
||||
'temperature' => 0.2, // Deterministic
|
||||
'max_tokens' => 500,
|
||||
]),
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( !isset( $body['choices'][0]['message']['content'] ) ) {
|
||||
return new WP_Error(
|
||||
'invalid_response',
|
||||
'Invalid response from OpenRouter API.'
|
||||
);
|
||||
}
|
||||
|
||||
$content = $body['choices'][0]['message']['content'];
|
||||
|
||||
// Parse JSON from response
|
||||
$recommendation = json_decode( $content, true );
|
||||
|
||||
if ( !$recommendation ) {
|
||||
return new WP_Error(
|
||||
'invalid_json',
|
||||
'Could not parse agent recommendation.'
|
||||
);
|
||||
}
|
||||
|
||||
return $recommendation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system prompt (embedded in class)
|
||||
*/
|
||||
private static function get_system_prompt() {
|
||||
return <<<'PROMPT'
|
||||
You are a Model Configuration Recommender for WP Agentic Writer.
|
||||
|
||||
Your job: Convert user answers (goal, budget, volume, preferences)
|
||||
into an optimal OpenRouter model preset for 6 tasks:
|
||||
- chat, clarity, planning, writing, refinement, image
|
||||
|
||||
You MUST:
|
||||
1. Respect budget constraints strictly
|
||||
2. Optimize cost:quality tradeoff for use case
|
||||
3. Prefer Gemini Flash for planning/chat (best value)
|
||||
4. Prefer Claude Sonnet for writing (industry standard)
|
||||
5. Disable images if budget <$0.01/article
|
||||
6. Return ONLY valid JSON (no extra text)
|
||||
|
||||
Budget tiers (for reference):
|
||||
- Ultra (<$0.01/article): Gemini Flash only, no images
|
||||
- Budget ($0.01–0.05): DeepSeek + Flash, FLUX.2 klein images
|
||||
- Balanced ($0.05–0.20): Gemini Flash + Claude Sonnet, Riverflow images
|
||||
- Premium (>$0.20): GPT-5.2/Opus + Flash, FLUX.2 max images
|
||||
|
||||
Valid model slugs (OpenRouter):
|
||||
- deepseek-v3
|
||||
- google/gemini-3-flash-preview
|
||||
- anthropic/claude-3.5-sonnet
|
||||
- anthropic/claude-sonnet-4
|
||||
- mistral/mistral-small
|
||||
- openai/gpt-5.2
|
||||
- black-forest-labs/flux.2-klein
|
||||
- black-forest-labs/flux.2-max
|
||||
- sourceful/riverflow-v2-max
|
||||
|
||||
Return ONLY this JSON structure (no markdown, no extra text):
|
||||
{
|
||||
"preset_name": "string",
|
||||
"rationale": "string",
|
||||
"estimated_cost_per_article": number,
|
||||
"models": {
|
||||
"chat": "string",
|
||||
"clarity": "string",
|
||||
"planning": "string",
|
||||
"writing": "string",
|
||||
"refinement": "string",
|
||||
"image": "string or null"
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "string",
|
||||
"workflow_expectation": "string"
|
||||
}
|
||||
}
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user permissions
|
||||
*/
|
||||
public static function check_permissions() {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
}
|
||||
|
||||
// Hook to register on init
|
||||
add_action( 'rest_api_init', [ 'Agentic_Writer_Model_Recommender', 'register_endpoints' ] );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend UI Component
|
||||
|
||||
### React Component (Gutenberg/Admin UI)
|
||||
|
||||
```jsx
|
||||
// File: components/ModelRecommender.jsx
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Modal, RadioControl, Spinner } from '@wordpress/components';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
export const ModelRecommender = ({ onApplyConfig }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [answers, setAnswers] = useState({
|
||||
goal: null,
|
||||
budget_per_article: null,
|
||||
articles_per_month: null,
|
||||
images_needed: 'no',
|
||||
provider_preference: null,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [recommendation, setRecommendation] = useState(null);
|
||||
|
||||
const questions = [
|
||||
{
|
||||
id: 'goal',
|
||||
question: 'What\'s your main goal for this plugin?',
|
||||
options: [
|
||||
{ value: 'dev_blog', label: 'Dev blog / technical tutorials' },
|
||||
{ value: 'agency_content', label: 'Agency client content' },
|
||||
{ value: 'hobby_writing', label: 'Hobby writing / personal blog' },
|
||||
{ value: 'marketing', label: 'Marketing / sales content' },
|
||||
{ value: 'research', label: 'Research papers / long-form' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'budget_per_article',
|
||||
question: 'What\'s your budget limit per article?',
|
||||
options: [
|
||||
{ value: '<0.01', label: 'Ultra-budget (< $0.01)' },
|
||||
{ value: '0.01-0.05', label: 'Budget ($0.01–$0.05)' },
|
||||
{ value: '0.05-0.20', label: 'Balanced ($0.05–$0.20)' },
|
||||
{ value: '>0.20', label: 'Premium (> $0.20)' },
|
||||
{ value: 'no_limit', label: 'No budget limit' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'articles_per_month',
|
||||
question: 'How many articles per month?',
|
||||
options: [
|
||||
{ value: '1-3', label: '1–3 articles/month' },
|
||||
{ value: '4-8', label: '4–8 articles/month' },
|
||||
{ value: '9-15', label: '9–15 articles/month' },
|
||||
{ value: '15+', label: '15+ articles/month' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'images_needed',
|
||||
question: 'Do you need AI-generated images?',
|
||||
options: [
|
||||
{ value: 'yes', label: 'Yes, high-quality hero images' },
|
||||
{ value: 'optional', label: 'Optional / nice-to-have' },
|
||||
{ value: 'no', label: 'No, I\'ll upload my own' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'provider_preference',
|
||||
question: '(Optional) Any provider preference?',
|
||||
options: [
|
||||
{ value: null, label: 'No preference' },
|
||||
{ value: 'google', label: 'Google Gemini' },
|
||||
{ value: 'openai', label: 'OpenAI GPT' },
|
||||
{ value: 'anthropic', label: 'Anthropic Claude' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleAnswerChange = (answerId, value) => {
|
||||
setAnswers(prev => ({ ...prev, [answerId]: value }));
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
if (currentStep < questions.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
// Submit to backend
|
||||
await submitRecommendation();
|
||||
}
|
||||
};
|
||||
|
||||
const submitRecommendation = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const rec = await apiFetch({
|
||||
path: '/agentic-writer/v1/recommend-models',
|
||||
method: 'POST',
|
||||
data: answers,
|
||||
});
|
||||
setRecommendation(rec);
|
||||
} catch (error) {
|
||||
alert('Error generating recommendation: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onApplyConfig(recommendation);
|
||||
setIsOpen(false);
|
||||
setCurrentStep(0);
|
||||
setRecommendation(null);
|
||||
};
|
||||
|
||||
const currentQuestion = questions[currentStep];
|
||||
const isAnswered = answers[currentQuestion.id] !== null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
Open Model Recommender
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<Modal
|
||||
title="AI Model Recommender"
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
style={{ width: '500px' }}
|
||||
>
|
||||
{recommendation ? (
|
||||
// Results view
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>Recommended Configuration</h3>
|
||||
<p><strong>{recommendation.preset_name}</strong></p>
|
||||
<p><em>{recommendation.rationale}</em></p>
|
||||
|
||||
<table style={{ width: '100%', marginTop: '20px', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Chat</strong></td>
|
||||
<td>{recommendation.models.chat}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Clarity</strong></td>
|
||||
<td>{recommendation.models.clarity}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Planning</strong></td>
|
||||
<td>{recommendation.models.planning}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Writing</strong></td>
|
||||
<td>{recommendation.models.writing}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Refinement</strong></td>
|
||||
<td>{recommendation.models.refinement}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Image</strong></td>
|
||||
<td>{recommendation.models.image || 'Disabled'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style={{ marginTop: '20px' }}>
|
||||
<strong>Estimated cost:</strong> ${recommendation.estimated_cost_per_article.toFixed(4)}/article
|
||||
</p>
|
||||
|
||||
<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleApply}
|
||||
>
|
||||
Apply This Configuration
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setRecommendation(null);
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Questions view
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p><strong>Question {currentStep + 1} of {questions.length}</strong></p>
|
||||
<h3>{currentQuestion.question}</h3>
|
||||
|
||||
<RadioControl
|
||||
selected={answers[currentQuestion.id]}
|
||||
options={currentQuestion.options}
|
||||
onChange={(value) => handleAnswerChange(currentQuestion.id, value)}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!isAnswered || loading}
|
||||
>
|
||||
{loading ? <Spinner /> : (currentStep === questions.length - 1 ? 'Get Recommendation' : 'Next')}
|
||||
</Button>
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conversation Examples
|
||||
|
||||
### Example 1: Ultra-Budget Dev Blogger
|
||||
|
||||
**User answers:**
|
||||
- Goal: Dev blog
|
||||
- Budget: <$0.01/article
|
||||
- Volume: 2 articles/month
|
||||
- Images: No
|
||||
- Provider: No preference
|
||||
|
||||
**Agent response:**
|
||||
```json
|
||||
{
|
||||
"preset_name": "Ultra-Budget: Gemini Flash Only",
|
||||
"rationale": "Your ultra-low budget and dev blog focus make Gemini 3 Flash the perfect fit. It's optimized for coding + reasoning at nearly free cost. No images keeps cost minimal.",
|
||||
"estimated_cost_per_article": 0.0082,
|
||||
"models": {
|
||||
"chat": "google/gemini-3-flash-preview",
|
||||
"clarity": "google/gemini-3-flash-preview",
|
||||
"planning": "google/gemini-3-flash-preview",
|
||||
"writing": "google/gemini-3-flash-preview",
|
||||
"refinement": "google/gemini-3-flash-preview",
|
||||
"image": null
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "Solo dev bloggers with minimal budget; testing the plugin before investing.",
|
||||
"workflow_expectation": "Output may need 1–2 editing rounds before publishing."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Agency with Quality Focus
|
||||
|
||||
**User answers:**
|
||||
- Goal: Agency client content
|
||||
- Budget: >$0.20/article
|
||||
- Volume: 20 articles/month
|
||||
- Images: Yes, high-quality
|
||||
- Provider: No preference
|
||||
|
||||
**Agent response:**
|
||||
```json
|
||||
{
|
||||
"preset_name": "Premium: GPT-5.2 + Gemini Flash + FLUX.2 max",
|
||||
"rationale": "Your agency volume and quality-first mindset justify premium models. GPT-5.2 delivers publication-ready prose; FLUX.2 max generates hero images. Your clients will notice the difference.",
|
||||
"estimated_cost_per_article": 0.3142,
|
||||
"models": {
|
||||
"chat": "google/gemini-3-flash-preview",
|
||||
"clarity": "anthropic/claude-sonnet-4",
|
||||
"planning": "google/gemini-3-flash-preview",
|
||||
"writing": "openai/gpt-5.2",
|
||||
"refinement": "openai/gpt-5.2",
|
||||
"image": "black-forest-labs/flux.2-max"
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "Agencies publishing 15+ articles/month; thought leadership, flagship posts, client deliverables.",
|
||||
"workflow_expectation": "Minimal editing required; publish with high confidence."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Balanced Hobbyist
|
||||
|
||||
**User answers:**
|
||||
- Goal: Hobby writing / personal blog
|
||||
- Budget: $0.05–$0.20/article
|
||||
- Volume: 4 articles/month
|
||||
- Images: Optional
|
||||
- Provider: Google Gemini
|
||||
|
||||
**Agent response:**
|
||||
```json
|
||||
{
|
||||
"preset_name": "Balanced: Gemini Flash + Claude Sonnet + Riverflow",
|
||||
"rationale": "Your modest volume and provider preference align perfectly with the Balanced preset. All-Gemini planning + Claude writing = great quality at fair cost. Riverflow images optional but affordable.",
|
||||
"estimated_cost_per_article": 0.1359,
|
||||
"models": {
|
||||
"chat": "google/gemini-3-flash-preview",
|
||||
"clarity": "google/gemini-3-flash-preview",
|
||||
"planning": "google/gemini-3-flash-preview",
|
||||
"writing": "anthropic/claude-3.5-sonnet",
|
||||
"refinement": "anthropic/claude-3.5-sonnet",
|
||||
"image": "sourceful/riverflow-v2-max"
|
||||
},
|
||||
"recommendations": {
|
||||
"when_to_use": "Regular hobby blogging or personal portfolio; professional output without premium cost.",
|
||||
"workflow_expectation": "One refinement cycle typical; polished final product."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases & Fallbacks
|
||||
|
||||
### 1. API failure (OpenRouter down)
|
||||
|
||||
**Fallback behavior:**
|
||||
```php
|
||||
// If recommender API fails, load from static presets
|
||||
if ( is_wp_error( $recommendation ) ) {
|
||||
$recommendation = self::get_fallback_preset_by_budget(
|
||||
$params['budget_per_article']
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Budget = "no_limit"
|
||||
|
||||
**Logic:**
|
||||
- Treat as ">$0.20" (Premium tier).
|
||||
- Recommend frontier models + best images.
|
||||
|
||||
### 3. Invalid JSON from agent
|
||||
|
||||
**Fallback:**
|
||||
- Log error to error_log.
|
||||
- Show message: "Recommender had trouble generating a config. Try adjusting your answers."
|
||||
- Offer manual preset picker instead.
|
||||
|
||||
### 4. User cancels mid-flow
|
||||
|
||||
**Behavior:**
|
||||
- Modal closes cleanly.
|
||||
- No settings changed.
|
||||
- State resets for next usage.
|
||||
|
||||
---
|
||||
|
||||
## Development Checklist
|
||||
|
||||
- [ ] **Backend setup**
|
||||
- [ ] Create `includes/class-model-recommender.php`
|
||||
- [ ] Register REST endpoint `/agentic-writer/v1/recommend-models`
|
||||
- [ ] Implement `call_recommender_agent()` with OpenRouter API
|
||||
- [ ] Add error handling + fallbacks
|
||||
- [ ] Test with real OpenRouter key
|
||||
|
||||
- [ ] **System prompt**
|
||||
- [ ] Write and refine system prompt (see above)
|
||||
- [ ] Test with 3–5 example user inputs
|
||||
- [ ] Verify JSON output is parseable
|
||||
- [ ] Ensure cost estimates match model-presets.md
|
||||
|
||||
- [ ] **Frontend component**
|
||||
- [ ] Create React component: `ModelRecommender.jsx`
|
||||
- [ ] Implement question flow (5 questions)
|
||||
- [ ] Add results display with config summary
|
||||
- [ ] Wire up "Apply Configuration" button
|
||||
- [ ] Test modal UX (open/close/back/next)
|
||||
|
||||
- [ ] **Integration**
|
||||
- [ ] Add button to settings page → "AI Model Configuration" card
|
||||
- [ ] Wire up `onApplyConfig` callback to save settings
|
||||
- [ ] Refresh model selectors after apply
|
||||
- [ ] Show success toast message
|
||||
|
||||
- [ ] **Testing**
|
||||
- [ ] Test all 5 questions with valid OpenRouter API key
|
||||
- [ ] Test budget boundary cases ($0.01, $0.05, $0.20)
|
||||
- [ ] Test edge cases (ultra-budget, no-limit, specific providers)
|
||||
- [ ] Test API failure + fallback behavior
|
||||
- [ ] Test invalid JSON response handling
|
||||
- [ ] Test user cancellation mid-flow
|
||||
|
||||
- [ ] **Documentation**
|
||||
- [ ] Add to settings help: "Click 'Open Model Recommender' for personalized setup"
|
||||
- [ ] Document system prompt in code comments
|
||||
- [ ] Add troubleshooting guide: "Recommender not working? Check API key."
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **API Key Protection:**
|
||||
- Only server-side calls to OpenRouter (key never exposed to client)
|
||||
- REST endpoint requires `manage_options` capability
|
||||
|
||||
2. **Rate Limiting:**
|
||||
- Add WordPress nonce to prevent CSRF
|
||||
- Limit calls to 1 per minute per user
|
||||
|
||||
3. **Input Validation:**
|
||||
- Validate all user answers against whitelist
|
||||
- Reject invalid budget ranges
|
||||
|
||||
```php
|
||||
// Example: Add nonce validation
|
||||
register_rest_route(
|
||||
'agentic-writer/v1',
|
||||
'/recommend-models',
|
||||
[
|
||||
'callback' => [ self::class, 'get_recommendation' ],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' )
|
||||
&& isset( $_REQUEST['_wpnonce'] )
|
||||
&& wp_verify_nonce( $_REQUEST['_wpnonce'], 'agentic_recommender' );
|
||||
}
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cost Estimation for Development
|
||||
|
||||
| Task | Time | Notes |
|
||||
|------|------|-------|
|
||||
| Backend endpoint + system prompt | 3–4 hours | Includes testing with OpenRouter |
|
||||
| React component + UI | 3–4 hours | Includes modal, form flow, validation |
|
||||
| Integration with settings page | 1–2 hours | Wire up button, callbacks, save logic |
|
||||
| Testing + refinement | 2–3 hours | Edge cases, error handling |
|
||||
| **Total** | **9–13 hours** | Can be split across team |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Priority Order)
|
||||
|
||||
1. **Refine system prompt** – Test with Claude to ensure it generates correct JSON
|
||||
2. **Build backend endpoint** – Implement `/recommend-models` route with error handling
|
||||
3. **Build React component** – Create modal UI with question flow
|
||||
4. **Integration** – Wire up to settings page
|
||||
5. **Testing** – Full QA with real OpenRouter API
|
||||
|
||||
---
|
||||
|
||||
**Document version:** 1.0
|
||||
**Date:** January 22, 2026
|
||||
**Author:** WP Agentic Writer Product Team
|
||||
**Status:** Ready for Development
|
||||
Reference in New Issue
Block a user