1
- from typing import Optional , Dict , Any
2
1
import logging
2
+ import uuid
3
+ from typing import Any , Dict , Optional
3
4
4
5
import openai
5
- from pydantic import BaseModel , Extra
6
- from fastapi import APIRouter , Depends , BackgroundTasks , HTTPException
6
+ from fastapi import APIRouter , BackgroundTasks , Depends , HTTPException
7
7
from openai import OpenAI
8
+ from pydantic import BaseModel , Extra
8
9
from sqlmodel import Session
9
10
10
11
from app .api .deps import get_current_user_org , get_db
11
- from app .crud . credentials import get_provider_credential
12
+ from app .api . routes . threads import send_callback
12
13
from app .crud .assistants import get_assistant_by_id
14
+ from app .crud .credentials import get_provider_credential
13
15
from app .models import UserOrganization
14
16
from app .utils import APIResponse
17
+ from app .core .langfuse .langfuse import LangfuseTracer
15
18
16
19
logger = logging .getLogger (__name__ )
17
-
18
20
router = APIRouter (tags = ["responses" ])
19
21
20
22
@@ -67,9 +69,7 @@ class _APIResponse(BaseModel):
67
69
diagnostics : Optional [Diagnostics ] = None
68
70
69
71
class Config :
70
- extra = (
71
- Extra .allow
72
- ) # This allows additional fields to be included in the response
72
+ extra = Extra .allow
73
73
74
74
75
75
class ResponsesAPIResponse (APIResponse [_APIResponse ]):
@@ -78,13 +78,11 @@ class ResponsesAPIResponse(APIResponse[_APIResponse]):
78
78
79
79
def get_file_search_results (response ):
80
80
results : list [FileResultChunk ] = []
81
-
82
81
for tool_call in response .output :
83
82
if tool_call .type == "file_search_call" :
84
83
results .extend (
85
84
[FileResultChunk (score = hit .score , text = hit .text ) for hit in results ]
86
85
)
87
-
88
86
return results
89
87
90
88
@@ -99,14 +97,29 @@ def get_additional_data(request: dict) -> dict:
99
97
100
98
101
99
def process_response (
102
- request : ResponsesAPIRequest , client : OpenAI , assistant , organization_id : int
100
+ request : ResponsesAPIRequest ,
101
+ client : OpenAI ,
102
+ assistant ,
103
+ tracer : LangfuseTracer ,
103
104
):
104
- """Process a response and send callback with results."""
105
+ """Process a response and send callback with results, with Langfuse tracing ."""
105
106
logger .info (
106
- f"Starting generating response for assistant_id={ request .assistant_id } , project_id={ request .project_id } , organization_id={ organization_id } "
107
+ f"Starting generating response for assistant_id={ request .assistant_id } , project_id={ request .project_id } "
108
+ )
109
+
110
+ tracer .start_trace (
111
+ name = "generate_response_async" ,
112
+ input = {"question" : request .question , "assistant_id" : request .assistant_id },
113
+ metadata = {"callback_url" : request .callback_url },
114
+ )
115
+
116
+ tracer .start_generation (
117
+ name = "openai_response" ,
118
+ input = {"question" : request .question },
119
+ metadata = {"model" : assistant .model , "temperature" : assistant .temperature },
107
120
)
121
+
108
122
try :
109
- # Create response with or without tools based on vector_store_id
110
123
params = {
111
124
"model" : assistant .model ,
112
125
"previous_response_id" : request .response_id ,
@@ -128,11 +141,34 @@ def process_response(
128
141
response = client .responses .create (** params )
129
142
130
143
response_chunks = get_file_search_results (response )
144
+
131
145
logger .info (
132
- f"Successfully generated response: response_id={ response .id } , assistant={ request .assistant_id } , project_id={ request .project_id } , organization_id={ organization_id } "
146
+ f"Successfully generated response: response_id={ response .id } , assistant={ request .assistant_id } , project_id={ request .project_id } "
147
+ )
148
+
149
+ tracer .end_generation (
150
+ output = {
151
+ "response_id" : response .id ,
152
+ "message" : response .output_text ,
153
+ },
154
+ usage = {
155
+ "input" : response .usage .input_tokens ,
156
+ "output" : response .usage .output_tokens ,
157
+ "total" : response .usage .total_tokens ,
158
+ "unit" : "TOKENS" ,
159
+ },
160
+ model = response .model ,
161
+ )
162
+
163
+ tracer .update_trace (
164
+ tags = [response .id ],
165
+ output = {
166
+ "status" : "success" ,
167
+ "message" : response .output_text ,
168
+ "error" : None ,
169
+ },
133
170
)
134
171
135
- # Convert request to dict and include all fields
136
172
request_dict = request .model_dump ()
137
173
callback_response = ResponsesAPIResponse .success_response (
138
174
data = _APIResponse (
@@ -146,37 +182,26 @@ def process_response(
146
182
total_tokens = response .usage .total_tokens ,
147
183
model = response .model ,
148
184
),
149
- ** {
150
- k : v
151
- for k , v in request_dict .items ()
152
- if k
153
- not in {
154
- "project_id" ,
155
- "assistant_id" ,
156
- "callback_url" ,
157
- "response_id" ,
158
- "question" ,
159
- }
160
- },
161
- ),
185
+ ** get_additional_data (request_dict ),
186
+ )
162
187
)
163
188
except openai .OpenAIError as e :
164
189
error_message = handle_openai_error (e )
165
190
logger .error (
166
- f"OpenAI API error during response processing: { error_message } , project_id={ request .project_id } , organization_id={ organization_id } " ,
167
- exc_info = True ,
191
+ f"OpenAI API error during response processing: { error_message } , project_id={ request .project_id } "
168
192
)
193
+ tracer .log_error (error_message , response_id = request .response_id )
169
194
callback_response = ResponsesAPIResponse .failure_response (error = error_message )
170
195
196
+ tracer .flush ()
197
+
171
198
if request .callback_url :
172
199
logger .info (
173
- f"Sending callback to URL: { request .callback_url } , assistant={ request .assistant_id } , project_id={ request .project_id } , organization_id= { organization_id } "
200
+ f"Sending callback to URL: { request .callback_url } , assistant={ request .assistant_id } , project_id={ request .project_id } "
174
201
)
175
- from app .api .routes .threads import send_callback
176
-
177
202
send_callback (request .callback_url , callback_response .model_dump ())
178
203
logger .info (
179
- f"Callback sent successfully, assistant={ request .assistant_id } , project_id={ request .project_id } , organization_id= { organization_id } "
204
+ f"Callback sent successfully, assistant={ request .assistant_id } , project_id={ request .project_id } "
180
205
)
181
206
182
207
@@ -187,23 +212,19 @@ async def responses(
187
212
_session : Session = Depends (get_db ),
188
213
_current_user : UserOrganization = Depends (get_current_user_org ),
189
214
):
190
- """Asynchronous endpoint that processes requests in background."""
215
+ """Asynchronous endpoint that processes requests in background with Langfuse tracing ."""
191
216
logger .info (
192
217
f"Processing response request for assistant_id={ request .assistant_id } , project_id={ request .project_id } , organization_id={ _current_user .organization_id } "
193
218
)
194
219
195
- # Get assistant details
196
220
assistant = get_assistant_by_id (
197
221
_session , request .assistant_id , _current_user .organization_id
198
222
)
199
223
if not assistant :
200
224
logger .warning (
201
225
f"Assistant not found: assistant_id={ request .assistant_id } , project_id={ request .project_id } , organization_id={ _current_user .organization_id } " ,
202
226
)
203
- raise HTTPException (
204
- status_code = 404 ,
205
- detail = "Assistant not found or not active" ,
206
- )
227
+ raise HTTPException (status_code = 404 , detail = "Assistant not found or not active" )
207
228
208
229
credentials = get_provider_credential (
209
230
session = _session ,
@@ -212,8 +233,8 @@ async def responses(
212
233
project_id = request .project_id ,
213
234
)
214
235
if not credentials or "api_key" not in credentials :
215
- logger .warning (
216
- f"OpenAI API key not configured for org_id={ _current_user .organization_id } , project_id={ request .project_id } , organization_id= { _current_user . organization_id } "
236
+ logger .error (
237
+ f"OpenAI API key not configured for org_id={ _current_user .organization_id } , project_id={ request .project_id } "
217
238
)
218
239
return {
219
240
"success" : False ,
@@ -224,8 +245,30 @@ async def responses(
224
245
225
246
client = OpenAI (api_key = credentials ["api_key" ])
226
247
227
- # Send immediate response
228
- initial_response = {
248
+ langfuse_credentials = get_provider_credential (
249
+ session = _session ,
250
+ org_id = _current_user .organization_id ,
251
+ provider = "langfuse" ,
252
+ project_id = request .project_id ,
253
+ )
254
+ tracer = LangfuseTracer (
255
+ credentials = langfuse_credentials ,
256
+ response_id = request .response_id ,
257
+ )
258
+
259
+ background_tasks .add_task (
260
+ process_response ,
261
+ request ,
262
+ client ,
263
+ assistant ,
264
+ tracer ,
265
+ )
266
+
267
+ logger .info (
268
+ f"Background task scheduled for response processing: assistant_id={ request .assistant_id } , project_id={ request .project_id } , organization_id={ _current_user .organization_id } "
269
+ )
270
+
271
+ return {
229
272
"success" : True ,
230
273
"data" : {
231
274
"status" : "processing" ,
@@ -236,26 +279,14 @@ async def responses(
236
279
"metadata" : None ,
237
280
}
238
281
239
- # Schedule background task
240
- background_tasks .add_task (
241
- process_response , request , client , assistant , _current_user .organization_id
242
- )
243
- logger .info (
244
- f"Background task scheduled for response processing: assistant_id={ request .assistant_id } , project_id={ request .project_id } , organization_id={ _current_user .organization_id } "
245
- )
246
-
247
- return initial_response
248
-
249
282
250
283
@router .post ("/responses/sync" , response_model = ResponsesAPIResponse )
251
284
async def responses_sync (
252
285
request : ResponsesSyncAPIRequest ,
253
286
_session : Session = Depends (get_db ),
254
287
_current_user : UserOrganization = Depends (get_current_user_org ),
255
288
):
256
- """
257
- Synchronous endpoint for benchmarking OpenAI responses API
258
- """
289
+ """Synchronous endpoint for benchmarking OpenAI responses API with Langfuse tracing."""
259
290
credentials = get_provider_credential (
260
291
session = _session ,
261
292
org_id = _current_user .organization_id ,
@@ -269,6 +300,27 @@ async def responses_sync(
269
300
270
301
client = OpenAI (api_key = credentials ["api_key" ])
271
302
303
+ langfuse_credentials = get_provider_credential (
304
+ session = _session ,
305
+ org_id = _current_user .organization_id ,
306
+ provider = "langfuse" ,
307
+ project_id = request .project_id ,
308
+ )
309
+ tracer = LangfuseTracer (
310
+ credentials = langfuse_credentials ,
311
+ response_id = request .response_id ,
312
+ )
313
+
314
+ tracer .start_trace (
315
+ name = "generate_response_sync" ,
316
+ input = {"question" : request .question },
317
+ )
318
+ tracer .start_generation (
319
+ name = "openai_response" ,
320
+ input = {"question" : request .question },
321
+ metadata = {"model" : request .model , "temperature" : request .temperature },
322
+ )
323
+
272
324
try :
273
325
response = client .responses .create (
274
326
model = request .model ,
@@ -288,6 +340,31 @@ async def responses_sync(
288
340
289
341
response_chunks = get_file_search_results (response )
290
342
343
+ tracer .end_generation (
344
+ output = {
345
+ "response_id" : response .id ,
346
+ "message" : response .output_text ,
347
+ },
348
+ usage = {
349
+ "input" : response .usage .input_tokens ,
350
+ "output" : response .usage .output_tokens ,
351
+ "total" : response .usage .total_tokens ,
352
+ "unit" : "TOKENS" ,
353
+ },
354
+ model = response .model ,
355
+ )
356
+
357
+ tracer .update_trace (
358
+ tags = [response .id ],
359
+ output = {
360
+ "status" : "success" ,
361
+ "message" : response .output_text ,
362
+ "error" : None ,
363
+ },
364
+ )
365
+
366
+ tracer .flush ()
367
+
291
368
return ResponsesAPIResponse .success_response (
292
369
data = _APIResponse (
293
370
status = "success" ,
@@ -300,7 +377,10 @@ async def responses_sync(
300
377
total_tokens = response .usage .total_tokens ,
301
378
model = response .model ,
302
379
),
303
- ),
380
+ )
304
381
)
305
382
except openai .OpenAIError as e :
306
- return Exception (error = handle_openai_error (e ))
383
+ error_message = handle_openai_error (e )
384
+ tracer .log_error (error_message , response_id = request .response_id )
385
+ tracer .flush ()
386
+ return ResponsesAPIResponse .failure_response (error = error_message )
0 commit comments