|
7 | 7 | from sqlmodel import col, delete, select |
8 | 8 |
|
9 | 9 | from langflow.api.utils import DbSession, custom_params |
| 10 | +from langflow.api.utils.flow_utils import compute_virtual_flow_id |
10 | 11 | from langflow.schema.message import MessageResponse |
11 | 12 | from langflow.services.auth.utils import get_current_active_user |
12 | 13 | from langflow.services.database.models.flow.model import Flow |
@@ -304,6 +305,164 @@ async def delete_messages_sessions( |
304 | 305 | } |
305 | 306 |
|
306 | 307 |
|
| 308 | +@router.get("/messages/shared/sessions") |
| 309 | +async def get_shared_message_sessions( |
| 310 | + session: DbSession, |
| 311 | + current_user: Annotated[User, Depends(get_current_active_user)], |
| 312 | + source_flow_id: Annotated[UUID, Query(description="The original public flow ID")], |
| 313 | +) -> list[str]: |
| 314 | + """Get session IDs for a shared/public flow, scoped to the authenticated user. |
| 315 | +
|
| 316 | + Uses a deterministic virtual flow_id derived from the user's ID and the |
| 317 | + original flow ID. Only messages stored under this virtual flow_id are returned. |
| 318 | + """ |
| 319 | + try: |
| 320 | + virtual_flow_id = compute_virtual_flow_id(current_user.id, source_flow_id) |
| 321 | + stmt = select(MessageTable.session_id).distinct() |
| 322 | + stmt = stmt.where(MessageTable.flow_id == virtual_flow_id) |
| 323 | + stmt = stmt.where(col(MessageTable.session_id).isnot(None)) |
| 324 | + |
| 325 | + session_ids = await session.exec(stmt) |
| 326 | + return list(session_ids) |
| 327 | + except Exception as e: |
| 328 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 329 | + |
| 330 | + |
| 331 | +@router.get("/messages/shared") |
| 332 | +async def get_shared_messages( |
| 333 | + session: DbSession, |
| 334 | + current_user: Annotated[User, Depends(get_current_active_user)], |
| 335 | + source_flow_id: Annotated[UUID, Query(description="The original public flow ID")], |
| 336 | + session_id: Annotated[str | None, Query()] = None, |
| 337 | + order_by: Annotated[str | None, Query()] = "timestamp", |
| 338 | +) -> list[MessageResponse]: |
| 339 | + """Get messages for a shared/public flow, scoped to the authenticated user. |
| 340 | +
|
| 341 | + Uses a deterministic virtual flow_id derived from the user's ID and the |
| 342 | + original flow ID. Only messages stored under this virtual flow_id are returned. |
| 343 | + """ |
| 344 | + try: |
| 345 | + virtual_flow_id = compute_virtual_flow_id(current_user.id, source_flow_id) |
| 346 | + stmt = select(MessageTable) |
| 347 | + stmt = stmt.where(MessageTable.flow_id == virtual_flow_id) |
| 348 | + |
| 349 | + if session_id: |
| 350 | + from urllib.parse import unquote |
| 351 | + |
| 352 | + decoded_session_id = unquote(session_id) |
| 353 | + stmt = stmt.where(MessageTable.session_id == decoded_session_id) |
| 354 | + allowed_order_fields = {"timestamp", "sender", "sender_name", "session_id", "text"} |
| 355 | + if order_by: |
| 356 | + if order_by not in allowed_order_fields: |
| 357 | + raise HTTPException(status_code=400, detail=f"Invalid order_by field: {order_by}") |
| 358 | + order_col = getattr(MessageTable, order_by).asc() |
| 359 | + stmt = stmt.order_by(order_col) |
| 360 | + |
| 361 | + messages = await session.exec(stmt) |
| 362 | + return [MessageResponse.model_validate(d, from_attributes=True) for d in messages] |
| 363 | + except HTTPException: |
| 364 | + raise |
| 365 | + except Exception as e: |
| 366 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 367 | + |
| 368 | + |
| 369 | +@router.delete("/messages/shared/session/{session_id}", status_code=204) |
| 370 | +async def delete_shared_messages_session( |
| 371 | + session_id: str, |
| 372 | + session: DbSession, |
| 373 | + current_user: Annotated[User, Depends(get_current_active_user)], |
| 374 | + source_flow_id: Annotated[UUID, Query(description="The original public flow ID")], |
| 375 | +): |
| 376 | + """Delete messages for a session on a shared/public flow, scoped to the authenticated user.""" |
| 377 | + try: |
| 378 | + virtual_flow_id = compute_virtual_flow_id(current_user.id, source_flow_id) |
| 379 | + stmt = ( |
| 380 | + delete(MessageTable) |
| 381 | + .where(MessageTable.flow_id == virtual_flow_id) |
| 382 | + .where(MessageTable.session_id == session_id) |
| 383 | + ) |
| 384 | + await session.exec(stmt) |
| 385 | + except Exception as e: |
| 386 | + await session.rollback() |
| 387 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 388 | + |
| 389 | + |
| 390 | +@router.put("/messages/shared/{message_id}", response_model=MessageRead) |
| 391 | +async def update_shared_message( |
| 392 | + message_id: UUID, |
| 393 | + message: MessageUpdate, |
| 394 | + session: DbSession, |
| 395 | + current_user: Annotated[User, Depends(get_current_active_user)], |
| 396 | + source_flow_id: Annotated[UUID, Query(description="The original public flow ID")], |
| 397 | +): |
| 398 | + """Update a message on a shared/public flow, scoped to the authenticated user.""" |
| 399 | + try: |
| 400 | + virtual_flow_id = compute_virtual_flow_id(current_user.id, source_flow_id) |
| 401 | + db_message = ( |
| 402 | + await session.exec( |
| 403 | + select(MessageTable).where( |
| 404 | + MessageTable.id == message_id, |
| 405 | + MessageTable.flow_id == virtual_flow_id, |
| 406 | + ) |
| 407 | + ) |
| 408 | + ).first() |
| 409 | + except Exception as e: |
| 410 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 411 | + |
| 412 | + if not db_message: |
| 413 | + raise HTTPException(status_code=404, detail="Message not found") |
| 414 | + |
| 415 | + try: |
| 416 | + message_dict = message.model_dump(exclude_unset=True, exclude_none=True) |
| 417 | + if "text" in message_dict and message_dict["text"] != db_message.text: |
| 418 | + message_dict["edit"] = True |
| 419 | + db_message.sqlmodel_update(message_dict) |
| 420 | + session.add(db_message) |
| 421 | + await session.flush() |
| 422 | + await session.refresh(db_message) |
| 423 | + except Exception as e: |
| 424 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 425 | + return db_message |
| 426 | + |
| 427 | + |
| 428 | +@router.patch("/messages/shared/session/{old_session_id}") |
| 429 | +async def rename_shared_session( |
| 430 | + old_session_id: str, |
| 431 | + new_session_id: Annotated[str, Query(description="The new session ID")], |
| 432 | + session: DbSession, |
| 433 | + current_user: Annotated[User, Depends(get_current_active_user)], |
| 434 | + source_flow_id: Annotated[UUID, Query(description="The original public flow ID")], |
| 435 | +) -> list[MessageResponse]: |
| 436 | + """Rename a session on a shared/public flow, scoped to the authenticated user.""" |
| 437 | + try: |
| 438 | + virtual_flow_id = compute_virtual_flow_id(current_user.id, source_flow_id) |
| 439 | + stmt = select(MessageTable).where( |
| 440 | + MessageTable.flow_id == virtual_flow_id, |
| 441 | + MessageTable.session_id == old_session_id, |
| 442 | + ) |
| 443 | + messages = list(await session.exec(stmt)) |
| 444 | + except Exception as e: |
| 445 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 446 | + |
| 447 | + if not messages: |
| 448 | + raise HTTPException(status_code=404, detail="No messages found with the given session ID") |
| 449 | + |
| 450 | + try: |
| 451 | + for message in messages: |
| 452 | + message.session_id = new_session_id |
| 453 | + session.add_all(messages) |
| 454 | + await session.flush() |
| 455 | + |
| 456 | + result = [] |
| 457 | + for message in messages: |
| 458 | + await session.refresh(message) |
| 459 | + result.append(MessageResponse.model_validate(message, from_attributes=True)) |
| 460 | + except Exception as e: |
| 461 | + raise HTTPException(status_code=500, detail=str(e)) from e |
| 462 | + |
| 463 | + return result |
| 464 | + |
| 465 | + |
307 | 466 | @router.get("/transactions", dependencies=[Depends(get_current_active_user)]) |
308 | 467 | async def get_transactions( |
309 | 468 | flow_id: Annotated[UUID, Query()], |
|
0 commit comments