cache the summary
This commit is contained in:
@@ -37,14 +37,17 @@ class TasksAPI extends ApiClient {
|
||||
/**
|
||||
* Summarizes a conversation.
|
||||
* @param {string} conversationId - The conversation ID to summarize.
|
||||
* @param {AbortSignal} [signal] - AbortSignal to cancel the request.
|
||||
* @param {Object} [options] - Additional options.
|
||||
* @param {boolean} [options.forceRegenerate] - Force regeneration of cached summary.
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
|
||||
* @returns {Promise} A promise that resolves with the summary.
|
||||
*/
|
||||
summarize(conversationId, signal) {
|
||||
summarize(conversationId, { forceRegenerate = false, signal } = {}) {
|
||||
return axios.post(
|
||||
`${this.url}/summarize`,
|
||||
{
|
||||
conversation_display_id: conversationId,
|
||||
force_regenerate: forceRegenerate,
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
@@ -350,7 +350,7 @@ const deleteConversation = () => {
|
||||
:class="showMetaSection ? 'top-8' : 'top-4'"
|
||||
>
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
<ConversationSummary ref="summaryRef" :conversation-id="chat.id" />
|
||||
<ConversationSummary ref="summaryRef" :chat="chat" />
|
||||
<span class="font-normal leading-4 text-xxs">
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -8,38 +9,56 @@ import TasksAPI from 'dashboard/api/captain/tasks';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
chat: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const summary = ref('');
|
||||
const error = ref('');
|
||||
|
||||
const captainTasksEnabled = computed(() => {
|
||||
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
|
||||
});
|
||||
|
||||
const cachedSummary = computed(() => props.chat?.cached_summary || '');
|
||||
const cachedSummaryAt = computed(() => props.chat?.cached_summary_at || 0);
|
||||
const lastActivityAt = computed(() => props.chat?.last_activity_at || 0);
|
||||
|
||||
const isStale = computed(() => {
|
||||
if (!cachedSummaryAt.value) return true;
|
||||
return lastActivityAt.value > cachedSummaryAt.value;
|
||||
});
|
||||
|
||||
const formattedSummary = computed(() => {
|
||||
return summary.value ? formatMessage(summary.value) : '';
|
||||
return cachedSummary.value ? formatMessage(cachedSummary.value) : '';
|
||||
});
|
||||
|
||||
const fetchSummary = async () => {
|
||||
isLoading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const result = await TasksAPI.summarize(props.conversationId);
|
||||
const result = await TasksAPI.summarize(props.chat.id, {
|
||||
forceRegenerate: false,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
data: { message: generatedSummary },
|
||||
} = result;
|
||||
summary.value = generatedMessage || '';
|
||||
|
||||
if (generatedSummary) {
|
||||
store.dispatch('updateConversationCachedSummary', {
|
||||
conversationId: props.chat.id,
|
||||
cachedSummary: generatedSummary,
|
||||
cachedSummaryAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError' && e.name !== 'CanceledError') {
|
||||
error.value = e.response?.data?.error || t('CHAT_LIST.SUMMARY.ERROR');
|
||||
@@ -55,7 +74,8 @@ const toggleSummary = async () => {
|
||||
return;
|
||||
}
|
||||
isExpanded.value = true;
|
||||
if (!summary.value && !error.value) {
|
||||
// Only fetch if no cached summary
|
||||
if (!cachedSummary.value && !error.value) {
|
||||
await fetchSummary();
|
||||
}
|
||||
};
|
||||
@@ -68,10 +88,10 @@ const onButtonClick = e => {
|
||||
defineExpose({
|
||||
isExpanded,
|
||||
isLoading,
|
||||
summary,
|
||||
error,
|
||||
formattedSummary,
|
||||
captainTasksEnabled,
|
||||
isStale,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -359,7 +359,9 @@
|
||||
"DESCRIPTION": "Generate an AI-powered summary of this conversation",
|
||||
"GENERATE": "Generate Summary",
|
||||
"REGENERATE": "Regenerate",
|
||||
"REFRESH": "Refresh",
|
||||
"RETRY": "Retry",
|
||||
"STALE": "Summary may be outdated",
|
||||
"ERROR": "Failed to generate summary",
|
||||
"EMPTY": "No summary available"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -16,35 +17,56 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const isLoading = ref(false);
|
||||
const summary = ref('');
|
||||
const error = ref('');
|
||||
const hasFetched = ref(false);
|
||||
|
||||
const captainTasksEnabled = computed(() => {
|
||||
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
|
||||
});
|
||||
|
||||
const formattedSummary = computed(() => {
|
||||
return summary.value ? formatMessage(summary.value) : '';
|
||||
const cachedSummary = computed(() => currentChat.value?.cached_summary || '');
|
||||
const cachedSummaryAt = computed(
|
||||
() => currentChat.value?.cached_summary_at || 0
|
||||
);
|
||||
const lastActivityAt = computed(() => currentChat.value?.last_activity_at || 0);
|
||||
|
||||
const isStale = computed(() => {
|
||||
if (!cachedSummaryAt.value) return true;
|
||||
return lastActivityAt.value > cachedSummaryAt.value;
|
||||
});
|
||||
|
||||
const fetchSummary = async () => {
|
||||
if (!captainTasksEnabled.value || hasFetched.value) return;
|
||||
const hasSummary = computed(() => !!cachedSummary.value);
|
||||
|
||||
const formattedSummary = computed(() => {
|
||||
return cachedSummary.value ? formatMessage(cachedSummary.value) : '';
|
||||
});
|
||||
|
||||
const fetchSummary = async (forceRegenerate = false) => {
|
||||
if (!captainTasksEnabled.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = '';
|
||||
hasFetched.value = true;
|
||||
|
||||
try {
|
||||
const result = await TasksAPI.summarize(props.conversationId);
|
||||
const result = await TasksAPI.summarize(props.conversationId, {
|
||||
forceRegenerate,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
data: { message: generatedSummary },
|
||||
} = result;
|
||||
summary.value = generatedMessage || '';
|
||||
|
||||
if (generatedSummary) {
|
||||
store.dispatch('updateConversationCachedSummary', {
|
||||
conversationId: currentChat.value.id,
|
||||
cachedSummary: generatedSummary,
|
||||
cachedSummaryAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError' && e.name !== 'CanceledError') {
|
||||
error.value =
|
||||
@@ -55,18 +77,11 @@ const fetchSummary = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const refetch = () => {
|
||||
hasFetched.value = false;
|
||||
summary.value = '';
|
||||
error.value = '';
|
||||
fetchSummary();
|
||||
};
|
||||
const regenerate = () => fetchSummary(true);
|
||||
|
||||
watch(
|
||||
() => props.conversationId,
|
||||
() => {
|
||||
hasFetched.value = false;
|
||||
summary.value = '';
|
||||
error.value = '';
|
||||
}
|
||||
);
|
||||
@@ -74,6 +89,8 @@ watch(
|
||||
defineExpose({
|
||||
fetchSummary,
|
||||
captainTasksEnabled,
|
||||
cachedSummary,
|
||||
isStale,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -90,11 +107,11 @@ defineExpose({
|
||||
size="sm"
|
||||
variant="link"
|
||||
class="ml-2"
|
||||
@click="refetch"
|
||||
@click="() => fetchSummary(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!hasFetched" class="flex flex-col items-center gap-3 py-2">
|
||||
<div v-else-if="!hasSummary" class="flex flex-col items-center gap-3 py-2">
|
||||
<p class="text-sm text-n-slate-11 text-center mb-0">
|
||||
{{ t('CONVERSATION_SIDEBAR.SUMMARY.DESCRIPTION') }}
|
||||
</p>
|
||||
@@ -102,49 +119,37 @@ defineExpose({
|
||||
:label="t('CONVERSATION_SIDEBAR.SUMMARY.GENERATE')"
|
||||
icon="i-material-symbols-auto-awesome"
|
||||
size="sm"
|
||||
@click="fetchSummary"
|
||||
@click="() => fetchSummary(false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="summary"
|
||||
class="summary-content text-sm text-n-slate-11 animate-fade-in [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-1 [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_strong]:text-n-slate-12"
|
||||
v-html="formattedSummary"
|
||||
/>
|
||||
|
||||
<div v-else class="text-sm text-n-slate-11 py-2">
|
||||
{{ t('CONVERSATION_SIDEBAR.SUMMARY.EMPTY') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasFetched && !isLoading && !error"
|
||||
class="mt-3 pt-3 border-t border-n-weak"
|
||||
>
|
||||
<Button
|
||||
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REGENERATE')"
|
||||
icon="i-lucide-refresh-cw"
|
||||
size="sm"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
@click="refetch"
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="isStale"
|
||||
class="flex items-center gap-2 mb-2 text-xs text-n-amber-11"
|
||||
>
|
||||
<span>{{ t('CONVERSATION_SIDEBAR.SUMMARY.STALE') }}</span>
|
||||
<Button
|
||||
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REFRESH')"
|
||||
size="xs"
|
||||
variant="link"
|
||||
@click="regenerate"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="summary-content text-sm text-n-slate-11 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-1 [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_strong]:text-n-slate-12"
|
||||
v-html="formattedSummary"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t border-n-weak">
|
||||
<Button
|
||||
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REGENERATE')"
|
||||
icon="i-lucide-refresh-cw"
|
||||
size="sm"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
@click="regenerate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+3
-1
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,33 +1,78 @@
|
||||
<prompt>
|
||||
<role>Support analyst</role>
|
||||
<role>AI support conversation summarizer</role>
|
||||
|
||||
<task>Extract the CURRENT STATE, not the conversation history.</task>
|
||||
<goal>
|
||||
Produce a high-signal summary that lets a new agent understand the CURRENT STATE in under 10 seconds.
|
||||
</goal>
|
||||
|
||||
<what_to_output>
|
||||
- What is the customer's problem or request?
|
||||
- What is unresolved or pending?
|
||||
- Key details needed to help (IDs, errors, configs)
|
||||
</what_to_output>
|
||||
<output_contract>
|
||||
<format>bullet_list</format>
|
||||
<only_output_bullets>true</only_output_bullets>
|
||||
<max_bullets>10</max_bullets>
|
||||
<bullet_rules>
|
||||
<single_sentence>true</single_sentence>
|
||||
<concise>true</concise>
|
||||
<no_redundancy>true</no_redundancy>
|
||||
</bullet_rules>
|
||||
<language>same_as_user</language>
|
||||
<markdown>
|
||||
<bold_required>true</bold_required>
|
||||
<code_in_backticks>true</code_in_backticks>
|
||||
<no_headings>true</no_headings>
|
||||
</markdown>
|
||||
</output_contract>
|
||||
|
||||
<forbidden>
|
||||
- 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
|
||||
</forbidden>
|
||||
<what_to_capture>
|
||||
<priority_order>
|
||||
<item>What is the customer trying to achieve right now?</item>
|
||||
<item>What is the current blocker/problem?</item>
|
||||
<item>What has already been done that changed the state?</item>
|
||||
<item>What is the explicitly stated next step (who does what)?</item>
|
||||
</priority_order>
|
||||
</what_to_capture>
|
||||
|
||||
<format>
|
||||
- 1 to 3 bullets maximum
|
||||
- Direct statements about the situation
|
||||
- Bold important details
|
||||
- Same language as conversation
|
||||
</format>
|
||||
<importance_filter>
|
||||
<include_only_if_actionable_or_state_changing>
|
||||
<rule>A bullet must describe a blocker, a confirmed action taken, a decision, or a required next step.</rule>
|
||||
<rule>If a detail does not help an agent act or understand the blocker, omit it.</rule>
|
||||
</include_only_if_actionable_or_state_changing>
|
||||
<drop_as_noise>
|
||||
<item>Greetings, thanks, apologies, pleasantries, offers of help.</item>
|
||||
<item>Repeated statements that do not add new information.</item>
|
||||
<item>Meta commentary like “the current blocker is…” if it repeats an earlier bullet.</item>
|
||||
</drop_as_noise>
|
||||
</importance_filter>
|
||||
|
||||
<examples>
|
||||
<good>**Billing issue**: Customer charged twice for order #12345, wants refund</good>
|
||||
<good>**Password reset** not working - tried 3 times, no email received</good>
|
||||
<bad>Customer contacted support about billing. Agent asked for order number. Customer provided #12345.</bad>
|
||||
<bad>No specific problem identified from the messages.</bad>
|
||||
</examples>
|
||||
<anti_hallucination>
|
||||
<hard_rules>
|
||||
<item>Use ONLY information explicitly present in the conversation.</item>
|
||||
<item>Do NOT compute or derive facts (no time math like “15 minutes from now”, no inferred causes).</item>
|
||||
<item>Do NOT invent resolution, closure, or “no further steps” statements.</item>
|
||||
<item>Do NOT restate the same fact in multiple bullets; merge into one.</item>
|
||||
</hard_rules>
|
||||
</anti_hallucination>
|
||||
|
||||
<formatting_rules>
|
||||
<bold_usage>
|
||||
<rule>Bold the key nouns/verbs (problem, blocker, action, next step, key artifact).</rule>
|
||||
<examples>
|
||||
<example>**Meeting invite** not received.</example>
|
||||
<example>Agent **sent calendar invite** for **4:45 PM GMT+1**.</example>
|
||||
</examples>
|
||||
</bold_usage>
|
||||
<role_words>
|
||||
<rule>Prefer **Customer** and **Agent** over “user” and “support agent”.</rule>
|
||||
</role_words>
|
||||
</formatting_rules>
|
||||
|
||||
<final_validation>
|
||||
<checklist>
|
||||
<item>Every bullet is one sentence.</item>
|
||||
<item>Every bullet contains at least one **bold** phrase.</item>
|
||||
<item>No bullet contains inferred/derived info.</item>
|
||||
<item>No two bullets repeat the same meaning.</item>
|
||||
<item>Bullets reflect current state and next action (if explicitly stated).</item>
|
||||
</checklist>
|
||||
<reject_if_fails>true</reject_if_fails>
|
||||
</final_validation>
|
||||
</prompt>
|
||||
|
||||
Reference in New Issue
Block a user