2023-06-27 11:26:33 -04:00
# frozen_string_literal: true
module DiscourseAi
module Summarization
module Strategies
2024-07-03 20:48:18 -04:00
class FoldContent
2023-06-27 11:26:33 -04:00
def initialize ( completion_model )
2024-07-03 20:48:18 -04:00
@llm = DiscourseAi :: Completions :: Llm . proxy ( completion_model )
raise " Invalid model provided for summarization strategy " if @llm . llm_model . nil?
2023-06-27 11:26:33 -04:00
end
2024-07-03 20:48:18 -04:00
attr_reader :llm
2023-06-27 11:26:33 -04:00
2023-11-23 10:58:54 -05:00
def summarize ( content , user , & on_partial_blk )
2023-06-27 11:26:33 -04:00
opts = content . except ( :contents )
2024-07-03 17:10:31 -04:00
initial_chunks =
rebalance_chunks (
content [ :contents ] . map { | c | { ids : [ c [ :id ] ] , summary : format_content_item ( c ) } } ,
)
2023-12-06 17:00:24 -05:00
2024-07-03 17:10:31 -04:00
# Special case where we can do all the summarization in one pass.
if initial_chunks . length == 1
{
summary :
2024-07-03 20:48:18 -04:00
summarize_single ( initial_chunks . first [ :summary ] , user , opts , & on_partial_blk ) ,
2024-07-03 17:10:31 -04:00
chunks : [ ] ,
}
else
2024-07-03 20:48:18 -04:00
summarize_chunks ( initial_chunks , user , opts , & on_partial_blk )
2024-07-03 17:10:31 -04:00
end
2023-12-06 17:00:24 -05:00
end
2023-07-13 16:05:41 -04:00
2024-07-03 20:48:18 -04:00
def display_name
llm_model & . name || " unknown model "
end
2023-12-06 17:00:24 -05:00
private
2024-07-03 20:48:18 -04:00
def llm_model
llm . llm_model
end
def summarize_chunks ( chunks , user , opts , & on_partial_blk )
2024-07-03 17:10:31 -04:00
# Safely assume we always have more than one chunk.
2024-07-03 20:48:18 -04:00
summarized_chunks = summarize_in_chunks ( chunks , user , opts )
2024-07-03 17:10:31 -04:00
total_summaries_size =
2024-07-03 20:48:18 -04:00
llm_model . tokenizer_class . size ( summarized_chunks . map { | s | s [ :summary ] . to_s } . join )
2024-07-03 17:10:31 -04:00
2024-07-03 20:48:18 -04:00
if total_summaries_size < available_tokens
2024-07-03 17:10:31 -04:00
# Chunks are small enough, we can concatenate them.
{
summary :
concatenate_summaries (
summarized_chunks . map { | s | s [ :summary ] } ,
user ,
& on_partial_blk
) ,
chunks : summarized_chunks ,
}
else
# We have summarized chunks but we can't concatenate them yet. Split them into smaller summaries and summarize again.
2024-07-03 20:48:18 -04:00
rebalanced_chunks = rebalance_chunks ( summarized_chunks )
2024-07-03 17:10:31 -04:00
2024-07-03 20:48:18 -04:00
summarize_chunks ( rebalanced_chunks , user , opts , & on_partial_blk )
2024-07-03 17:10:31 -04:00
end
end
2023-11-23 10:58:54 -05:00
def format_content_item ( item )
" ( #{ item [ :id ] } #{ item [ :poster ] } said: #{ item [ :text ] } "
end
2024-07-03 20:48:18 -04:00
def rebalance_chunks ( chunks )
2024-07-03 17:10:31 -04:00
section = { ids : [ ] , summary : " " }
chunks =
chunks . reduce ( [ ] ) do | sections , chunk |
2024-07-03 20:48:18 -04:00
if llm_model . tokenizer_class . can_expand_tokens? (
2024-07-03 17:10:31 -04:00
section [ :summary ] ,
chunk [ :summary ] ,
2024-07-03 20:48:18 -04:00
available_tokens ,
2024-07-03 17:10:31 -04:00
)
section [ :summary ] += chunk [ :summary ]
section [ :ids ] = section [ :ids ] . concat ( chunk [ :ids ] )
else
sections << section
section = chunk
end
sections
end
chunks << section if section [ :summary ] . present?
chunks
end
2024-07-03 20:48:18 -04:00
def summarize_single ( text , user , opts , & on_partial_blk )
2023-11-23 10:58:54 -05:00
prompt = summarization_prompt ( text , opts )
2024-05-13 23:28:46 -04:00
llm . generate ( prompt , user : user , feature_name : " summarize " , & on_partial_blk )
2023-11-23 10:58:54 -05:00
end
2024-07-03 20:48:18 -04:00
def summarize_in_chunks ( chunks , user , opts )
2024-07-03 17:10:31 -04:00
chunks . map do | chunk |
prompt = summarization_prompt ( chunk [ :summary ] , opts )
chunk [ :summary ] = llm . generate (
prompt ,
user : user ,
max_tokens : 300 ,
feature_name : " summarize " ,
)
chunk
end
end
2024-07-03 20:48:18 -04:00
def concatenate_summaries ( summaries , user , & on_partial_blk )
2024-07-03 17:10:31 -04:00
prompt = DiscourseAi :: Completions :: Prompt . new ( << ~ TEXT . strip )
You are a summarization bot that effectively concatenates disjoint summaries , creating a cohesive narrative .
The narrative you create is in the form of one or multiple paragraphs .
Your reply MUST BE a single concatenated summary using the summaries I ' ll provide to you .
I 'm NOT interested in anything other than the concatenated summary, don' t include additional text or comments .
You understand and generate Discourse forum Markdown .
You format the response , including links , using Markdown .
TEXT
prompt . push ( type : :user , content : << ~ TEXT . strip )
THESE are the summaries , each one separated by a newline , all of them inside < input > < / input> XML tags:
< input >
#{summaries.join("\n")}
< / input>
TEXT
llm . generate ( prompt , user : user , & on_partial_blk )
end
2023-11-23 10:58:54 -05:00
def summarization_prompt ( input , opts )
2024-03-20 01:33:05 -04:00
insts = + << ~ TEXT
You are an advanced summarization bot that generates concise , coherent summaries of provided text .
- Only include the summary , without any additional commentary .
- You understand and generate Discourse forum Markdown ; including links , _italics_ , ** bold ** .
- Maintain the original language of the text being summarized .
- Aim for summaries to be 400 words or less .
2023-11-23 14:33:37 -05:00
2023-11-23 10:58:54 -05:00
TEXT
2024-03-20 01:33:05 -04:00
insts << << ~ TEXT if opts [ :resource_path ]
- Each post is formatted as " <POST_NUMBER>) <USERNAME> <MESSAGE> "
- Cite specific noteworthy posts using the format [ NAME ] ( #{opts[:resource_path]}/POST_NUMBER)
- Example : link to the 3 rd post by sam : [ sam ] ( #{opts[:resource_path]}/3)
- Example : link to the 6 th post by jane : [ agreed with ] ( #{opts[:resource_path]}/6)
- Example : link to the 13 th post by joe : [ #13](#{opts[:resource_path]}/13)
- When formatting usernames either use @USERNMAE OR [ USERNAME ] ( #{opts[:resource_path]}/POST_NUMBER)
2023-11-23 10:58:54 -05:00
TEXT
2024-01-12 12:36:44 -05:00
prompt = DiscourseAi :: Completions :: Prompt . new ( insts . strip )
2023-11-23 10:58:54 -05:00
if opts [ :resource_path ]
2024-01-12 12:36:44 -05:00
prompt . push (
type : :user ,
content :
2024-03-20 01:33:05 -04:00
" Here are the posts inside <input></input> XML tags: \n \n <input>1) user1 said: I love Mondays 2) user2 said: I hate Mondays</input> \n \n Generate a concise, coherent summary of the text above maintaining the original language. " ,
2024-01-12 12:36:44 -05:00
)
prompt . push (
type : :model ,
content :
2024-03-20 01:33:05 -04:00
" Two users are sharing their feelings toward Mondays. [user1]( #{ opts [ :resource_path ] } /1) hates them, while [user2]( #{ opts [ :resource_path ] } /2) loves them. " ,
2024-01-12 12:36:44 -05:00
)
2023-11-23 10:58:54 -05:00
end
2024-01-12 12:36:44 -05:00
prompt . push ( type : :user , content : << ~ TEXT . strip )
#{opts[:content_title].present? ? "The discussion title is: " + opts[:content_title] + ".\n" : ""}
Here are the posts , inside < input > < / input> XML tags:
< input >
#{input}
< / input>
2024-03-20 01:33:05 -04:00
Generate a concise , coherent summary of the text above maintaining the original language .
2024-01-12 12:36:44 -05:00
TEXT
2023-11-23 10:58:54 -05:00
prompt
end
2024-07-03 20:48:18 -04:00
def available_tokens
# Reserve tokens for the response and the base prompt
# ~500 words
reserved_tokens = 700
llm_model . max_prompt_tokens - reserved_tokens
end
2023-06-27 11:26:33 -04:00
end
end
end
end