{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php",
"old_string": " private function searchMultipleCollections(array $embedding, array $collections, int $limit): array\n {\n $allResults = [];\n foreach ($collections as $collection) {\n try {\n $results = $this->qdrant->search($embedding, $collection, $limit);\n foreach ($results as &$result) { $result['payload']['_collection'] = $collection; }\n $allResults = array_merge($allResults, $results);\n } catch (\\RuntimeException) { continue; }\n }\n foreach ($allResults as &$result) {\n $processedAt = $result['payload']['processed_at'] ?? null;\n $documentDate = $processedAt !== null ? new \\DateTime($processedAt) : new \\DateTime();\n $authorityScore = (float) ($result['payload']['authority_score'] ?? 0.5);\n $result['weighted_score'] = $this->scoring->calculateScore($result['score'], $documentDate, $authorityScore);\n }\n unset($result);\n usort($allResults, static fn ($a, $b) => ($b['weighted_score'] ?? 0.0) <=> ($a['weighted_score'] ?? 0.0));\n return array_slice($allResults, 0, $limit);\n }\n\n private function buildContext(array $searchResults, int $maxTokens = 3000): string\n {\n $contextParts = [];\n $totalChars = 0;\n $maxChars = $maxTokens * 4;\n foreach ($searchResults as $index => $result) {\n $payload = $result['payload'];\n $content = (string) ($payload['content'] ?? $payload['content_preview'] ?? '');\n $docTitle = (string) ($payload['document_title'] ?? $payload['title'] ?? 'Unbekannt');\n if ($totalChars + strlen($content) > $maxChars) { break; }\n $contextParts[] = sprintf('[Quelle %d: %s]%s%s', $index + 1, $docTitle, \"\\n\", $content);\n $totalChars += strlen($content);\n }\n return implode(\"\\n\\n---\\n\\n\", $contextParts);\n }",
"new_string": " \/** @param array<string> $collections *\/\n private function searchWithSemantics(string $query, array $collections, int $limit): array\n {\n $allResults = [];\n\n foreach ($collections as $collection) {\n if ($collection === 'documents') {\n \/\/ Use ContentSearchService for semantic-enriched search\n $semanticResults = $this->searchService->search($query, [], $limit);\n foreach ($semanticResults as $result) {\n $allResults[] = [\n 'chunk_id' => $result['chunk_id'],\n 'content' => $result['content'],\n 'title' => $result['source_path'] ?? $result['heading_path'] ?? 'Unbekannt',\n 'score' => $result['relevance_score'],\n \/\/ Semantic metadata\n 'summary' => $result['summary'] ?? null,\n 'keywords' => $result['keywords'] ?? [],\n 'intent' => $result['intent'] ?? null,\n 'discourse_role' => $result['discourse_role'] ?? null,\n 'sentiment' => $result['sentiment'] ?? null,\n 'frame' => $result['frame'] ?? null,\n '_collection' => 'documents',\n ];\n }\n } else {\n \/\/ Fallback to QdrantService for other collections\n try {\n $embedding = $this->ollama->getEmbedding($query);\n $results = $this->qdrant->search($embedding, $collection, $limit);\n foreach ($results as $result) {\n $payload = $result['payload'] ?? [];\n $allResults[] = [\n 'chunk_id' => $payload['chunk_id'] ?? 0,\n 'content' => $payload['content'] ?? $payload['content_preview'] ?? '',\n 'title' => $payload['document_title'] ?? $payload['title'] ?? 'Unbekannt',\n 'score' => $result['score'] ?? 0.0,\n '_collection' => $collection,\n ];\n }\n } catch (\\RuntimeException) {\n continue;\n }\n }\n }\n\n \/\/ Sort by score and limit\n usort($allResults, static fn ($a, $b) => ($b['score'] ?? 0.0) <=> ($a['score'] ?? 0.0));\n\n return array_slice($allResults, 0, $limit);\n }\n\n private function buildSemanticContext(array $searchResults, int $maxTokens = 3000): string\n {\n $contextParts = [];\n $totalChars = 0;\n $maxChars = $maxTokens * 4;\n\n foreach ($searchResults as $index => $result) {\n $content = (string) ($result['content'] ?? '');\n $title = (string) ($result['title'] ?? 'Unbekannt');\n\n if ($totalChars + strlen($content) > $maxChars) {\n break;\n }\n\n \/\/ Build semantic header if available\n $semanticInfo = [];\n if (isset($result['discourse_role'])) {\n $semanticInfo[] = match ($result['discourse_role']) {\n 'definition' => 'Definition',\n 'thesis' => 'These',\n 'evidence' => 'Beleg',\n 'example' => 'Beispiel',\n 'summary' => 'Zusammenfassung',\n default => $result['discourse_role'],\n };\n }\n if (isset($result['intent'])) {\n $semanticInfo[] = match ($result['intent']) {\n 'explain' => 'erklärt',\n 'define' => 'definiert',\n 'argue' => 'argumentiert',\n 'compare' => 'vergleicht',\n 'exemplify' => 'veranschaulicht',\n default => $result['intent'],\n };\n }\n\n $header = sprintf('[Quelle %d: %s]', $index + 1, $title);\n if ($semanticInfo !== []) {\n $header .= ' (' . implode(', ', $semanticInfo) . ')';\n }\n\n $contextParts[] = $header . \"\\n\" . $content;\n $totalChars += strlen($content);\n }\n\n return implode(\"\\n\\n---\\n\\n\", $contextParts);\n }"
}
}
{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php",
"oldString": " private function searchMultipleCollections(array $embedding, array $collections, int $limit): array\n {\n $allResults = [];\n foreach ($collections as $collection) {\n try {\n $results = $this->qdrant->search($embedding, $collection, $limit);\n foreach ($results as &$result) { $result['payload']['_collection'] = $collection; }\n $allResults = array_merge($allResults, $results);\n } catch (\\RuntimeException) { continue; }\n }\n foreach ($allResults as &$result) {\n $processedAt = $result['payload']['processed_at'] ?? null;\n $documentDate = $processedAt !== null ? new \\DateTime($processedAt) : new \\DateTime();\n $authorityScore = (float) ($result['payload']['authority_score'] ?? 0.5);\n $result['weighted_score'] = $this->scoring->calculateScore($result['score'], $documentDate, $authorityScore);\n }\n unset($result);\n usort($allResults, static fn ($a, $b) => ($b['weighted_score'] ?? 0.0) <=> ($a['weighted_score'] ?? 0.0));\n return array_slice($allResults, 0, $limit);\n }\n\n private function buildContext(array $searchResults, int $maxTokens = 3000): string\n {\n $contextParts = [];\n $totalChars = 0;\n $maxChars = $maxTokens * 4;\n foreach ($searchResults as $index => $result) {\n $payload = $result['payload'];\n $content = (string) ($payload['content'] ?? $payload['content_preview'] ?? '');\n $docTitle = (string) ($payload['document_title'] ?? $payload['title'] ?? 'Unbekannt');\n if ($totalChars + strlen($content) > $maxChars) { break; }\n $contextParts[] = sprintf('[Quelle %d: %s]%s%s', $index + 1, $docTitle, \"\\n\", $content);\n $totalChars += strlen($content);\n }\n return implode(\"\\n\\n---\\n\\n\", $contextParts);\n }",
"newString": " \/** @param array<string> $collections *\/\n private function searchWithSemantics(string $query, array $collections, int $limit): array\n {\n $allResults = [];\n\n foreach ($collections as $collection) {\n if ($collection === 'documents') {\n \/\/ Use ContentSearchService for semantic-enriched search\n $semanticResults = $this->searchService->search($query, [], $limit);\n foreach ($semanticResults as $result) {\n $allResults[] = [\n 'chunk_id' => $result['chunk_id'],\n 'content' => $result['content'],\n 'title' => $result['source_path'] ?? $result['heading_path'] ?? 'Unbekannt',\n 'score' => $result['relevance_score'],\n \/\/ Semantic metadata\n 'summary' => $result['summary'] ?? null,\n 'keywords' => $result['keywords'] ?? [],\n 'intent' => $result['intent'] ?? null,\n 'discourse_role' => $result['discourse_role'] ?? null,\n 'sentiment' => $result['sentiment'] ?? null,\n 'frame' => $result['frame'] ?? null,\n '_collection' => 'documents',\n ];\n }\n } else {\n \/\/ Fallback to QdrantService for other collections\n try {\n $embedding = $this->ollama->getEmbedding($query);\n $results = $this->qdrant->search($embedding, $collection, $limit);\n foreach ($results as $result) {\n $payload = $result['payload'] ?? [];\n $allResults[] = [\n 'chunk_id' => $payload['chunk_id'] ?? 0,\n 'content' => $payload['content'] ?? $payload['content_preview'] ?? '',\n 'title' => $payload['document_title'] ?? $payload['title'] ?? 'Unbekannt',\n 'score' => $result['score'] ?? 0.0,\n '_collection' => $collection,\n ];\n }\n } catch (\\RuntimeException) {\n continue;\n }\n }\n }\n\n \/\/ Sort by score and limit\n usort($allResults, static fn ($a, $b) => ($b['score'] ?? 0.0) <=> ($a['score'] ?? 0.0));\n\n return array_slice($allResults, 0, $limit);\n }\n\n private function buildSemanticContext(array $searchResults, int $maxTokens = 3000): string\n {\n $contextParts = [];\n $totalChars = 0;\n $maxChars = $maxTokens * 4;\n\n foreach ($searchResults as $index => $result) {\n $content = (string) ($result['content'] ?? '');\n $title = (string) ($result['title'] ?? 'Unbekannt');\n\n if ($totalChars + strlen($content) > $maxChars) {\n break;\n }\n\n \/\/ Build semantic header if available\n $semanticInfo = [];\n if (isset($result['discourse_role'])) {\n $semanticInfo[] = match ($result['discourse_role']) {\n 'definition' => 'Definition',\n 'thesis' => 'These',\n 'evidence' => 'Beleg',\n 'example' => 'Beispiel',\n 'summary' => 'Zusammenfassung',\n default => $result['discourse_role'],\n };\n }\n if (isset($result['intent'])) {\n $semanticInfo[] = match ($result['intent']) {\n 'explain' => 'erklärt',\n 'define' => 'definiert',\n 'argue' => 'argumentiert',\n 'compare' => 'vergleicht',\n 'exemplify' => 'veranschaulicht',\n default => $result['intent'],\n };\n }\n\n $header = sprintf('[Quelle %d: %s]', $index + 1, $title);\n if ($semanticInfo !== []) {\n $header .= ' (' . implode(', ', $semanticInfo) . ')';\n }\n\n $contextParts[] = $header . \"\\n\" . $content;\n $totalChars += strlen($content);\n }\n\n return implode(\"\\n\\n---\\n\\n\", $contextParts);\n }",
"originalFile": "<?php\n\ndeclare(strict_types=1);\n\nnamespace UseCases\\Chat;\n\n\/\/ @responsibility: Orchestriert Chat-Nachrichten mit SSE-Progress-Events\n\nuse Domain\\Constants;\nuse Domain\\Repository\\ChatMessageRepositoryInterface;\nuse Domain\\Repository\\ChatSessionRepositoryInterface;\nuse Domain\\Service\\SearchServiceInterface;\nuse Infrastructure\\AI\\ClaudeService;\nuse Infrastructure\\AI\\ContentQualityValidator;\nuse Infrastructure\\AI\\OllamaService;\nuse Infrastructure\\AI\\QdrantService;\nuse Infrastructure\\AI\\ScoringService;\nuse Infrastructure\\Persistence\\ContentConfigRepository;\n\nclass StreamingChatMessageUseCase\n{\n \/** @var callable|null *\/\n private $progressCallback;\n\n private float $stepStart;\n\n public function __construct(\n private OllamaService $ollama,\n private QdrantService $qdrant,\n private ClaudeService $claude,\n private ScoringService $scoring,\n private ChatSessionRepositoryInterface $sessionRepo,\n private ChatMessageRepositoryInterface $messageRepo,\n private ContentConfigRepository $configRepo,\n private ContentQualityValidator $qualityValidator,\n private SearchServiceInterface $searchService\n ) {\n }\n\n \/**\n * Set progress callback for SSE events\n *\n * @param callable $callback fn(string $step, string $message, ?int $durationMs): void\n *\/\n public function setProgressCallback(callable $callback): void\n {\n $this->progressCallback = $callback;\n }\n\n private function emit(string $step, string $message, ?int $durationMs = null): void\n {\n if ($this->progressCallback !== null) {\n ($this->progressCallback)($step, $message, $durationMs);\n }\n }\n\n private function startStep(): void\n {\n $this->stepStart = microtime(true);\n }\n\n private function endStep(string $step, string $message): void\n {\n $durationMs = (int) round((microtime(true) - $this->stepStart) * Constants::MS_PER_SECOND);\n $this->emit($step, $message, $durationMs);\n }\n\n \/** Execute chat with streaming progress. @param array<string> $collections *\/\n public function execute(\n string $sessionUuid, string $message, string $model, array $collections = ['documents'],\n int $contextLimit = 5, int $authorProfileId = 0, int $systemPromptId = 1,\n float $temperature = 0.7, int $maxTokens = 4096, int $structureId = 0, bool $qualityCheck = false\n ): ChatResponse {\n $totalStart = microtime(true);\n \/\/ Step 1: Validate session\n $this->emit('session', 'Session validieren...');\n $this->startStep();\n $session = $this->sessionRepo->findByUuid($sessionUuid);\n if ($session === null) {\n $this->emit('error', 'Session nicht gefunden');\n return ChatResponse::error('Session nicht gefunden.');\n }\n $sessionId = $session->getId() ?? 0;\n $this->endStep('session_done', 'Session validiert');\n \/\/ Step 2: Validate message\n $message = trim($message);\n if ($message === '') {\n $this->emit('error', 'Keine Nachricht');\n return ChatResponse::error('Bitte gib eine Frage ein.');\n }\n \/\/ Step 3: Save user message\n $this->emit('save_user', 'User-Nachricht speichern...');\n $this->startStep();\n $this->messageRepo->save(sessionId: $sessionId, role: 'user', content: $message, model: $model);\n $this->endStep('save_user_done', 'User-Nachricht gespeichert');\n \/\/ Step 4: Auto-set title\n $currentTitle = $session->getTitle();\n if ($currentTitle === null || $currentTitle === 'Neuer Chat') {\n $this->sessionRepo->updateTitle($sessionId, mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : ''));\n }\n \/\/ Step 5: Get prompts\n $this->emit('prompts', 'Prompts laden...');\n $this->startStep();\n $stylePrompt = $this->getStylePromptFromProfile($authorProfileId);\n $systemPrompt = $this->getSystemPromptById($systemPromptId);\n $structurePrompt = $this->getStructurePrompt($structureId);\n if ($structurePrompt !== null) { $systemPrompt = ($systemPrompt ?? '') . \"\\n\\n\" . $structurePrompt; }\n $this->endStep('prompts_done', 'Prompts geladen');\n \/\/ RAG Pipeline\n $searchResults = [];\n $context = '';\n if ($collections !== []) {\n \/\/ Step 6+7: Semantic search via ContentSearchService\n $this->emit('search', 'Semantische Suche in ' . count($collections) . ' Collection(s)...');\n $this->startStep();\n $searchResults = $this->searchWithSemantics($message, $collections, $contextLimit);\n $semanticCount = count(array_filter($searchResults, static fn ($r) => isset($r['intent'])));\n $this->endStep('search_done', count($searchResults) . ' Chunks gefunden (' . $semanticCount . ' mit Semantik)');\n \/\/ Step 8: Build context with semantic metadata\n if ($searchResults !== []) {\n $this->emit('context', 'Kontext aufbauen...');\n $this->startStep();\n $context = $this->buildSemanticContext($searchResults);\n $this->endStep('context_done', 'Kontext erstellt (' . strlen($context) . ' Zeichen)');\n }\n }\n\n \/\/ Step 9: LLM Request\n $isOllama = str_starts_with($model, 'ollama:');\n $isClaude = str_starts_with($model, 'claude-');\n $hasContext = $context !== '';\n $this->emit('llm', 'Anfrage an ' . ($isOllama ? substr($model, 7) : $model) . '...');\n $this->startStep();\n $llmStart = microtime(true);\n try {\n if ($isClaude) {\n $userPrompt = $hasContext ? $this->claude->buildRagPrompt($message, $context) : $message;\n $effectiveSystemPrompt = $systemPrompt ?? ($hasContext ? $this->claude->getDefaultSystemPrompt() : 'Du bist ein hilfreicher Assistent. Antworte auf Deutsch, präzise und hilfreich.');\n if ($stylePrompt !== null && $stylePrompt !== '') { $effectiveSystemPrompt .= \"\\n\\n\" . $stylePrompt; }\n $llmResponse = $this->claude->ask($userPrompt, $effectiveSystemPrompt, $model, $maxTokens, $temperature);\n $answer = $llmResponse['text'];\n $usage = $llmResponse['usage'];\n } elseif ($isOllama) {\n $ollamaModel = substr($model, 7);\n $instructions = array_filter([$systemPrompt, $stylePrompt]);\n $instructionBlock = $instructions !== [] ? implode(\"\\n\\n\", $instructions) . \"\\n\\n\" : '';\n $userPrompt = $hasContext ? sprintf(\"%sKontext:\\n\\n%s\\n\\n---\\n\\nFrage: %s\", $instructionBlock, $context, $message) : $instructionBlock . $message;\n $answer = $this->ollama->generate($userPrompt, $ollamaModel, $temperature);\n $usage = null;\n } else {\n $this->emit('error', \"Unbekanntes Modell: {$model}\");\n return ChatResponse::error(\"Unknown model \\\"{$model}\\\".\");\n }\n } catch (\\RuntimeException $e) {\n $this->emit('error', 'LLM-Fehler: ' . $e->getMessage());\n return ChatResponse::error('LLM request failed: ' . $e->getMessage());\n }\n $llmDuration = (int) round((microtime(true) - $llmStart) * Constants::MS_PER_SECOND);\n $tokenInfo = $usage !== null ? \" ({$usage['input_tokens']} in \/ {$usage['output_tokens']} out)\" : '';\n $this->emit('llm_done', \"Antwort erhalten{$tokenInfo}\", $llmDuration);\n \/\/ Step 10: Extract sources\n $this->emit('sources', 'Quellen extrahieren...');\n $this->startStep();\n $sources = $this->extractSources($searchResults);\n $this->endStep('sources_done', count($sources) . ' Quellen extrahiert');\n \/\/ Step 11: Save assistant message\n $this->emit('save_assistant', 'Antwort speichern...');\n $this->startStep();\n $sourcesForStorage = array_map(static fn (array $s): string => json_encode($s, JSON_THROW_ON_ERROR), $sources);\n $this->messageRepo->save(\n sessionId: $sessionId, role: 'assistant', content: $answer, model: $model,\n tokensInput: $usage['input_tokens'] ?? null, tokensOutput: $usage['output_tokens'] ?? null,\n sources: $sourcesForStorage, startMicrotime: $llmStart, endMicrotime: microtime(true),\n authorProfileId: $authorProfileId > 0 ? $authorProfileId : null,\n systemPromptId: $systemPromptId > 0 ? $systemPromptId : null,\n collectionsJson: json_encode($collections), contextLimit: $contextLimit\n );\n $this->endStep('save_assistant_done', 'Antwort gespeichert');\n\n \/\/ Step 12: Quality check\n $qualityValidation = null;\n if ($qualityCheck) {\n $this->emit('quality', 'Qualitätsprüfung...');\n $this->startStep();\n $structureName = $structureId > 0 ? $this->getStructureName($structureId) : null;\n $qualityValidation = $this->qualityValidator->validate(\n question: $message,\n answer: $answer,\n sources: $sources,\n structureName: $structureName\n );\n $score = round($qualityValidation['score']);\n $this->endStep('quality_done', \"Qualität: {$score}%\");\n }\n\n \/\/ Final\n $totalDuration = (int) round((microtime(true) - $totalStart) * Constants::MS_PER_SECOND);\n $this->emit('complete', \"Fertig in {$totalDuration}ms\", $totalDuration);\n\n \/\/ Build response\n $result = [\n 'answer' => $answer,\n 'sources' => $sources,\n 'usage' => $usage,\n 'chunks_used' => count($searchResults),\n ];\n\n $response = ChatResponse::fromServiceResponse($result, (microtime(true) - $totalStart));\n if ($qualityValidation !== null) {\n $response = $response->withQualityValidation($qualityValidation);\n }\n\n retur... [TRUNCATED-bc23eca20ad72346]",
"structuredPatch": [
{
"oldStart": 213,
"oldLines": 40,
"newStart": 213,
"newLines": 104,
"lines": [
" return $response;",
" }",
" ",
"- private function searchMultipleCollections(array $embedding, array $collections, int $limit): array",
"+ \/** @param array<string> $collections *\/",
"+ private function searchWithSemantics(string $query, array $collections, int $limit): array",
" {",
" $allResults = [];",
"+",
" foreach ($collections as $collection) {",
"- try {",
"- $results = $this->qdrant->search($embedding, $collection, $limit);",
"- foreach ($results as &$result) { $result['payload']['_collection'] = $collection; }",
"- $allResults = array_merge($allResults, $results);",
"- } catch (\\RuntimeException) { continue; }",
"+ if ($collection === 'documents') {",
"+ \/\/ Use ContentSearchService for semantic-enriched search",
"+ $semanticResults = $this->searchService->search($query, [], $limit);",
"+ foreach ($semanticResults as $result) {",
"+ $allResults[] = [",
"+ 'chunk_id' => $result['chunk_id'],",
"+ 'content' => $result['content'],",
"+ 'title' => $result['source_path'] ?? $result['heading_path'] ?? 'Unbekannt',",
"+ 'score' => $result['relevance_score'],",
"+ \/\/ Semantic metadata",
"+ 'summary' => $result['summary'] ?? null,",
"+ 'keywords' => $result['keywords'] ?? [],",
"+ 'intent' => $result['intent'] ?? null,",
"+ 'discourse_role' => $result['discourse_role'] ?? null,",
"+ 'sentiment' => $result['sentiment'] ?? null,",
"+ 'frame' => $result['frame'] ?? null,",
"+ '_collection' => 'documents',",
"+ ];",
"+ }",
"+ } else {",
"+ \/\/ Fallback to QdrantService for other collections",
"+ try {",
"+ $embedding = $this->ollama->getEmbedding($query);",
"+ $results = $this->qdrant->search($embedding, $collection, $limit);",
"+ foreach ($results as $result) {",
"+ $payload = $result['payload'] ?? [];",
"+ $allResults[] = [",
"+ 'chunk_id' => $payload['chunk_id'] ?? 0,",
"+ 'content' => $payload['content'] ?? $payload['content_preview'] ?? '',",
"+ 'title' => $payload['document_title'] ?? $payload['title'] ?? 'Unbekannt',",
"+ 'score' => $result['score'] ?? 0.0,",
"+ '_collection' => $collection,",
"+ ];",
"+ }",
"+ } catch (\\RuntimeException) {",
"+ continue;",
"+ }",
"+ }",
" }",
"- foreach ($allResults as &$result) {",
"- $processedAt = $result['payload']['processed_at'] ?? null;",
"- $documentDate = $processedAt !== null ? new \\DateTime($processedAt) : new \\DateTime();",
"- $authorityScore = (float) ($result['payload']['authority_score'] ?? 0.5);",
"- $result['weighted_score'] = $this->scoring->calculateScore($result['score'], $documentDate, $authorityScore);",
"- }",
"- unset($result);",
"- usort($allResults, static fn ($a, $b) => ($b['weighted_score'] ?? 0.0) <=> ($a['weighted_score'] ?? 0.0));",
"+",
"+ \/\/ Sort by score and limit",
"+ usort($allResults, static fn ($a, $b) => ($b['score'] ?? 0.0) <=> ($a['score'] ?? 0.0));",
"+",
" return array_slice($allResults, 0, $limit);",
" }",
" ",
"- private function buildContext(array $searchResults, int $maxTokens = 3000): string",
"+ private function buildSemanticContext(array $searchResults, int $maxTokens = 3000): string",
" {",
" $contextParts = [];",
" $totalChars = 0;",
" $maxChars = $maxTokens * 4;",
"+",
" foreach ($searchResults as $index => $result) {",
"- $payload = $result['payload'];",
"- $content = (string) ($payload['content'] ?? $payload['content_preview'] ?? '');",
"- $docTitle = (string) ($payload['document_title'] ?? $payload['title'] ?? 'Unbekannt');",
"- if ($totalChars + strlen($content) > $maxChars) { break; }",
"- $contextParts[] = sprintf('[Quelle %d: %s]%s%s', $index + 1, $docTitle, \"\\n\", $content);",
"+ $content = (string) ($result['content'] ?? '');",
"+ $title = (string) ($result['title'] ?? 'Unbekannt');",
"+",
"+ if ($totalChars + strlen($content) > $maxChars) {",
"+ break;",
"+ }",
"+",
"+ \/\/ Build semantic header if available",
"+ $semanticInfo = [];",
"+ if (isset($result['discourse_role'])) {",
"+ $semanticInfo[] = match ($result['discourse_role']) {",
"+ 'definition' => 'Definition',",
"+ 'thesis' => 'These',",
"+ 'evidence' => 'Beleg',",
"+ 'example' => 'Beispiel',",
"+ 'summary' => 'Zusammenfassung',",
"+ default => $result['discourse_role'],",
"+ };",
"+ }",
"+ if (isset($result['intent'])) {",
"+ $semanticInfo[] = match ($result['intent']) {",
"+ 'explain' => 'erklärt',",
"+ 'define' => 'definiert',",
"+ 'argue' => 'argumentiert',",
"+ 'compare' => 'vergleicht',",
"+ 'exemplify' => 'veranschaulicht',",
"+ default => $result['intent'],",
"+ };",
"+ }",
"+",
"+ $header = sprintf('[Quelle %d: %s]', $index + 1, $title);",
"+ if ($semanticInfo !== []) {",
"+ $header .= ' (' . implode(', ', $semanticInfo) . ')';",
"+ }",
"+",
"+ $contextParts[] = $header . \"\\n\" . $content;",
" $totalChars += strlen($content);",
" }",
"+",
" return implode(\"\\n\\n---\\n\\n\", $contextParts);",
" }",
" "
]
}
],
"userModified": false,
"replaceAll": false
}
}