packages feed

langchain-hs-0.0.5.0: src/Langchain/TextSplitter/RecursiveCharacter.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Langchain.TextSplitter.RecursiveCharacter
Description : Hierarchical recursive character text splitting with chunk overlap
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

Recursively splits text by trying different separators in order (paragraphs, lines, spaces, characters)
to keep semantically related pieces of text together.
-}
module Langchain.TextSplitter.RecursiveCharacter
  ( RecursiveCharacterSplitterOps (..)
  , defaultRecursiveCharacterSplitterOps
  , splitTextRecursive
  )
where

import Data.Int (Int64)
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T

-- | Configuration options for recursive character text splitter
data RecursiveCharacterSplitterOps = RecursiveCharacterSplitterOps
  { chunkSize :: Int64
  , chunkOverlap :: Int64
  , separators :: [Text]
  }
  deriving (Show, Eq)

-- | Default options: 1000 char chunks, 200 char overlap, standard hierarchy of separators
defaultRecursiveCharacterSplitterOps :: RecursiveCharacterSplitterOps
defaultRecursiveCharacterSplitterOps =
  RecursiveCharacterSplitterOps
    { chunkSize = 1000
    , chunkOverlap = 200
    , separators = ["\n\n", "\n", " ", ""]
    }

-- | Split text recursively using the specified separators hierarchy
splitTextRecursive :: RecursiveCharacterSplitterOps -> Text -> [Text]
splitTextRecursive _ "" = []
splitTextRecursive
  RecursiveCharacterSplitterOps
    { chunkSize = maxSize
    , chunkOverlap = maxOverlap
    , separators = ss
    }
  text = splitRecursive ss text
    where
      splitRecursive :: [Text] -> Text -> [Text]
      splitRecursive [] txt = splitByLen txt
      splitRecursive ("" : _) txt = splitByLen txt
      splitRecursive (sep : seps) txt = mergeWithOverlap sep splitParts
        where
          splitParts = concatMap (splitRecursive seps) $ filter (not . T.null) $ T.splitOn sep txt

      splitByLen :: Text -> [Text]
      splitByLen "" = []
      splitByLen txt = chunk : splitByLen remainder
        where
          (chunk, remainder) = T.splitAt maxSize txt

      mergeWithOverlap :: Text -> [Text] -> [Text]
      mergeWithOverlap _ [] = []
      mergeWithOverlap sep parts = reverse $ go [] 0 [] parts
        where
          sepLen = T.length sep
          toText = T.intercalate sep . reverse

          go :: [Text] -> Int64 -> [Text] -> [Text] -> [Text]
          go acc chunkLen chunkParts pss =
            case pss of
              [] -> acc'
              (part : restParts) ->
                let partLen = T.length part
                    chunkLen' = chunkLen + sepBefore chunkParts + partLen
                 in if chunkLen' <= maxSize
                      then go acc chunkLen' (part : chunkParts) restParts
                      else
                        let (overlapParts, overlapLen) = takeWhileOverlap chunkParts 0 []
                            carryLen = overlapLen + sepBefore overlapParts + partLen
                         in go acc' carryLen (part : reverse overlapParts) restParts
            where
              acc' = toText chunkParts : acc

              sepBefore ps = if null ps then 0 else sepLen

              takeWhileOverlap [] overlapLen overlapAcc = (overlapAcc, overlapLen)
              takeWhileOverlap (overlapPart : restOverlap) overlapLen overlapAcc
                | overlapLen' > maxOverlap = (overlapAcc, overlapLen)
                | otherwise = takeWhileOverlap restOverlap overlapLen' (overlapPart : overlapAcc)
                where
                  overlapLen' = overlapLen + sepBefore overlapAcc + T.length overlapPart