first commit all files

This commit is contained in:
dwindown
2026-01-28 00:26:00 +07:00
parent 65dd207a74
commit 97426d5ab1
72 changed files with 91484 additions and 0 deletions

931
recommender-impl-brief.md Normal file
View 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 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