-
-
diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js
index 559eaaad9..e3acc5f6f 100644
--- a/app/javascript/dashboard/store/modules/conversations/actions.js
+++ b/app/javascript/dashboard/store/modules/conversations/actions.js
@@ -412,6 +412,17 @@ const actions = {
});
},
+ updateConversationCachedSummary(
+ { commit },
+ { conversationId, cachedSummary, cachedSummaryAt }
+ ) {
+ commit(types.UPDATE_CONVERSATION_CACHED_SUMMARY, {
+ conversationId,
+ cachedSummary,
+ cachedSummaryAt,
+ });
+ },
+
setChatStatusFilter({ commit }, data) {
commit(types.CHANGE_CHAT_STATUS_FILTER, data);
},
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index 8d3ef9a9a..190584060 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -116,6 +116,16 @@ export const mutations = {
chat.last_activity_at = lastActivityAt;
}
},
+ [types.UPDATE_CONVERSATION_CACHED_SUMMARY](
+ _state,
+ { conversationId, cachedSummary, cachedSummaryAt }
+ ) {
+ const [chat] = _state.allConversations.filter(c => c.id === conversationId);
+ if (chat) {
+ chat.cached_summary = cachedSummary;
+ chat.cached_summary_at = cachedSummaryAt;
+ }
+ },
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
chat.priority = priority;
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index 1ecafc493..cc3442f51 100644
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -51,6 +51,7 @@ export default {
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY',
+ UPDATE_CONVERSATION_CACHED_SUMMARY: 'UPDATE_CONVERSATION_CACHED_SUMMARY',
UPDATE_CONVERSATION_CALL_STATUS: 'UPDATE_CONVERSATION_CALL_STATUS',
UPDATE_MESSAGE_CALL_STATUS: 'UPDATE_MESSAGE_CALL_STATUS',
SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES',
diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
index 4cb13f543..e05a1cda0 100644
--- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
+++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
@@ -56,6 +56,8 @@ json.first_reply_created_at conversation.first_reply_created_at.to_i
json.unread_count conversation.unread_incoming_messages.count
json.last_non_activity_message conversation.messages.where(account_id: conversation.account_id).non_activity_messages.first.try(:push_event_data)
json.last_activity_at conversation.last_activity_at.to_i
+json.cached_summary conversation.cached_summary
+json.cached_summary_at conversation.cached_summary_at.to_i
json.priority conversation.priority
json.waiting_since conversation.waiting_since.to_i.to_i
json.sla_policy_id conversation.sla_policy_id
diff --git a/db/migrate/20260129180004_add_cached_summary_to_conversations.rb b/db/migrate/20260129180004_add_cached_summary_to_conversations.rb
new file mode 100644
index 000000000..5f094e20b
--- /dev/null
+++ b/db/migrate/20260129180004_add_cached_summary_to_conversations.rb
@@ -0,0 +1,6 @@
+class AddCachedSummaryToConversations < ActiveRecord::Migration[7.1]
+ def change
+ add_column :conversations, :cached_summary, :text
+ add_column :conversations, :cached_summary_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 148e7769c..327110247 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
+ActiveRecord::Schema[7.1].define(version: 2026_01_29_180004) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -681,6 +681,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
t.datetime "waiting_since"
t.text "cached_label_list"
t.bigint "assignee_agent_bot_id"
+ t.text "cached_summary"
+ t.datetime "cached_summary_at"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/tasks_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/tasks_controller.rb
index d7208d678..de27e5cac 100644
--- a/enterprise/app/controllers/api/v1/accounts/captain/tasks_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/captain/tasks_controller.rb
@@ -15,7 +15,8 @@ class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseContr
def summarize
result = Captain::SummaryService.new(
account: Current.account,
- conversation_display_id: params[:conversation_display_id]
+ conversation_display_id: params[:conversation_display_id],
+ force_regenerate: params[:force_regenerate].present?
).perform
render_result(result)
diff --git a/lib/captain/summary_service.rb b/lib/captain/summary_service.rb
index 16ee57b51..1e1e24f76 100644
--- a/lib/captain/summary_service.rb
+++ b/lib/captain/summary_service.rb
@@ -1,14 +1,10 @@
class Captain::SummaryService < Captain::BaseTaskService
- pattr_initialize [:account!, :conversation_display_id!]
+ pattr_initialize [:account!, :conversation_display_id!, { force_regenerate: false }]
def perform
- make_api_call(
- model: GPT_MODEL,
- messages: [
- { role: 'system', content: prompt_from_file('summary') },
- { role: 'user', content: conversation.to_llm_text(include_contact_details: false) }
- ]
- )
+ return cached_response if use_cache?
+
+ generate_and_cache_summary
end
private
@@ -16,4 +12,37 @@ class Captain::SummaryService < Captain::BaseTaskService
def event_name
'summarize'
end
+
+ def use_cache?
+ return false if force_regenerate
+ return false if conversation.cached_summary.blank?
+ return false if conversation.cached_summary_at.blank?
+
+ conversation.cached_summary_at >= conversation.last_activity_at
+ end
+
+ def cached_response
+ { message: conversation.cached_summary }
+ end
+
+ def generate_and_cache_summary
+ result = make_api_call(
+ model: GPT_MODEL,
+ messages: [
+ { role: 'system', content: prompt_from_file('summary') },
+ { role: 'user', content: conversation.to_llm_text(include_contact_details: false) }
+ ]
+ )
+
+ cache_summary(result[:message]) if result[:message].present? && result[:error].blank?
+
+ result
+ end
+
+ def cache_summary(summary)
+ conversation.update(
+ cached_summary: summary,
+ cached_summary_at: Time.current
+ )
+ end
end
diff --git a/lib/integrations/openai/openai_prompts/summary.liquid b/lib/integrations/openai/openai_prompts/summary.liquid
index 3b73ca5ed..f14f73cc5 100644
--- a/lib/integrations/openai/openai_prompts/summary.liquid
+++ b/lib/integrations/openai/openai_prompts/summary.liquid
@@ -1,33 +1,78 @@
- Support analyst
+ AI support conversation summarizer
- Extract the CURRENT STATE, not the conversation history.
+
+ Produce a high-signal summary that lets a new agent understand the CURRENT STATE in under 10 seconds.
+
-
- - What is the customer's problem or request?
- - What is unresolved or pending?
- - Key details needed to help (IDs, errors, configs)
-
+
+ bullet_list
+ true
+ 10
+
+ true
+ true
+ true
+
+ same_as_user
+
+ true
+ true
+ true
+
+
-
- - DO NOT narrate the conversation ("Customer said", "Agent replied", "User asked")
- - DO NOT list sequence of events
- - DO NOT describe what's missing or absent
- - DO NOT include greetings, thanks, or pleasantries
- - DO NOT add meta-commentary
-
+
+
+ What is the customer trying to achieve right now?
+ What is the current blocker/problem?
+ What has already been done that changed the state?
+ What is the explicitly stated next step (who does what)?
+
+
-
- - 1 to 3 bullets maximum
- - Direct statements about the situation
- - Bold important details
- - Same language as conversation
-
+
+
+ A bullet must describe a blocker, a confirmed action taken, a decision, or a required next step.
+ If a detail does not help an agent act or understand the blocker, omit it.
+
+
+ Greetings, thanks, apologies, pleasantries, offers of help.
+ Repeated statements that do not add new information.
+ Meta commentary like “the current blocker is…” if it repeats an earlier bullet.
+
+
-
- **Billing issue**: Customer charged twice for order #12345, wants refund
- **Password reset** not working - tried 3 times, no email received
- Customer contacted support about billing. Agent asked for order number. Customer provided #12345.
- No specific problem identified from the messages.
-
+
+
+ Use ONLY information explicitly present in the conversation.
+ Do NOT compute or derive facts (no time math like “15 minutes from now”, no inferred causes).
+ Do NOT invent resolution, closure, or “no further steps” statements.
+ Do NOT restate the same fact in multiple bullets; merge into one.
+
+
+
+
+
+ Bold the key nouns/verbs (problem, blocker, action, next step, key artifact).
+
+ **Meeting invite** not received.
+ Agent **sent calendar invite** for **4:45 PM GMT+1**.
+
+
+
+ Prefer **Customer** and **Agent** over “user” and “support agent”.
+
+
+
+
+
+ Every bullet is one sentence.
+ Every bullet contains at least one **bold** phrase.
+ No bullet contains inferred/derived info.
+ No two bullets repeat the same meaning.
+ Bullets reflect current state and next action (if explicitly stated).
+
+ true
+