-
Notifications
You must be signed in to change notification settings - Fork 32
Add AskAI message feedback endpoint #2297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 19 commits
ba8a7ef
4ce3f48
1e52f45
264ac88
2d08faa
25eb256
7926de7
4df1e5c
d0883cf
3dadebf
b48be34
c2b7c3c
79e4117
5de5770
a112b98
6eea0cb
b6b2cbf
5f6a852
07ff6e6
01cd487
ce47e59
e562c5c
d313731
ce6d0ea
7dca09e
65ad494
f622191
717d3d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { logWarn } from '../../../telemetry/logging' | ||
| import { traceSpan } from '../../../telemetry/tracing' | ||
| import { Reaction, useChatActions, useMessageReaction } from './chat.store' | ||
| import { useMutation } from '@tanstack/react-query' | ||
| import { useCallback, useRef } from 'react' | ||
|
|
||
| export type { Reaction } from './chat.store' | ||
|
|
||
| const DEBOUNCE_MS = 500 | ||
|
|
||
| interface MessageFeedbackRequest { | ||
| messageId: string | ||
| conversationId: string | ||
| reaction: Reaction | ||
| } | ||
|
|
||
| interface UseMessageFeedbackReturn { | ||
| selectedReaction: Reaction | null | ||
| submitFeedback: (reaction: Reaction) => void | ||
| } | ||
|
|
||
| const submitFeedbackToApi = async ( | ||
| payload: MessageFeedbackRequest | ||
| ): Promise<void> => { | ||
| await traceSpan('submit message-feedback', async (span) => { | ||
| span.setAttribute('gen_ai.conversation.id', payload.conversationId) // correlation with backend | ||
| span.setAttribute('ask_ai.message.id', payload.messageId) | ||
| span.setAttribute('ask_ai.feedback.reaction', payload.reaction) | ||
|
|
||
| const response = await fetch('/docs/_api/v1/ask-ai/message-feedback', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| logWarn('Failed to submit feedback', { | ||
| 'http.status_code': response.status, | ||
| 'ask_ai.message.id': payload.messageId, | ||
| 'ask_ai.feedback.reaction': payload.reaction, | ||
| }) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| export const useMessageFeedback = ( | ||
| messageId: string, | ||
| conversationId: string | null | ||
| ): UseMessageFeedbackReturn => { | ||
| const selectedReaction = useMessageReaction(messageId) | ||
| const { setMessageFeedback } = useChatActions() | ||
| const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>( | ||
| null | ||
| ) | ||
|
|
||
| const mutation = useMutation({ | ||
|
Check failure on line 58 in src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageFeedback.ts
|
||
| mutationFn: submitFeedbackToApi, | ||
| onError: (error) => { | ||
| logWarn('Error submitting feedback', { | ||
| 'error.message': | ||
| error instanceof Error ? error.message : String(error), | ||
| }) | ||
| // Don't reset selection on error - user intent was clear | ||
| }, | ||
| }) | ||
|
|
||
| const submitFeedback = useCallback( | ||
| (reaction: Reaction) => { | ||
| if (!conversationId) { | ||
| logWarn('Cannot submit feedback without conversationId', { | ||
| 'ask_ai.message.id': messageId, | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| // Ignore if same reaction already selected | ||
| if (selectedReaction === reaction) { | ||
| return | ||
| } | ||
|
|
||
| // Optimistic update - stored in Zustand so it persists across tab switches | ||
| setMessageFeedback(messageId, reaction) | ||
|
|
||
| // Cancel any pending debounced submission | ||
| if (debounceTimeoutRef.current) { | ||
| clearTimeout(debounceTimeoutRef.current) | ||
| } | ||
|
|
||
| // Debounce the API call - only send the final selection | ||
| debounceTimeoutRef.current = setTimeout(() => { | ||
| mutation.mutate({ | ||
| messageId, | ||
| conversationId, | ||
| reaction, | ||
| }) | ||
| }, DEBOUNCE_MS) | ||
| }, | ||
| [ | ||
| messageId, | ||
| conversationId, | ||
| selectedReaction, | ||
| mutation, | ||
| setMessageFeedback, | ||
| ] | ||
| ) | ||
|
|
||
| return { | ||
| selectedReaction, | ||
| submitFeedback, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Licensed to Elasticsearch B.V under one or more agreements. | ||
| // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. | ||
| // See the LICENSE file in the project root for more information | ||
|
|
||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace Elastic.Documentation.Api.Core.AskAi; | ||
|
|
||
| /// <summary> | ||
| /// Request model for submitting feedback on a specific Ask AI message. | ||
| /// Using Guid type ensures automatic validation during JSON deserialization. | ||
| /// </summary> | ||
| public record AskAiMessageFeedbackRequest( | ||
| Guid MessageId, | ||
| Guid ConversationId, | ||
| Reaction Reaction | ||
| ); | ||
|
|
||
| /// <summary> | ||
| /// The user's reaction to an Ask AI message. | ||
| /// </summary> | ||
| [JsonConverter(typeof(JsonStringEnumConverter<Reaction>))] | ||
| public enum Reaction | ||
| { | ||
| [JsonStringEnumMemberName("thumbsUp")] | ||
| ThumbsUp, | ||
|
|
||
| [JsonStringEnumMemberName("thumbsDown")] | ||
| ThumbsDown | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Licensed to Elasticsearch B.V under one or more agreements. | ||
| // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. | ||
| // See the LICENSE file in the project root for more information | ||
|
|
||
| using System.Diagnostics; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Elastic.Documentation.Api.Core.AskAi; | ||
|
|
||
| /// <summary> | ||
| /// Use case for handling Ask AI message feedback submissions. | ||
| /// </summary> | ||
| public class AskAiMessageFeedbackUsecase( | ||
| IAskAiMessageFeedbackGateway feedbackGateway, | ||
| ILogger<AskAiMessageFeedbackUsecase> logger) | ||
| { | ||
| private static readonly ActivitySource FeedbackActivitySource = new(TelemetryConstants.AskAiFeedbackSourceName); | ||
|
|
||
| public async Task SubmitFeedback(AskAiMessageFeedbackRequest request, string? euid, CancellationToken ctx) | ||
| { | ||
| using var activity = FeedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal); | ||
| _ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); // correlation with chat traces | ||
| _ = activity?.SetTag("ask_ai.message.id", request.MessageId); | ||
| _ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant()); | ||
| // Note: user.euid is automatically added to spans by EuidSpanProcessor | ||
|
|
||
| // MessageId and ConversationId are Guid types, so no sanitization needed | ||
| logger.LogInformation( | ||
| "Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}", | ||
| request.MessageId, | ||
| request.ConversationId, | ||
|
||
| request.Reaction); | ||
|
|
||
| var record = new AskAiMessageFeedbackRecord( | ||
| request.MessageId, | ||
| request.ConversationId, | ||
| request.Reaction, | ||
| euid | ||
| ); | ||
|
|
||
| await feedbackGateway.RecordFeedbackAsync(record, ctx); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.