how can i make plotly plots in chainlit persistent knowing that: only JSON serialized fields in the user session that will be saved and restored.

and with this code, with this code version when loading a previous chat the messages are loaded but not the plots, improve it to also load the plots without redownloading them from the sandbox, . i think that you should use this exemple to do that:

@cl.on_chat_start async def on_chat_start(): pn = os.getenv("DEFAULT_PARTNER") if not pn: await cl.Message(content="Erreur: Variable d'environnement DEFAULT_PARTNER non définie.").send() return print(f"[DEBUG CL] on_chat_start: partner_name={pn}") cl.user_session.set("partner_name", pn) cl.user_session.set("langgraph_initialized_for_thread", False) cl.user_session.set("displayed_plot_filenames", set()) # Initialize empty set for tracking displayed plots

@cl.on_chat_resume async def on_chat_resume(thread: ThreadDict): tid = thread["id"] pn = thread.get("metadata", {}).get("partner_name") or os.getenv("DEFAULT_PARTNER") if not pn: await cl.Message(content="Erreur: Partenaire non défini pour cette conversation.").send() return await initialize_langgraph_components(tid, pn) st = cl.user_session.get("lg_agent_state", {"messages": []}) cl.user_session.set("displayed_plot_filenames", set()) # Reset displayed plots set on resume await cl.Message( content=f"Conversation reprise (Partenaire {pn}). {len(st.get('messages', []))} messages enregistrés." ).send()

###############################################################################

7. Main message handler constants
###############################################################################

### NEW/MODIFIED SECTION START
###############################################################################

8. Main message handler
###############################################################################

Tool name mapping for better display in the UI
TOOL_DISPLAY_NAMES = { "generic_parser": "🔍 Parsing Data", "complete_python_task": "🐍 Executing Python Code", # Add other tool names here }

def get_tool_display_name(tool_name: str) -> str: """Returns a user-friendly name for a tool, or the original name if not found.""" return TOOL_DISPLAY_NAMES.get(tool_name, tool_name)

### NEW/MODIFIED SECTION END
###############################################################################

8. Helper functions for on_message
###############################################################################

async def send_suggestions(suggestions: List[str]): if not suggestions: return

text

max_suggestions = 3
limited_suggestions = suggestions[:max_suggestions]

actions = []
for i, suggestion in enumerate(limited_suggestions, 1):
    actions.append(
        cl.Action(
            name="suggestion",
            value=suggestion,
            label=f"💡 {suggestion}",
            description=f"Click to use suggestion {i}",
            payload={"text": suggestion}  # <- required field
        )
    )

await cl.Message(
    content="**Voici quelques suggestions pour continuer :**",
    actions=actions
).send()
async def handle_plot_display(plot_path: str, active_thread_id: str, sandbox_client: SandboxClient) -> Optional[cl.Plotly]: """Helper function to handle plot display with better error handling""" try: print(f"[DEBUG] Processing plot: {plot_path}")

text

    pickle_bytes = await sandbox_client.download_plot(
        session_id=active_thread_id,
        plot_name=plot_path,
    )
    
    if not pickle_bytes:
        print(f"[WARNING] No data received for plot: {plot_path}")
        return None
        
    fig_obj = pickle.loads(pickle_bytes)
    fig_obj = apply_company_style(fig_obj)
    
    plot_elem = cl.Plotly(
        name=os.path.basename(plot_path).rsplit(".", 1)[0],
        figure=fig_obj,
        display="inline",
    )
    
    print(f"[DEBUG] Successfully created plot element for: {plot_path}")
    return plot_elem
    
except Exception as e:
    print(f"[ERROR PLOT] Failed to process plot {plot_path}: {e}")
    traceback.print_exc()
    return None
###############################################################################

9. Action callback (fixed - no duplicates)
###############################################################################

@cl.action_callback("suggestion") async def handle_suggestion_click(action: cl.Action): """ Called when the user clicks a suggestion button. We send the suggestion as a user message. """ text = action.payload.get("text", "") print(f"[DEBUG] Suggestion clicked: {text}")

text

# create a **user** message without showing it again
user_msg = cl.Message(
    content=text,
    author="User",
    type="user_message"
)
await user_msg.send()
await on_message(user_msg)
###############################################################################

10. Main message handler (complete and fixed)
###############################################################################

@cl.on_message async def on_message(msg_event: cl.Message): active_thread_id = cl.context.session.thread_id if not active_thread_id: print(f"[CRITICAL CL] cl.context.session.thread_id is None in on_message!") await cl.Message(content="Erreur critique: Impossible d'identifier la session.").send() cl.user_session.set("langgraph_initialized_for_thread", False) return

text

print(f"[DEBUG CL] Processing message for thread_id: {active_thread_id}")
print(f"[DEBUG CL] Message content: {msg_event.content[:100]}...")

# --- Boiler-plate setup ----------------------------------------------------
if not cl.user_session.get("langgraph_initialized_for_thread") or \
   cl.user_session.get("thread_id") != active_thread_id:
    print(f"[DEBUG CL] Initialising LangGraph for thread_id: {active_thread_id}")
    partner_name_for_init = cl.user_session.get("partner_name") or os.getenv("DEFAULT_PARTNER")
    if not partner_name_for_init:
        await cl.Message(content="Erreur: Partenaire non défini.").send()
        return
    await initialize_langgraph_components(active_thread_id, partner_name_for_init)

lg_agent = cl.user_session.get("lg_agent")
lg_agent_state: Optional[AgentState] = cl.user_session.get("lg_agent_state")
partner_name = cl.user_session.get("partner_name")

sandbox_client: Optional[SandboxClient] = cl.user_session.get("sandbox_client")
if not sandbox_client:
    try:
        sandbox_client = SandboxClient()
        cl.user_session.set("sandbox_client", sandbox_client)
        print("[DEBUG CL] Sandbox client initialized")
    except Exception as e:
        print(f"[ERROR CL] Failed to initialize sandbox client: {e}")
        await cl.Message(content="Erreur de configuration du client Sandbox.").send()
        return

if not lg_agent or lg_agent_state is None or not partner_name:
    await cl.Message(content="Erreur: Agent non initialisé. Veuillez rafraîchir.").send()
    return

# ---------- Build messages and config ----------
human_message_obj = HumanMessage(content=msg_event.content, id=str(uuid.uuid4()))
messages_for_lg_agent_input = list(lg_agent_state.get("messages", [])) + [human_message_obj]

config_for_run = RunnableConfig(
    configurable={"thread_id": active_thread_id, "session_id": active_thread_id, "partner": partner_name}
)

print(f"[DEBUG CL] Starting agent run with {len(messages_for_lg_agent_input)} messages")

# ---------- Chainlit UI scaffolding ----------
agent_run_step = cl.Step(name="🤔 Agent Plan & Execution", type="run", parent_id=None)
agent_run_step.input = msg_event.content
await agent_run_step.send()
agent_plan_updated = False

assistant_final_msg = cl.Message(content="", author="Assistant", parent_id=agent_run_step.id)
all_plot_elements = []

plots_from_previous_turns = set(lg_agent_state.get("output_image_paths", []))
tool_steps: Dict[str, cl.Step] = {}
messages_from_current_run = []
generated_plot_paths_this_turn = set()
current_run_lg_agent_state_dict = None

print(f"[DEBUG CL] Previous plots: {len(plots_from_previous_turns)}")

try:
    async for item in lg_agent.astream(
        {"messages": messages_for_lg_agent_input},
        config=config_for_run,
        stream_mode=["updates", "messages"],
        subgraphs=True,
    ):
        if len(item) < 3:
            continue
        namespace_tuple, stream_mode, payload = item[:3]

        # 1) AGENT subgraph ---------------------------------------------------
        if namespace_tuple and namespace_tuple[-1].startswith("agent:"):
            if stream_mode == "updates":
                # Handle agent updates
                if "agent" in payload:
                    current_run_lg_agent_state_dict = payload["agent"]
                    for msg in payload["agent"].get("messages", []):
                        if msg not in messages_from_current_run:
                            messages_from_current_run.append(msg)
                        
                        # Handle AI messages with tool calls
                        if isinstance(msg, AIMessage) and msg.tool_calls:
                            if not agent_plan_updated:
                                tool_names = [get_tool_display_name(tc["name"]) for tc in msg.tool_calls]
                                agent_run_step.output = f"Planning to use: {', '.join(tool_names)}"
                                await agent_run_step.update()
                                agent_plan_updated = True
                                print(f"[DEBUG CL] Agent plan updated with tools: {tool_names}")
                            
                            # Create tool steps for each tool call
                            for tc in msg.tool_calls:
                                tool_id = tc["id"]
                                if tool_id not in tool_steps:
                                    tool_step = cl.Step(
                                        name=f"Tool: {get_tool_display_name(tc['name'])}",
                                        type="tool",
                                        parent_id=agent_run_step.id,
                                    )
                                    tool_step.input = f"```json\n{json.dumps(tc.get('args', {}), indent=2, ensure_ascii=False)}\n```"
                                    await tool_step.send()
                                    tool_steps[tool_id] = tool_step
                                    print(f"[DEBUG CL] Created tool step for: {tc['name']}")

                # Handle tool results
                if "tools" in payload:
                    current_run_lg_agent_state_dict = payload["tools"]
                    for msg in payload["tools"].get("messages", []):
                        if msg not in messages_from_current_run:
                            messages_from_current_run.append(msg)
                        
                        # Handle tool message results
                        if isinstance(msg, ToolMessage):
                            tc_id = msg.tool_call_id
                            if tc_id in tool_steps:
                                ts = tool_steps[tc_id]
                                if ts.output:
                                    continue
                                
                                content = _parse_tool_content(msg.content)
                                if isinstance(content, dict) and "error" in content:
                                    ts.output = f"❌ Error: {content['error']}"
                                    ts.is_error = True
                                    print(f"[DEBUG CL] Tool error for {tc_id}: {content['error']}")
                                else:
                                    out_str = json.dumps(content, indent=2, ensure_ascii=False) if isinstance(content, (dict, list)) else str(content)
                                    ts.output = f"✅ Success\n```json\n{out_str}```"
                                    print(f"[DEBUG CL] Tool success for {tc_id}")
                                await ts.update()

                    # Handle new plots generated during this run
                    if "output_image_paths" in payload["tools"]:
                        all_figure_paths_from_agent = payload["tools"]["output_image_paths"]
                        current_figure_paths_set = {p for p in all_figure_paths_from_agent if "_figures_" in os.path.basename(p)}
                        new_plots_this_turn = current_figure_paths_set - plots_from_previous_turns
                        
                        print(f"[DEBUG CL] New plots this turn: {len(new_plots_this_turn)}")
                        
                        for plot_path in new_plots_this_turn:
                            if plot_path in generated_plot_paths_this_turn:
                                continue
                            
                            print(f"[DEBUG CL] Processing new plot: {plot_path}")
                            plot_elem = await handle_plot_display(plot_path, active_thread_id, sandbox_client)
                            if plot_elem:
                                all_plot_elements.append(plot_elem)
                                generated_plot_paths_this_turn.add(plot_path)
                                print(f"[DEBUG CL] Added plot element: {plot_path}")

            elif stream_mode == "messages":
                # Handle streaming AI messages
                message, _ = payload
                if isinstance(message, AIMessage) and message.content:
                    delta = message.content
                    if not assistant_final_msg.content:
                        await assistant_final_msg.send()
                    await assistant_final_msg.stream_token(delta)

        # 2) ROOT graph – suggestions ---------------------------------------
        elif not namespace_tuple and stream_mode == "updates":
            if (
                isinstance(payload, dict)
                and payload
                and "generate_suggestions" in payload
                and isinstance(payload.get("generate_suggestions"), dict)
                and "suggestions" in payload["generate_suggestions"]
            ):
                suggestions = payload["generate_suggestions"]["suggestions"]
                print(f"[DEBUG CL] Received {len(suggestions)} suggestions from agent")
                if suggestions:
                    await send_suggestions(suggestions)

except Exception as e_stream:
    print(f"[ERROR CL STREAM] {e_stream}", file=sys.stdout)
    traceback.print_exc()
    await cl.Message(content=f"Une erreur inattendue s'est produite: {e_stream}").send()
    return

finally:
    # Finalize the agent run step
    if not agent_plan_updated:
        agent_run_step.output = "✅ Agent execution completed"
        await agent_run_step.update()

    # Attach plots to the final message
    if all_plot_elements:
        print(f"[DEBUG CL] Attaching {len(all_plot_elements)} plot elements to final message")
        assistant_final_msg.elements = all_plot_elements
        await assistant_final_msg.update()

    # Ensure the assistant message is sent if it wasn't already
    if not assistant_final_msg.content:
        assistant_final_msg.content = "✅ Task completed"
        await assistant_final_msg.send()

    # Persist state updates
    if lg_agent_state is not None and current_run_lg_agent_state_dict is not None:
        print("[DEBUG CL] Persisting agent state updates")
        
        # Merge messages from current run
        merged = lg_agent_state.get("messages", []) + ensure_message_ids(messages_from_current_run)
        lg_agent_state["messages"], _ = remove_duplicate_messages(merged)

        # Update output image paths
        if "output_image_paths" in current_run_lg_agent_state_dict:
            lg_agent_state["output_image_paths"] = current_run_lg_agent_state_dict["output_image_paths"]

        # Update other state fields if present
        for key in ["current_variables", "data_description", "intermediate_outputs"]:
            if key in current_run_lg_agent_state_dict:
                lg_agent_state[key] = current_run_lg_agent_state_dict[key]

        cl.user_session.set("lg_agent_state", lg_agent_state)
        print(f"[DEBUG CL] Agent state updated with {len(lg_agent_state['messages'])} total messages")

print(f"[DEBUG CL] === Turn End for Thread: {active_thread_id} ===\n")