mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(langfuse): preserve trace sampling for feedback
This commit is contained in:
parent
5ad650696d
commit
35efbcc982
10 changed files with 85 additions and 2 deletions
|
|
@ -12,6 +12,8 @@ const {
|
|||
encodeAndFormatAudios,
|
||||
encodeAndFormatVideos,
|
||||
encodeAndFormatDocuments,
|
||||
isLangfuseTraceSampled,
|
||||
traceIdForMessage,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
Constants,
|
||||
|
|
@ -716,6 +718,9 @@ class BaseClient {
|
|||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
...(isAgentsEndpoint(this.options.endpoint) && {
|
||||
langfuseSampled: isLangfuseTraceSampled(traceIdForMessage(responseMessageId)),
|
||||
}),
|
||||
isEdited,
|
||||
model: this.getResponseModel(),
|
||||
sender: this.sender,
|
||||
|
|
|
|||
|
|
@ -746,6 +746,30 @@ describe('BaseClient', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('persists the generation-time Langfuse sampling decision for agent responses', async () => {
|
||||
const previousSampleRate = process.env.LANGFUSE_SAMPLE_RATE;
|
||||
process.env.LANGFUSE_SAMPLE_RATE = '0';
|
||||
TestClient.options.endpoint = 'agents';
|
||||
const saveSpy = jest.spyOn(TestClient, 'saveMessageToDatabase');
|
||||
|
||||
try {
|
||||
const response = await TestClient.sendMessage('Hello, world!', { user: {} });
|
||||
|
||||
expect(response.langfuseSampled).toBe(false);
|
||||
expect(saveSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ langfuseSampled: false }),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
} finally {
|
||||
if (previousSampleRate == null) {
|
||||
delete process.env.LANGFUSE_SAMPLE_RATE;
|
||||
} else {
|
||||
process.env.LANGFUSE_SAMPLE_RATE = previousSampleRate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle existing conversation when getConvo retrieves one', async () => {
|
||||
const existingConvo = {
|
||||
conversationId: 'existing-convo-id',
|
||||
|
|
|
|||
|
|
@ -445,6 +445,7 @@ router.put(
|
|||
if (!isAssistantsEndpoint(updatedMessage.endpoint)) {
|
||||
sendFeedbackScore({
|
||||
traceId: traceIdForMessage(messageId),
|
||||
sampled: updatedMessage.langfuseSampled,
|
||||
feedback: updatedMessage.feedback,
|
||||
appConfig: req.config,
|
||||
metadata: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
hasLangfuseEnvCredentials,
|
||||
isLangfuseFanoutEnabled,
|
||||
isLangfuseTenantExportEnabled,
|
||||
isLangfuseTracingEnabled,
|
||||
isLangfuseTraceSampled,
|
||||
usesLangfuseMultiTenantRouting,
|
||||
} from './policy';
|
||||
|
|
@ -106,8 +107,13 @@ function getConfiguredScoreDestination(
|
|||
export function getScoreDestinations(
|
||||
appConfig: AppConfig | undefined,
|
||||
traceId: string,
|
||||
sampled?: boolean,
|
||||
): LangfuseScoreDestination[] {
|
||||
if (!isLangfuseTraceSampled(traceId)) {
|
||||
if (
|
||||
!isLangfuseTracingEnabled() ||
|
||||
sampled === false ||
|
||||
(sampled == null && !isLangfuseTraceSampled(traceId))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,32 @@ describe('Langfuse feedback scores', () => {
|
|||
expect(getFetchMock()).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves a sampled trace when the sample rate decreases', async () => {
|
||||
process.env.LANGFUSE_SAMPLE_RATE = '0.1';
|
||||
const { sendFeedbackScore } = await loadFeedback();
|
||||
|
||||
await sendFeedbackScore({
|
||||
traceId: '658f74b0a232417fc3e6e4d9ef5f563a',
|
||||
sampled: true,
|
||||
feedback: { rating: 'thumbsUp' },
|
||||
});
|
||||
|
||||
expect(getFetchMock()).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('preserves an excluded trace when the sample rate increases', async () => {
|
||||
process.env.LANGFUSE_SAMPLE_RATE = '1';
|
||||
const { sendFeedbackScore } = await loadFeedback();
|
||||
|
||||
await sendFeedbackScore({
|
||||
traceId: '86d413435f8b0d7f32d4d010ce769e2e',
|
||||
sampled: false,
|
||||
feedback: { rating: 'thumbsUp' },
|
||||
});
|
||||
|
||||
expect(getFetchMock()).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('posts feedback scores to central fanout and tenant Langfuse projects', async () => {
|
||||
enableTenantFanout();
|
||||
delete process.env.TENANT_ISOLATION_STRICT;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type LangfuseFeedbackMetadata = Record<string, string | number | boolean
|
|||
|
||||
export type SendFeedbackScoreParams = {
|
||||
traceId: string;
|
||||
sampled?: boolean;
|
||||
feedback?: LangfuseFeedback | null;
|
||||
metadata?: LangfuseFeedbackMetadata;
|
||||
observationId?: string;
|
||||
|
|
@ -102,6 +103,7 @@ function buildScorePayload({
|
|||
|
||||
export async function sendFeedbackScore({
|
||||
traceId,
|
||||
sampled,
|
||||
feedback,
|
||||
metadata = {},
|
||||
observationId,
|
||||
|
|
@ -111,7 +113,7 @@ export async function sendFeedbackScore({
|
|||
return;
|
||||
}
|
||||
|
||||
const destinations = getScoreDestinations(appConfig, traceId);
|
||||
const destinations = getScoreDestinations(appConfig, traceId, sampled);
|
||||
if (destinations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,20 @@ describe('Message Operations', () => {
|
|||
expect(updatedMessage?.text).toBe('Updated text');
|
||||
});
|
||||
|
||||
it('returns the generation-time Langfuse sampling decision with feedback updates', async () => {
|
||||
await saveMessage(mockCtx, {
|
||||
...mockMessageData,
|
||||
langfuseSampled: true,
|
||||
});
|
||||
|
||||
const result = await updateMessage(mockCtx.userId, {
|
||||
messageId: 'msg123',
|
||||
feedback: { rating: 'thumbsUp' },
|
||||
});
|
||||
|
||||
expect(result?.langfuseSampled).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an error if message is not found', async () => {
|
||||
await expect(
|
||||
updateMessage(mockCtx.userId, { messageId: 'nonexistent', text: 'Test' }),
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
tokenCount: updatedMessage.tokenCount,
|
||||
feedback: updatedMessage.feedback,
|
||||
endpoint: updatedMessage.endpoint,
|
||||
langfuseSampled: updatedMessage.langfuseSampled,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error('Error updating message:', err);
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ const messageSchema: Schema<IMessage> = new Schema(
|
|||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
langfuseSampled: {
|
||||
type: Boolean,
|
||||
},
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface IMessage extends Document {
|
|||
tag: TFeedbackTag | undefined;
|
||||
text?: string;
|
||||
};
|
||||
langfuseSampled?: boolean;
|
||||
_meiliIndex?: boolean;
|
||||
files?: unknown[];
|
||||
plugin?: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue