| 1 |
<?php |
| 2 |
|
| 3 |
namespace Better_Payment\Lite\AI\Services; |
| 4 |
|
| 5 |
if ( ! defined( 'ABSPATH' ) ) { |
| 6 |
exit; |
| 7 |
} |
| 8 |
|
| 9 |
/** |
| 10 |
* Campaign brief writer / refiner — the wizard's pre-generation step. |
| 11 |
* |
| 12 |
* Runs {@see AIService} in the text-only 'brief' mode. Given the brief the Smart |
| 13 |
* Prompt Wizard assembled (and, optionally, a plain-language instruction), it |
| 14 |
* returns an improved brief *as text* — nothing is applied to any campaign. The |
| 15 |
* user reviews and edits the result, then hands it to the 'generate' turn. |
| 16 |
* |
| 17 |
* This class exists so the wizard can offer "Refine with AI" without the generate |
| 18 |
* pipeline: no operations, no layout, no guards to run — just prose in, prose out. |
| 19 |
* The one guarantee it adds on top of the raw turn is that the reply is cleaned of |
| 20 |
* any machine wrapping (stray quotes, code fences) before it reaches the editable |
| 21 |
* brief box, since the model is told to emit bare prose but a prompt is a request. |
| 22 |
* |
| 23 |
* @see \Better_Payment\Lite\AI\Prompt\Templates (brief.php) for the hard rules — |
| 24 |
* chiefly that it never invents or alters a goal amount or an end date, which |
| 25 |
* are owned by the wizard's structured answers and enforced at generation. |
| 26 |
*/ |
| 27 |
class BriefWriter { |
| 28 |
|
| 29 |
/** |
| 30 |
* Improve a campaign brief. |
| 31 |
* |
| 32 |
* @param string $brief The current brief text (the wizard's assembled |
| 33 |
* brief, or a previously refined one). |
| 34 |
* @param string $instruction Optional plain-language "make it more…" request. |
| 35 |
* '' means a general polish. |
| 36 |
* @return array|\WP_Error { brief: string, usage: array } |
| 37 |
*/ |
| 38 |
public static function write( string $brief, string $instruction = '' ) { |
| 39 |
$brief = trim( $brief ); |
| 40 |
if ( '' === $brief ) { |
| 41 |
return new \WP_Error( |
| 42 |
'ai_brief_missing', |
| 43 |
__( 'There is no brief to refine yet.', 'better-payment' ), |
| 44 |
[ 'status' => 400 ] |
| 45 |
); |
| 46 |
} |
| 47 |
|
| 48 |
$result = AIService::run( 'brief', self::compose_message( $brief, trim( $instruction ) ) ); |
| 49 |
if ( is_wp_error( $result ) ) { |
| 50 |
return $result; |
| 51 |
} |
| 52 |
|
| 53 |
$text = self::clean( (string) ( $result['assistant_message'] ?? '' ) ); |
| 54 |
if ( '' === $text ) { |
| 55 |
return new \WP_Error( |
| 56 |
'ai_brief_empty', |
| 57 |
__( 'The assistant did not return an improved brief. Please try again.', 'better-payment' ), |
| 58 |
[ 'status' => 502 ] |
| 59 |
); |
| 60 |
} |
| 61 |
|
| 62 |
return [ |
| 63 |
'brief' => $text, |
| 64 |
'usage' => $result['usage'] ?? [], |
| 65 |
]; |
| 66 |
} |
| 67 |
|
| 68 |
/** |
| 69 |
* Build the user message: the current brief, plus the improvement instruction |
| 70 |
* when there is one. Labelled plainly so the model knows which part is the |
| 71 |
* brief to rewrite and which is the direction to follow. |
| 72 |
* |
| 73 |
* @param string $brief |
| 74 |
* @param string $instruction |
| 75 |
* @return string |
| 76 |
*/ |
| 77 |
private static function compose_message( string $brief, string $instruction ): string { |
| 78 |
if ( '' === $instruction ) { |
| 79 |
return "Current brief:\n\n" . $brief |
| 80 |
. "\n\nRewrite this brief so it is more vivid, specific and complete, following the rules above."; |
| 81 |
} |
| 82 |
|
| 83 |
return "Current brief:\n\n" . $brief |
| 84 |
. "\n\nImprove the brief following this instruction: " . $instruction; |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Strip machine wrapping from a reply meant to be bare prose. |
| 89 |
* |
| 90 |
* The model is told to return the brief and nothing else, but occasionally |
| 91 |
* wraps it in a code fence or surrounding quotes. Remove those so they don't |
| 92 |
* land in the user's editable brief box. This unwraps structure; it does not |
| 93 |
* rewrite the writing. |
| 94 |
* |
| 95 |
* @param string $text |
| 96 |
* @return string |
| 97 |
*/ |
| 98 |
private static function clean( string $text ): string { |
| 99 |
$text = trim( $text ); |
| 100 |
|
| 101 |
// A fully fenced reply → keep only the fence's contents. |
| 102 |
if ( preg_match( '/^```[a-z]*\s*\n(.*?)\n?```$/is', $text, $matches ) ) { |
| 103 |
$text = trim( $matches[1] ); |
| 104 |
} |
| 105 |
|
| 106 |
// Surrounding matched quotes the model sometimes adds around the whole brief. |
| 107 |
if ( strlen( $text ) >= 2 ) { |
| 108 |
$first = $text[0]; |
| 109 |
$last = $text[ strlen( $text ) - 1 ]; |
| 110 |
if ( ( '"' === $first && '"' === $last ) || ( "'" === $first && "'" === $last ) ) { |
| 111 |
$text = trim( substr( $text, 1, -1 ) ); |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
return $text; |
| 116 |
} |
| 117 |
} |
| 118 |
|