Skip to content

Commit fffbb1a

Browse files
authored
feat: add user-feedback example (#13)
1 parent d8876de commit fffbb1a

38 files changed

+15708
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Here, we try to highlight amazing work by our contributors and partners.
1414
| [Tracing Pipecat Applications](./applications/langchat) | A Pipecat application sending traces to Langfuse. | [@aabedraba](https://github.com/aabedraba) |
1515
| [Tracing MCP Servers](./applications/mcp-tracing) | An example on using the OpenAI agents SDK together with an MCP server. | [@aabedraba](https://github.com/aabedraba) |
1616
| [RAG Observability and Evals](./applications/rag) | A RAG application that uses Langfuse for tracing and evals. | [@aabedraba](https://github.com/aabedraba) |
17+
| [User Feedback](./applications/user-feedback) | An example on collecting user feedback using Langfuse Web SDK. | [@aabedraba](https://github.com/aabedraba) |
1718

1819
## Deployment Examples
1920

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
42+
43+
# claude code
44+
.claude
45+
.mcp.json
46+
CLAUDE.md
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# User Feedback with Langfuse
2+
3+
A Next.js chat application demonstrating how to collect and integrate user feedback into Langfuse traces for AI observability and evaluation.
4+
5+
![Demo Screenshot](./assets/demo-screenshot.png)
6+
7+
## Key Features
8+
9+
- **Real-time Feedback Collection**: Thumbs up/down rating with optional comments on AI responses
10+
- **Langfuse Tracing**: Full observability of LLM calls with OpenTelemetry integration
11+
- **Feedback-to-Trace Linking**: User feedback automatically sent to Langfuse as scores tied to specific traces
12+
- **Session Management**: Track conversation context across multiple messages
13+
14+
## Prerequisites
15+
16+
- Node.js 18+
17+
- OpenAI API key
18+
- Langfuse credentials
19+
20+
## Setup
21+
22+
1. Install dependencies:
23+
24+
```bash
25+
npm install
26+
```
27+
28+
2. Create `.env` file with your API keys:
29+
30+
```bash
31+
# OpenAI API Key
32+
OPENAI_API_KEY=your-openai-api-key
33+
34+
# Langfuse Configuration
35+
NEXT_PUBLIC_LANGFUSE_HOST=https://cloud.langfuse.com
36+
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=pk-lf-fe2c726b-38cd-4068-be67-d3f786499b82
37+
LANGFUSE_SECRET_KEY=sk-lf-...
38+
```
39+
40+
Get your Langfuse keys from [https://cloud.langfuse.com](https://cloud.langfuse.com)
41+
42+
## How to Run
43+
44+
Start the development server:
45+
46+
```bash
47+
npm run dev
48+
```
49+
50+
Open [http://localhost:3000](http://localhost:3000) with your browser to interact with the chat interface.
51+
52+
## How It Works
53+
54+
### 1. Tracing LLM Calls
55+
56+
The application uses Langfuse's OpenTelemetry integration to automatically trace all LLM calls:
57+
58+
- Each chat message generates a trace in Langfuse with session tracking
59+
- Traces capture input, output, model used, and token usage
60+
- The trace ID is used as the message ID for linking feedback
61+
62+
### 2. Collecting User Feedback
63+
64+
Users can provide feedback on AI responses through:
65+
66+
- **Thumbs up/down**: Simple binary feedback (1 or 0)
67+
- **Optional comments**: Additional context about the rating
68+
69+
The feedback UI appears below each assistant message in the chat interface.
70+
71+
![Feedback UI](./assets/feedback-ui.png)
72+
73+
### 3. Sending Feedback to Langfuse
74+
75+
When a user provides feedback, it's automatically sent to Langfuse as a score:
76+
77+
```typescript
78+
langfuse.score({
79+
traceId: messageId, // Links to the original trace
80+
id: `user-feedback-${messageId}`,
81+
name: "user-feedback",
82+
value: value, // 1 for thumbs up, 0 for thumbs down
83+
comment: comment, // Optional user comment
84+
});
85+
```
86+
87+
### 4. Viewing Feedback in Langfuse
88+
89+
In your Langfuse dashboard, you can:
90+
91+
- View user feedback scores alongside traces
92+
- Filter traces by feedback ratings
93+
- Analyze patterns in user satisfaction
94+
- Use feedback for model evaluation and improvement
95+
96+
![Langfuse Score](./assets/langfuse-score.png)
97+
98+
## Learn More
99+
100+
- [Langfuse Documentation](https://langfuse.com/docs)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { openai } from '@ai-sdk/openai';
2+
import {
3+
observe,
4+
updateActiveObservation,
5+
updateActiveTrace,
6+
getActiveTraceId,
7+
} from "@langfuse/tracing";
8+
import { trace } from "@opentelemetry/api";
9+
import { convertToModelMessages, streamText, UIMessage } from "ai";
10+
import { after } from "next/server";
11+
12+
import { langfuseSpanProcessor } from "@/instrumentation";
13+
14+
const handler = async (req: Request) => {
15+
const {
16+
messages,
17+
chatId,
18+
model = 'gpt-4o-mini'
19+
}: { messages: UIMessage[]; chatId: string; model?: string } =
20+
await req.json();
21+
22+
// Set session id on active trace
23+
const inputText = messages[messages.length - 1].parts.find(
24+
(part) => part.type === "text"
25+
)?.text;
26+
27+
updateActiveObservation({
28+
input: inputText,
29+
});
30+
31+
updateActiveTrace({
32+
name: "langfuse-chatbot",
33+
sessionId: chatId,
34+
input: inputText,
35+
});
36+
37+
const result = streamText({
38+
model: openai(model),
39+
messages: convertToModelMessages(messages),
40+
system: `Your are helpful Langfuse assistant. Help the user with their questions about Langfuse.`,
41+
experimental_telemetry: {
42+
isEnabled: true,
43+
},
44+
onFinish: async (result) => {
45+
updateActiveObservation({
46+
output: result.content,
47+
});
48+
updateActiveTrace({
49+
output: result.content,
50+
});
51+
52+
// End span manually after stream has finished
53+
trace.getActiveSpan()?.end();
54+
},
55+
onError: async (error) => {
56+
updateActiveObservation({
57+
output: error,
58+
level: "ERROR"
59+
});
60+
updateActiveTrace({
61+
output: error,
62+
});
63+
64+
// End span manually after stream has finished
65+
trace.getActiveSpan()?.end();
66+
},
67+
});
68+
69+
// Important in serverless environments: schedule flush after request is finished
70+
after(async () => await langfuseSpanProcessor.forceFlush());
71+
72+
return result.toUIMessageStreamResponse({
73+
generateMessageId: () => getActiveTraceId() || "",
74+
sendSources: true,
75+
sendReasoning: true,
76+
});
77+
};
78+
79+
export const POST = observe(handler, {
80+
name: "handle-chat-message",
81+
endOnExit: false, // end observation _after_ stream has finished
82+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
@import "tailwindcss";
2+
@import "tw-animate-css";
3+
4+
@theme inline {
5+
--color-background: var(--background);
6+
--color-foreground: var(--foreground);
7+
--font-sans: var(--font-geist-sans);
8+
--font-mono: var(--font-geist-mono);
9+
--color-sidebar-ring: var(--sidebar-ring);
10+
--color-sidebar-border: var(--sidebar-border);
11+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
12+
--color-sidebar-accent: var(--sidebar-accent);
13+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
14+
--color-sidebar-primary: var(--sidebar-primary);
15+
--color-sidebar-foreground: var(--sidebar-foreground);
16+
--color-sidebar: var(--sidebar);
17+
--color-chart-5: var(--chart-5);
18+
--color-chart-4: var(--chart-4);
19+
--color-chart-3: var(--chart-3);
20+
--color-chart-2: var(--chart-2);
21+
--color-chart-1: var(--chart-1);
22+
--color-ring: var(--ring);
23+
--color-input: var(--input);
24+
--color-border: var(--border);
25+
--color-destructive: var(--destructive);
26+
--color-accent-foreground: var(--accent-foreground);
27+
--color-accent: var(--accent);
28+
--color-muted-foreground: var(--muted-foreground);
29+
--color-muted: var(--muted);
30+
--color-secondary-foreground: var(--secondary-foreground);
31+
--color-secondary: var(--secondary);
32+
--color-primary-foreground: var(--primary-foreground);
33+
--color-primary: var(--primary);
34+
--color-popover-foreground: var(--popover-foreground);
35+
--color-popover: var(--popover);
36+
--color-card-foreground: var(--card-foreground);
37+
--color-card: var(--card);
38+
--radius-sm: calc(var(--radius) - 4px);
39+
--radius-md: calc(var(--radius) - 2px);
40+
--radius-lg: var(--radius);
41+
--radius-xl: calc(var(--radius) + 4px);
42+
}
43+
44+
:root {
45+
--radius: 0.625rem;
46+
--background: oklch(1 0 0);
47+
--foreground: oklch(0.145 0 0);
48+
--card: oklch(1 0 0);
49+
--card-foreground: oklch(0.145 0 0);
50+
--popover: oklch(1 0 0);
51+
--popover-foreground: oklch(0.145 0 0);
52+
--primary: oklch(0.205 0 0);
53+
--primary-foreground: oklch(0.985 0 0);
54+
--secondary: oklch(0.97 0 0);
55+
--secondary-foreground: oklch(0.205 0 0);
56+
--muted: oklch(0.97 0 0);
57+
--muted-foreground: oklch(0.556 0 0);
58+
--accent: oklch(0.97 0 0);
59+
--accent-foreground: oklch(0.205 0 0);
60+
--destructive: oklch(0.577 0.245 27.325);
61+
--border: oklch(0.922 0 0);
62+
--input: oklch(0.922 0 0);
63+
--ring: oklch(0.708 0 0);
64+
--chart-1: oklch(0.646 0.222 41.116);
65+
--chart-2: oklch(0.6 0.118 184.704);
66+
--chart-3: oklch(0.398 0.07 227.392);
67+
--chart-4: oklch(0.828 0.189 84.429);
68+
--chart-5: oklch(0.769 0.188 70.08);
69+
--sidebar: oklch(0.985 0 0);
70+
--sidebar-foreground: oklch(0.145 0 0);
71+
--sidebar-primary: oklch(0.205 0 0);
72+
--sidebar-primary-foreground: oklch(0.985 0 0);
73+
--sidebar-accent: oklch(0.97 0 0);
74+
--sidebar-accent-foreground: oklch(0.205 0 0);
75+
--sidebar-border: oklch(0.922 0 0);
76+
--sidebar-ring: oklch(0.708 0 0);
77+
}
78+
79+
@layer base {
80+
* {
81+
@apply border-border outline-ring/50;
82+
}
83+
body {
84+
@apply bg-background text-foreground;
85+
}
86+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
3+
import "./globals.css";
4+
import { Card, CardContent } from "@/components/ui/card";
5+
6+
const geistSans = Geist({
7+
variable: "--font-geist-sans",
8+
subsets: ["latin"],
9+
});
10+
11+
const geistMono = Geist_Mono({
12+
variable: "--font-geist-mono",
13+
subsets: ["latin"],
14+
});
15+
16+
export const metadata: Metadata = {
17+
title: "Create Next App",
18+
description: "Generated by create next app",
19+
};
20+
21+
export default function RootLayout({
22+
children,
23+
}: Readonly<{
24+
children: React.ReactNode;
25+
}>) {
26+
return (
27+
<html lang="en">
28+
<body
29+
className={`${geistSans.variable} ${geistMono.variable} antialiased h-screen flex items-center justify-center p-4 overflow-hidden`}
30+
>
31+
<Card className="w-full max-w-4xl h-full flex flex-col">
32+
<CardContent className="p-0 h-full flex flex-col overflow-hidden">
33+
{children}
34+
</CardContent>
35+
</Card>
36+
</body>
37+
</html>
38+
);
39+
}

0 commit comments

Comments
 (0)