Skip to content

Commit fa925ed

Browse files
authored
Merge pull request #87 from buerokratt/wip
Rag 149- Show chunk context in Test LLM Connection Page (buerokratt#173)
2 parents ced54ab + c33f951 commit fa925ed

File tree

6 files changed

+130
-11
lines changed

6 files changed

+130
-11
lines changed

GUI/src/pages/TestModel/TestLLM.scss

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,44 @@
4141
line-height: 1.5;
4242
color: #555;
4343
}
44+
45+
.context-section {
46+
margin-top: 20px;
47+
48+
.context-list {
49+
display: flex;
50+
flex-direction: column;
51+
gap: 12px;
52+
margin-top: 8px;
53+
}
54+
55+
.context-item {
56+
padding: 12px;
57+
background-color: #ffffff;
58+
border: 1px solid #e0e0e0;
59+
border-radius: 6px;
60+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
61+
62+
.context-rank {
63+
margin-bottom: 8px;
64+
padding-bottom: 4px;
65+
border-bottom: 1px solid #f0f0f0;
66+
67+
strong {
68+
color: #2563eb;
69+
font-size: 0.875rem;
70+
font-weight: 600;
71+
}
72+
}
73+
74+
.context-content {
75+
color: #374151;
76+
line-height: 1.5;
77+
font-size: 0.9rem;
78+
white-space: pre-wrap;
79+
}
80+
}
81+
}
4482
}
4583

4684
.testModalList {

GUI/src/pages/TestModel/index.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMutation, useQuery } from '@tanstack/react-query';
2-
import { Button, FormSelect, FormTextarea } from 'components';
2+
import { Button, FormSelect, FormTextarea, Collapsible } from 'components';
33
import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner';
44
import { FC, useState } from 'react';
55
import { useTranslation } from 'react-i18next';
@@ -19,6 +19,9 @@ const TestLLM: FC = () => {
1919
text: '',
2020
});
2121

22+
// Sort context by rank
23+
const sortedContext = inferenceResult?.chunks?.toSorted((a, b) => a.rank - b.rank) ?? [];
24+
2225
// Fetch LLM connections for dropdown - using the working legacy endpoint for now
2326
const { data: connections, isLoading: isLoadingConnections } = useQuery({
2427
queryKey: llmConnectionsQueryKeys.list({
@@ -99,7 +102,7 @@ const TestLLM: FC = () => {
99102
onSelectionChange={(selection) => {
100103
handleChange('connectionId', selection?.value as string);
101104
}}
102-
value={testLLM?.connectionId === null ? t('testModels.connectionNotExist') || 'Connection does not exist' : undefined}
105+
value={testLLM?.connectionId === null ? t('testModels.connectionNotExist') || 'Connection does not exist' : undefined}
103106
defaultValue={testLLM?.connectionId ?? undefined}
104107
/>
105108
</div>
@@ -126,15 +129,38 @@ const TestLLM: FC = () => {
126129

127130
{/* Inference Result */}
128131

129-
{inferenceResult && (
132+
{inferenceResult && !inferenceMutation.isLoading && (
130133
<div className="inference-results-container">
131-
<div className="result-item">
132-
<strong>{t('testModels.responseLabel') || 'Response:'}</strong>
133-
<div className="response-content">
134-
{inferenceResult.content}
134+
<div className="result-item">
135+
<strong>Response:</strong>
136+
<div className="response-content">
137+
{inferenceResult.content}
138+
</div>
135139
</div>
140+
141+
{/* Context Section */}
142+
{
143+
sortedContext && sortedContext?.length > 0 && (
144+
<div className="context-section">
145+
<Collapsible title={`Context (${sortedContext?.length} chunks)`} defaultOpen={false}>
146+
<div className="context-list">
147+
{sortedContext?.map((contextItem, index) => (
148+
<div key={index} className="context-item">
149+
<div className="context-rank">
150+
<strong>Rank {contextItem.rank}</strong>
151+
</div>
152+
<div className="context-content">
153+
{contextItem.chunkRetrieved}
154+
</div>
155+
</div>
156+
))}
157+
</div>
158+
</Collapsible>
159+
</div>
160+
)
161+
}
162+
136163
</div>
137-
</div>
138164
)}
139165

140166
{/* Error State */}

GUI/src/services/inference.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export interface InferenceResponse {
2525
llmServiceActive: boolean;
2626
questionOutOfLlmScope: boolean;
2727
content: string;
28+
chunks?: {
29+
rank: number,
30+
chunkRetrieved: string
31+
}[]
2832
};
2933
}
3034

src/llm_orchestration_service.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
PromptRefinerOutput,
1919
ContextGenerationRequest,
2020
TestOrchestrationResponse,
21+
ChunkInfo,
2122
)
2223
from prompt_refine_manager.prompt_refiner import PromptRefinerAgent
2324
from src.response_generator.response_generate import ResponseGeneratorAgent
@@ -922,6 +923,7 @@ def handle_input_guardrails(
922923
questionOutOfLLMScope=False,
923924
inputGuardFailed=True,
924925
content=INPUT_GUARDRAIL_VIOLATION_MESSAGE,
926+
chunks=None,
925927
)
926928
else:
927929
return OrchestrationResponse(
@@ -1606,6 +1608,31 @@ def _initialize_response_generator(
16061608
logger.error(f"Failed to initialize response generator: {str(e)}")
16071609
raise
16081610

1611+
@staticmethod
1612+
def _format_chunks_for_test_response(
1613+
relevant_chunks: Optional[List[Dict[str, Union[str, float, Dict[str, Any]]]]],
1614+
) -> Optional[List[ChunkInfo]]:
1615+
"""
1616+
Format retrieved chunks for test response.
1617+
1618+
Args:
1619+
relevant_chunks: List of retrieved chunks with metadata
1620+
1621+
Returns:
1622+
List of ChunkInfo objects with rank and content, or None if no chunks
1623+
"""
1624+
if not relevant_chunks:
1625+
return None
1626+
1627+
formatted_chunks = []
1628+
for rank, chunk in enumerate(relevant_chunks, start=1):
1629+
# Extract text content - prefer "text" key, fallback to "content"
1630+
chunk_text = chunk.get("text", chunk.get("content", ""))
1631+
if isinstance(chunk_text, str) and chunk_text.strip():
1632+
formatted_chunks.append(ChunkInfo(rank=rank, chunkRetrieved=chunk_text))
1633+
1634+
return formatted_chunks if formatted_chunks else None
1635+
16091636
@observe(name="generate_rag_response", as_type="generation")
16101637
def _generate_rag_response(
16111638
self,
@@ -1639,6 +1666,7 @@ def _generate_rag_response(
16391666
questionOutOfLLMScope=False,
16401667
inputGuardFailed=False,
16411668
content=TECHNICAL_ISSUE_MESSAGE,
1669+
chunks=self._format_chunks_for_test_response(relevant_chunks),
16421670
)
16431671
else:
16441672
return OrchestrationResponse(
@@ -1706,6 +1734,7 @@ def _generate_rag_response(
17061734
questionOutOfLLMScope=True,
17071735
inputGuardFailed=False,
17081736
content=OUT_OF_SCOPE_MESSAGE,
1737+
chunks=self._format_chunks_for_test_response(relevant_chunks),
17091738
)
17101739
else:
17111740
return OrchestrationResponse(
@@ -1725,6 +1754,7 @@ def _generate_rag_response(
17251754
questionOutOfLLMScope=False,
17261755
inputGuardFailed=False,
17271756
content=answer,
1757+
chunks=self._format_chunks_for_test_response(relevant_chunks),
17281758
)
17291759
else:
17301760
return OrchestrationResponse(
@@ -1765,6 +1795,7 @@ def _generate_rag_response(
17651795
questionOutOfLLMScope=False,
17661796
inputGuardFailed=False,
17671797
content=TECHNICAL_ISSUE_MESSAGE,
1798+
chunks=self._format_chunks_for_test_response(relevant_chunks),
17681799
)
17691800
else:
17701801
return OrchestrationResponse(

src/llm_orchestration_service_api.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,20 +332,30 @@ def test_orchestrate_llm_request(
332332
conversationHistory=[],
333333
url="test-context",
334334
environment=request.environment,
335-
connection_id=str(request.connectionId),
335+
connection_id=str(request.connectionId)
336+
if request.connectionId is not None
337+
else None,
336338
)
337339

338340
logger.info(f"This is full request constructed for testing: {full_request}")
339341

340342
# Process the request using the same logic
341343
response = orchestration_service.process_orchestration_request(full_request)
342344

343-
# Convert to TestOrchestrationResponse (exclude chatId)
345+
# If response is already TestOrchestrationResponse (when environment is testing), return it directly
346+
if isinstance(response, TestOrchestrationResponse):
347+
logger.info(
348+
f"Successfully processed test request for environment: {request.environment}"
349+
)
350+
return response
351+
352+
# Convert to TestOrchestrationResponse (exclude chatId) for other cases
344353
test_response = TestOrchestrationResponse(
345354
llmServiceActive=response.llmServiceActive,
346355
questionOutOfLLMScope=response.questionOutOfLLMScope,
347356
inputGuardFailed=response.inputGuardFailed,
348357
content=response.content,
358+
chunks=None, # OrchestrationResponse doesn't have chunks
349359
)
350360

351361
logger.info(

src/models/request_models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,17 @@ class TestOrchestrationRequest(BaseModel):
230230
..., description="Environment context"
231231
)
232232
connectionId: Optional[int] = Field(
233-
..., description="Optional connection identifier"
233+
None, description="Optional connection identifier"
234234
)
235235

236236

237+
class ChunkInfo(BaseModel):
238+
"""Model for chunk information in test response."""
239+
240+
rank: int = Field(..., description="Rank of the retrieved chunk")
241+
chunkRetrieved: str = Field(..., description="Content of the retrieved chunk")
242+
243+
237244
class TestOrchestrationResponse(BaseModel):
238245
"""Model for test orchestration response (without chatId)."""
239246

@@ -245,3 +252,6 @@ class TestOrchestrationResponse(BaseModel):
245252
..., description="Whether input guard validation failed"
246253
)
247254
content: str = Field(..., description="Response content with citations")
255+
chunks: Optional[List[ChunkInfo]] = Field(
256+
default=None, description="Retrieved chunks with rank and content"
257+
)

0 commit comments

Comments
 (0)