Packages
langchain
0.8.13
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/text_splitter/character_text_splitter.ex
defmodule LangChain.TextSplitter.CharacterTextSplitter do
@moduledoc """
The `CharacterTextSplitter` is a length based text splitter
that divides text based on specified characters.
This splitter provides consistent chunk sizes.
It operates as follows:
- It splits the text at specified `separator` characters.
- It takes a `chunk_size` parameter that determines the maximum number of tokens
in each chunk.
- If no separator is found within the `chunk_size`,
it will create a chunk larger than the specified size.
The purpose is to prepare text for processing
by large language models with limited context windows,
or where a shorter context window is desired.
A `CharacterTextSplitter` is defined using a schema.
* `separator` - String that splits a given text.
* `chunk_size` - Integer number of tokens that a chunk should have.
* `chunk_overlap` - Integer number of tokens that two consecutive chunks should share.
* `keep_separator` - Either `:discard_separator`, `:start` or `:end`. If `:discard_separator`, the separator is discarded from the output chunks. `:start` and `:end` keep the separator at the start or end of the output chunks. Defaults to `:discard_separator`.
* `is_separator_regex` - Boolean defaulting to `false`. If `true`, the `separator` string is not escaped. Defaults to `false`
* `tokenizer` - Function that takes a string and returns the number of tokens. Defaults to `&String.length/1`.
"""
use Ecto.Schema
import Ecto.Changeset
alias LangChain.LangChainError
alias LangChain.TextSplitter
alias __MODULE__
@primary_key false
embedded_schema do
field :separator, :string, default: " "
field :chunk_size, :integer
field :chunk_overlap, :integer
field :keep_separator, Ecto.Enum,
values: [:discard_separator, :start, :end],
default: :discard_separator
field :is_separator_regex, :boolean, default: false
field :tokenizer, :any, virtual: true, default: &String.length/1
end
@type t :: %CharacterTextSplitter{}
@update_fields [
:separator,
:chunk_size,
:chunk_overlap,
:keep_separator,
:is_separator_regex,
:tokenizer
]
@create_fields @update_fields
@doc """
Build a new CharacterTextSplitter and return an `:ok`/`:error` tuple with the result.
"""
def new(attrs \\ %{}) do
%TextSplitter.CharacterTextSplitter{}
|> cast(attrs, @create_fields, empty_values: [nil])
|> apply_action(:insert)
end
@doc """
Build a new CharacterTextSplitter and return it or raise an error if invalid.
"""
def new!(attrs \\ %{}) do
case new(attrs) do
{:ok, character_text_spliiter} ->
character_text_spliiter
{:error, changeset} ->
raise LangChainError, changeset
end
end
@doc """
Splits text based on a given character.
By default, the `separator` character is discarded
iex> text_splitter = CharacterTextSplitter.new!(%{separator: " ", chunk_size: 3, chunk_overlap: 0})
iex> text = "foo bar baz"
iex> CharacterTextSplitter.split_text(text_splitter, text)
["foo", "bar", "baz"]
We can keep the separator at the end of a chunk, providing the
`keep_separator: :end` option:
iex> text_splitter = CharacterTextSplitter.new!(%{separator: ".", chunk_size: 3, chunk_overlap: 0, keep_separator: :end})
iex> text = "foo.bar.baz"
iex> CharacterTextSplitter.split_text(text_splitter, text)
["foo.", "bar.", "baz"]
In order to keep the separator at the beginning of a chunk, provide the
`keep_separator: :start` option:
iex> text_splitter = CharacterTextSplitter.new!(%{separator: ".", chunk_size: 3, chunk_overlap: 0, keep_separator: :start})
iex> text = "foo.bar.baz"
iex> CharacterTextSplitter.split_text(text_splitter, text)
["foo", ".bar", ".baz"]
The last two examples used a regex special character as a `separator`.
Plain strings are escaped and parsed as regex before splitting.
If you want to use a complex regex as `separator` you can,
but make sure to pass the `is_separator_regex: true` option:
iex> text_splitter = CharacterTextSplitter.new!(%{separator: Regex.escape("."), chunk_size: 3, chunk_overlap: 0, keep_separator: :start, is_separator_regex: true})
iex> text = "foo.bar.baz"
iex> CharacterTextSplitter.split_text(text_splitter, text)
["foo", ".bar", ".baz"]
You can control the overlap of chunks trhough the `chunk_overlap` parameter:
iex> text_splitter = CharacterTextSplitter.new!(%{separator: " ", chunk_size: 7, chunk_overlap: 3})
iex> text = "foo bar baz"
iex> CharacterTextSplitter.split_text(text_splitter, text)
["foo bar", "bar baz"]
"""
def split_text(%CharacterTextSplitter{} = text_splitter, text) do
text
|> split_text_with_regex(text_splitter)
|> TextSplitter.merge_splits(text_splitter, text_splitter.separator)
end
@doc false
def split_text_with_regex(
text,
%CharacterTextSplitter{} = text_splitter
) do
{:ok, separator} =
if text_splitter.is_separator_regex do
text_splitter.separator |> Regex.compile()
else
text_splitter.separator
|> Regex.escape()
|> Regex.compile()
end
chunk_and_join = fn x ->
x
|> Enum.chunk_every(2)
|> Enum.map(&Enum.join(&1, ""))
end
if Enum.any?(
[:end, :start],
fn x -> x == text_splitter.keep_separator end
) do
splits =
separator
|> Regex.split(text, include_captures: true)
case text_splitter.keep_separator do
:start ->
[
splits |> List.first()
| splits
|> Enum.drop(1)
|> chunk_and_join.()
]
:end ->
splits
|> chunk_and_join.()
end
else
separator
|> Regex.split(text)
end
|> Enum.filter(fn x -> x != "" end)
end
end