StreamingChatMessageUseCase.php
- Pfad:
src/UseCases/Chat/StreamingChatMessageUseCase.php - Namespace: UseCases\Chat
- Zeilen: 332 | Größe: 13,928 Bytes
- Geändert: 2025-12-30 03:15:34 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 72
- Dependencies: 60 (25%)
- LOC: 9 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 19
- constructor Infrastructure\AI\OllamaService
- constructor Infrastructure\AI\QdrantService
- constructor Infrastructure\AI\ClaudeService
- constructor Infrastructure\AI\ScoringService
- constructor Domain\Repository\ChatSessionRepositoryInterface
- constructor Domain\Repository\ChatMessageRepositoryInterface
- constructor UseCases\Chat\ChatPromptLoader
- constructor Infrastructure\AI\ContentQualityValidator
- constructor UseCases\Chat\RagContextBuilder
- constructor Infrastructure\Logging\KiProtokollService
- use Domain\Constants
- use Domain\Repository\ChatMessageRepositoryInterface
- use Domain\Repository\ChatSessionRepositoryInterface
- use Infrastructure\AI\ClaudeService
- use Infrastructure\AI\ContentQualityValidator
- use Infrastructure\AI\OllamaService
- use Infrastructure\AI\QdrantService
- use Infrastructure\AI\ScoringService
- use Infrastructure\Logging\KiProtokollService
Klassen 1
-
StreamingChatMessageUseCaseclass Zeile 19
Funktionen 7
-
__construct()public Zeile 26 -
setProgressCallback()public Zeile 47 -
emit()private Zeile 52 -
startStep()private Zeile 59 -
endStep()private Zeile 64 -
execute()public Zeile 75 -
addQdrantFallback()Zeile 298
Verwendet von 3
- ChatController.php constructor
- ChatController.php use
- ChatServiceProvider.php use
Versionen 29
-
v29
2025-12-29 09:09 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v28
2025-12-29 08:47 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v27
2025-12-29 08:46 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v26
2025-12-29 08:46 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v25
2025-12-29 08:46 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v24
2025-12-29 08:45 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v23
2025-12-29 08:45 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v22
2025-12-29 08:45 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v21
2025-12-29 00:04 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v20
2025-12-29 00:03 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v19
2025-12-29 00:02 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v18
2025-12-29 00:02 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v17
2025-12-29 00:02 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v16
2025-12-28 23:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v15
2025-12-28 23:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v14
2025-12-28 23:25 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v13
2025-12-28 23:23 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v12
2025-12-28 23:22 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v11
2025-12-28 23:21 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v10
2025-12-28 23:21 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v9
2025-12-28 23:21 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v8
2025-12-28 02:37 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v7
2025-12-28 02:36 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v6
2025-12-28 02:35 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v5
2025-12-28 02:34 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v4
2025-12-27 23:16 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v3
2025-12-27 23:16 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v2
2025-12-27 11:23 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation -
v1
2025-12-26 20:31 | claude-code-hook | modified
Claude Code Pre-Hook Backup vor Edit-Operation
Code
<?php
declare(strict_types=1);
namespace UseCases\Chat;
// @responsibility: Orchestriert Chat-Nachrichten mit SSE-Progress-Events
use Domain\Constants;
use Domain\Repository\ChatMessageRepositoryInterface;
use Domain\Repository\ChatSessionRepositoryInterface;
use Infrastructure\AI\ClaudeService;
use Infrastructure\AI\ContentQualityValidator;
use Infrastructure\AI\OllamaService;
use Infrastructure\AI\QdrantService;
use Infrastructure\AI\ScoringService;
use Infrastructure\Logging\KiProtokollService;
class StreamingChatMessageUseCase
{
/** @var callable|null */
private $progressCallback;
private float $stepStart = 0.0;
public function __construct(
private OllamaService $ollama,
private QdrantService $qdrant,
private ClaudeService $claude,
ScoringService $scoring,
private ChatSessionRepositoryInterface $sessionRepo,
private ChatMessageRepositoryInterface $messageRepo,
private ChatPromptLoader $promptLoader,
private ContentQualityValidator $qualityValidator,
private RagContextBuilder $ragBuilder,
private KiProtokollService $protokollService
) {
// $scoring kept for BC but no longer used - ContentSearchService handles reranking
unset($scoring);
}
/**
* Set progress callback for SSE events
*
* @param callable $callback fn(string $step, string $message, ?int $durationMs): void
*/
public function setProgressCallback(callable $callback): void
{
$this->progressCallback = $callback;
}
private function emit(string $step, string $message, ?int $durationMs = null): void
{
if ($this->progressCallback !== null) {
($this->progressCallback)($step, $message, $durationMs);
}
}
private function startStep(): void
{
$this->stepStart = microtime(true);
}
private function endStep(string $step, string $message): void
{
$durationMs = (int) round((microtime(true) - $this->stepStart) * Constants::MS_PER_SECOND);
$this->emit($step, $message, $durationMs);
}
/**
* Execute chat with streaming progress.
*
* @param array<string> $collections
*/
public function execute(
string $sessionUuid,
string $message,
string $model,
array $collections = ['documents'],
int $contextLimit = 5,
int $authorProfileId = 0,
int $systemPromptId = 1,
float $temperature = 0.7,
int $maxTokens = 4096,
int $structureId = 0,
bool $qualityCheck = false,
string $requestIp = '127.0.0.1'
): ChatResponse {
$totalStart = microtime(true);
// Log to protokoll (crash-safe, returns null on failure)
$protokollId = $this->protokollService->logRequest('web-chat', $message, $model, $requestIp);
// Step 1: Validate session
$this->emit('session', 'Session validieren...');
$this->startStep();
$session = $this->sessionRepo->findByUuid($sessionUuid);
if ($session === null) {
$this->emit('error', 'Session nicht gefunden');
if ($protokollId !== null) {
$this->protokollService->logFailure($protokollId, 'Session nicht gefunden');
}
return ChatResponse::error('Session nicht gefunden.');
}
$sessionId = $session->getId() ?? 0;
$this->endStep('session_done', 'Session validiert');
// Step 2: Validate message
$message = trim($message);
if ($message === '') {
$this->emit('error', 'Keine Nachricht');
if ($protokollId !== null) {
$this->protokollService->logFailure($protokollId, 'Leere Nachricht');
}
return ChatResponse::error('Bitte gib eine Frage ein.');
}
// Step 3: Save user message
$this->emit('save_user', 'User-Nachricht speichern...');
$this->startStep();
$this->messageRepo->save(sessionId: $sessionId, role: 'user', content: $message, model: $model);
$this->endStep('save_user_done', 'User-Nachricht gespeichert');
// Step 4: Auto-set title
$currentTitle = $session->getTitle();
if ($currentTitle === null || $currentTitle === 'Neuer Chat') {
$this->sessionRepo->updateTitle($sessionId, mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : ''));
}
// Step 5: Get prompts
$this->emit('prompts', 'Prompts laden...');
$this->startStep();
$stylePrompt = $this->promptLoader->getStylePrompt($authorProfileId);
$systemPrompt = $this->promptLoader->getSystemPrompt($systemPromptId);
$structurePrompt = $this->promptLoader->getStructurePrompt($structureId);
if ($structurePrompt !== null) {
$systemPrompt = ($systemPrompt ?? '') . "\n\n" . $structurePrompt;
}
$this->endStep('prompts_done', 'Prompts geladen');
// RAG Pipeline
$searchResults = [];
$context = '';
if ($collections !== []) {
// Step 6+7: Semantic search
$this->emit('search', 'Semantische Suche in ' . count($collections) . ' Collection(s)...');
$this->startStep();
$searchResults = $this->ragBuilder->search($message, $collections, $contextLimit);
// Add Qdrant fallback for non-documents collections
$searchResults = $this->addQdrantFallback($searchResults, $message, $collections, $contextLimit);
$semanticCount = count(array_filter($searchResults, static fn ($r) => isset($r['intent'])));
$this->endStep('search_done', count($searchResults) . ' Chunks gefunden (' . $semanticCount . ' mit Semantik)');
// Step 8: Build context
if ($searchResults !== []) {
$this->emit('context', 'Kontext aufbauen...');
$this->startStep();
$context = $this->ragBuilder->buildContext($searchResults);
$this->endStep('context_done', 'Kontext erstellt (' . strlen($context) . ' Zeichen)');
}
}
// Step 9: LLM Request
$isOllama = str_starts_with($model, 'ollama:');
$isClaude = str_starts_with($model, 'claude-');
$hasContext = $context !== '';
$this->emit('llm', 'Anfrage an ' . ($isOllama ? substr($model, 7) : $model) . '...');
$this->startStep();
$llmStart = microtime(true);
try {
if ($isClaude) {
$userPrompt = $hasContext ? $this->claude->buildRagPrompt($message, $context) : $message;
$effectiveSystemPrompt = $systemPrompt ?? ($hasContext ? $this->claude->getDefaultSystemPrompt() : 'Du bist ein hilfreicher Assistent. Antworte auf Deutsch, präzise und hilfreich.');
if ($stylePrompt !== null && $stylePrompt !== '') {
$effectiveSystemPrompt .= "\n\n" . $stylePrompt;
}
// Update protokoll with full prompt before LLM call
if ($protokollId !== null) {
$fullPrompt = "=== SYSTEM ===\n" . $effectiveSystemPrompt . "\n\n=== USER ===\n" . $userPrompt;
$this->protokollService->updateFullPrompt($protokollId, $fullPrompt);
}
$llmResponse = $this->claude->ask($userPrompt, $effectiveSystemPrompt, $model, $maxTokens, $temperature);
$answer = $llmResponse['text'];
$usage = $llmResponse['usage'];
} elseif ($isOllama) {
$ollamaModel = substr($model, 7);
$instructions = array_filter([$systemPrompt, $stylePrompt]);
$instructionBlock = $instructions !== [] ? implode("\n\n", $instructions) . "\n\n" : '';
$userPrompt = $hasContext ? sprintf("%sKontext:\n\n%s\n\n---\n\nFrage: %s", $instructionBlock, $context, $message) : $instructionBlock . $message;
// Update protokoll with full prompt before LLM call
if ($protokollId !== null) {
$this->protokollService->updateFullPrompt($protokollId, $userPrompt);
}
$answer = $this->ollama->generate($userPrompt, $ollamaModel, $temperature);
$usage = null;
} else {
$this->emit('error', "Unbekanntes Modell: {$model}");
if ($protokollId !== null) {
$this->protokollService->logFailure($protokollId, "Unbekanntes Modell: {$model}");
}
return ChatResponse::error("Unknown model \"{$model}\".");
}
} catch (\RuntimeException $e) {
$this->emit('error', 'LLM-Fehler: ' . $e->getMessage());
if ($protokollId !== null) {
$this->protokollService->logFailure($protokollId, 'LLM-Fehler: ' . $e->getMessage());
}
return ChatResponse::error('LLM request failed: ' . $e->getMessage());
}
$llmDuration = (int) round((microtime(true) - $llmStart) * Constants::MS_PER_SECOND);
$tokenInfo = $usage !== null ? " ({$usage['input_tokens']} in / {$usage['output_tokens']} out)" : '';
$this->emit('llm_done', "Antwort erhalten{$tokenInfo}", $llmDuration);
// Step 10: Extract sources
$this->emit('sources', 'Quellen extrahieren...');
$this->startStep();
$sources = $this->ragBuilder->extractSources($searchResults);
$this->endStep('sources_done', count($sources) . ' Quellen extrahiert');
// Step 11: Save assistant message
$this->emit('save_assistant', 'Antwort speichern...');
$this->startStep();
$sourcesForStorage = array_map(static fn (array $s): string => json_encode($s, JSON_THROW_ON_ERROR), $sources);
$this->messageRepo->save(
sessionId: $sessionId,
role: 'assistant',
content: $answer,
model: $model,
tokensInput: $usage['input_tokens'] ?? null,
tokensOutput: $usage['output_tokens'] ?? null,
sources: $sourcesForStorage,
startMicrotime: $llmStart,
endMicrotime: microtime(true),
authorProfileId: $authorProfileId > 0 ? $authorProfileId : null,
systemPromptId: $systemPromptId > 0 ? $systemPromptId : null,
collectionsJson: json_encode($collections),
contextLimit: $contextLimit,
llmRequestId: $protokollId
);
$this->endStep('save_assistant_done', 'Antwort gespeichert');
// Log success to protokoll
if ($protokollId !== null) {
$this->protokollService->logSuccess(
$protokollId,
$answer,
$llmDuration,
$usage['input_tokens'] ?? null,
$usage['output_tokens'] ?? null
);
}
// Step 12: Quality check
$qualityValidation = null;
if ($qualityCheck) {
$this->emit('quality', 'Qualitätsprüfung...');
$this->startStep();
$structureName = $structureId > 0 ? $this->promptLoader->getStructureName($structureId) : null;
$qualityValidation = $this->qualityValidator->validate(
question: $message,
answer: $answer,
sources: $sources,
structureName: $structureName
);
$score = round($qualityValidation['score']);
$this->endStep('quality_done', "Qualität: {$score}%");
}
// Final
$totalDuration = (int) round((microtime(true) - $totalStart) * Constants::MS_PER_SECOND);
$this->emit('complete', "Fertig in {$totalDuration}ms", $totalDuration);
// Build response
$result = [
'answer' => $answer,
'sources' => $sources,
'usage' => $usage,
'chunks_used' => count($searchResults),
];
$response = ChatResponse::fromServiceResponse($result, (microtime(true) - $totalStart));
if ($qualityValidation !== null) {
$response = $response->withQualityValidation($qualityValidation);
}
return $response;
}
/**
* Add Qdrant search results for non-documents collections.
*
* @param array<array<string, mixed>> $results Existing results
* @param array<string> $collections Collections to search
* @return array<array<string, mixed>> Combined results
*/
private function addQdrantFallback(array $results, string $query, array $collections, int $limit): array
{
$nonDocCollections = array_filter($collections, static fn ($c) => $c !== 'documents');
if ($nonDocCollections === []) {
return $results;
}
foreach ($nonDocCollections as $collection) {
try {
$embedding = $this->ollama->getEmbedding($query);
$qdrantResults = $this->qdrant->search($embedding, $collection, $limit);
foreach ($qdrantResults as $result) {
$payload = $result['payload'];
$results[] = [
'chunk_id' => $payload['chunk_id'] ?? 0,
'content' => $payload['content'] ?? $payload['content_preview'] ?? '',
'title' => $payload['document_title'] ?? $payload['title'] ?? 'Unbekannt',
'score' => $result['score'],
'_collection' => $collection,
];
}
} catch (\RuntimeException) {
continue;
}
}
// Sort by score and limit
usort($results, static fn ($a, $b) => ($b['score'] ?? 0.0) <=> ($a['score'] ?? 0.0));
return array_slice($results, 0, $limit);
}
}