llm = $llm; $this->cost_tracker = $cost_tracker; } /** * Generate a blog post from an article idea. * * @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for. * @param bool $eval_mode True to suppress publish/image side effects. * @return string Generated HTML content. */ public function generate( PRAutoBlogger_Article_Idea $idea, bool $eval_mode = false ): string { // In eval mode, suppress publishing and image generation side effects. define( 'PRAUTOBLOGGER_EVAL_MODE', $eval_mode ); $mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' ); $request = new PRAutoBlogger_Content_Request( $idea, $mode, get_option( 'prautoblogger_tone', 'informational' ), absint( get_option( 'prautoblogger_min_word_count', 800 ) ), absint( get_option( 'prautoblogger_max_word_count', 2000 ) ), get_option( 'prautoblogger_niche_description', '' ), json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?? array(), get_option( 'prautoblogger_writing_instructions', '' ) ); return 'single_pass' === $mode ? $this->generate_single_pass( $request ) : $this->generate_multi_step( $request ); } /** * Single-pass: one LLM call produces complete article (Economy tier). * * The log row's stage stays 'draft' (historical contract: a draft row * without an outline row marks a single-pass run) but the prompt key * is passed explicitly — the stage map would resolve 'draft' to * 'content.draft'. * * @param PRAutoBlogger_Content_Request $request Generation request. * @return string Generated HTML content. */ private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string { $model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL ); return $this->execute_stage( 'draft', 'single_pass_generation', PRAutoBlogger_Content_Prompts::build_single_pass( $request ), $request, $model, array( 'temperature' => 0.7, 'max_tokens' => 4000, ), 'content.single_pass' ); } /** * Multi-step: outline → draft → polish. * * @param PRAutoBlogger_Content_Request $request Generation request. * @return string Generated HTML content. */ private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string { $model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL ); $outline = $this->execute_stage( 'outline', 'outline_generation', PRAutoBlogger_Content_Prompts::build_outline( $request ), $request, $model, array( 'temperature' => 0.5, 'max_tokens' => 1500, ) ); $draft = $this->execute_stage( 'draft', 'draft_generation', PRAutoBlogger_Content_Prompts::build_draft( $request, $outline ), $request, $model, array( 'temperature' => 0.7, 'max_tokens' => 4000, ) ); return $this->execute_stage( 'polish', 'polish_generation', PRAutoBlogger_Content_Prompts::build_polish( $draft ), $request, $model, array( 'temperature' => 0.4, 'max_tokens' => 4000, ) ); } /** * Run one writing stage: Opik span → LLM call → span close → cost log. * * Consolidates the per-stage boilerplate that was previously duplicated * across the four stage methods (v0.18.0). The system prompt is rebuilt * per stage exactly as before. * * Side effects: one OpenRouter API call, one Opik span (when tracing), * one generation_log row. * * @param string $stage Log stage value ('outline'|'draft'|'polish'). * @param string $span_name Opik span name. * @param string $user_prompt Rendered user prompt for the stage. * @param PRAutoBlogger_Content_Request $request Generation request (system prompt input). * @param string $model Model identifier. * @param array $options LLM options (temperature, max_tokens). * @param string|null $prompt_key Explicit registry key for prompt_version * stamping (null = derive from stage). * @return string Stage output content. */ private function execute_stage( string $stage, string $span_name, string $user_prompt, PRAutoBlogger_Content_Request $request, string $model, array $options, ?string $prompt_key = null ): string { $ctx = PRAutoBlogger_Opik_Trace_Context::current(); $span_id = $ctx->start_span( array( 'name' => $span_name, 'type' => 'llm', 'model' => $model, 'provider' => 'openrouter', ) ); $response = $this->llm->send_chat_completion( array( array( 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ), ), array( 'role' => 'user', 'content' => $user_prompt, ), ), $model, $options ); $ctx->end_span( $span_id, array( 'usage' => array( 'prompt_tokens' => $response['prompt_tokens'], 'completion_tokens' => $response['completion_tokens'], 'total_tokens' => $response['prompt_tokens'] + $response['completion_tokens'], ), ) ); $this->cost_tracker->log_api_call( null, $stage, $this->llm->get_provider_name(), $response['model'], $response['prompt_tokens'], $response['completion_tokens'], 'success', '', null, $prompt_key ); return $response['content']; } /** * Generate content in eval mode (no publishing, no image generation). * * @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for. * @return string Generated HTML content. */ public function generate_eval( PRAutoBlogger_Article_Idea $idea ): string { return $this->generate( $idea, true ); } }