2026-01-27 13:33:36 +02:00
|
|
|
"""
|
|
|
|
|
Utilities for working with message content.
|
|
|
|
|
|
|
|
|
|
Message content in new_chat_messages can be stored in various formats:
|
|
|
|
|
- String: Simple text content
|
|
|
|
|
- List: Array of content parts [{"type": "text", "text": "..."}, {"type": "tool-call", ...}]
|
|
|
|
|
- Dict: Single content object
|
|
|
|
|
|
|
|
|
|
These utilities help extract and transform content for different use cases.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-02-09 17:15:33 -08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
2026-01-27 13:33:36 +02:00
|
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-02-06 18:09:32 +02:00
|
|
|
from sqlalchemy.orm import selectinload
|
2026-01-27 13:33:36 +02:00
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from app.db import ChatVisibility
|
|
|
|
|
|
2026-01-27 13:33:36 +02:00
|
|
|
|
|
|
|
|
def extract_text_content(content: str | dict | list) -> str:
|
|
|
|
|
"""Extract plain text content from various message formats."""
|
|
|
|
|
if isinstance(content, str):
|
|
|
|
|
return content
|
|
|
|
|
if isinstance(content, dict):
|
|
|
|
|
# Handle dict with 'text' key
|
|
|
|
|
if "text" in content:
|
|
|
|
|
return content["text"]
|
|
|
|
|
return str(content)
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
# Handle list of parts (e.g., [{"type": "text", "text": "..."}])
|
|
|
|
|
texts = []
|
|
|
|
|
for part in content:
|
|
|
|
|
if isinstance(part, dict) and part.get("type") == "text":
|
|
|
|
|
texts.append(part.get("text", ""))
|
|
|
|
|
elif isinstance(part, str):
|
|
|
|
|
texts.append(part)
|
|
|
|
|
return "\n".join(texts) if texts else ""
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def bootstrap_history_from_db(
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
thread_id: int,
|
2026-02-09 16:49:11 -08:00
|
|
|
thread_visibility: ChatVisibility | None = None,
|
2026-01-27 13:33:36 +02:00
|
|
|
) -> list[HumanMessage | AIMessage]:
|
|
|
|
|
"""
|
|
|
|
|
Load message history from database and convert to LangChain format.
|
|
|
|
|
|
|
|
|
|
Used for cloned chats where the LangGraph checkpointer has no state,
|
|
|
|
|
but we have messages in the database that should be used as context.
|
|
|
|
|
|
2026-02-06 18:09:32 +02:00
|
|
|
When thread_visibility is SEARCH_SPACE, user messages are prefixed with
|
|
|
|
|
the author's display name so the LLM sees who said what.
|
|
|
|
|
|
2026-01-27 13:33:36 +02:00
|
|
|
Args:
|
|
|
|
|
session: Database session
|
|
|
|
|
thread_id: The chat thread ID
|
2026-02-06 18:09:32 +02:00
|
|
|
thread_visibility: When SEARCH_SPACE, user messages get author prefix
|
2026-01-27 13:33:36 +02:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of LangChain messages (HumanMessage/AIMessage)
|
|
|
|
|
"""
|
2026-02-06 18:09:32 +02:00
|
|
|
from app.db import ChatVisibility, NewChatMessage
|
2026-01-27 13:33:36 +02:00
|
|
|
|
2026-02-06 18:09:32 +02:00
|
|
|
is_shared = thread_visibility == ChatVisibility.SEARCH_SPACE
|
|
|
|
|
stmt = (
|
2026-01-27 13:33:36 +02:00
|
|
|
select(NewChatMessage)
|
|
|
|
|
.filter(NewChatMessage.thread_id == thread_id)
|
|
|
|
|
.order_by(NewChatMessage.created_at)
|
|
|
|
|
)
|
2026-02-06 18:09:32 +02:00
|
|
|
if is_shared:
|
|
|
|
|
stmt = stmt.options(selectinload(NewChatMessage.author))
|
|
|
|
|
result = await session.execute(stmt)
|
2026-01-27 13:33:36 +02:00
|
|
|
db_messages = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
langchain_messages: list[HumanMessage | AIMessage] = []
|
|
|
|
|
|
|
|
|
|
for msg in db_messages:
|
|
|
|
|
text_content = extract_text_content(msg.content)
|
|
|
|
|
if not text_content:
|
|
|
|
|
continue
|
|
|
|
|
if msg.role == "user":
|
2026-02-06 18:09:32 +02:00
|
|
|
if is_shared:
|
|
|
|
|
author_name = (
|
2026-02-09 16:49:11 -08:00
|
|
|
msg.author.display_name if msg.author else None
|
|
|
|
|
) or "A team member"
|
2026-02-06 18:09:32 +02:00
|
|
|
text_content = f"**[{author_name}]:** {text_content}"
|
2026-01-27 13:33:36 +02:00
|
|
|
langchain_messages.append(HumanMessage(content=text_content))
|
|
|
|
|
elif msg.role == "assistant":
|
|
|
|
|
langchain_messages.append(AIMessage(content=text_content))
|
|
|
|
|
|
|
|
|
|
return langchain_messages
|