wpmind/includes/API/Services/StructuredOutputService.php
LinuxJoy adb6c49301 style: phpcbf 自动格式化 — 全量 WPCS 3.x 规范对齐
135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、
函数声明空格、前置自增、尾逗号等纯格式化改动。
零逻辑变更,php -l + token 级对比验证通过。

新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:48:46 +08:00

255 lines
6.1 KiB
PHP
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.

<?php
/**
* Structured Output Service
*
* 处理结构化输出JSON Schema和批量处理
*
* @package WPMind
* @subpackage API\Services
* @since 3.7.0
*/
declare(strict_types=1);
namespace WPMind\API\Services;
use WP_Error;
/**
* Structured Output Service
*
* @since 3.7.0
*/
class StructuredOutputService extends AbstractService {
private ChatService $chat_service;
public function __construct( ChatService $chat_service ) {
$this->chat_service = $chat_service;
}
/**
* 结构化输出JSON Schema
*
* @since 2.6.0
* @param array|string $messages 消息
* @param array $schema JSON Schema
* @param array $options 选项
* @return array|WP_Error
*/
public function structured( $messages, array $schema, array $options = [] ) {
$defaults = [
'context' => 'structured',
'max_tokens' => 2000,
'temperature' => 0.3,
'retries' => 3,
];
$options = wp_parse_args( $options, $defaults );
$context = $options['context'];
$schema_json = wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );
$schema_prompt = "你必须返回严格符合以下 JSON Schema 的 JSON 对象。不要返回其他内容,只返回 JSON\n\n```json\n{$schema_json}\n```";
if ( is_string( $messages ) ) {
$messages = [
[
'role' => 'system',
'content' => $schema_prompt,
],
[
'role' => 'user',
'content' => $messages,
],
];
} else {
array_unshift(
$messages,
[
'role' => 'system',
'content' => $schema_prompt,
]
);
}
$max_retries = $options['retries'];
$last_error = null;
for ( $attempt = 1; $attempt <= $max_retries; $attempt++ ) {
$result = $this->chat_service->chat(
$messages,
[
'context' => $context,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'json_mode' => true,
'cache_ttl' => 0,
]
);
if ( is_wp_error( $result ) ) {
return $result;
}
$content = $result['content'];
$parsed = json_decode( $content, true );
if ( json_last_error() === JSON_ERROR_NONE ) {
if ( $this->validate_schema( $parsed, $schema ) ) {
return [
'data' => $parsed,
'provider' => $result['provider'],
'model' => $result['model'],
'usage' => $result['usage'],
'attempts' => $attempt,
];
}
}
$last_error = json_last_error_msg();
$messages[] = [
'role' => 'assistant',
'content' => $content,
];
$messages[] = [
'role' => 'user',
'content' => "JSON 解析失败或不符合 Schema: {$last_error}。请重新生成严格符合 Schema 的 JSON。",
];
}
return new WP_Error(
'wpmind_structured_failed',
sprintf( __( '结构化输出失败(尝试 %1$d 次): %2$s', 'wpmind' ), $max_retries, $last_error )
);
}
/**
* 批量处理
*
* @since 2.6.0
* @param array $items 要处理的项目数组
* @param string $prompt_template Prompt 模板
* @param array $options 选项
* @return array|WP_Error
*/
public function batch( array $items, string $prompt_template, array $options = [] ) {
$defaults = [
'context' => 'batch',
'max_tokens' => 500,
'temperature' => 0.7,
'concurrency' => 1,
'delay_ms' => 100,
'stop_on_error' => false,
];
$options = wp_parse_args( $options, $defaults );
$context = $options['context'];
$results = [];
$errors = [];
do_action( 'wpmind_before_request', 'batch', compact( 'items', 'prompt_template', 'options' ), $context );
foreach ( $items as $index => $item ) {
$item_str = is_array( $item ) ? wp_json_encode( $item, JSON_UNESCAPED_UNICODE ) : (string) $item;
$prompt = str_replace( '{{item}}', $item_str, $prompt_template );
$prompt = str_replace( '{{index}}', (string) $index, $prompt );
$result = $this->chat_service->chat(
$prompt,
[
'context' => $context . '_item_' . $index,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'cache_ttl' => 0,
]
);
if ( is_wp_error( $result ) ) {
$errors[ $index ] = $result->get_error_message();
if ( $options['stop_on_error'] ) {
break;
}
$results[ $index ] = null;
} else {
$results[ $index ] = [
'content' => $result['content'],
'usage' => $result['usage'],
];
}
if ( $options['delay_ms'] > 0 && $index < count( $items ) - 1 ) {
usleep( $options['delay_ms'] * 1000 );
}
}
$total_tokens = array_sum(
array_map(
function ( $r ) {
return $r['usage']['total_tokens'] ?? 0;
},
array_filter( $results )
)
);
do_action( 'wpmind_after_request', 'batch', $results, compact( 'items', 'options' ), [ 'total_tokens' => $total_tokens ] );
return [
'results' => $results,
'errors' => $errors,
'total_items' => count( $items ),
'success_count' => count( array_filter( $results ) ),
'error_count' => count( $errors ),
'total_tokens' => $total_tokens,
];
}
/**
* 验证 JSON Schema简化版
*
* @param array $data 数据
* @param array $schema Schema
* @return bool
*/
public function validate_schema( array $data, array $schema ): bool {
if ( isset( $schema['required'] ) ) {
foreach ( $schema['required'] as $field ) {
if ( ! isset( $data[ $field ] ) ) {
return false;
}
}
}
if ( isset( $schema['properties'] ) ) {
foreach ( $schema['properties'] as $key => $prop ) {
if ( ! isset( $data[ $key ] ) ) {
continue;
}
$value = $data[ $key ];
$type = $prop['type'] ?? null;
if ( $type === 'string' && ! is_string( $value ) ) {
return false;
}
if ( $type === 'integer' && ! is_int( $value ) ) {
return false;
}
if ( $type === 'number' && ! is_numeric( $value ) ) {
return false;
}
if ( $type === 'boolean' && ! is_bool( $value ) ) {
return false;
}
if ( $type === 'array' && ! is_array( $value ) ) {
return false;
}
if ( $type === 'object' && ! is_array( $value ) ) {
return false;
}
}
}
return true;
}
}