OpenRAG 0.1.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. agent.py +669 -0
  2. api/__init__.py +0 -0
  3. api/auth.py +84 -0
  4. api/chat.py +181 -0
  5. api/connector_router.py +78 -0
  6. api/connectors.py +437 -0
  7. api/docling.py +120 -0
  8. api/documents.py +103 -0
  9. api/flows.py +66 -0
  10. api/knowledge_filter.py +468 -0
  11. api/langflow_files.py +296 -0
  12. api/models.py +148 -0
  13. api/nudges.py +44 -0
  14. api/oidc.py +111 -0
  15. api/router.py +206 -0
  16. api/search.py +53 -0
  17. api/settings.py +762 -0
  18. api/tasks.py +35 -0
  19. api/upload.py +202 -0
  20. auth_context.py +75 -0
  21. auth_middleware.py +84 -0
  22. config/__init__.py +0 -0
  23. config/config_manager.py +214 -0
  24. config/settings.py +602 -0
  25. connectors/__init__.py +11 -0
  26. connectors/base.py +149 -0
  27. connectors/connection_manager.py +532 -0
  28. connectors/google_drive/__init__.py +4 -0
  29. connectors/google_drive/connector.py +1112 -0
  30. connectors/google_drive/oauth.py +152 -0
  31. connectors/langflow_connector_service.py +342 -0
  32. connectors/onedrive/__init__.py +4 -0
  33. connectors/onedrive/connector.py +494 -0
  34. connectors/onedrive/oauth.py +322 -0
  35. connectors/service.py +402 -0
  36. connectors/sharepoint/__init__.py +4 -0
  37. connectors/sharepoint/connector.py +567 -0
  38. connectors/sharepoint/oauth.py +329 -0
  39. main.py +1186 -0
  40. models/__init__.py +0 -0
  41. models/processors.py +769 -0
  42. models/tasks.py +46 -0
  43. openrag-0.1.25.dist-info/METADATA +166 -0
  44. openrag-0.1.25.dist-info/RECORD +112 -0
  45. openrag-0.1.25.dist-info/WHEEL +5 -0
  46. openrag-0.1.25.dist-info/entry_points.txt +2 -0
  47. openrag-0.1.25.dist-info/licenses/LICENSE +201 -0
  48. openrag-0.1.25.dist-info/top_level.txt +12 -0
  49. services/__init__.py +0 -0
  50. services/auth_service.py +401 -0
  51. services/chat_service.py +537 -0
  52. services/conversation_persistence_service.py +130 -0
  53. services/document_service.py +213 -0
  54. services/flows_service.py +906 -0
  55. services/knowledge_filter_service.py +347 -0
  56. services/langflow_file_service.py +338 -0
  57. services/langflow_history_service.py +172 -0
  58. services/langflow_mcp_service.py +275 -0
  59. services/models_service.py +372 -0
  60. services/monitor_service.py +305 -0
  61. services/search_service.py +477 -0
  62. services/session_ownership_service.py +109 -0
  63. services/task_service.py +435 -0
  64. session_manager.py +235 -0
  65. tui/__init__.py +8 -0
  66. tui/_assets/docker-compose-cpu.yml +127 -0
  67. tui/_assets/docker-compose.yml +126 -0
  68. tui/_assets/documents/docling.pdf +0 -0
  69. tui/_assets/documents/ibm_anthropic.pdf +0 -0
  70. tui/_assets/documents/openrag-documentation.pdf +0 -0
  71. tui/_assets/documents/warmup_ocr.pdf +0 -0
  72. tui/_assets/flows/components/ollama_embedding.json +142 -0
  73. tui/_assets/flows/components/ollama_llm.json +682 -0
  74. tui/_assets/flows/components/ollama_llm_text.json +683 -0
  75. tui/_assets/flows/components/watsonx_embedding.json +207 -0
  76. tui/_assets/flows/components/watsonx_llm.json +521 -0
  77. tui/_assets/flows/components/watsonx_llm_text.json +521 -0
  78. tui/_assets/flows/ingestion_flow.json +4199 -0
  79. tui/_assets/flows/openrag_agent.json +2875 -0
  80. tui/_assets/flows/openrag_nudges.json +2350 -0
  81. tui/_assets/flows/openrag_url_mcp.json +3621 -0
  82. tui/main.py +521 -0
  83. tui/managers/__init__.py +1 -0
  84. tui/managers/container_manager.py +892 -0
  85. tui/managers/docling_manager.py +488 -0
  86. tui/managers/env_manager.py +442 -0
  87. tui/screens/__init__.py +1 -0
  88. tui/screens/config.py +690 -0
  89. tui/screens/diagnostics.py +572 -0
  90. tui/screens/logs.py +356 -0
  91. tui/screens/monitor.py +858 -0
  92. tui/screens/welcome.py +446 -0
  93. tui/utils/__init__.py +1 -0
  94. tui/utils/clipboard.py +50 -0
  95. tui/utils/platform.py +209 -0
  96. tui/utils/validation.py +139 -0
  97. tui/widgets/__init__.py +3 -0
  98. tui/widgets/command_modal.py +379 -0
  99. tui/widgets/diagnostics_notification.py +40 -0
  100. tui/widgets/error_notification.py +40 -0
  101. tui/widgets/waves.py +146 -0
  102. utils/__init__.py +0 -0
  103. utils/container_utils.py +298 -0
  104. utils/document_processing.py +370 -0
  105. utils/embedding_fields.py +178 -0
  106. utils/embeddings.py +198 -0
  107. utils/file_utils.py +60 -0
  108. utils/gpu_detection.py +47 -0
  109. utils/hash_utils.py +76 -0
  110. utils/logging_config.py +117 -0
  111. utils/opensearch_queries.py +55 -0
  112. utils/process_pool.py +13 -0
agent.py ADDED
@@ -0,0 +1,669 @@
1
+ from utils.logging_config import get_logger
2
+
3
+ logger = get_logger(__name__)
4
+
5
+ # Import persistent storage
6
+ from services.conversation_persistence_service import conversation_persistence
7
+
8
+ # In-memory storage for active conversation threads (preserves function calls)
9
+ active_conversations = {}
10
+
11
+
12
+ def get_user_conversations(user_id: str):
13
+ """Get conversation metadata for a user from persistent storage"""
14
+ return conversation_persistence.get_user_conversations(user_id)
15
+
16
+
17
+ def get_conversation_thread(user_id: str, previous_response_id: str = None):
18
+ """Get or create a specific conversation thread with function call preservation"""
19
+ from datetime import datetime
20
+
21
+ # Create user namespace if it doesn't exist
22
+ if user_id not in active_conversations:
23
+ active_conversations[user_id] = {}
24
+
25
+ # If we have a previous_response_id, try to get the existing conversation
26
+ if previous_response_id and previous_response_id in active_conversations[user_id]:
27
+ logger.debug(
28
+ f"Retrieved existing conversation for user {user_id}, response_id {previous_response_id}"
29
+ )
30
+ return active_conversations[user_id][previous_response_id]
31
+
32
+ # Create new conversation thread
33
+ new_conversation = {
34
+ "messages": [
35
+ {
36
+ "role": "system",
37
+ "content": "You are a helpful assistant that can use tools to answer questions and perform tasks. You are part of OpenRAG, an assistant that analyzes documents and provides informations about them. When asked about what is OpenRAG, answer the following:\n\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n\n**OpenSearch** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n\n**Docling** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\"",
38
+ }
39
+ ],
40
+ "previous_response_id": previous_response_id, # Parent response_id for branching
41
+ "created_at": datetime.now(),
42
+ "last_activity": datetime.now(),
43
+ }
44
+
45
+ return new_conversation
46
+
47
+
48
+ def store_conversation_thread(user_id: str, response_id: str, conversation_state: dict):
49
+ """Store conversation both in memory (with function calls) and persist metadata to disk"""
50
+ # 1. Store full conversation in memory for function call preservation
51
+ if user_id not in active_conversations:
52
+ active_conversations[user_id] = {}
53
+ active_conversations[user_id][response_id] = conversation_state
54
+
55
+ # 2. Store only essential metadata to disk (simplified JSON)
56
+ messages = conversation_state.get("messages", [])
57
+ first_user_msg = next((msg for msg in messages if msg.get("role") == "user"), None)
58
+ title = "New Chat"
59
+ if first_user_msg:
60
+ content = first_user_msg.get("content", "")
61
+ title = content[:50] + "..." if len(content) > 50 else content
62
+
63
+ metadata_only = {
64
+ "response_id": response_id,
65
+ "title": title,
66
+ "endpoint": "langflow",
67
+ "created_at": conversation_state.get("created_at"),
68
+ "last_activity": conversation_state.get("last_activity"),
69
+ "previous_response_id": conversation_state.get("previous_response_id"),
70
+ "total_messages": len(
71
+ [msg for msg in messages if msg.get("role") in ["user", "assistant"]]
72
+ ),
73
+ # Don't store actual messages - Langflow has them
74
+ }
75
+
76
+ conversation_persistence.store_conversation_thread(
77
+ user_id, response_id, metadata_only
78
+ )
79
+
80
+
81
+ # Legacy function for backward compatibility
82
+ def get_user_conversation(user_id: str):
83
+ """Get the most recent conversation for a user (for backward compatibility)"""
84
+ # Check in-memory conversations first (with function calls)
85
+ if user_id in active_conversations and active_conversations[user_id]:
86
+ latest_response_id = max(
87
+ active_conversations[user_id].keys(),
88
+ key=lambda k: active_conversations[user_id][k]["last_activity"],
89
+ )
90
+ return active_conversations[user_id][latest_response_id]
91
+
92
+ # Fallback to metadata-only conversations
93
+ conversations = get_user_conversations(user_id)
94
+ if not conversations:
95
+ return get_conversation_thread(user_id)
96
+
97
+ # Return the most recently active conversation metadata
98
+ latest_conversation = max(conversations.values(), key=lambda c: c["last_activity"])
99
+ return latest_conversation
100
+
101
+
102
+ # Generic async response function for streaming
103
+ async def async_response_stream(
104
+ client,
105
+ prompt: str,
106
+ model: str,
107
+ extra_headers: dict = None,
108
+ previous_response_id: str = None,
109
+ log_prefix: str = "response",
110
+ ):
111
+ logger.info("User prompt received", prompt=prompt)
112
+
113
+ try:
114
+ # Build request parameters
115
+ request_params = {
116
+ "model": model,
117
+ "input": prompt,
118
+ "stream": True,
119
+ "include": ["tool_call.results"],
120
+ }
121
+ if previous_response_id is not None:
122
+ request_params["previous_response_id"] = previous_response_id
123
+
124
+ if "x-api-key" not in client.default_headers:
125
+ if hasattr(client, "api_key") and extra_headers is not None:
126
+ extra_headers["x-api-key"] = client.api_key
127
+
128
+ if extra_headers:
129
+ request_params["extra_headers"] = extra_headers
130
+
131
+ response = await client.responses.create(**request_params)
132
+
133
+ full_response = ""
134
+ chunk_count = 0
135
+ async for chunk in response:
136
+ chunk_count += 1
137
+ logger.debug(
138
+ "Stream chunk received", chunk_count=chunk_count, chunk=str(chunk)
139
+ )
140
+
141
+ # Yield the raw event as JSON for the UI to process
142
+ import json
143
+
144
+ # Also extract text content for logging
145
+ if hasattr(chunk, "output_text") and chunk.output_text:
146
+ full_response += chunk.output_text
147
+ elif hasattr(chunk, "delta") and chunk.delta:
148
+ # Handle delta properly - it might be a dict or string
149
+ if isinstance(chunk.delta, dict):
150
+ delta_text = (
151
+ chunk.delta.get("content", "")
152
+ or chunk.delta.get("text", "")
153
+ or str(chunk.delta)
154
+ )
155
+ else:
156
+ delta_text = str(chunk.delta)
157
+ full_response += delta_text
158
+
159
+ # Send the raw event as JSON followed by newline for easy parsing
160
+ try:
161
+ # Try to serialize the chunk object
162
+ if hasattr(chunk, "model_dump"):
163
+ # Pydantic model
164
+ chunk_data = chunk.model_dump()
165
+ elif hasattr(chunk, "__dict__"):
166
+ chunk_data = chunk.__dict__
167
+ else:
168
+ chunk_data = str(chunk)
169
+
170
+ yield (json.dumps(chunk_data, default=str) + "\n").encode("utf-8")
171
+ except Exception as e:
172
+ # Fallback to string representation
173
+ logger.warning("JSON serialization failed", error=str(e))
174
+ yield (
175
+ json.dumps(
176
+ {"error": f"Serialization failed: {e}", "raw": str(chunk)}
177
+ )
178
+ + "\n"
179
+ ).encode("utf-8")
180
+
181
+ logger.debug("Stream complete", total_chunks=chunk_count)
182
+ logger.info("Response generated", log_prefix=log_prefix, response=full_response)
183
+
184
+ except Exception as e:
185
+ logger.error("Exception in streaming", error=str(e))
186
+ import traceback
187
+
188
+ traceback.print_exc()
189
+ raise
190
+
191
+
192
+ # Generic async response function for non-streaming
193
+ async def async_response(
194
+ client,
195
+ prompt: str,
196
+ model: str,
197
+ extra_headers: dict = None,
198
+ previous_response_id: str = None,
199
+ log_prefix: str = "response",
200
+ ):
201
+ try:
202
+ logger.info("User prompt received", prompt=prompt)
203
+
204
+ # Build request parameters
205
+ request_params = {
206
+ "model": model,
207
+ "input": prompt,
208
+ "stream": False,
209
+ "include": ["tool_call.results"],
210
+ }
211
+ if previous_response_id is not None:
212
+ request_params["previous_response_id"] = previous_response_id
213
+ if extra_headers:
214
+ request_params["extra_headers"] = extra_headers
215
+
216
+ if "x-api-key" not in client.default_headers:
217
+ if hasattr(client, "api_key") and extra_headers is not None:
218
+ extra_headers["x-api-key"] = client.api_key
219
+
220
+ response = await client.responses.create(**request_params)
221
+
222
+ response_text = response.output_text
223
+ logger.info("Response generated", log_prefix=log_prefix, response=response_text)
224
+
225
+ # Extract and store response_id if available
226
+ response_id = getattr(response, "id", None) or getattr(
227
+ response, "response_id", None
228
+ )
229
+
230
+ return response_text, response_id, response
231
+ except Exception as e:
232
+ logger.error("Exception in non-streaming response", error=str(e))
233
+ import traceback
234
+
235
+ traceback.print_exc()
236
+ raise
237
+
238
+
239
+ # Unified streaming function for both chat and langflow
240
+ async def async_stream(
241
+ client,
242
+ prompt: str,
243
+ model: str,
244
+ extra_headers: dict = None,
245
+ previous_response_id: str = None,
246
+ log_prefix: str = "response",
247
+ ):
248
+ async for chunk in async_response_stream(
249
+ client,
250
+ prompt,
251
+ model,
252
+ extra_headers=extra_headers,
253
+ previous_response_id=previous_response_id,
254
+ log_prefix=log_prefix,
255
+ ):
256
+ yield chunk
257
+
258
+
259
+ # Async langflow function (non-streaming only)
260
+ async def async_langflow(
261
+ langflow_client,
262
+ flow_id: str,
263
+ prompt: str,
264
+ extra_headers: dict = None,
265
+ previous_response_id: str = None,
266
+ ):
267
+ response_text, response_id, response_obj = await async_response(
268
+ langflow_client,
269
+ prompt,
270
+ flow_id,
271
+ extra_headers=extra_headers,
272
+ previous_response_id=previous_response_id,
273
+ log_prefix="langflow",
274
+ )
275
+ return response_text, response_id
276
+
277
+
278
+ # Async langflow function for streaming (alias for compatibility)
279
+ async def async_langflow_stream(
280
+ langflow_client,
281
+ flow_id: str,
282
+ prompt: str,
283
+ extra_headers: dict = None,
284
+ previous_response_id: str = None,
285
+ ):
286
+ logger.debug("Starting langflow stream", prompt=prompt)
287
+ try:
288
+ async for chunk in async_stream(
289
+ langflow_client,
290
+ prompt,
291
+ flow_id,
292
+ extra_headers=extra_headers,
293
+ previous_response_id=previous_response_id,
294
+ log_prefix="langflow",
295
+ ):
296
+ logger.debug(
297
+ "Yielding chunk from langflow stream",
298
+ chunk_preview=chunk[:100].decode("utf-8", errors="replace"),
299
+ )
300
+ yield chunk
301
+ logger.debug("Langflow stream completed")
302
+ except Exception as e:
303
+ logger.error("Exception in langflow stream", error=str(e))
304
+ import traceback
305
+
306
+ traceback.print_exc()
307
+ raise
308
+
309
+
310
+ # Async chat function (non-streaming only)
311
+ async def async_chat(
312
+ async_client,
313
+ prompt: str,
314
+ user_id: str,
315
+ model: str = "gpt-4.1-mini",
316
+ previous_response_id: str = None,
317
+ ):
318
+ logger.debug(
319
+ "async_chat called", user_id=user_id, previous_response_id=previous_response_id
320
+ )
321
+
322
+ # Get the specific conversation thread (or create new one)
323
+ conversation_state = get_conversation_thread(user_id, previous_response_id)
324
+ logger.debug(
325
+ "Got conversation state", message_count=len(conversation_state["messages"])
326
+ )
327
+
328
+ # Add user message to conversation with timestamp
329
+ from datetime import datetime
330
+
331
+ user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
332
+ conversation_state["messages"].append(user_message)
333
+ logger.debug(
334
+ "Added user message", message_count=len(conversation_state["messages"])
335
+ )
336
+
337
+ response_text, response_id, response_obj = await async_response(
338
+ async_client,
339
+ prompt,
340
+ model,
341
+ previous_response_id=previous_response_id,
342
+ log_prefix="agent",
343
+ )
344
+ logger.debug(
345
+ "Got response", response_preview=response_text[:50], response_id=response_id
346
+ )
347
+
348
+ # Add assistant response to conversation with response_id, timestamp, and full response object
349
+ assistant_message = {
350
+ "role": "assistant",
351
+ "content": response_text,
352
+ "response_id": response_id,
353
+ "timestamp": datetime.now(),
354
+ "response_data": response_obj.model_dump()
355
+ if hasattr(response_obj, "model_dump")
356
+ else str(response_obj), # Store complete response for function calls
357
+ }
358
+ conversation_state["messages"].append(assistant_message)
359
+ logger.debug(
360
+ "Added assistant message", message_count=len(conversation_state["messages"])
361
+ )
362
+
363
+ # Store the conversation thread with its response_id
364
+ if response_id:
365
+ conversation_state["last_activity"] = datetime.now()
366
+ store_conversation_thread(user_id, response_id, conversation_state)
367
+ logger.debug(
368
+ "Stored conversation thread", user_id=user_id, response_id=response_id
369
+ )
370
+
371
+ # Debug: Check what's in user_conversations now
372
+ conversations = get_user_conversations(user_id)
373
+ logger.debug(
374
+ "User conversations updated",
375
+ user_id=user_id,
376
+ conversation_count=len(conversations),
377
+ conversation_ids=list(conversations.keys()),
378
+ )
379
+ else:
380
+ logger.warning("No response_id received, conversation not stored")
381
+
382
+ return response_text, response_id
383
+
384
+
385
+ # Async chat function for streaming (alias for compatibility)
386
+ async def async_chat_stream(
387
+ async_client,
388
+ prompt: str,
389
+ user_id: str,
390
+ model: str = "gpt-4.1-mini",
391
+ previous_response_id: str = None,
392
+ ):
393
+ # Get the specific conversation thread (or create new one)
394
+ conversation_state = get_conversation_thread(user_id, previous_response_id)
395
+
396
+ # Add user message to conversation with timestamp
397
+ from datetime import datetime
398
+
399
+ user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
400
+ conversation_state["messages"].append(user_message)
401
+
402
+ full_response = ""
403
+ response_id = None
404
+ async for chunk in async_stream(
405
+ async_client,
406
+ prompt,
407
+ model,
408
+ previous_response_id=previous_response_id,
409
+ log_prefix="agent",
410
+ ):
411
+ # Extract text content to build full response for history
412
+ try:
413
+ import json
414
+
415
+ chunk_data = json.loads(chunk.decode("utf-8"))
416
+ if "delta" in chunk_data and "content" in chunk_data["delta"]:
417
+ full_response += chunk_data["delta"]["content"]
418
+ # Extract response_id from chunk
419
+ if "id" in chunk_data:
420
+ response_id = chunk_data["id"]
421
+ elif "response_id" in chunk_data:
422
+ response_id = chunk_data["response_id"]
423
+ except:
424
+ pass
425
+ yield chunk
426
+
427
+ # Add the complete assistant response to message history with response_id and timestamp
428
+ if full_response:
429
+ assistant_message = {
430
+ "role": "assistant",
431
+ "content": full_response,
432
+ "response_id": response_id,
433
+ "timestamp": datetime.now(),
434
+ }
435
+ conversation_state["messages"].append(assistant_message)
436
+
437
+ # Store the conversation thread with its response_id
438
+ if response_id:
439
+ conversation_state["last_activity"] = datetime.now()
440
+ store_conversation_thread(user_id, response_id, conversation_state)
441
+ logger.debug(
442
+ f"Stored conversation thread for user {user_id} with response_id: {response_id}"
443
+ )
444
+
445
+
446
+ # Async langflow function with conversation storage (non-streaming)
447
+ async def async_langflow_chat(
448
+ langflow_client,
449
+ flow_id: str,
450
+ prompt: str,
451
+ user_id: str,
452
+ extra_headers: dict = None,
453
+ previous_response_id: str = None,
454
+ store_conversation: bool = True,
455
+ ):
456
+ logger.debug(
457
+ "async_langflow_chat called",
458
+ user_id=user_id,
459
+ previous_response_id=previous_response_id,
460
+ )
461
+
462
+ if store_conversation:
463
+ # Get the specific conversation thread (or create new one)
464
+ conversation_state = get_conversation_thread(user_id, previous_response_id)
465
+ logger.debug(
466
+ "Got langflow conversation state",
467
+ message_count=len(conversation_state["messages"]),
468
+ )
469
+
470
+ # Add user message to conversation with timestamp
471
+ from datetime import datetime
472
+
473
+ if store_conversation:
474
+ user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
475
+ conversation_state["messages"].append(user_message)
476
+ logger.debug(
477
+ "Added user message to langflow",
478
+ message_count=len(conversation_state["messages"]),
479
+ )
480
+
481
+ response_text, response_id, response_obj = await async_response(
482
+ langflow_client,
483
+ prompt,
484
+ flow_id,
485
+ extra_headers=extra_headers,
486
+ previous_response_id=previous_response_id,
487
+ log_prefix="langflow",
488
+ )
489
+ logger.debug(
490
+ "Got langflow response",
491
+ response_preview=response_text[:50],
492
+ response_id=response_id,
493
+ )
494
+ logger.debug(
495
+ "Got langflow response",
496
+ response_preview=response_text[:50],
497
+ response_id=response_id,
498
+ )
499
+
500
+ if store_conversation:
501
+ # Add assistant response to conversation with response_id and timestamp
502
+ assistant_message = {
503
+ "role": "assistant",
504
+ "content": response_text,
505
+ "response_id": response_id,
506
+ "timestamp": datetime.now(),
507
+ "response_data": response_obj.model_dump()
508
+ if hasattr(response_obj, "model_dump")
509
+ else str(response_obj), # Store complete response for function calls
510
+ }
511
+ conversation_state["messages"].append(assistant_message)
512
+ logger.debug(
513
+ "Added assistant message to langflow",
514
+ message_count=len(conversation_state["messages"]),
515
+ )
516
+
517
+ if not store_conversation:
518
+ return response_text, response_id
519
+
520
+ # Store the conversation thread with its response_id
521
+ if response_id:
522
+ conversation_state["last_activity"] = datetime.now()
523
+ store_conversation_thread(user_id, response_id, conversation_state)
524
+
525
+ # Claim session ownership for this user
526
+ try:
527
+ from services.session_ownership_service import session_ownership_service
528
+
529
+ session_ownership_service.claim_session(user_id, response_id)
530
+ logger.debug(f"Claimed session {response_id} for user {user_id}")
531
+ except Exception as e:
532
+ logger.warning(f"Failed to claim session ownership: {e}")
533
+
534
+ logger.debug(
535
+ f"Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
536
+ )
537
+ logger.debug(
538
+ "Stored langflow conversation thread",
539
+ user_id=user_id,
540
+ response_id=response_id,
541
+ )
542
+
543
+ # Debug: Check what's in user_conversations now
544
+ conversations = get_user_conversations(user_id)
545
+ logger.debug(
546
+ "User conversations updated",
547
+ user_id=user_id,
548
+ conversation_count=len(conversations),
549
+ conversation_ids=list(conversations.keys()),
550
+ )
551
+ else:
552
+ logger.warning("No response_id received from langflow, conversation not stored")
553
+
554
+ return response_text, response_id
555
+
556
+
557
+ # Async langflow function with conversation storage (streaming)
558
+ async def async_langflow_chat_stream(
559
+ langflow_client,
560
+ flow_id: str,
561
+ prompt: str,
562
+ user_id: str,
563
+ extra_headers: dict = None,
564
+ previous_response_id: str = None,
565
+ ):
566
+ logger.debug(
567
+ "async_langflow_chat_stream called",
568
+ user_id=user_id,
569
+ previous_response_id=previous_response_id,
570
+ )
571
+
572
+ # Get the specific conversation thread (or create new one)
573
+ conversation_state = get_conversation_thread(user_id, previous_response_id)
574
+
575
+ # Add user message to conversation with timestamp
576
+ from datetime import datetime
577
+
578
+ user_message = {"role": "user", "content": prompt, "timestamp": datetime.now()}
579
+ conversation_state["messages"].append(user_message)
580
+
581
+ full_response = ""
582
+ response_id = None
583
+ collected_chunks = [] # Store all chunks for function call data
584
+
585
+ async for chunk in async_stream(
586
+ langflow_client,
587
+ prompt,
588
+ flow_id,
589
+ extra_headers=extra_headers,
590
+ previous_response_id=previous_response_id,
591
+ log_prefix="langflow",
592
+ ):
593
+ # Extract text content to build full response for history
594
+ try:
595
+ import json
596
+
597
+ chunk_data = json.loads(chunk.decode("utf-8"))
598
+ collected_chunks.append(chunk_data) # Collect all chunk data
599
+
600
+ if "delta" in chunk_data and "content" in chunk_data["delta"]:
601
+ full_response += chunk_data["delta"]["content"]
602
+ # Extract response_id from chunk
603
+ if "id" in chunk_data:
604
+ response_id = chunk_data["id"]
605
+ elif "response_id" in chunk_data:
606
+ response_id = chunk_data["response_id"]
607
+ except:
608
+ pass
609
+ yield chunk
610
+
611
+ # Add the complete assistant response to message history with response_id, timestamp, and function call data
612
+ if full_response:
613
+ assistant_message = {
614
+ "role": "assistant",
615
+ "content": full_response,
616
+ "response_id": response_id,
617
+ "timestamp": datetime.now(),
618
+ "chunks": collected_chunks, # Store complete chunk data for function calls
619
+ }
620
+ conversation_state["messages"].append(assistant_message)
621
+
622
+ # Store the conversation thread with its response_id
623
+ if response_id:
624
+ conversation_state["last_activity"] = datetime.now()
625
+ store_conversation_thread(user_id, response_id, conversation_state)
626
+
627
+ # Claim session ownership for this user
628
+ try:
629
+ from services.session_ownership_service import session_ownership_service
630
+
631
+ session_ownership_service.claim_session(user_id, response_id)
632
+ logger.debug(f"Claimed session {response_id} for user {user_id}")
633
+ except Exception as e:
634
+ logger.warning(f"Failed to claim session ownership: {e}")
635
+
636
+ logger.debug(
637
+ f"Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
638
+ )
639
+
640
+
641
+ def delete_user_conversation(user_id: str, response_id: str) -> bool:
642
+ """Delete a conversation for a user from both memory and persistent storage"""
643
+ deleted = False
644
+
645
+ try:
646
+ # Delete from in-memory storage
647
+ if user_id in active_conversations and response_id in active_conversations[user_id]:
648
+ del active_conversations[user_id][response_id]
649
+ logger.debug(f"Deleted conversation {response_id} from memory for user {user_id}")
650
+ deleted = True
651
+
652
+ # Delete from persistent storage
653
+ conversation_deleted = conversation_persistence.delete_conversation_thread(user_id, response_id)
654
+ if conversation_deleted:
655
+ logger.debug(f"Deleted conversation {response_id} from persistent storage for user {user_id}")
656
+ deleted = True
657
+
658
+ # Release session ownership
659
+ try:
660
+ from services.session_ownership_service import session_ownership_service
661
+ session_ownership_service.release_session(user_id, response_id)
662
+ logger.debug(f"Released session ownership for {response_id} for user {user_id}")
663
+ except Exception as e:
664
+ logger.warning(f"Failed to release session ownership: {e}")
665
+
666
+ return deleted
667
+ except Exception as e:
668
+ logger.error(f"Error deleting conversation {response_id} for user {user_id}: {e}")
669
+ return False
api/__init__.py ADDED
File without changes