diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e20ce6..cbb50a3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [0.4.3] — 2026-04-19 + +### Fixed +- **"Analysis response was not valid JSON" with some models.** Models like + GPT 5.1 Nano ignore `response_format: json_object` and wrap JSON in markdown + fences or preamble text. New `PRAutoBlogger_Json_Extractor` utility strips + fences and extracts the outermost JSON object. Applied to both the content + analyzer and chief editor parsers. Raw response now logged on failure for + debugging. + ## [0.4.2] — 2026-04-19 ### Fixed diff --git a/includes/core/class-chief-editor.php b/includes/core/class-chief-editor.php index 560cec7..007ae57 100755 --- a/includes/core/class-chief-editor.php +++ b/includes/core/class-chief-editor.php @@ -12,6 +12,7 @@ declare(strict_types=1); * Dependencies: LLM_Provider_Interface, Cost_Tracker. * * @see core/class-content-generator.php — Produces the content we review. + * @see core/class-json-extractor.php — Tolerant JSON parsing for LLM output. * @see core/class-publisher.php — Publishes approved content. * @see ARCHITECTURE.md — Data flow step 5. */ @@ -145,10 +146,11 @@ class PRAutoBlogger_Chief_Editor { * @return PRAutoBlogger_Editorial_Review */ private function parse_review_response( string $content ): PRAutoBlogger_Editorial_Review { - $data = json_decode( $content, true ); + $data = PRAutoBlogger_Json_Extractor::decode( $content ); if ( ! is_array( $data ) || ! isset( $data['verdict'] ) ) { PRAutoBlogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' ); + PRAutoBlogger_Logger::instance()->debug( 'Raw editor response: ' . mb_substr( $content, 0, 500 ), 'editor' ); return new PRAutoBlogger_Editorial_Review( [ 'verdict' => 'rejected', 'notes' => 'Editor response could not be parsed.', diff --git a/includes/core/class-content-analyzer.php b/includes/core/class-content-analyzer.php index 52fc11d..6abaf25 100755 --- a/includes/core/class-content-analyzer.php +++ b/includes/core/class-content-analyzer.php @@ -15,6 +15,7 @@ declare(strict_types=1); * @see core/class-analysis-prompts.php — System/user prompt builders + performance context. * @see providers/interface-llm-provider.php — LLM used for analysis. * @see core/class-cost-tracker.php — Logs API costs. + * @see core/class-json-extractor.php — Tolerant JSON parsing for LLM output. * @see core/class-idea-scorer.php — Consumes analysis results. * @see ARCHITECTURE.md — Data flow step 2. */ @@ -175,9 +176,10 @@ class PRAutoBlogger_Content_Analyzer { * @return PRAutoBlogger_Analysis_Result[] */ private function parse_analysis_response( string $content, array $source_items ): array { - $data = json_decode( $content, true ); + $data = PRAutoBlogger_Json_Extractor::decode( $content ); if ( ! isset( $data['patterns'] ) || ! is_array( $data['patterns'] ) ) { PRAutoBlogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' ); + PRAutoBlogger_Logger::instance()->debug( 'Raw analysis response: ' . mb_substr( $content, 0, 500 ), 'analyzer' ); return []; } diff --git a/includes/core/class-json-extractor.php b/includes/core/class-json-extractor.php new file mode 100644 index 0000000..06ffc95 --- /dev/null +++ b/includes/core/class-json-extractor.php @@ -0,0 +1,60 @@ +|null Decoded associative array, or null on failure. + */ + public static function decode( string $raw ): ?array { + $trimmed = trim( $raw ); + + // 1. Clean JSON — best case. + $decoded = json_decode( $trimmed, true ); + if ( is_array( $decoded ) ) { + return $decoded; + } + + // 2. Markdown-fenced JSON (```json ... ``` or ``` ... ```). + if ( preg_match( '/```(?:json)?\s*\n?(.*?)\n?\s*```/s', $trimmed, $matches ) ) { + $decoded = json_decode( trim( $matches[1] ), true ); + if ( is_array( $decoded ) ) { + return $decoded; + } + } + + // 3. Brute-force: find outermost { ... } substring. + $first_brace = strpos( $trimmed, '{' ); + $last_brace = strrpos( $trimmed, '}' ); + if ( false !== $first_brace && false !== $last_brace && $last_brace > $first_brace ) { + $candidate = substr( $trimmed, $first_brace, $last_brace - $first_brace + 1 ); + $decoded = json_decode( $candidate, true ); + if ( is_array( $decoded ) ) { + return $decoded; + } + } + + return null; + } +} diff --git a/prautoblogger.php b/prautoblogger.php index 08157ee..bcaeb28 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -8,7 +8,7 @@ declare(strict_types=1); * Plugin Name: PRAutoBlogger * Plugin URI: https://peptiderepo.com/prautoblogger * Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics. - * Version: 0.4.2 + * Version: 0.4.3 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: PeptideRepo @@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { | and limits without magic strings. */ -define( 'PRAUTOBLOGGER_VERSION', '0.4.2' ); +define( 'PRAUTOBLOGGER_VERSION', '0.4.3' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );