diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index c9d01de..4b3a279 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,6 +1,7 @@ import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai'; import { getAIModel } from '@/lib/ai-providers'; import { findCachedResponse } from '@/lib/cached-responses'; +import { setTraceInput, setTraceOutput, getTelemetryConfig, wrapWithObserve } from '@/lib/langfuse'; import { getSystemPrompt } from '@/lib/system-prompts'; import { z } from "zod"; @@ -61,7 +62,27 @@ function createCachedStreamResponse(xml: string): Response { // Inner handler function async function handleChatRequest(req: Request): Promise { - const { messages, xml } = await req.json(); + const { messages, xml, sessionId } = await req.json(); + + // Get user IP for Langfuse tracking + const forwardedFor = req.headers.get('x-forwarded-for'); + const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous'; + + // Validate sessionId for Langfuse (must be string, max 200 chars) + const validSessionId = sessionId && typeof sessionId === 'string' && sessionId.length <= 200 + ? sessionId + : undefined; + + // Extract user input text for Langfuse trace + const currentMessage = messages[messages.length - 1]; + const userInputText = currentMessage?.parts?.find((p: any) => p.type === 'text')?.text || ''; + + // Update Langfuse trace with input, session, and user + setTraceInput({ + input: userInputText, + sessionId: validSessionId, + userId: userId, + }); // === FILE VALIDATION START === const fileValidation = validateFileParts(messages); @@ -191,9 +212,19 @@ ${lastMessageText} messages: allMessages, ...(providerOptions && { providerOptions }), ...(headers && { headers }), - onFinish: ({ usage, providerMetadata }) => { - console.log('[Cache] providerMetadata:', JSON.stringify(providerMetadata, null, 2)); + // Langfuse telemetry config (returns undefined if not configured) + ...(getTelemetryConfig({ sessionId: validSessionId, userId }) && { + experimental_telemetry: getTelemetryConfig({ sessionId: validSessionId, userId }), + }), + onFinish: ({ text, usage, providerMetadata }) => { + console.log('[Cache] Full providerMetadata:', JSON.stringify(providerMetadata, null, 2)); console.log('[Cache] Usage:', JSON.stringify(usage, null, 2)); + // Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry) + // AI SDK uses inputTokens/outputTokens, Langfuse expects promptTokens/completionTokens + setTraceOutput(text, { + promptTokens: usage?.inputTokens, + completionTokens: usage?.outputTokens, + }); }, tools: { // Client-side tool that will be executed on the client @@ -260,7 +291,8 @@ IMPORTANT: Keep edits concise: return result.toUIMessageStreamResponse(); } -export async function POST(req: Request) { +// Wrap handler with error handling +async function safeHandler(req: Request): Promise { try { return await handleChatRequest(req); } catch (error) { @@ -268,3 +300,10 @@ export async function POST(req: Request) { return Response.json({ error: 'Internal server error' }, { status: 500 }); } } + +// Wrap with Langfuse observe (if configured) +const observedHandler = wrapWithObserve(safeHandler); + +export async function POST(req: Request) { + return observedHandler(req); +} diff --git a/app/api/log-feedback/route.ts b/app/api/log-feedback/route.ts new file mode 100644 index 0000000..a0dbb09 --- /dev/null +++ b/app/api/log-feedback/route.ts @@ -0,0 +1,103 @@ +import { getLangfuseClient } from '@/lib/langfuse'; +import { randomUUID } from 'crypto'; +import { z } from 'zod'; + +const feedbackSchema = z.object({ + messageId: z.string().min(1).max(200), + feedback: z.enum(['good', 'bad']), + sessionId: z.string().min(1).max(200).optional(), +}); + +export async function POST(req: Request) { + const langfuse = getLangfuseClient(); + if (!langfuse) { + return Response.json({ success: true, logged: false }); + } + + // Validate input + let data; + try { + data = feedbackSchema.parse(await req.json()); + } catch { + return Response.json({ success: false, error: 'Invalid input' }, { status: 400 }); + } + + const { messageId, feedback, sessionId } = data; + + // Get user IP for tracking + const forwardedFor = req.headers.get('x-forwarded-for'); + const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous'; + + try { + // Find the most recent chat trace for this session to attach the score to + const tracesResponse = await langfuse.api.trace.list({ + sessionId, + limit: 1, + }); + + const traces = tracesResponse.data || []; + const latestTrace = traces[0]; + + if (!latestTrace) { + // No trace found for this session - create a standalone feedback trace + const traceId = randomUUID(); + const timestamp = new Date().toISOString(); + + await langfuse.api.ingestion.batch({ + batch: [ + { + type: 'trace-create', + id: randomUUID(), + timestamp, + body: { + id: traceId, + name: 'user-feedback', + sessionId, + userId, + input: { messageId, feedback }, + metadata: { source: 'feedback-button', note: 'standalone - no chat trace found' }, + timestamp, + }, + }, + { + type: 'score-create', + id: randomUUID(), + timestamp, + body: { + id: randomUUID(), + traceId, + name: 'user-feedback', + value: feedback === 'good' ? 1 : 0, + comment: `User gave ${feedback} feedback`, + }, + }, + ], + }); + } else { + // Attach score to the existing chat trace + const timestamp = new Date().toISOString(); + + await langfuse.api.ingestion.batch({ + batch: [ + { + type: 'score-create', + id: randomUUID(), + timestamp, + body: { + id: randomUUID(), + traceId: latestTrace.id, + name: 'user-feedback', + value: feedback === 'good' ? 1 : 0, + comment: `User gave ${feedback} feedback`, + }, + }, + ], + }); + } + + return Response.json({ success: true, logged: true }); + } catch (error) { + console.error('Langfuse feedback error:', error); + return Response.json({ success: false, error: 'Failed to log feedback' }, { status: 500 }); + } +} diff --git a/app/api/log-save/route.ts b/app/api/log-save/route.ts new file mode 100644 index 0000000..e4862c9 --- /dev/null +++ b/app/api/log-save/route.ts @@ -0,0 +1,65 @@ +import { getLangfuseClient } from '@/lib/langfuse'; +import { randomUUID } from 'crypto'; +import { z } from 'zod'; + +const saveSchema = z.object({ + filename: z.string().min(1).max(255), + format: z.enum(['drawio', 'png', 'svg']), + sessionId: z.string().min(1).max(200).optional(), +}); + +export async function POST(req: Request) { + const langfuse = getLangfuseClient(); + if (!langfuse) { + return Response.json({ success: true, logged: false }); + } + + // Validate input + let data; + try { + data = saveSchema.parse(await req.json()); + } catch { + return Response.json({ success: false, error: 'Invalid input' }, { status: 400 }); + } + + const { filename, format, sessionId } = data; + + try { + const timestamp = new Date().toISOString(); + + // Find the most recent chat trace for this session to attach the save flag + const tracesResponse = await langfuse.api.trace.list({ + sessionId, + limit: 1, + }); + + const traces = tracesResponse.data || []; + const latestTrace = traces[0]; + + if (latestTrace) { + // Add a score to the existing trace to flag that user saved + await langfuse.api.ingestion.batch({ + batch: [ + { + type: 'score-create', + id: randomUUID(), + timestamp, + body: { + id: randomUUID(), + traceId: latestTrace.id, + name: 'diagram-saved', + value: 1, + comment: `User saved diagram as ${filename}.${format}`, + }, + }, + ], + }); + } + // If no trace found, skip logging (user hasn't chatted yet) + + return Response.json({ success: true, logged: !!latestTrace }); + } catch (error) { + console.error('Langfuse save error:', error); + return Response.json({ success: false, error: 'Failed to log save' }, { status: 500 }); + } +} diff --git a/components/chat-input.tsx b/components/chat-input.tsx index 0525a50..0316271 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -95,6 +95,7 @@ interface ChatInputProps { onFileChange?: (files: File[]) => void; showHistory?: boolean; onToggleHistory?: (show: boolean) => void; + sessionId?: string; error?: Error | null; } @@ -108,6 +109,7 @@ export function ChatInput({ onFileChange = () => {}, showHistory = false, onToggleHistory = () => {}, + sessionId, error = null, }: ChatInputProps) { const { diagramHistory, saveDiagramToFile } = useDiagram(); @@ -325,7 +327,7 @@ export function ChatInput({ open={showSaveDialog} onOpenChange={setShowSaveDialog} onSave={(filename, format) => - saveDiagramToFile(filename, format) + saveDiagramToFile(filename, format, sessionId) } defaultFilename={`diagram-${new Date() .toISOString() diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index ff9d1d1..d461569 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -6,7 +6,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import ExamplePanel from "./chat-example-panel"; import { UIMessage } from "ai"; import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils"; -import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, RotateCcw, Pencil } from "lucide-react"; +import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, ThumbsUp, ThumbsDown, RotateCcw, Pencil } from "lucide-react"; import { CodeBlock } from "./code-block"; interface EditPair { @@ -67,6 +67,7 @@ interface ChatMessageDisplayProps { error?: Error | null; setInput: (input: string) => void; setFiles: (files: File[]) => void; + sessionId?: string; onRegenerate?: (messageIndex: number) => void; onEditMessage?: (messageIndex: number, newText: string) => void; } @@ -76,6 +77,7 @@ export function ChatMessageDisplay({ error, setInput, setFiles, + sessionId, onRegenerate, onEditMessage, }: ChatMessageDisplayProps) { @@ -88,6 +90,7 @@ export function ChatMessageDisplay({ ); const [copiedMessageId, setCopiedMessageId] = useState(null); const [copyFailedMessageId, setCopyFailedMessageId] = useState(null); + const [feedback, setFeedback] = useState>({}); const [editingMessageId, setEditingMessageId] = useState(null); const [editText, setEditText] = useState(""); @@ -103,6 +106,34 @@ export function ChatMessageDisplay({ } }; + const submitFeedback = async (messageId: string, value: "good" | "bad") => { + // Toggle off if already selected + if (feedback[messageId] === value) { + setFeedback((prev) => { + const next = { ...prev }; + delete next[messageId]; + return next; + }); + return; + } + + setFeedback((prev) => ({ ...prev, [messageId]: value })); + + try { + await fetch("/api/log-feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + messageId, + feedback: value, + sessionId, + }), + }); + } catch (error) { + console.warn("Failed to log feedback:", error); + } + }; + const handleDisplayChart = useCallback( (xml: string) => { const currentXml = xml || ""; @@ -436,6 +467,32 @@ export function ChatMessageDisplay({ )} + {/* Divider */} +
+ {/* Thumbs up */} + + {/* Thumbs down */} +
)} diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 6c9b6f5..3db4703 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -63,6 +63,9 @@ export default function ChatPanel({ const [showHistory, setShowHistory] = useState(false); const [input, setInput] = useState(""); + // Generate a unique session ID for Langfuse tracing + const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`); + // Store XML snapshots for each user message (keyed by message index) const xmlSnapshotsRef = useRef>(new Map()); @@ -217,6 +220,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a { body: { xml: chartXml, + sessionId, }, } ); @@ -297,6 +301,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a { body: { xml: savedXml, + sessionId, }, } ); @@ -353,6 +358,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a { body: { xml: savedXml, + sessionId, }, } ); @@ -440,6 +446,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a error={error} setInput={setInput} setFiles={handleFileChange} + sessionId={sessionId} onRegenerate={handleRegenerate} onEditMessage={handleEditMessage} /> @@ -455,12 +462,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a onClearChat={() => { setMessages([]); clearDiagram(); + setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`); xmlSnapshotsRef.current.clear(); }} files={files} onFileChange={handleFileChange} showHistory={showHistory} onToggleHistory={setShowHistory} + sessionId={sessionId} error={error} /> diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index a9d5a4f..1995f71 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -16,7 +16,7 @@ interface DiagramContextType { drawioRef: React.Ref; handleDiagramExport: (data: any) => void; clearDiagram: () => void; - saveDiagramToFile: (filename: string, format: ExportFormat) => void; + saveDiagramToFile: (filename: string, format: ExportFormat, sessionId?: string) => void; } const DiagramContext = createContext(undefined); @@ -107,7 +107,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { setDiagramHistory([]); }; - const saveDiagramToFile = (filename: string, format: ExportFormat) => { + const saveDiagramToFile = (filename: string, format: ExportFormat, sessionId?: string) => { if (!drawioRef.current) { console.warn("Draw.io editor not ready"); return; @@ -145,6 +145,9 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { extension = ".svg"; } + // Log save event to Langfuse (flags the trace) + logSaveToLangfuse(filename, format, sessionId); + // Handle download let url: string; if (typeof fileContent === "string" && fileContent.startsWith("data:")) { @@ -174,6 +177,19 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { drawioRef.current.exportDiagram({ format: drawioFormat }); }; + // Log save event to Langfuse (just flags the trace, doesn't send content) + const logSaveToLangfuse = async (filename: string, format: string, sessionId?: string) => { + try { + await fetch("/api/log-save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filename, format, sessionId }), + }); + } catch (error) { + console.warn("Failed to log save to Langfuse:", error); + } + }; + return ( { + const spanName = otelSpan.name; + // Skip Next.js HTTP infrastructure spans + if (spanName.startsWith('POST /') || + spanName.startsWith('GET /') || + spanName.includes('BaseServer') || + spanName.includes('handleRequest')) { + return false; + } + return true; + }, + }); + + const tracerProvider = new NodeTracerProvider({ + spanProcessors: [langfuseSpanProcessor], + }); + + // Register globally so AI SDK's telemetry also uses this processor + tracerProvider.register(); +} diff --git a/lib/langfuse.ts b/lib/langfuse.ts new file mode 100644 index 0000000..a966059 --- /dev/null +++ b/lib/langfuse.ts @@ -0,0 +1,95 @@ +import { observe, updateActiveTrace } from '@langfuse/tracing'; +import { LangfuseClient } from '@langfuse/client'; +import * as api from '@opentelemetry/api'; + +// Singleton LangfuseClient instance for direct API calls +let langfuseClient: LangfuseClient | null = null; + +export function getLangfuseClient(): LangfuseClient | null { + if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) { + return null; + } + + if (!langfuseClient) { + langfuseClient = new LangfuseClient({ + publicKey: process.env.LANGFUSE_PUBLIC_KEY, + secretKey: process.env.LANGFUSE_SECRET_KEY, + baseUrl: process.env.LANGFUSE_BASEURL, + }); + } + + return langfuseClient; +} + +// Check if Langfuse is configured +export function isLangfuseEnabled(): boolean { + return !!process.env.LANGFUSE_PUBLIC_KEY; +} + +// Update trace with input data at the start of request +export function setTraceInput(params: { + input: string; + sessionId?: string; + userId?: string; +}) { + if (!isLangfuseEnabled()) return; + + updateActiveTrace({ + name: 'chat', + input: params.input, + sessionId: params.sessionId, + userId: params.userId, + }); +} + +// Update trace with output and end the span +export function setTraceOutput(output: string, usage?: { promptTokens?: number; completionTokens?: number }) { + if (!isLangfuseEnabled()) return; + + updateActiveTrace({ output }); + + const activeSpan = api.trace.getActiveSpan(); + if (activeSpan) { + // Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them + if (usage?.promptTokens) { + activeSpan.setAttribute('ai.usage.promptTokens', usage.promptTokens); + activeSpan.setAttribute('gen_ai.usage.input_tokens', usage.promptTokens); + } + if (usage?.completionTokens) { + activeSpan.setAttribute('ai.usage.completionTokens', usage.completionTokens); + activeSpan.setAttribute('gen_ai.usage.output_tokens', usage.completionTokens); + } + activeSpan.end(); + } +} + +// Get telemetry config for streamText +export function getTelemetryConfig(params: { + sessionId?: string; + userId?: string; +}) { + if (!isLangfuseEnabled()) return undefined; + + return { + isEnabled: true, + // Disable automatic input recording to avoid uploading large base64 images to Langfuse media + // User text input is recorded manually via setTraceInput + recordInputs: false, + recordOutputs: true, + metadata: { + sessionId: params.sessionId, + userId: params.userId, + }, + }; +} + +// Wrap a handler with Langfuse observe +export function wrapWithObserve( + handler: (req: Request) => Promise +): (req: Request) => Promise { + if (!isLangfuseEnabled()) { + return handler; + } + + return observe(handler, { name: 'chat', endOnExit: false }); +} diff --git a/package-lock.json b/package-lock.json index 598398c..f5d9f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,12 @@ "@ai-sdk/openai": "^2.0.19", "@ai-sdk/react": "^2.0.22", "@aws-sdk/credential-providers": "^3.943.0", + "@langfuse/client": "^4.4.9", + "@langfuse/otel": "^4.4.4", + "@langfuse/tracing": "^4.4.9", "@next/third-parties": "^16.0.6", "@openrouter/ai-sdk-provider": "^1.2.3", + "@opentelemetry/sdk-trace-node": "^2.2.0", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.2.6", @@ -2338,6 +2342,62 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langfuse/client": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@langfuse/client/-/client-4.4.9.tgz", + "integrity": "sha512-Y7bU70tMx/lYOU/A7NGvXXVZoL3AiFigbf7EwS5PVFc0xd34eRUmvwdLwEtuK7CnYTyxIZTzVVP2KEaicWCYZg==", + "license": "MIT", + "dependencies": { + "@langfuse/core": "^4.4.9", + "@langfuse/tracing": "^4.4.9", + "mustache": "^4.2.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@langfuse/core": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@langfuse/core/-/core-4.4.9.tgz", + "integrity": "sha512-9Hz/eH6dkOP8E/FLt1fsAQR8RE/TF8Ag/39GmY8JjN1o/Tl/MFJfK2QvqRGrkjDkIkMJGOSD+iQmV2pYm4upDA==", + "license": "MIT", + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@langfuse/otel": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@langfuse/otel/-/otel-4.4.9.tgz", + "integrity": "sha512-T9tSucDC3cvdeUB8fTGXm6ZZcC4kOEreJuM/C66nHQ26+DWtx2AN75JWuadc2E+VU/K+BjTb5XAkBceWYtvXjw==", + "license": "MIT", + "dependencies": { + "@langfuse/core": "^4.4.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-trace-otlp-http": ">=0.202.0 <1.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.1" + } + }, + "node_modules/@langfuse/tracing": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@langfuse/tracing/-/tracing-4.4.9.tgz", + "integrity": "sha512-if+G/v9NsyTKj40KKX96bRSdMXwyDbVL4GJQvmwQ9SxvGYF+d99pGFB7L6QOeCd1KBHMdmDe733ncmvCnSHJ9w==", + "license": "MIT", + "dependencies": { + "@langfuse/core": "^4.4.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2606,6 +2666,273 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", + "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz", + "integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.2.0.tgz", + "integrity": "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.2.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -4620,7 +4947,6 @@ "version": "20.17.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -8326,6 +8652,13 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -9209,6 +9542,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9718,6 +10060,31 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10942,7 +11309,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unified": { diff --git a/package.json b/package.json index 3c95afe..670ff24 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,12 @@ "@ai-sdk/openai": "^2.0.19", "@ai-sdk/react": "^2.0.22", "@aws-sdk/credential-providers": "^3.943.0", + "@langfuse/client": "^4.4.9", + "@langfuse/otel": "^4.4.4", + "@langfuse/tracing": "^4.4.9", "@next/third-parties": "^16.0.6", "@openrouter/ai-sdk-provider": "^1.2.3", + "@opentelemetry/sdk-trace-node": "^2.2.0", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.2.6",