69 lines
1.9 KiB
PHP
69 lines
1.9 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Exceptions\PromptRenderException;
|
||
|
|
use RuntimeException;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Pure prompt template renderer. No framework dependencies.
|
||
|
|
*
|
||
|
|
* Variable syntax: `{{ name }}` where name matches `[A-Za-z0-9_-]+`.
|
||
|
|
* Rendering is strict: it throws PromptRenderException when any variable
|
||
|
|
* lacks a string value. Values are inserted verbatim, in a single pass
|
||
|
|
* (inserted values are never re-scanned for variables).
|
||
|
|
*/
|
||
|
|
class PromptRenderer
|
||
|
|
{
|
||
|
|
public const VARIABLE_PATTERN = '/\{\{\s*([A-Za-z0-9_-]+)\s*\}\}/';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract the unique variable names in first-occurrence order.
|
||
|
|
*
|
||
|
|
* @return list<string>
|
||
|
|
*/
|
||
|
|
public function extractVariables(string $body): array
|
||
|
|
{
|
||
|
|
if (preg_match_all(self::VARIABLE_PATTERN, $body, $matches) === false) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
return array_values(array_unique($matches[1]));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render the template with the given values.
|
||
|
|
*
|
||
|
|
* @param array<string, mixed> $values
|
||
|
|
*
|
||
|
|
* @throws PromptRenderException when any variable has no string value
|
||
|
|
* @throws RuntimeException when the template cannot be processed
|
||
|
|
*/
|
||
|
|
public function render(string $body, array $values): string
|
||
|
|
{
|
||
|
|
$missing = [];
|
||
|
|
|
||
|
|
foreach ($this->extractVariables($body) as $variable) {
|
||
|
|
if (! array_key_exists($variable, $values) || ! is_string($values[$variable])) {
|
||
|
|
$missing[] = $variable;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($missing !== []) {
|
||
|
|
throw PromptRenderException::missing($missing);
|
||
|
|
}
|
||
|
|
|
||
|
|
$rendered = preg_replace_callback(
|
||
|
|
self::VARIABLE_PATTERN,
|
||
|
|
static fn (array $matches): string => (string) ($values[$matches[1]] ?? $matches[0]),
|
||
|
|
$body
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($rendered === null) {
|
||
|
|
throw new RuntimeException('Prompt template could not be rendered.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return $rendered;
|
||
|
|
}
|
||
|
|
}
|