Source code for klea_utils.nodes.summarise_memory

#!/usr/bin/env python3
"""
Summarise conversation history node

File: klea_utils/nodes/summarise_memory.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import logging
from typing import Any, override

from langchain_core.messages import AIMessage
from pydantic import BaseModel

from ..llm import content_to_str, get_last_n_conversations, split_output_by_section
from .base import BaseLLMNode


[docs] class SummariseMemoryNode(BaseLLMNode): model_type = "chat" model_defaults = {"temperature": 0.3, "max_output_tokens": 4096} """Node that summarises conversation history into a context summary. Uses _pre_exec() to skip execution if there aren't enough recent messages. Does NOT append the summary to messages -- it's metadata, not a turn. Expects state to have the following fields: - messages: list of messages - summarised_till: index of messages that have been summarised already - context_summary: previous memory/context summary """ def __init__( self, logger: logging.Logger, label: str, llm_models: dict[str, Any], summarisation_threshold: int = 10, memory: bool = False, ): """Initialise the summarisation node. :param logger: Logger instance :param label: Human-readable label for UI progress display :param llm_models: ``{role: LLMModel}`` dict (from ``BaseLangGraph.llm_models``) :param summarisation_threshold: Minimum number of messages before summarising :param memory: Whether to include conversation history in the prompt """ super().__init__( logger=logger, label=label, llm_models=llm_models, output_schema=None, memory=memory, ) self.summarisation_threshold = summarisation_threshold self.conversation = "" @override def _pre_exec(self, state: BaseModel) -> bool: """Skip if not enough recent conversations to summarise.""" self.conversation, human_messages, ai_messages = get_last_n_conversations( state.messages, # type: ignore state.summarised_till, # type: ignore None, ) conversations_num = len(human_messages) + len(ai_messages) if conversations_num < self.summarisation_threshold: self.logger.debug( f"Not enough conversations to summarise yet: " f"{conversations_num}/{self.summarisation_threshold}" ) return False return True @override def _get_prompt_variables(self, state: BaseModel) -> dict: """Format prompt with conversation data.""" return { "old_summary": state.context_summary, # type: ignore "conversation": self.conversation, } @override def _update_state(self, result: Any, state: BaseModel) -> dict[str, Any]: """Extract summary from raw AIMessage output.""" self.logger.debug(f"Current history summary is:\n{result.content}") content = content_to_str(result.content) thought, answer = split_output_by_section(content, "<think>", "</think>") return { "context_summary": answer, "summarised_till": len(state.messages), # type: ignore } # TODO: may need updating @override def _get_default_error_result(self) -> AIMessage: """Return default result when processing fails.""" return AIMessage(content="")