1
1
"""
2
2
title: Langfuse Filter Pipeline
3
3
author: open-webui
4
- date: 2025-02-20
5
- version: 1.5
4
+ date: 2025-03-28
5
+ version: 1.7
6
6
license: MIT
7
7
description: A filter pipeline that uses Langfuse.
8
8
requirements: langfuse
20
20
21
21
22
22
def get_last_assistant_message_obj (messages : List [dict ]) -> dict :
23
+ """Retrieve the last assistant message from the message list."""
23
24
for message in reversed (messages ):
24
25
if message ["role" ] == "assistant" :
25
26
return message
@@ -33,6 +34,10 @@ class Valves(BaseModel):
33
34
secret_key : str
34
35
public_key : str
35
36
host : str
37
+ # New valve that controls whether task names are added as tags:
38
+ insert_tags : bool = True
39
+ # New valve that controls whether to use model name instead of model ID for generation
40
+ use_model_name_instead_of_id_for_generation : bool = False
36
41
debug : bool = False
37
42
38
43
def __init__ (self ):
@@ -45,18 +50,21 @@ def __init__(self):
45
50
"secret_key" : os .getenv ("LANGFUSE_SECRET_KEY" , "your-secret-key-here" ),
46
51
"public_key" : os .getenv ("LANGFUSE_PUBLIC_KEY" , "your-public-key-here" ),
47
52
"host" : os .getenv ("LANGFUSE_HOST" , "https://cloud.langfuse.com" ),
53
+ "use_model_name_instead_of_id_for_generation" : os .getenv ("USE_MODEL_NAME" , "false" ).lower () == "true" ,
48
54
"debug" : os .getenv ("DEBUG_MODE" , "false" ).lower () == "true" ,
49
55
}
50
56
)
51
57
52
58
self .langfuse = None
53
- # Keep track of the trace and the last-created generation for each chat_id
54
59
self .chat_traces = {}
55
- self .chat_generations = {}
56
60
self .suppressed_logs = set ()
61
+ # Dictionary to store model names for each chat
62
+ self .model_names = {}
63
+
64
+ # Only these tasks will be treated as LLM "generations":
65
+ self .GENERATION_TASKS = {"llm_response" }
57
66
58
67
def log (self , message : str , suppress_repeats : bool = False ):
59
- """Logs messages to the terminal if debugging is enabled."""
60
68
if self .valves .debug :
61
69
if suppress_repeats :
62
70
if message in self .suppressed_logs :
@@ -96,47 +104,44 @@ def set_langfuse(self):
96
104
f"Langfuse error: { e } Please re-enter your Langfuse credentials in the pipeline settings."
97
105
)
98
106
99
- async def inlet (self , body : dict , user : Optional [ dict ] = None ) -> dict :
107
+ def _build_tags (self , task_name : str ) -> list :
100
108
"""
101
- Inlet handles the incoming request (usually a user message).
102
- - If no trace exists yet for this chat_id, we create a new trace.
103
- - If a trace does exist, we simply create a new generation for the new user message.
109
+ Builds a list of tags based on valve settings, ensuring we always add
110
+ 'open-webui' and skip user_response / llm_response from becoming tags themselves.
104
111
"""
112
+ tags_list = []
113
+ if self .valves .insert_tags :
114
+ # Always add 'open-webui'
115
+ tags_list .append ("open-webui" )
116
+ # Add the task_name if it's not one of the excluded defaults
117
+ if task_name not in ["user_response" , "llm_response" ]:
118
+ tags_list .append (task_name )
119
+ return tags_list
120
+
121
+ async def inlet (self , body : dict , user : Optional [dict ] = None ) -> dict :
105
122
if self .valves .debug :
106
123
print (f"[DEBUG] Received request: { json .dumps (body , indent = 2 )} " )
107
124
108
125
self .log (f"Inlet function called with body: { body } and user: { user } " )
109
126
110
127
metadata = body .get ("metadata" , {})
128
+ chat_id = metadata .get ("chat_id" , str (uuid .uuid4 ()))
129
+ metadata ["chat_id" ] = chat_id
130
+ body ["metadata" ] = metadata
111
131
112
- # ---------------------------------------------------------
113
- # Prepend the system prompt from metadata to the system message:
132
+ # Extract and store both model name and ID if available
114
133
model_info = metadata .get ("model" , {})
115
- params_info = model_info .get ("params" , {})
116
- system_prompt = params_info .get ("system" , "" )
117
-
118
- if system_prompt :
119
- for msg in body ["messages" ]:
120
- if msg .get ("role" ) == "system" :
121
- # Only prepend if it hasn't already been prepended:
122
- if not msg ["content" ].startswith ("System Prompt:" ):
123
- msg ["content" ] = f"System Prompt:\n { system_prompt } \n \n { msg ['content' ]} "
124
- break
125
- # ---------------------------------------------------------
126
-
127
- # Fix SYSTEM MESSAGE prefix issue: Only apply for "task_generation"
128
- if "chat_id" not in metadata :
129
- if "task_generation" in metadata .get ("type" , "" ).lower ():
130
- chat_id = f"SYSTEM MESSAGE { uuid .uuid4 ()} "
131
- self .log (f"Task Generation detected, assigned SYSTEM MESSAGE ID: { chat_id } " )
132
- else :
133
- chat_id = str (uuid .uuid4 ()) # Regular chat messages
134
- self .log (f"Assigned normal chat_id: { chat_id } " )
135
-
136
- metadata ["chat_id" ] = chat_id
137
- body ["metadata" ] = metadata
134
+ model_id = body .get ("model" )
135
+
136
+ # Store model information for this chat
137
+ if chat_id not in self .model_names :
138
+ self .model_names [chat_id ] = {"id" : model_id }
138
139
else :
139
- chat_id = metadata ["chat_id" ]
140
+ self .model_names [chat_id ]["id" ] = model_id
141
+
142
+ if isinstance (model_info , dict ) and "name" in model_info :
143
+ self .model_names [chat_id ]["name" ] = model_info ["name" ]
144
+ self .log (f"Stored model info - name: '{ model_info ['name' ]} ', id: '{ model_id } ' for chat_id: { chat_id } " )
140
145
141
146
required_keys = ["model" , "messages" ]
142
147
missing_keys = [key for key in required_keys if key not in body ]
@@ -146,100 +151,108 @@ async def inlet(self, body: dict, user: Optional[dict] = None) -> dict:
146
151
raise ValueError (error_message )
147
152
148
153
user_email = user .get ("email" ) if user else None
154
+ # Defaulting to 'user_response' if no task is provided
155
+ task_name = metadata .get ("task" , "user_response" )
156
+
157
+ # Build tags
158
+ tags_list = self ._build_tags (task_name )
149
159
150
- # Check if we already have a trace for this chat
151
160
if chat_id not in self .chat_traces :
152
- # Create a new trace and generation
153
- self .log (f"Creating new chat trace for chat_id: { chat_id } " )
161
+ self .log (f"Creating new trace for chat_id: { chat_id } " )
154
162
155
163
trace_payload = {
156
- "name" : f"filter: { __name__ } " ,
164
+ "name" : f"chat: { chat_id } " ,
157
165
"input" : body ,
158
166
"user_id" : user_email ,
159
- "metadata" : { "chat_id" : chat_id } ,
167
+ "metadata" : metadata ,
160
168
"session_id" : chat_id ,
161
169
}
162
170
171
+ if tags_list :
172
+ trace_payload ["tags" ] = tags_list
173
+
163
174
if self .valves .debug :
164
175
print (f"[DEBUG] Langfuse trace request: { json .dumps (trace_payload , indent = 2 )} " )
165
176
166
177
trace = self .langfuse .trace (** trace_payload )
167
-
178
+ self .chat_traces [chat_id ] = trace
179
+ else :
180
+ trace = self .chat_traces [chat_id ]
181
+ self .log (f"Reusing existing trace for chat_id: { chat_id } " )
182
+ if tags_list :
183
+ trace .update (tags = tags_list )
184
+
185
+ # Update metadata with type
186
+ metadata ["type" ] = task_name
187
+ metadata ["interface" ] = "open-webui"
188
+
189
+ # If it's a task that is considered an LLM generation
190
+ if task_name in self .GENERATION_TASKS :
191
+ # Determine which model value to use based on the use_model_name valve
192
+ model_id = self .model_names .get (chat_id , {}).get ("id" , body ["model" ])
193
+ model_name = self .model_names .get (chat_id , {}).get ("name" , "unknown" )
194
+
195
+ # Pick primary model identifier based on valve setting
196
+ model_value = model_name if self .valves .use_model_name_instead_of_id_for_generation else model_id
197
+
198
+ # Add both values to metadata regardless of valve setting
199
+ metadata ["model_id" ] = model_id
200
+ metadata ["model_name" ] = model_name
201
+
168
202
generation_payload = {
169
- "name" : chat_id ,
170
- "model" : body [ "model" ] ,
203
+ "name" : f" { task_name } : { str ( uuid . uuid4 ()) } " ,
204
+ "model" : model_value ,
171
205
"input" : body ["messages" ],
172
- "metadata" : { "interface" : "open-webui" } ,
206
+ "metadata" : metadata ,
173
207
}
208
+ if tags_list :
209
+ generation_payload ["tags" ] = tags_list
174
210
175
211
if self .valves .debug :
176
212
print (f"[DEBUG] Langfuse generation request: { json .dumps (generation_payload , indent = 2 )} " )
177
213
178
- generation = trace .generation (** generation_payload )
179
-
180
- self .chat_traces [chat_id ] = trace
181
- self .chat_generations [chat_id ] = generation
182
- self .log (f"Trace and generation objects successfully created for chat_id: { chat_id } " )
183
-
214
+ trace .generation (** generation_payload )
184
215
else :
185
- # Re-use existing trace but create a new generation for each new message
186
- self .log (f"Re-using existing chat trace for chat_id: { chat_id } " )
187
- trace = self .chat_traces [chat_id ]
188
-
189
- new_generation_payload = {
190
- "name" : f"{ chat_id } :{ str (uuid .uuid4 ())} " ,
191
- "model" : body ["model" ],
216
+ # Otherwise, log it as an event
217
+ event_payload = {
218
+ "name" : f"{ task_name } :{ str (uuid .uuid4 ())} " ,
219
+ "metadata" : metadata ,
192
220
"input" : body ["messages" ],
193
- "metadata" : {"interface" : "open-webui" },
194
221
}
222
+ if tags_list :
223
+ event_payload ["tags" ] = tags_list
224
+
195
225
if self .valves .debug :
196
- print (f"[DEBUG] Langfuse new_generation request: { json .dumps (new_generation_payload , indent = 2 )} " )
226
+ print (f"[DEBUG] Langfuse event request: { json .dumps (event_payload , indent = 2 )} " )
197
227
198
- new_generation = trace .generation (** new_generation_payload )
199
- self .chat_generations [chat_id ] = new_generation
228
+ trace .event (** event_payload )
200
229
201
230
return body
202
231
203
232
async def outlet (self , body : dict , user : Optional [dict ] = None ) -> dict :
204
- """
205
- Outlet handles the response body (usually the assistant message).
206
- It will finalize/end the generation created for the user request.
207
- """
208
233
self .log (f"Outlet function called with body: { body } " )
209
234
210
235
chat_id = body .get ("chat_id" )
236
+ metadata = body .get ("metadata" , {})
237
+ # Defaulting to 'llm_response' if no task is provided
238
+ task_name = metadata .get ("task" , "llm_response" )
211
239
212
- # If no trace or generation exist, attempt to register again
213
- if chat_id not in self .chat_traces or chat_id not in self .chat_generations :
214
- self .log (f"[WARNING] No matching chat trace found for chat_id: { chat_id } , attempting to re-register." )
240
+ # Build tags
241
+ tags_list = self ._build_tags (task_name )
242
+
243
+ if chat_id not in self .chat_traces :
244
+ self .log (f"[WARNING] No matching trace found for chat_id: { chat_id } , attempting to re-register." )
245
+ # Re-run inlet to register if somehow missing
215
246
return await self .inlet (body , user )
216
247
217
248
trace = self .chat_traces [chat_id ]
218
- generation = self .chat_generations [chat_id ]
219
249
220
- # Get the last assistant message from the conversation
221
250
assistant_message = get_last_assistant_message (body ["messages" ])
222
251
assistant_message_obj = get_last_assistant_message_obj (body ["messages" ])
223
252
224
- # ---------------------------------------------------------
225
- # If the outlet contains a sources array, append it after the "System Prompt:"
226
- # section in the system message:
227
- if assistant_message_obj and "sources" in assistant_message_obj and assistant_message_obj ["sources" ]:
228
- for msg in body ["messages" ]:
229
- if msg .get ("role" ) == "system" :
230
- if msg ["content" ].startswith ("System Prompt:" ):
231
- # Format the sources nicely
232
- sources_str = "\n \n " .join (
233
- json .dumps (src , indent = 2 ) for src in assistant_message_obj ["sources" ]
234
- )
235
- msg ["content" ] += f"\n \n Sources:\n { sources_str } "
236
- break
237
- # ---------------------------------------------------------
238
-
239
- # Extract usage if available
240
253
usage = None
241
254
if assistant_message_obj :
242
- info = assistant_message_obj .get ("info " , {})
255
+ info = assistant_message_obj .get ("usage " , {})
243
256
if isinstance (info , dict ):
244
257
input_tokens = info .get ("prompt_eval_count" ) or info .get ("prompt_tokens" )
245
258
output_tokens = info .get ("eval_count" ) or info .get ("completion_tokens" )
@@ -251,20 +264,58 @@ async def outlet(self, body: dict, user: Optional[dict] = None) -> dict:
251
264
}
252
265
self .log (f"Usage data extracted: { usage } " )
253
266
254
- # Optionally update the trace with the final assistant output
267
+ # Update the trace output with the last assistant message
255
268
trace .update (output = assistant_message )
256
269
257
- # End the generation with the final assistant message and updated conversation
258
- generation_payload = {
259
- "input" : body ["messages" ], # include the entire conversation
260
- "metadata" : {"interface" : "open-webui" },
261
- "usage" : usage ,
262
- }
270
+ metadata ["type" ] = task_name
271
+ metadata ["interface" ] = "open-webui"
272
+
273
+ if task_name in self .GENERATION_TASKS :
274
+ # Determine which model value to use based on the use_model_name valve
275
+ model_id = self .model_names .get (chat_id , {}).get ("id" , body .get ("model" ))
276
+ model_name = self .model_names .get (chat_id , {}).get ("name" , "unknown" )
277
+
278
+ # Pick primary model identifier based on valve setting
279
+ model_value = model_name if self .valves .use_model_name_instead_of_id_for_generation else model_id
280
+
281
+ # Add both values to metadata regardless of valve setting
282
+ metadata ["model_id" ] = model_id
283
+ metadata ["model_name" ] = model_name
284
+
285
+ # If it's an LLM generation
286
+ generation_payload = {
287
+ "name" : f"{ task_name } :{ str (uuid .uuid4 ())} " ,
288
+ "model" : model_value , # <-- Use model name or ID based on valve setting
289
+ "input" : body ["messages" ],
290
+ "metadata" : metadata ,
291
+ "usage" : usage ,
292
+ }
293
+ if tags_list :
294
+ generation_payload ["tags" ] = tags_list
263
295
264
- if self .valves .debug :
265
- print (f"[DEBUG] Langfuse generation end request: { json .dumps (generation_payload , indent = 2 )} " )
296
+ if self .valves .debug :
297
+ print (f"[DEBUG] Langfuse generation end request: { json .dumps (generation_payload , indent = 2 )} " )
298
+
299
+ trace .generation ().end (** generation_payload )
300
+ self .log (f"Generation ended for chat_id: { chat_id } " )
301
+ else :
302
+ # Otherwise log as an event
303
+ event_payload = {
304
+ "name" : f"{ task_name } :{ str (uuid .uuid4 ())} " ,
305
+ "metadata" : metadata ,
306
+ "input" : body ["messages" ],
307
+ }
308
+ if usage :
309
+ # If you want usage on event as well
310
+ event_payload ["metadata" ]["usage" ] = usage
311
+
312
+ if tags_list :
313
+ event_payload ["tags" ] = tags_list
314
+
315
+ if self .valves .debug :
316
+ print (f"[DEBUG] Langfuse event end request: { json .dumps (event_payload , indent = 2 )} " )
266
317
267
- generation . end (** generation_payload )
268
- self .log (f"Generation ended for chat_id: { chat_id } " )
318
+ trace . event (** event_payload )
319
+ self .log (f"Event logged for chat_id: { chat_id } " )
269
320
270
321
return body
0 commit comments