Packages

Graph-first agent framework on the BEAM — durable execution, HITL, time-travel, checkpoints, agents, and bidirectional MCP. No Python.

Current section

Files

Jump to
el_graph lib el_graph nodes summarize.ex
Raw

lib/el_graph/nodes/summarize.ex

defmodule ElGraph.Nodes.Summarize do
@moduledoc """
컨텍스트 압축 노드 (SPEC §4, M4). 긴 대화의 오래된 메시지를 LLM 요약으로 치환한다.
그래프 노드로 끼운다 (보통 agent 직전):
|> ElGraph.add_node(:compact,
{ElGraph.Nodes.Summarize, :run, [[
llm: {ElGraph.LLM.OpenAI, api_key: key},
trigger: {:messages, 20}, # messages가 20개 초과면 압축
keep: {:messages, 6}, # 최근 6개는 원문 유지
store: {Store.ETS, store_config, ["conversations", "c1"]} # 축출분 보관(선택)
]]})
`:messages` 채널이 `ElGraph.Reducers.append` reducer일 때 동작한다(노드가
`{:replace, list}` 마커로 채널을 치환한다). living summary: [요약 메시지 | 최근 N개].
"""
alias ElGraph.LLM
@summary_prompt "다음은 이전 대화의 오래된 부분이다. 핵심 정보·결정·맥락을 보존해 " <>
"한국어로 간결히 요약하라. 이후 대화가 이 요약에 의존한다."
@doc false
def run(%{messages: messages}, _ctx, opts) do
{:messages, trigger} = Keyword.fetch!(opts, :trigger)
if length(messages) > trigger do
compact(messages, opts)
else
%{}
end
end
defp compact(messages, opts) do
{:messages, keep_n} = Keyword.fetch!(opts, :keep)
{evict, keep} = Enum.split(messages, length(messages) - keep_n)
summary = summarize(evict, Keyword.fetch!(opts, :llm))
maybe_store(evict, Keyword.get(opts, :store))
%{messages: {:replace, [summary | keep]}}
end
defp summarize(evict, {llm_mod, llm_config}) do
transcript = Enum.map_join(evict, "\n", fn m -> "#{m.role}: #{m.content}" end)
case llm_mod.chat(llm_config, [LLM.user("#{@summary_prompt}\n\n#{transcript}")], []) do
{:ok, %{message: %{content: content}}} ->
%{role: :system, content: "[이전 대화 요약] #{content}"}
{:error, reason} ->
raise ElGraph.LLMError, reason: reason
end
end
defp maybe_store(_evict, nil), do: :ok
defp maybe_store(evict, {store_mod, store_config, namespace}) do
key = "evicted-#{System.unique_integer([:positive, :monotonic])}"
store_mod.put(store_config, namespace, key, evict)
end
end