packages feed

langchain-hs-0.0.5.0: src/Langchain/Memory/Summary.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- |
Module      : Langchain.Memory.Summary
Description : Summary-based conversation memory with progressive LLM summarization
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

Progressively summarizes older conversation history using a ChatModel when history exceeds a threshold.
-}
module Langchain.Memory.Summary
  ( SummaryMemory (..)
  , newSummaryMemory
  , getSummary
  ) where

import Control.Concurrent.STM
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Text (Text)
import qualified Data.Text as T

import Langchain.Core.Model
  ( ChatModel (..)
  , Message (..)
  , Role (..)
  , extractMessageText
  , systemMessage
  , userMessage
  )
import Langchain.Memory.Core (BaseMemory (..))

-- | Progressive summarization memory backed by STM TVars
data SummaryMemory model = SummaryMemory
  { summaryModel :: model
  , maxMessageThreshold :: !Int
  , summaryBufferVar :: !(TVar Text)
  , recentMessagesVar :: !(TVar [Message])
  }

-- | Construct a new SummaryMemory instance
newSummaryMemory :: MonadIO m => model -> Int -> [Message] -> m (SummaryMemory model)
newSummaryMemory model threshold initMsgs = liftIO $ do
  sVar <- newTVarIO ""
  mVar <- newTVarIO initMsgs
  pure $ SummaryMemory model threshold sVar mVar

-- | Retrieve the current accumulated summary text
getSummary :: MonadIO m => SummaryMemory model -> m Text
getSummary SummaryMemory {..} = liftIO $ readTVarIO summaryBufferVar

instance (ChatModel model) => BaseMemory (SummaryMemory model) where
  messages SummaryMemory {..} = liftIO $ do
    sumTxt <- readTVarIO summaryBufferVar
    recent <- readTVarIO recentMessagesVar
    if T.null sumTxt
      then pure recent
      else pure (systemMessage ("Summary of previous conversation:\n" <> sumTxt) : recent)

  addMessage SummaryMemory {..} newMsg = do
    (shouldSummarize, toSummarize, _) <- liftIO $ atomically $ do
      modifyTVar' recentMessagesVar (\msgs -> msgs ++ [newMsg])
      currentMsgs <- readTVar recentMessagesVar
      if length currentMsgs > maxMessageThreshold
        then do
          let (old, keep) = splitAt (length currentMsgs - max 2 (maxMessageThreshold `div` 2)) currentMsgs
          writeTVar recentMessagesVar keep
          pure (True, old, keep)
        else pure (False, [], currentMsgs)

    when (shouldSummarize && not (null toSummarize)) $ do
      currentSummary <- liftIO $ readTVarIO summaryBufferVar
      let summaryPrompt =
            "Current summary:\n"
              <> currentSummary
              <> "\n\nNew lines to summarize:\n"
              <> formatMessages toSummarize
              <> "\n\nPlease provide an updated, concise summary of the conversation above."
      aiResp <- invoke summaryModel [userMessage summaryPrompt] Nothing
      let newSummary = extractMessageText aiResp
      liftIO $ atomically $ writeTVar summaryBufferVar newSummary

  clear SummaryMemory {..} = liftIO $ atomically $ do
    writeTVar summaryBufferVar ""
    writeTVar recentMessagesVar []

formatMessages :: [Message] -> Text
formatMessages msgs =
  T.unlines
    [ formatRole (messageRole m) <> ": " <> extractMessageText m
    | m <- msgs
    ]
  where
    formatRole System = "System"
    formatRole User = "Human"
    formatRole Assistant = "AI"
    formatRole Tool = "Tool"
    formatRole Developer = "Developer"
    formatRole Function = "Function"