first commit
This commit is contained in:
88
src/Agent/ThinkSuppressor.php
Normal file
88
src/Agent/ThinkSuppressor.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?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 = '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user