Spaces:
Running
Running
| """Session persistence utilities for the ChemGraph Streamlit UI. | |
| Bridges the gap between the UI's in-memory ``conversation_history`` | |
| format and the :class:`~chemgraph.memory.store.SessionStore` persistence | |
| layer. Every function is Streamlit-free so it can be unit-tested without | |
| a running Streamlit runtime. | |
| """ | |
| from __future__ import annotations | |
| import uuid | |
| from typing import Any, Optional | |
| from chemgraph.memory.schemas import Session, SessionMessage | |
| from ui.message_utils import normalize_message_content | |
| # --------------------------------------------------------------------------- | |
| # Session ID generation | |
| # --------------------------------------------------------------------------- | |
| def generate_session_id() -> str: | |
| """Return a short unique session identifier (first 8 chars of a UUID4). | |
| Mirrors the convention used by :class:`chemgraph.agent.llm_agent.ChemGraph`. | |
| """ | |
| return str(uuid.uuid4())[:8] | |
| # --------------------------------------------------------------------------- | |
| # Conversation history <--> SessionMessage conversion | |
| # --------------------------------------------------------------------------- | |
| def messages_from_result(result: Any) -> list[SessionMessage]: | |
| """Extract :class:`SessionMessage` objects from a single agent run result. | |
| *result* is the value stored in ``conversation_history[i]["result"]``, | |
| which may be a list of LangChain messages, a dict with a ``"messages"`` | |
| key, or a plain object. | |
| Parameters | |
| ---------- | |
| result : Any | |
| Agent run result stored in conversation history. | |
| Returns | |
| ------- | |
| list[SessionMessage] | |
| Session messages extracted from the result. | |
| """ | |
| raw_messages: list[Any] = [] | |
| if isinstance(result, list): | |
| raw_messages = result | |
| elif isinstance(result, dict) and "messages" in result: | |
| raw_messages = list(result["messages"]) | |
| else: | |
| raw_messages = [result] | |
| session_messages: list[SessionMessage] = [] | |
| for msg in raw_messages: | |
| role: Optional[str] = None | |
| content = "" | |
| tool_name: Optional[str] = None | |
| if hasattr(msg, "type") and hasattr(msg, "content"): | |
| # LangChain message object | |
| role = _langchain_type_to_role(msg.type) | |
| content = normalize_message_content(msg.content) | |
| tool_name = getattr(msg, "name", None) | |
| elif isinstance(msg, dict): | |
| role = _langchain_type_to_role(msg.get("type", "")) | |
| content = normalize_message_content(msg.get("content", "")) | |
| tool_name = msg.get("name") | |
| else: | |
| role = "ai" | |
| content = normalize_message_content(str(msg)) | |
| if role and content: | |
| session_messages.append( | |
| SessionMessage(role=role, content=content, tool_name=tool_name) | |
| ) | |
| return session_messages | |
| def conversation_entry_to_messages(entry: dict) -> list[SessionMessage]: | |
| """Convert a single conversation-history entry to :class:`SessionMessage` objects. | |
| An entry has the shape ``{"query": str, "result": ..., "thread_id": int}``. | |
| We produce one ``human`` message for the query, followed by messages | |
| extracted from the result. | |
| Parameters | |
| ---------- | |
| entry : dict | |
| Conversation-history entry from Streamlit session state. | |
| Returns | |
| ------- | |
| list[SessionMessage] | |
| Messages suitable for persistence. | |
| """ | |
| out: list[SessionMessage] = [] | |
| query = entry.get("query", "").strip() | |
| if query: | |
| out.append(SessionMessage(role="human", content=query)) | |
| result = entry.get("result") | |
| if result is not None: | |
| out.extend(messages_from_result(result)) | |
| return out | |
| def session_to_conversation_history(session: Session) -> list[dict]: | |
| """Rebuild the UI ``conversation_history`` list from a stored :class:`Session`. | |
| Groups messages into exchanges by splitting on ``human`` role messages. | |
| Each exchange becomes ``{"query": str, "result": {"messages": [...]}, | |
| "thread_id": 1}``. | |
| Parameters | |
| ---------- | |
| session : Session | |
| Stored session loaded from the session database. | |
| Returns | |
| ------- | |
| list[dict] | |
| Conversation-history entries for the UI. | |
| """ | |
| history: list[dict] = [] | |
| current_query: Optional[str] = None | |
| current_messages: list[dict] = [] | |
| for msg in session.messages: | |
| if msg.role == "human": | |
| # Flush previous exchange | |
| if current_query is not None: | |
| history.append( | |
| { | |
| "query": current_query, | |
| "result": {"messages": current_messages}, | |
| "thread_id": 1, | |
| "log_dir": session.log_dir, | |
| } | |
| ) | |
| current_query = msg.content | |
| current_messages = [] | |
| else: | |
| # Represent as a simple dict with the fields the UI renderers | |
| # inspect: type, content, name. | |
| entry: dict[str, Any] = { | |
| "type": msg.role, | |
| "content": msg.content, | |
| } | |
| if msg.tool_name: | |
| entry["name"] = msg.tool_name | |
| current_messages.append(entry) | |
| # Flush last exchange | |
| if current_query is not None: | |
| history.append( | |
| { | |
| "query": current_query, | |
| "result": {"messages": current_messages}, | |
| "thread_id": 1, | |
| "log_dir": session.log_dir, | |
| } | |
| ) | |
| return history | |
| # --------------------------------------------------------------------------- | |
| # Internal helpers | |
| # --------------------------------------------------------------------------- | |
| def _langchain_type_to_role(msg_type: str) -> str: | |
| """Map a LangChain message ``type`` to a SessionMessage ``role``. | |
| Parameters | |
| ---------- | |
| msg_type : str | |
| LangChain message type. | |
| Returns | |
| ------- | |
| str | |
| Session message role. | |
| """ | |
| mapping = { | |
| "human": "human", | |
| "ai": "ai", | |
| "tool": "tool", | |
| "system": "ai", | |
| "function": "tool", | |
| } | |
| return mapping.get(msg_type, "ai") | |