89 lines
2.5 KiB
PHP
89 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Agent;
|
|
|
|
/**
|
|
* ThinkSuppressor
|
|
*
|
|
* Robust streaming-safe suppressor for internal <think>...</think> sections.
|
|
*
|
|
* Key properties:
|
|
* - Handles token fragmentation (partial tags across tokens)
|
|
* - Stateful per stream, stateless per request
|
|
* - Does not buffer full responses
|
|
* - Deterministic and predictable
|
|
*/
|
|
final class ThinkSuppressor
|
|
{
|
|
/** Indicates whether the stream is currently inside a <think> block. */
|
|
private bool $insideThink = false;
|
|
|
|
/** Indicates whether the think section has been fully closed. */
|
|
private bool $thinkSectionCompleted = false;
|
|
|
|
/**
|
|
* Rolling buffer for detecting fragmented tags across tokens.
|
|
*/
|
|
private string $rollingBuffer = '';
|
|
|
|
/**
|
|
* Maximum buffer length needed to safely detect tags.
|
|
*/
|
|
private int $maxBufferLength = 32;
|
|
|
|
/**
|
|
* Filters a single token from the LLM stream.
|
|
*
|
|
* @param string $token Raw token from the LLM
|
|
* @return string Cleaned token safe for user output
|
|
*/
|
|
public function filter(string $token): string
|
|
{
|
|
// Append to rolling buffer
|
|
$this->rollingBuffer .= $token;
|
|
if (strlen($this->rollingBuffer) > $this->maxBufferLength) {
|
|
$this->rollingBuffer = substr($this->rollingBuffer, -$this->maxBufferLength);
|
|
}
|
|
|
|
// If think section is already completed, just strip stray closing tags
|
|
if ($this->thinkSectionCompleted) {
|
|
return str_replace('</think>', '', $token);
|
|
}
|
|
|
|
// Detect fragmented opening <think> tag
|
|
if (!$this->insideThink && str_contains($this->rollingBuffer, '<think>')) {
|
|
$this->insideThink = true;
|
|
return '';
|
|
}
|
|
|
|
// Detect fragmented closing </think> tag
|
|
if ($this->insideThink && str_contains($this->rollingBuffer, '</think>')) {
|
|
$this->insideThink = false;
|
|
$this->thinkSectionCompleted = true;
|
|
|
|
// Emit a single line break after think section ends
|
|
return "\n";
|
|
}
|
|
|
|
// Suppress all content while inside <think>...</think>
|
|
if ($this->insideThink) {
|
|
return '';
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Resets the suppressor state.
|
|
* Must be called before starting a new stream.
|
|
*/
|
|
public function reset(): void
|
|
{
|
|
$this->insideThink = false;
|
|
$this->thinkSectionCompleted = false;
|
|
$this->rollingBuffer = '';
|
|
}
|
|
}
|