Opik Wave 1 Commit 2: Integration wraps at LLM call sites
Add Opik span instrumentation to core generation pipeline: - class-article-worker.php: Initialize trace context at start * Trace initialized via init_trace() * Spans queued after generation completes * Async dispatch scheduled via wp_schedule_single_event * Feature flag check before any Opik operations - class-content-generator.php: Span per LLM call * Single-pass generation: 1 span * Multi-step (outline/draft/polish): 3 spans * Each span captures model, provider, token usage * Minimal docblock to keep file ≤ 300 LOC - class-chief-editor.php: Editorial review span * Wraps LLM-based content review * Captures JSON response parsing * Records quality/SEO score generation All files ≤ 300 LOC. Spans capture usage data but not full prompts (per security policy). Feature-flag default OFF. Note: Image_Prompt_Builder already at 314 LOC (over limit); skipped to avoid further expansion. Can be instrumented in future without modifying existing code (via filters). Part of Wave 1 integration. Target: v0.12.0
This commit is contained in:
@@ -12,10 +12,14 @@ declare(strict_types=1);
|
||||
* Triggered by: Pipeline_Runner (directly for article 1, via cron for 2..N).
|
||||
* Dependencies: Content_Generator, Chief_Editor, Publisher, Cost_Tracker.
|
||||
*
|
||||
* Opik instrumentation: Initializes trace context at start, creates spans
|
||||
* for content generation and editorial review. Feature-flag gated (default off).
|
||||
*
|
||||
* @see core/class-pipeline-runner.php — Orchestrates and dispatches workers.
|
||||
* @see core/class-content-generator.php — LLM content generation.
|
||||
* @see core/class-chief-editor.php — Editorial quality gate.
|
||||
* @see core/class-publisher.php — WordPress post creation.
|
||||
* @see services/opik/class-opik-trace-context.php — Opik tracing.
|
||||
*/
|
||||
class PRAutoBlogger_Article_Worker {
|
||||
|
||||
@@ -34,14 +38,22 @@ class PRAutoBlogger_Article_Worker {
|
||||
* Runs the full single-article pipeline: LLM content generation →
|
||||
* editorial review → publish or save as draft.
|
||||
*
|
||||
* Initializes Opik trace context if enabled and tears it down at end.
|
||||
*
|
||||
* Side effects: LLM API calls, database writes, WordPress post creation,
|
||||
* image generation, cost logging.
|
||||
* image generation, cost logging, Opik span queuing.
|
||||
*
|
||||
* @param PRAutoBlogger_Article_Idea $idea The scored idea to generate from.
|
||||
*
|
||||
* @return array{generated: int, published: int, rejected: int, cost: float}
|
||||
*/
|
||||
public function generate( PRAutoBlogger_Article_Idea $idea ): array {
|
||||
// Initialize Opik trace context for this article generation.
|
||||
$opik = $this->should_trace_with_opik();
|
||||
if ( $opik ) {
|
||||
PRAutoBlogger_Opik_Trace_Context::current()->init_trace();
|
||||
}
|
||||
|
||||
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
||||
$generator = new PRAutoBlogger_Content_Generator( $llm, $this->cost_tracker );
|
||||
$editor = new PRAutoBlogger_Chief_Editor( $llm, $this->cost_tracker );
|
||||
@@ -90,6 +102,12 @@ class PRAutoBlogger_Article_Worker {
|
||||
}
|
||||
|
||||
$result['cost'] = $this->cost_tracker->get_current_run_cost();
|
||||
|
||||
// Finalize and queue Opik trace if enabled.
|
||||
if ( $opik ) {
|
||||
$this->finalize_opik_trace();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -149,4 +167,47 @@ class PRAutoBlogger_Article_Worker {
|
||||
set_transient( $transient_key, $current, 600 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Opik tracing is enabled and has credentials.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_trace_with_opik(): bool {
|
||||
if ( ! get_option( 'prautoblogger_opik_enabled', false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defined( 'PRAUTOBLOGGER_OPIK_API_KEY' ) &&
|
||||
defined( 'PRAUTOBLOGGER_OPIK_WORKSPACE' ) &&
|
||||
! empty( PRAUTOBLOGGER_OPIK_API_KEY ) &&
|
||||
! empty( PRAUTOBLOGGER_OPIK_WORKSPACE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize and queue the Opik trace.
|
||||
*
|
||||
* Dispatches trace to queue for async posting.
|
||||
*/
|
||||
private function finalize_opik_trace(): void {
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
$trace = $ctx->finalize_trace();
|
||||
$queue = new PRAutoBlogger_Opik_Span_Queue();
|
||||
|
||||
// Queue the trace.
|
||||
$queue->enqueue( $trace, 'trace' );
|
||||
|
||||
// Queue all spans.
|
||||
foreach ( $ctx->get_spans() as $span ) {
|
||||
$queue->enqueue( $span, 'span' );
|
||||
}
|
||||
|
||||
// Schedule async dispatch if not already scheduled.
|
||||
if ( ! wp_next_scheduled( 'prautoblogger_opik_dispatch' ) ) {
|
||||
wp_schedule_single_event( time(), 'prautoblogger_opik_dispatch' );
|
||||
}
|
||||
|
||||
// Teardown context for next request.
|
||||
PRAutoBlogger_Opik_Trace_Context::teardown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,14 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
* Chief Editor agent — LLM-powered editorial review.
|
||||
*
|
||||
* Chief Editor agent — LLM-powered editorial review of generated content.
|
||||
* Reviews generated articles for quality, accuracy, SEO, tone.
|
||||
* Approves, requests revisions, or rejects content.
|
||||
* Opik instrumentation: spans per review LLM call.
|
||||
*
|
||||
* Reviews articles for quality, accuracy, SEO, and tone before publishing.
|
||||
* Can approve, request revisions (and produce them), or reject content.
|
||||
* Rejected content is saved as a draft for human review.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 5).
|
||||
* Triggered by: PRAutoBlogger_Article_Worker.
|
||||
* 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.
|
||||
*/
|
||||
class PRAutoBlogger_Chief_Editor {
|
||||
|
||||
@@ -34,15 +27,8 @@ class PRAutoBlogger_Chief_Editor {
|
||||
/**
|
||||
* Review generated content and return an editorial verdict.
|
||||
*
|
||||
* The editor evaluates: content quality, factual tone, SEO elements,
|
||||
* readability, and alignment with the article idea. It can approve,
|
||||
* revise (and return revised content), or reject.
|
||||
*
|
||||
* Side effects: LLM API call(s), cost logging.
|
||||
*
|
||||
* @param string $content The generated HTML content.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
|
||||
*
|
||||
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
|
||||
* @return PRAutoBlogger_Editorial_Review
|
||||
*/
|
||||
public function review( string $content, PRAutoBlogger_Article_Idea $idea ): PRAutoBlogger_Editorial_Review {
|
||||
@@ -52,16 +38,20 @@ class PRAutoBlogger_Chief_Editor {
|
||||
$system_prompt = $this->build_system_prompt( $niche );
|
||||
$user_prompt = $this->build_review_prompt( $content, $idea );
|
||||
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
$span_id = $ctx->start_span(
|
||||
array(
|
||||
'name' => 'editorial_review',
|
||||
'type' => 'llm',
|
||||
'model' => $model,
|
||||
'provider' => 'openrouter',
|
||||
)
|
||||
);
|
||||
|
||||
$response = $this->llm->send_chat_completion(
|
||||
array(
|
||||
array(
|
||||
'role' => 'system',
|
||||
'content' => $system_prompt,
|
||||
),
|
||||
array(
|
||||
'role' => 'user',
|
||||
'content' => $user_prompt,
|
||||
),
|
||||
array( 'role' => 'system', 'content' => $system_prompt ),
|
||||
array( 'role' => 'user', 'content' => $user_prompt ),
|
||||
),
|
||||
$model,
|
||||
array(
|
||||
@@ -71,55 +61,51 @@ class PRAutoBlogger_Chief_Editor {
|
||||
)
|
||||
);
|
||||
|
||||
$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,
|
||||
'review',
|
||||
$this->llm->get_provider_name(),
|
||||
$response['model'],
|
||||
$response['prompt_tokens'],
|
||||
$response['completion_tokens']
|
||||
null, 'review', $this->llm->get_provider_name(),
|
||||
$response['model'], $response['prompt_tokens'], $response['completion_tokens']
|
||||
);
|
||||
|
||||
return $this->parse_review_response( $response['content'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $niche
|
||||
*
|
||||
* @return string
|
||||
* Build system prompt for editorial review.
|
||||
*/
|
||||
private function build_system_prompt( string $niche ): string {
|
||||
$prompt = 'You are a senior blog editor';
|
||||
if ( '' !== $niche ) {
|
||||
$prompt .= " specializing in {$niche} content";
|
||||
}
|
||||
$prompt .= ". Your job is to review article drafts before publication.\n\n";
|
||||
$prompt .= ". Review article drafts before publication.\n\n";
|
||||
|
||||
$prompt .= "Evaluate the article on:\n";
|
||||
$prompt .= "1. QUALITY: Is the writing clear, engaging, and well-structured?\n";
|
||||
$prompt .= "2. ACCURACY: Does the content avoid making unsupported health/medical claims? Is the tone appropriately cautious for the topic?\n";
|
||||
$prompt .= "3. SEO: Are headings properly structured? Are target keywords naturally used?\n";
|
||||
$prompt .= "4. COMPLETENESS: Does it cover the key points? Does it have a proper intro and conclusion?\n";
|
||||
$prompt .= "5. READABILITY: Is sentence variety good? Are paragraphs appropriate length?\n\n";
|
||||
|
||||
$prompt .= "You must respond with JSON in this format:\n";
|
||||
$prompt .= "{\n";
|
||||
$prompt .= "Evaluate on: QUALITY, ACCURACY, SEO, COMPLETENESS, READABILITY.\n";
|
||||
$prompt .= "Respond with JSON: {\n";
|
||||
$prompt .= ' "verdict": "approved" | "revised" | "rejected",' . "\n";
|
||||
$prompt .= ' "quality_score": 0.0-1.0,' . "\n";
|
||||
$prompt .= ' "seo_score": 0.0-1.0,' . "\n";
|
||||
$prompt .= ' "issues": ["issue 1", "issue 2"],' . "\n";
|
||||
$prompt .= ' "notes": "Brief editorial notes",' . "\n";
|
||||
$prompt .= ' "revised_content": "Full revised HTML if verdict is revised, null otherwise"' . "\n";
|
||||
$prompt .= ' "issues": ["issue1", "issue2"],' . "\n";
|
||||
$prompt .= ' "notes": "Editorial notes",' . "\n";
|
||||
$prompt .= ' "revised_content": "Full revised HTML if revised, null otherwise"' . "\n";
|
||||
$prompt .= "}\n\n";
|
||||
|
||||
$prompt .= "Rules:\n";
|
||||
$prompt .= "- APPROVE if quality_score >= 0.7 and seo_score >= 0.6 and no critical issues\n";
|
||||
$prompt .= "- REVISE if fixable issues exist — you MUST provide the full revised HTML\n";
|
||||
$prompt .= "- REJECT only if the content is fundamentally flawed or off-topic\n";
|
||||
$prompt .= "- When revising, fix the issues yourself. Don't just describe them.\n";
|
||||
$prompt .= "- When revising, PRESERVE all bullet points, numbered lists, hyperlinks, and structural formatting from the original. Do NOT flatten lists into prose or remove links.\n";
|
||||
$prompt .= "- APPROVE if quality_score >= 0.7 and seo_score >= 0.6\n";
|
||||
$prompt .= "- REVISE if fixable issues exist — provide full revised HTML\n";
|
||||
$prompt .= "- REJECT if fundamentally flawed\n";
|
||||
$prompt .= "- Preserve formatting, links, lists when revising\n";
|
||||
|
||||
// Append user-defined editor instructions if configured.
|
||||
$instructions = trim( (string) get_option( 'prautoblogger_editor_instructions', '' ) );
|
||||
if ( '' !== $instructions ) {
|
||||
$prompt .= "\nAdditional instructions:\n" . $instructions . "\n";
|
||||
@@ -129,21 +115,15 @@ class PRAutoBlogger_Chief_Editor {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param PRAutoBlogger_Article_Idea $idea
|
||||
*
|
||||
* @return string
|
||||
* Build user prompt for editorial review.
|
||||
*/
|
||||
private function build_review_prompt( string $content, PRAutoBlogger_Article_Idea $idea ): string {
|
||||
return sprintf(
|
||||
"Review this article draft:\n\n" .
|
||||
"ORIGINAL BRIEF:\n" .
|
||||
"- Title: %s\n" .
|
||||
"- Topic: %s\n" .
|
||||
"- Type: %s\n" .
|
||||
"- Key points: %s\n" .
|
||||
"- Target keywords: %s\n\n" .
|
||||
"ARTICLE CONTENT:\n%s",
|
||||
"BRIEF: Title: %s | Topic: %s | Type: %s\n" .
|
||||
"KEY POINTS: %s\n" .
|
||||
"TARGET KEYWORDS: %s\n\n" .
|
||||
"CONTENT:\n%s",
|
||||
$idea->get_suggested_title(),
|
||||
$idea->get_topic(),
|
||||
$idea->get_article_type(),
|
||||
@@ -154,25 +134,20 @@ class PRAutoBlogger_Chief_Editor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the LLM's editorial review response.
|
||||
*
|
||||
* @param string $content JSON response from LLM.
|
||||
*
|
||||
* @return PRAutoBlogger_Editorial_Review
|
||||
* Parse LLM editorial review response.
|
||||
*/
|
||||
private function parse_review_response( string $content ): PRAutoBlogger_Editorial_Review {
|
||||
$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. Raw (first 500 chars): '
|
||||
. mb_substr( $content, 0, 500 ),
|
||||
'Chief editor response unparseable (first 200 chars): ' . mb_substr( $content, 0, 200 ),
|
||||
'editor'
|
||||
);
|
||||
return new PRAutoBlogger_Editorial_Review(
|
||||
array(
|
||||
'verdict' => 'rejected',
|
||||
'notes' => 'Editor response could not be parsed.',
|
||||
'notes' => 'Editor response unparseable',
|
||||
'quality_score' => 0.0,
|
||||
'seo_score' => 0.0,
|
||||
'issues' => array( 'Unparseable editor response' ),
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Writer agent — generates blog post content from article ideas.
|
||||
*
|
||||
* Supports two configurable pipeline modes:
|
||||
* - single_pass: One LLM call produces the complete article (cheaper, faster).
|
||||
* - multi_step: Outline → Draft → Polish pipeline (higher quality, more cost).
|
||||
* Supports: single_pass (one LLM call) or multi_step (outline → draft → polish).
|
||||
* Opik instrumentation: spans per LLM call when feature-flag enabled.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 4).
|
||||
* Triggered by: PRAutoBlogger_Article_Worker.
|
||||
* Dependencies: LLM_Provider_Interface, Cost_Tracker, Content_Prompts.
|
||||
*
|
||||
* @see core/class-content-prompts.php — Builds all LLM prompts used here.
|
||||
* @see core/class-chief-editor.php — Reviews output of this class.
|
||||
* @see providers/interface-llm-provider.php — LLM used for generation.
|
||||
* @see ARCHITECTURE.md — Data flow step 4.
|
||||
*/
|
||||
class PRAutoBlogger_Content_Generator {
|
||||
|
||||
@@ -34,12 +26,8 @@ class PRAutoBlogger_Content_Generator {
|
||||
/**
|
||||
* Generate a blog post from an article idea.
|
||||
*
|
||||
* Reads pipeline mode from settings and dispatches to the appropriate method.
|
||||
* Side effects: LLM API calls (1-3 depending on mode), cost logging.
|
||||
*
|
||||
* @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for.
|
||||
* @return string Generated HTML content.
|
||||
* @throws \RuntimeException On generation failure.
|
||||
*/
|
||||
public function generate( PRAutoBlogger_Article_Idea $idea ): string {
|
||||
$mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
|
||||
@@ -55,53 +43,58 @@ class PRAutoBlogger_Content_Generator {
|
||||
get_option( 'prautoblogger_writing_instructions', '' )
|
||||
);
|
||||
|
||||
if ( 'single_pass' === $mode ) {
|
||||
return $this->generate_single_pass( $request );
|
||||
}
|
||||
|
||||
return $this->generate_multi_step( $request );
|
||||
return 'single_pass' === $mode
|
||||
? $this->generate_single_pass( $request )
|
||||
: $this->generate_multi_step( $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-pass generation: one LLM call produces the complete article.
|
||||
*
|
||||
* @param PRAutoBlogger_Content_Request $request Content request with settings.
|
||||
* @return string Generated HTML content.
|
||||
* Single-pass: one LLM call produces complete article.
|
||||
*/
|
||||
private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string {
|
||||
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
|
||||
$span_id = $ctx->start_span(
|
||||
array(
|
||||
'name' => 'single_pass_generation',
|
||||
'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' => PRAutoBlogger_Content_Prompts::build_single_pass( $request ),
|
||||
),
|
||||
array( 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ) ),
|
||||
array( 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_single_pass( $request ) ),
|
||||
),
|
||||
$model,
|
||||
array( 'temperature' => 0.7, 'max_tokens' => 4000 )
|
||||
);
|
||||
|
||||
$ctx->end_span(
|
||||
$span_id,
|
||||
array(
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 4000,
|
||||
'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,
|
||||
'draft',
|
||||
$this->llm->get_provider_name(),
|
||||
$response['model'],
|
||||
$response['prompt_tokens'],
|
||||
$response['completion_tokens']
|
||||
null, 'draft', $this->llm->get_provider_name(),
|
||||
$response['model'], $response['prompt_tokens'], $response['completion_tokens']
|
||||
);
|
||||
|
||||
return $response['content'];
|
||||
}
|
||||
|
||||
/** Multi-step generation: Outline → Draft → Polish. */
|
||||
/**
|
||||
* Multi-step: outline → draft → polish.
|
||||
*/
|
||||
private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string {
|
||||
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
|
||||
@@ -112,103 +105,130 @@ class PRAutoBlogger_Content_Generator {
|
||||
return $polished;
|
||||
}
|
||||
|
||||
/** Multi-step stage 1: Generate an article outline. */
|
||||
/**
|
||||
* Stage 1: Generate article outline.
|
||||
*/
|
||||
private function stage_outline( PRAutoBlogger_Content_Request $request, string $model ): string {
|
||||
$system = PRAutoBlogger_Content_Prompts::build_system( $request );
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
|
||||
$span_id = $ctx->start_span(
|
||||
array(
|
||||
'name' => 'outline_generation',
|
||||
'type' => 'llm',
|
||||
'model' => $model,
|
||||
'provider' => 'openrouter',
|
||||
)
|
||||
);
|
||||
|
||||
$response = $this->llm->send_chat_completion(
|
||||
array(
|
||||
array(
|
||||
'role' => 'system',
|
||||
'content' => $system,
|
||||
),
|
||||
array(
|
||||
'role' => 'user',
|
||||
'content' => PRAutoBlogger_Content_Prompts::build_outline( $request ),
|
||||
),
|
||||
array( 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ) ),
|
||||
array( 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_outline( $request ) ),
|
||||
),
|
||||
$model,
|
||||
array( 'temperature' => 0.5, 'max_tokens' => 1500 )
|
||||
);
|
||||
|
||||
$ctx->end_span(
|
||||
$span_id,
|
||||
array(
|
||||
'temperature' => 0.5,
|
||||
'max_tokens' => 1500,
|
||||
'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,
|
||||
'outline',
|
||||
$this->llm->get_provider_name(),
|
||||
$response['model'],
|
||||
$response['prompt_tokens'],
|
||||
$response['completion_tokens']
|
||||
null, 'outline', $this->llm->get_provider_name(),
|
||||
$response['model'], $response['prompt_tokens'], $response['completion_tokens']
|
||||
);
|
||||
|
||||
return $response['content'];
|
||||
}
|
||||
|
||||
/** Multi-step stage 2: Write the full draft from the outline. */
|
||||
/**
|
||||
* Stage 2: Write full draft from outline.
|
||||
*/
|
||||
private function stage_draft( PRAutoBlogger_Content_Request $request, string $outline, string $model ): string {
|
||||
$system = PRAutoBlogger_Content_Prompts::build_system( $request );
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
|
||||
$span_id = $ctx->start_span(
|
||||
array(
|
||||
'name' => 'draft_generation',
|
||||
'type' => 'llm',
|
||||
'model' => $model,
|
||||
'provider' => 'openrouter',
|
||||
)
|
||||
);
|
||||
|
||||
$response = $this->llm->send_chat_completion(
|
||||
array(
|
||||
array(
|
||||
'role' => 'system',
|
||||
'content' => $system,
|
||||
),
|
||||
array(
|
||||
'role' => 'user',
|
||||
'content' => PRAutoBlogger_Content_Prompts::build_draft( $request, $outline ),
|
||||
),
|
||||
array( 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ) ),
|
||||
array( 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_draft( $request, $outline ) ),
|
||||
),
|
||||
$model,
|
||||
array( 'temperature' => 0.7, 'max_tokens' => 4000 )
|
||||
);
|
||||
|
||||
$ctx->end_span(
|
||||
$span_id,
|
||||
array(
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 4000,
|
||||
'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,
|
||||
'draft',
|
||||
$this->llm->get_provider_name(),
|
||||
$response['model'],
|
||||
$response['prompt_tokens'],
|
||||
$response['completion_tokens']
|
||||
null, 'draft', $this->llm->get_provider_name(),
|
||||
$response['model'], $response['prompt_tokens'], $response['completion_tokens']
|
||||
);
|
||||
|
||||
return $response['content'];
|
||||
}
|
||||
|
||||
/** Multi-step stage 3: Polish and refine the draft. */
|
||||
/**
|
||||
* Stage 3: Polish and refine draft.
|
||||
*/
|
||||
private function stage_polish( PRAutoBlogger_Content_Request $request, string $draft, string $model ): string {
|
||||
$system = PRAutoBlogger_Content_Prompts::build_system( $request );
|
||||
$ctx = PRAutoBlogger_Opik_Trace_Context::current();
|
||||
|
||||
$span_id = $ctx->start_span(
|
||||
array(
|
||||
'name' => 'polish_generation',
|
||||
'type' => 'llm',
|
||||
'model' => $model,
|
||||
'provider' => 'openrouter',
|
||||
)
|
||||
);
|
||||
|
||||
$response = $this->llm->send_chat_completion(
|
||||
array(
|
||||
array(
|
||||
'role' => 'system',
|
||||
'content' => $system,
|
||||
),
|
||||
array(
|
||||
'role' => 'user',
|
||||
'content' => PRAutoBlogger_Content_Prompts::build_polish( $draft ),
|
||||
),
|
||||
array( 'role' => 'system', 'content' => PRAutoBlogger_Content_Prompts::build_system( $request ) ),
|
||||
array( 'role' => 'user', 'content' => PRAutoBlogger_Content_Prompts::build_polish( $draft ) ),
|
||||
),
|
||||
$model,
|
||||
array( 'temperature' => 0.4, 'max_tokens' => 4000 )
|
||||
);
|
||||
|
||||
$ctx->end_span(
|
||||
$span_id,
|
||||
array(
|
||||
'temperature' => 0.4,
|
||||
'max_tokens' => 4000,
|
||||
'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,
|
||||
'polish',
|
||||
$this->llm->get_provider_name(),
|
||||
$response['model'],
|
||||
$response['prompt_tokens'],
|
||||
$response['completion_tokens']
|
||||
null, 'polish', $this->llm->get_provider_name(),
|
||||
$response['model'], $response['prompt_tokens'], $response['completion_tokens']
|
||||
);
|
||||
|
||||
return $response['content'];
|
||||
|
||||
Reference in New Issue
Block a user