Skip to content

Commit 42e22f9

Browse files
authored
Clean up OTel instrumentation for AskAi (#2144)
* Clean up OTel instrumentation * Add comment * Fix naming * Run prettier * Fix duplicate spans and other adjustments * Fix setting conversationId * Change naming from threadId to conversationId * formatting * Fix message rendering * Simplify streaming message * Run prettier * Move StreamTransformerBase to core package * Combine aiProviderStore and chat.store * Fix tests
1 parent 4eb28f6 commit 42e22f9

28 files changed

+377
-384
lines changed

Directory.Packages.props

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<PackageVersion Include="AWSSDK.SQS" Version="4.0.2" />
2626
<PackageVersion Include="AWSSDK.S3" Version="4.0.7.14" />
2727
<PackageVersion Include="Elastic.OpenTelemetry" Version="1.1.0" />
28-
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
28+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
2929
<PackageVersion Include="KubernetesClient" Version="17.0.14" />
3030
<PackageVersion Include="Elastic.Aspire.Hosting.Elasticsearch" Version="9.3.0" />
3131
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.1.4" />
@@ -76,10 +76,10 @@
7676
<ItemGroup>
7777
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" />
7878
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.0" />
79-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
80-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
81-
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
82-
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
79+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
80+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
81+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
82+
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
8383
</ItemGroup>
8484
<!-- Test packages -->
8585
<ItemGroup>
@@ -99,4 +99,4 @@
9999
</PackageVersion>
100100
<PackageVersion Include="xunit.v3" Version="2.0.2" />
101101
</ItemGroup>
102-
</Project>
102+
</Project>

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AiProviderSelector.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @jsxImportSource @emotion/react */
2-
import { useAiProviderStore } from './aiProviderStore'
2+
import { useChatActions, useAiProvider, type AiProvider } from './chat.store'
33
import { EuiRadioGroup } from '@elastic/eui'
44
import type { EuiRadioGroupOption } from '@elastic/eui'
55
import { css } from '@emotion/react'
@@ -22,16 +22,15 @@ const options: EuiRadioGroupOption[] = [
2222
]
2323

2424
export const AiProviderSelector = () => {
25-
const { provider, setProvider } = useAiProviderStore()
25+
const provider = useAiProvider()
26+
const { setAiProvider } = useChatActions()
2627

2728
return (
2829
<div css={containerStyles}>
2930
<EuiRadioGroup
3031
options={options}
3132
idSelected={provider}
32-
onChange={(id) =>
33-
setProvider(id as 'AgentBuilder' | 'LlmGateway')
34-
}
33+
onChange={(id) => setAiProvider(id as AiProvider)}
3534
name="aiProvider"
3635
legend={{
3736
children: 'AI Provider',

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiEvent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as z from 'zod'
44
// Event type constants for type-safe referencing
55
export const EventTypes = {
66
CONVERSATION_START: 'conversation_start',
7-
CHUNK: 'chunk',
8-
CHUNK_COMPLETE: 'chunk_complete',
7+
MESSAGE_CHUNK: 'message_chunk',
8+
MESSAGE_COMPLETE: 'message_complete',
99
SEARCH_TOOL_CALL: 'search_tool_call',
1010
TOOL_CALL: 'tool_call',
1111
TOOL_RESULT: 'tool_result',
@@ -23,14 +23,14 @@ export const ConversationStartEventSchema = z.object({
2323
})
2424

2525
export const ChunkEventSchema = z.object({
26-
type: z.literal(EventTypes.CHUNK),
26+
type: z.literal(EventTypes.MESSAGE_CHUNK),
2727
id: z.string(),
2828
timestamp: z.number(),
2929
content: z.string(),
3030
})
3131

3232
export const ChunkCompleteEventSchema = z.object({
33-
type: z.literal(EventTypes.CHUNK_COMPLETE),
33+
type: z.literal(EventTypes.MESSAGE_COMPLETE),
3434
id: z.string(),
3535
timestamp: z.number(),
3636
fullContent: z.string(),

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ jest.mock('./chat.store', () => ({
99
getState: jest.fn(),
1010
},
1111
useChatMessages: jest.fn(() => []),
12+
useAiProvider: jest.fn(() => 'LlmGateway'),
1213
useChatActions: jest.fn(() => ({
1314
submitQuestion: jest.fn(),
1415
clearChat: jest.fn(),
16+
setAiProvider: jest.fn(),
1517
})),
1618
}))
1719

@@ -94,14 +96,14 @@ describe('Chat Component', () => {
9496
id: '1',
9597
type: 'user' as const,
9698
content: 'What is Elasticsearch?',
97-
threadId: 'thread-1',
99+
conversationId: 'thread-1',
98100
timestamp: Date.now(),
99101
},
100102
{
101103
id: '2',
102104
type: 'ai' as const,
103105
content: 'Elasticsearch is a search engine...',
104-
threadId: 'thread-1',
106+
conversationId: 'thread-1',
105107
timestamp: Date.now(),
106108
status: 'complete' as const,
107109
},
@@ -245,14 +247,14 @@ describe('Chat Component', () => {
245247
id: '1',
246248
type: 'user' as const,
247249
content: 'Question',
248-
threadId: 'thread-1',
250+
conversationId: 'thread-1',
249251
timestamp: Date.now(),
250252
},
251253
{
252254
id: '2',
253255
type: 'ai' as const,
254256
content: 'Answer...',
255-
threadId: 'thread-1',
257+
conversationId: 'thread-1',
256258
timestamp: Date.now(),
257259
status: 'streaming' as const,
258260
},

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('ChatMessage Component', () => {
1313
id: '1',
1414
type: 'user',
1515
content: 'What is Elasticsearch?',
16-
threadId: 'thread-1',
16+
conversationId: 'thread-1',
1717
timestamp: Date.now(),
1818
}
1919

@@ -44,7 +44,7 @@ describe('ChatMessage Component', () => {
4444
id: '2',
4545
type: 'ai',
4646
content: 'Elasticsearch is a distributed search engine...',
47-
threadId: 'thread-1',
47+
conversationId: 'thread-1',
4848
timestamp: Date.now(),
4949
status: 'complete',
5050
}
@@ -102,7 +102,7 @@ describe('ChatMessage Component', () => {
102102
id: '3',
103103
type: 'ai',
104104
content: 'Elasticsearch is...',
105-
threadId: 'thread-1',
105+
conversationId: 'thread-1',
106106
timestamp: Date.now(),
107107
status: 'streaming',
108108
}
@@ -142,7 +142,7 @@ describe('ChatMessage Component', () => {
142142
id: '4',
143143
type: 'ai',
144144
content: 'Previous content...',
145-
threadId: 'thread-1',
145+
conversationId: 'thread-1',
146146
timestamp: Date.now(),
147147
status: 'error',
148148
}
@@ -176,7 +176,7 @@ describe('ChatMessage Component', () => {
176176
id: '5',
177177
type: 'ai',
178178
content: '# Heading\n\n**Bold text** and *italic*',
179-
threadId: 'thread-1',
179+
conversationId: 'thread-1',
180180
timestamp: Date.now(),
181181
status: 'complete',
182182
}

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,6 @@ interface ChatMessageProps {
6363
onRetry?: () => void
6464
}
6565

66-
const getAccumulatedContent = (messages: AskAiEvent[]) => {
67-
return messages
68-
.filter((m) => m.type === 'chunk')
69-
.map((m) => m.content)
70-
.join('')
71-
}
72-
7366
const splitContentAndReferences = (
7467
content: string
7568
): { mainContent: string; referencesJson: string | null } => {
@@ -144,7 +137,7 @@ const computeAiStatus = (
144137
m.type === EventTypes.SEARCH_TOOL_CALL ||
145138
m.type === EventTypes.TOOL_CALL ||
146139
m.type === EventTypes.TOOL_RESULT ||
147-
m.type === EventTypes.CHUNK
140+
m.type === EventTypes.MESSAGE_CHUNK
148141
)
149142
.sort((a, b) => a.timestamp - b.timestamp)
150143

@@ -166,9 +159,9 @@ const computeAiStatus = (
166159
case EventTypes.TOOL_RESULT:
167160
return STATUS_MESSAGES.ANALYZING
168161

169-
case EventTypes.CHUNK: {
162+
case EventTypes.MESSAGE_CHUNK: {
170163
const allContent = events
171-
.filter((m) => m.type === EventTypes.CHUNK)
164+
.filter((m) => m.type === EventTypes.MESSAGE_CHUNK)
172165
.map((m) => m.content)
173166
.join('')
174167

@@ -279,9 +272,9 @@ export const ChatMessage = ({
279272
)
280273
}
281274

282-
const content =
283-
streamingContent ||
284-
(events.length > 0 ? getAccumulatedContent(events) : message.content)
275+
// Use streamingContent during streaming, otherwise use message.content from store
276+
// message.content is updated atomically with status when CONVERSATION_END arrives
277+
const content = streamingContent || message.content
285278

286279
const hasError = message.status === 'error' || !!error
287280

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { ChatMessage } from './ChatMessage'
33
import {
44
ChatMessage as ChatMessageType,
55
useChatActions,
6-
useThreadId,
6+
useConversationId,
77
} from './chat.store'
88
import { useAskAi } from './useAskAi'
9-
import * as React from 'react'
109
import { useEffect, useRef } from 'react'
1110

1211
interface StreamingAiMessageProps {
@@ -22,20 +21,20 @@ export const StreamingAiMessage = ({
2221
updateAiMessage,
2322
hasMessageBeenSent,
2423
markMessageAsSent,
25-
setThreadId,
24+
setConversationId,
2625
} = useChatActions()
27-
const threadId = useThreadId()
26+
const conversationId = useConversationId()
2827
const contentRef = useRef('')
2928

3029
const { events, sendQuestion } = useAskAi({
31-
threadId: threadId ?? undefined,
30+
conversationId: conversationId ?? undefined,
3231
onEvent: (event) => {
3332
if (event.type === EventTypes.CONVERSATION_START) {
3433
// Capture conversationId from backend on first request
35-
if (event.conversationId && !threadId) {
36-
setThreadId(event.conversationId)
34+
if (event.conversationId && !conversationId) {
35+
setConversationId(event.conversationId)
3736
}
38-
} else if (event.type === EventTypes.CHUNK) {
37+
} else if (event.type === EventTypes.MESSAGE_CHUNK) {
3938
contentRef.current += event.content
4039
} else if (event.type === EventTypes.ERROR) {
4140
// Handle error events from the stream
@@ -45,10 +44,21 @@ export const StreamingAiMessage = ({
4544
'error'
4645
)
4746
} else if (event.type === EventTypes.CONVERSATION_END) {
48-
updateAiMessage(message.id, contentRef.current, 'complete')
47+
updateAiMessage(
48+
message.id,
49+
message.content || contentRef.current,
50+
'complete'
51+
)
4952
}
5053
},
51-
onError: () => {
54+
onError: (error) => {
55+
console.error('[AI Provider] Error in StreamingAiMessage:', {
56+
messageId: message.id,
57+
errorMessage: error.message,
58+
errorStack: error.stack,
59+
errorName: error.name,
60+
fullError: error,
61+
})
5262
updateAiMessage(
5363
message.id,
5464
message.content || 'Error occurred',
@@ -78,15 +88,16 @@ export const StreamingAiMessage = ({
7888
markMessageAsSent,
7989
])
8090

91+
// Always use contentRef.current if it has content (regardless of status)
92+
// This way we don't need to save to message.content and can just use streamingContent
93+
const streamingContentToPass =
94+
isLast && contentRef.current ? contentRef.current : undefined
95+
8196
return (
8297
<ChatMessage
8398
message={message}
8499
events={isLast ? events : []}
85-
streamingContent={
86-
isLast && message.status === 'streaming'
87-
? contentRef.current
88-
: undefined
89-
}
100+
streamingContent={streamingContentToPass}
90101
/>
91102
)
92103
}

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/aiProviderResolver.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/aiProviderStore.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/chat.store.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('chat.store', () => {
9494

9595
// Verify fresh state
9696
expect(chatStore.getState().chatMessages).toHaveLength(0)
97-
expect(chatStore.getState().threadId).toBeNull()
97+
expect(chatStore.getState().conversationId).toBeNull()
9898

9999
// Start new conversation
100100
act(() => {

0 commit comments

Comments
 (0)