fix p44
This commit is contained in:
@@ -2682,6 +2682,7 @@ final readonly class AgentRunner
|
||||
{
|
||||
$fullOutput = '';
|
||||
$thinkingNoticeShown = false;
|
||||
$stoppedByFinalAnswerGuard = false;
|
||||
$chunker = new StreamChunker();
|
||||
|
||||
$this->thinkSuppressor->reset();
|
||||
@@ -2706,11 +2707,36 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullOutput .= $cleanToken;
|
||||
$guardReason = null;
|
||||
$cleanToken = $this->guardFinalAnswerToken($fullOutput, $cleanToken, $guardReason);
|
||||
|
||||
$chunk = $chunker->push($cleanToken);
|
||||
if ($chunk !== null) {
|
||||
yield $this->systemMsg($chunk, 'answer');
|
||||
if ($cleanToken !== '') {
|
||||
$fullOutput .= $cleanToken;
|
||||
|
||||
$chunk = $chunker->push($cleanToken);
|
||||
if ($chunk !== null) {
|
||||
yield $this->systemMsg($chunk, 'answer');
|
||||
}
|
||||
}
|
||||
|
||||
if ($guardReason !== null) {
|
||||
$stoppedByFinalAnswerGuard = true;
|
||||
|
||||
$finalChunk = $chunker->flush();
|
||||
if ($finalChunk !== null) {
|
||||
yield $this->systemMsg($finalChunk, 'answer');
|
||||
}
|
||||
|
||||
$guardMessage = $this->agentRunnerConfig->getFinalAnswerGuardTruncationMessage();
|
||||
$fullOutput .= $guardMessage;
|
||||
yield $this->systemMsg($guardMessage, 'answer');
|
||||
|
||||
$this->agentLogger->warning('Final answer guard stopped LLM output', [
|
||||
'reason' => $guardReason,
|
||||
'outputLength' => mb_strlen($fullOutput, 'UTF-8'),
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
@@ -2730,6 +2756,10 @@ final readonly class AgentRunner
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
if ($stoppedByFinalAnswerGuard) {
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
$finalChunk = $chunker->flush();
|
||||
if ($finalChunk !== null) {
|
||||
yield $this->systemMsg($finalChunk, 'answer');
|
||||
@@ -2747,6 +2777,112 @@ final readonly class AgentRunner
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
private function guardFinalAnswerToken(string $currentOutput, string $nextToken, ?string &$reason): string
|
||||
{
|
||||
$reason = null;
|
||||
|
||||
if (!$this->agentRunnerConfig->isFinalAnswerGuardEnabled()) {
|
||||
return $nextToken;
|
||||
}
|
||||
|
||||
$maxOutputChars = max(1000, $this->agentRunnerConfig->getFinalAnswerGuardMaxOutputChars());
|
||||
$currentChars = mb_strlen($currentOutput, 'UTF-8');
|
||||
$nextChars = mb_strlen($nextToken, 'UTF-8');
|
||||
|
||||
if (($currentChars + $nextChars) > $maxOutputChars) {
|
||||
$reason = 'max_output_chars';
|
||||
$remainingChars = max(0, $maxOutputChars - $currentChars);
|
||||
|
||||
return $remainingChars > 0 ? mb_substr($nextToken, 0, $remainingChars, 'UTF-8') : '';
|
||||
}
|
||||
|
||||
$candidate = $currentOutput . $nextToken;
|
||||
$cutoffBytes = $this->detectRepeatedFinalAnswerLineCutoff($candidate);
|
||||
|
||||
if ($cutoffBytes === null) {
|
||||
return $nextToken;
|
||||
}
|
||||
|
||||
$reason = 'repeated_line';
|
||||
$currentBytes = strlen($currentOutput);
|
||||
|
||||
if ($cutoffBytes <= $currentBytes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strcut($nextToken, 0, $cutoffBytes - $currentBytes, 'UTF-8');
|
||||
}
|
||||
|
||||
private function detectRepeatedFinalAnswerLineCutoff(string $text): ?int
|
||||
{
|
||||
if (!$this->agentRunnerConfig->isFinalAnswerRepeatedLineGuardEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mb_strlen($text, 'UTF-8') < max(0, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinOutputChars())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match_all('/[^\r\n]+/u', $text, $matches, PREG_OFFSET_CAPTURE) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lines = $matches[0] ?? [];
|
||||
$window = max(10, $this->agentRunnerConfig->getFinalAnswerRepeatedLineTrailingWindowLines());
|
||||
if (count($lines) > $window) {
|
||||
$lines = array_slice($lines, -$window);
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
$maxRepetitions = max(1, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMaxRepetitions());
|
||||
|
||||
foreach ($lines as $lineMatch) {
|
||||
$line = (string) ($lineMatch[0] ?? '');
|
||||
$offset = (int) ($lineMatch[1] ?? 0);
|
||||
$normalizedLine = $this->normalizeFinalAnswerLineForRepetitionGuard($line);
|
||||
|
||||
if ($normalizedLine === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counts[$normalizedLine] = ($counts[$normalizedLine] ?? 0) + 1;
|
||||
|
||||
if ($counts[$normalizedLine] > $maxRepetitions) {
|
||||
return $offset;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeFinalAnswerLineForRepetitionGuard(string $line): string
|
||||
{
|
||||
$line = html_entity_decode(strip_tags($line), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$line = preg_replace('/^\s*(?:[-*•]+|\d+[.)])\s*/u', '', $line) ?? $line;
|
||||
$line = preg_replace('/\s+/u', ' ', $line) ?? $line;
|
||||
$line = trim($line, " \t\n\r\0\x0B:;.-");
|
||||
|
||||
if ($line === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getFinalAnswerRepeatedLineIgnorePatterns() as $pattern) {
|
||||
try {
|
||||
if (@preg_match($pattern, $line) === 1) {
|
||||
return '';
|
||||
}
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (mb_strlen($line, 'UTF-8') < max(1, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinLineChars())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strtolower($line, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic safety answer for environments where the LLM returns no tokens.
|
||||
*
|
||||
|
||||
@@ -684,6 +684,54 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredString('messages.no_llm_data_received');
|
||||
}
|
||||
|
||||
public function isFinalAnswerGuardEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('final_answer_guard.enabled');
|
||||
}
|
||||
|
||||
public function getFinalAnswerGuardMaxOutputChars(): int
|
||||
{
|
||||
return $this->getRequiredInt('final_answer_guard.max_output_chars');
|
||||
}
|
||||
|
||||
public function getFinalAnswerGuardTruncationMessage(): string
|
||||
{
|
||||
return $this->getRequiredString('final_answer_guard.truncation_message');
|
||||
}
|
||||
|
||||
public function isFinalAnswerRepeatedLineGuardEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('final_answer_guard.repeated_line.enabled');
|
||||
}
|
||||
|
||||
public function getFinalAnswerRepeatedLineMinOutputChars(): int
|
||||
{
|
||||
return $this->getRequiredInt('final_answer_guard.repeated_line.min_output_chars');
|
||||
}
|
||||
|
||||
public function getFinalAnswerRepeatedLineMinLineChars(): int
|
||||
{
|
||||
return $this->getRequiredInt('final_answer_guard.repeated_line.min_line_chars');
|
||||
}
|
||||
|
||||
public function getFinalAnswerRepeatedLineMaxRepetitions(): int
|
||||
{
|
||||
return $this->getRequiredInt('final_answer_guard.repeated_line.max_line_repetitions');
|
||||
}
|
||||
|
||||
public function getFinalAnswerRepeatedLineTrailingWindowLines(): int
|
||||
{
|
||||
return $this->getRequiredInt('final_answer_guard.repeated_line.trailing_window_lines');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFinalAnswerRepeatedLineIgnorePatterns(): array
|
||||
{
|
||||
return $this->getRequiredStringList('final_answer_guard.repeated_line.ignore_patterns');
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackMaxShopResults(): int
|
||||
{
|
||||
return $this->getRequiredInt('no_llm_fallback.max_shop_results');
|
||||
|
||||
@@ -42,7 +42,10 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
'runtime' => $this->runtimeConfig(),
|
||||
'index' => $this->indexConfig(),
|
||||
'model_generation' => $this->modelConfig(),
|
||||
'llm' => ['timeout_seconds' => $this->param('retriex.llm.timeout_seconds')],
|
||||
'llm' => [
|
||||
'timeout_seconds' => $this->param('retriex.llm.timeout_seconds'),
|
||||
'num_predict' => $this->param('retriex.llm.num_predict'),
|
||||
],
|
||||
'retrieval' => $this->retrievalConfig(),
|
||||
'prompt' => $this->promptConfig(),
|
||||
'agent' => $this->agentConfig(),
|
||||
@@ -639,6 +642,19 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
'generic_internal_error' => $this->agentRunnerConfig->getGenericInternalErrorMessage(),
|
||||
'debug_internal_error_prefix' => $this->agentRunnerConfig->getDebugInternalErrorPrefix(),
|
||||
],
|
||||
'final_answer_guard' => [
|
||||
'enabled' => $this->agentRunnerConfig->isFinalAnswerGuardEnabled(),
|
||||
'max_output_chars' => $this->agentRunnerConfig->getFinalAnswerGuardMaxOutputChars(),
|
||||
'truncation_message' => $this->agentRunnerConfig->getFinalAnswerGuardTruncationMessage(),
|
||||
'repeated_line' => [
|
||||
'enabled' => $this->agentRunnerConfig->isFinalAnswerRepeatedLineGuardEnabled(),
|
||||
'min_output_chars' => $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinOutputChars(),
|
||||
'min_line_chars' => $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinLineChars(),
|
||||
'max_line_repetitions' => $this->agentRunnerConfig->getFinalAnswerRepeatedLineMaxRepetitions(),
|
||||
'trailing_window_lines' => $this->agentRunnerConfig->getFinalAnswerRepeatedLineTrailingWindowLines(),
|
||||
'ignore_patterns' => $this->agentRunnerConfig->getFinalAnswerRepeatedLineIgnorePatterns(),
|
||||
],
|
||||
],
|
||||
'rag_evidence_guard' => [
|
||||
'cleanup_profile' => $this->agentRunnerConfig->getRagEvidenceCleanupProfile(),
|
||||
'stop_terms' => $this->agentRunnerConfig->getRagEvidenceStopTerms(),
|
||||
|
||||
@@ -22,6 +22,7 @@ final class OllamaClient
|
||||
public function __construct(
|
||||
private string $apiUrl,
|
||||
private string $timeoutSeconds,
|
||||
private int|string $numPredict,
|
||||
private ModelGenerationConfigProvider $configProvider
|
||||
) {}
|
||||
|
||||
@@ -188,13 +189,20 @@ final class OllamaClient
|
||||
private function buildOptions(): array
|
||||
{
|
||||
$this->config = $this->getConfig();
|
||||
return [
|
||||
$options = [
|
||||
'temperature' => $this->config->getTemperature(),
|
||||
'top_k' => $this->config->getTopK(),
|
||||
'top_p' => $this->config->getTopP(),
|
||||
'repeat_penalty' => $this->config->getRepeatPenalty(),
|
||||
'num_ctx' => $this->config->getNumCtx(),
|
||||
];
|
||||
|
||||
$numPredict = (int) $this->numPredict;
|
||||
if ($numPredict > 0) {
|
||||
$options['num_predict'] = $numPredict;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function requestTimeoutSeconds(): int
|
||||
|
||||
Reference in New Issue
Block a user