Skip to content

Commit 64b9f99

Browse files
Add langfuse integration
1 parent d54ffd7 commit 64b9f99

File tree

10 files changed

+189
-4
lines changed

10 files changed

+189
-4
lines changed

.github/workflows/_gcp-deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ jobs:
6060
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
6161
NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }}
6262
NEXT_PUBLIC_SENTRY_BACKEND_DSN=${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }}
63+
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=${{ vars.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY }}
64+
NEXT_PUBLIC_LANGFUSE_BASE_URL=${{ vars.NEXT_PUBLIC_LANGFUSE_BASE_URL }}
6365
SENTRY_SMUAT=${{ secrets.SENTRY_SMUAT }}
6466
SENTRY_ORG=${{ vars.SENTRY_ORG }}
6567
SENTRY_WEBAPP_PROJECT=${{ vars.SENTRY_WEBAPP_PROJECT }}

packages/web/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
"@hookform/resolvers": "^3.9.0",
5050
"@iconify/react": "^5.1.0",
5151
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
52+
"@opentelemetry/api-logs": "^0.203.0",
53+
"@opentelemetry/instrumentation": "^0.203.0",
54+
"@opentelemetry/sdk-logs": "^0.203.0",
5255
"@radix-ui/react-accordion": "^1.2.11",
5356
"@radix-ui/react-alert-dialog": "^1.1.5",
5457
"@radix-ui/react-avatar": "^1.1.2",
@@ -97,6 +100,7 @@
97100
"@uidotdev/usehooks": "^2.4.1",
98101
"@uiw/codemirror-themes": "^4.23.6",
99102
"@uiw/react-codemirror": "^4.23.0",
103+
"@vercel/otel": "^1.13.0",
100104
"@viz-js/lang-dot": "^1.0.4",
101105
"@xiechao/codemirror-lang-handlebars": "^1.0.4",
102106
"ai": "5.0.0-beta.21",
@@ -131,6 +135,8 @@
131135
"graphql": "^16.9.0",
132136
"http-status-codes": "^2.3.0",
133137
"input-otp": "^1.4.2",
138+
"langfuse": "^3.38.4",
139+
"langfuse-vercel": "^3.38.4",
134140
"lucide-react": "^0.517.0",
135141
"micromatch": "^4.0.8",
136142
"next": "14.2.26",

packages/web/src/app/api/(server)/chat/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
UIMessageStreamOptions,
3030
UIMessageStreamWriter,
3131
} from "ai";
32+
import { randomUUID } from "crypto";
3233
import { StatusCodes } from "http-status-codes";
3334
import { z } from "zod";
3435

@@ -153,6 +154,8 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
153154
}
154155
}
155156

157+
const traceId = randomUUID();
158+
156159
// Extract user messages and assistant answers.
157160
// We will use this as the context we carry between messages.
158161
const messageHistory =
@@ -197,6 +200,7 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
197200
data: source,
198201
});
199202
},
203+
traceId,
200204
});
201205

202206
await mergeStreamAsync(researchStream, writer, {
@@ -215,6 +219,7 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
215219
totalOutputTokens: totalUsage.outputTokens,
216220
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
217221
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
222+
traceId,
218223
}
219224
})
220225

packages/web/src/env.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export const env = createEnv({
113113
SOURCEBOT_CHAT_MAX_STEP_COUNT: numberSchema.default(20),
114114

115115
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE: booleanSchema.default('false'),
116+
117+
LANGFUSE_SECRET_KEY: z.string().optional(),
116118
},
117119
// @NOTE: Please make sure of the following:
118120
// - Make sure you destructure all client variables in
@@ -127,13 +129,18 @@ export const env = createEnv({
127129
NEXT_PUBLIC_POLLING_INTERVAL_MS: numberSchema.default(5000),
128130

129131
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: z.enum(SOURCEBOT_CLOUD_ENVIRONMENT).optional(),
132+
133+
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: z.string().optional(),
134+
NEXT_PUBLIC_LANGFUSE_BASE_URL: z.string().optional()
130135
},
131136
// For Next.js >= 13.4.4, you only need to destructure client variables:
132137
experimental__runtimeEnv: {
133138
NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK,
134139
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
135140
NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
136141
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT,
142+
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: process.env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
143+
NEXT_PUBLIC_LANGFUSE_BASE_URL: process.env.NEXT_PUBLIC_LANGFUSE_BASE_URL,
137144
},
138145
skipValidation: process.env.SKIP_ENV_VALIDATION === "1",
139146
emptyStringAsUndefined: true,

packages/web/src/features/chat/agent.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface AgentOptions {
2020
inputMessages: ModelMessage[];
2121
inputSources: Source[];
2222
onWriteSource: (source: Source) => void;
23+
traceId: string;
2324
}
2425

2526
// If the agent exceeds the step count, then we will stop.
@@ -36,6 +37,7 @@ export const createAgentStream = async ({
3637
inputSources,
3738
selectedRepos,
3839
onWriteSource,
40+
traceId,
3941
}: AgentOptions) => {
4042
const baseSystemPrompt = createBaseSystemPrompt({
4143
selectedRepos,
@@ -131,7 +133,14 @@ export const createAgentStream = async ({
131133
})
132134
}
133135
})
134-
}
136+
},
137+
// Only enable langfuse traces in cloud environments.
138+
experimental_telemetry: {
139+
isEnabled: env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined,
140+
metadata: {
141+
langfuseTraceId: traceId,
142+
},
143+
},
135144
});
136145

137146
return stream;

packages/web/src/features/chat/components/chatThread/answerCard.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,28 @@ import { submitFeedback } from "../../actions";
1616
import { isServiceError } from "@/lib/utils";
1717
import { useDomain } from "@/hooks/useDomain";
1818
import useCaptureEvent from "@/hooks/useCaptureEvent";
19+
import { LangfuseWeb } from "langfuse";
20+
import { env } from "@/env.mjs";
1921

2022
interface AnswerCardProps {
2123
answerText: string;
2224
messageId: string;
2325
chatId: string;
2426
feedback?: 'like' | 'dislike' | undefined;
27+
traceId?: string;
2528
}
2629

30+
const langfuseWeb = new LangfuseWeb({
31+
publicKey: env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
32+
baseUrl: env.NEXT_PUBLIC_LANGFUSE_BASE_URL,
33+
});
34+
2735
export const AnswerCard = forwardRef<HTMLDivElement, AnswerCardProps>(({
2836
answerText,
2937
messageId,
3038
chatId,
3139
feedback: _feedback,
40+
traceId,
3241
}, forwardedRef) => {
3342
const markdownRendererRef = useRef<HTMLDivElement>(null);
3443
const { tocItems, activeId } = useExtractTOCItems({ target: markdownRendererRef.current });
@@ -55,13 +64,13 @@ export const AnswerCard = forwardRef<HTMLDivElement, AnswerCardProps>(({
5564

5665
const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => {
5766
setIsSubmittingFeedback(true);
58-
67+
5968
const response = await submitFeedback({
6069
chatId,
6170
messageId,
6271
feedbackType
6372
}, domain);
64-
73+
6574
if (isServiceError(response)) {
6675
toast({
6776
description: `❌ Failed to submit feedback: ${response.message}`,
@@ -77,8 +86,14 @@ export const AnswerCard = forwardRef<HTMLDivElement, AnswerCardProps>(({
7786
chatId,
7887
messageId,
7988
});
89+
90+
langfuseWeb.score({
91+
traceId: traceId,
92+
name: 'user_feedback',
93+
value: feedbackType === 'like' ? 1 : 0,
94+
})
8095
}
81-
96+
8297
setIsSubmittingFeedback(false);
8398
}, [chatId, messageId, domain, toast, captureEvent]);
8499

packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
410410
chatId={chatId}
411411
messageId={assistantMessage.id}
412412
feedback={messageMetadata?.feedback?.type}
413+
traceId={messageMetadata?.traceId}
413414
/>
414415
) : !isStreaming && (
415416
<p className="text-destructive">Error: No answer response was provided</p>

packages/web/src/features/chat/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const sbChatMessageMetadataSchema = z.object({
5050
userId: z.string(),
5151
}).optional(),
5252
selectedRepos: z.array(z.string()).optional(),
53+
traceId: z.string().optional(),
5354
});
5455

5556
export type SBChatMessageMetadata = z.infer<typeof sbChatMessageMetadataSchema>;

packages/web/src/instrumentation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import * as Sentry from '@sentry/nextjs';
2+
import { registerOTel } from '@vercel/otel';
3+
import { LangfuseExporter } from 'langfuse-vercel';
4+
import { env } from './env.mjs';
25

36
export async function register() {
7+
if (env.LANGFUSE_SECRET_KEY && env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY) {
8+
registerOTel({
9+
serviceName: 'sourcebot',
10+
traceExporter: new LangfuseExporter({
11+
secretKey: env.LANGFUSE_SECRET_KEY,
12+
publicKey: env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
13+
baseUrl: env.NEXT_PUBLIC_LANGFUSE_BASE_URL,
14+
}),
15+
});
16+
}
17+
418
if (process.env.NEXT_RUNTIME === 'nodejs') {
519
await import('../sentry.server.config');
620
}

0 commit comments

Comments
 (0)