Files
wp-agentic-writer/recommender-impl-brief.md
2026-01-28 00:26:00 +07:00

932 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WP Agentic Writer: Model Recommender Implementation Brief
## Executive Summary
The **Model Recommender** is an interactive guided experience in the plugin settings that asks users 45 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.010.05, $0.050.20, >$0.20
Question 3: "How many articles per month?"
├─ 13, 48, 915, 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.010.05): DeepSeek + Flash, FLUX.2 klein images
- Balanced ($0.050.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.010.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.0080.012/article
If budget $0.010.05/article:
- Chat/Planning: Gemini Flash
- Writing/Refinement: Mistral Small or DeepSeek
- Images: FLUX.2 klein or null
- Estimated cost: $0.030.05/article
If budget $0.050.20/article:
- Chat/Clarity/Planning: Gemini Flash
- Writing/Refinement: Claude Sonnet
- Images: Riverflow V2 Max or FLUX.2 Pro
- Estimated cost: $0.100.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.250.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: "13 articles/month" },
{ value: "4-8", label: "48 articles/month" },
{ value: "9-15", label: "915 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.100.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 (410 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.010.05): DeepSeek + Flash, FLUX.2 klein images
- Balanced ($0.050.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: '13 articles/month' },
{ value: '4-8', label: '48 articles/month' },
{ value: '9-15', label: '915 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 12 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 35 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 | 34 hours | Includes testing with OpenRouter |
| React component + UI | 34 hours | Includes modal, form flow, validation |
| Integration with settings page | 12 hours | Wire up button, callbacks, save logic |
| Testing + refinement | 23 hours | Edge cases, error handling |
| **Total** | **913 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