Compare commits
7
Commits
eeff17dfd7
...
summaries
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38967fe75f | ||
|
|
6e80f90357 | ||
|
|
f0e9ffacd3 | ||
|
|
45a1a01a96 | ||
|
|
0e225d2f36 | ||
|
|
f45e82016d | ||
|
|
137f0e726b |
@@ -37,14 +37,17 @@ class TasksAPI extends ApiClient {
|
|||||||
/**
|
/**
|
||||||
* Summarizes a conversation.
|
* Summarizes a conversation.
|
||||||
* @param {string} conversationId - The conversation ID to summarize.
|
* @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.
|
* @returns {Promise} A promise that resolves with the summary.
|
||||||
*/
|
*/
|
||||||
summarize(conversationId, signal) {
|
summarize(conversationId, { forceRegenerate = false, signal } = {}) {
|
||||||
return axios.post(
|
return axios.post(
|
||||||
`${this.url}/summarize`,
|
`${this.url}/summarize`,
|
||||||
{
|
{
|
||||||
conversation_display_id: conversationId,
|
conversation_display_id: conversationId,
|
||||||
|
force_regenerate: forceRegenerate,
|
||||||
},
|
},
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
||||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
||||||
@@ -14,6 +15,8 @@ import PriorityMark from './PriorityMark.vue';
|
|||||||
import SLACardLabel from './components/SLACardLabel.vue';
|
import SLACardLabel from './components/SLACardLabel.vue';
|
||||||
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
||||||
import VoiceCallStatus from './VoiceCallStatus.vue';
|
import VoiceCallStatus from './VoiceCallStatus.vue';
|
||||||
|
import ConversationSummary from './ConversationSummary.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
activeLabel: { type: String, default: '' },
|
activeLabel: { type: String, default: '' },
|
||||||
@@ -46,8 +49,10 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const hovered = ref(false);
|
const hovered = ref(false);
|
||||||
|
const summaryRef = ref(null);
|
||||||
const showContextMenu = ref(false);
|
const showContextMenu = ref(false);
|
||||||
const contextMenu = ref({
|
const contextMenu = ref({
|
||||||
x: null,
|
x: null,
|
||||||
@@ -344,12 +349,15 @@ const deleteConversation = () => {
|
|||||||
class="absolute flex flex-col ltr:right-3 rtl:left-3"
|
class="absolute flex flex-col ltr:right-3 rtl:left-3"
|
||||||
:class="showMetaSection ? 'top-8' : 'top-4'"
|
:class="showMetaSection ? 'top-8' : 'top-4'"
|
||||||
>
|
>
|
||||||
<span class="ml-auto font-normal leading-4 text-xxs">
|
<div class="flex items-center gap-1 ml-auto">
|
||||||
<TimeAgo
|
<ConversationSummary ref="summaryRef" :chat="chat" />
|
||||||
:last-activity-timestamp="chat.timestamp"
|
<span class="font-normal leading-4 text-xxs">
|
||||||
:created-at-timestamp="chat.created_at"
|
<TimeAgo
|
||||||
/>
|
:last-activity-timestamp="chat.timestamp"
|
||||||
</span>
|
:created-at-timestamp="chat.created_at"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
class="shadow-lg rounded-full text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
class="shadow-lg rounded-full text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
||||||
:class="hasUnread ? 'block' : 'hidden'"
|
:class="hasUnread ? 'block' : 'hidden'"
|
||||||
@@ -366,6 +374,26 @@ const deleteConversation = () => {
|
|||||||
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||||
</template>
|
</template>
|
||||||
</CardLabels>
|
</CardLabels>
|
||||||
|
<!-- Expanded Summary Section -->
|
||||||
|
<div
|
||||||
|
v-if="summaryRef?.isExpanded"
|
||||||
|
class="mt-2 mx-2 mb-1 p-3 bg-n-alpha-1 dark:bg-n-alpha-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<div v-if="summaryRef?.isLoading" class="flex items-center gap-2">
|
||||||
|
<Spinner :size="16" class="text-n-slate-10" />
|
||||||
|
<span class="text-xs text-n-slate-11">
|
||||||
|
{{ t('CHAT_LIST.SUMMARY.LOADING') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="summaryRef?.error" class="text-xs text-n-ruby-11">
|
||||||
|
{{ summaryRef.error }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="summaryRef?.formattedSummary"
|
||||||
|
class="text-xs text-n-slate-11 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-0.5 [&_p]:my-1 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_strong]:text-n-slate-12"
|
||||||
|
v-html="summaryRef.formattedSummary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
v-if="showContextMenu"
|
v-if="showContextMenu"
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<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';
|
||||||
|
import TasksAPI from 'dashboard/api/captain/tasks';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import { useTrack } from 'dashboard/composables';
|
||||||
|
import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
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 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 cachedSummary.value ? formatMessage(cachedSummary.value) : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchSummary = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const result = await TasksAPI.summarize(props.chat.id, {
|
||||||
|
forceRegenerate: false,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: { message: generatedSummary },
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSummary = async () => {
|
||||||
|
if (isExpanded.value) {
|
||||||
|
isExpanded.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isExpanded.value = true;
|
||||||
|
useTrack(CAPTAIN_EVENTS.SUMMARIZE_USED, {
|
||||||
|
conversationId: props.chat.id,
|
||||||
|
uiFrom: 'conversation_list',
|
||||||
|
});
|
||||||
|
// Only fetch if no cached summary
|
||||||
|
if (!cachedSummary.value && !error.value) {
|
||||||
|
await fetchSummary();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onButtonClick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSummary();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
isExpanded,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
formattedSummary,
|
||||||
|
captainTasksEnabled,
|
||||||
|
isStale,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
v-if="captainTasksEnabled"
|
||||||
|
icon="i-material-symbols-auto-awesome"
|
||||||
|
slate
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
:title="t('CHAT_LIST.SUMMARY.TITLE')"
|
||||||
|
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
:class="{ '!opacity-100': isExpanded }"
|
||||||
|
@click="onButtonClick"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -34,11 +34,21 @@ function getEventPrefix(action) {
|
|||||||
* @param {string} action - The action type
|
* @param {string} action - The action type
|
||||||
* @param {number} conversationId - The conversation ID
|
* @param {number} conversationId - The conversation ID
|
||||||
* @param {number} [followUpCount] - Optional follow-up count
|
* @param {number} [followUpCount] - Optional follow-up count
|
||||||
|
* @param {string} [uiFrom] - Optional UI source identifier
|
||||||
* @returns {Object} The payload object
|
* @returns {Object} The payload object
|
||||||
*/
|
*/
|
||||||
function buildPayload(action, conversationId, followUpCount = undefined) {
|
function buildPayload(
|
||||||
|
action,
|
||||||
|
conversationId,
|
||||||
|
followUpCount = undefined,
|
||||||
|
uiFrom = undefined
|
||||||
|
) {
|
||||||
const payload = { conversationId };
|
const payload = { conversationId };
|
||||||
|
|
||||||
|
if (uiFrom) {
|
||||||
|
payload.uiFrom = uiFrom;
|
||||||
|
}
|
||||||
|
|
||||||
// Add operation for rewrite actions
|
// Add operation for rewrite actions
|
||||||
if (REWRITE_ACTIONS.includes(action)) {
|
if (REWRITE_ACTIONS.includes(action)) {
|
||||||
payload.operation = action;
|
payload.operation = action;
|
||||||
@@ -75,6 +85,7 @@ export function useCopilotReply() {
|
|||||||
const trackedConversationId = ref(null);
|
const trackedConversationId = ref(null);
|
||||||
|
|
||||||
const conversationId = computed(() => currentChat.value?.id);
|
const conversationId = computed(() => currentChat.value?.id);
|
||||||
|
const uiFrom = 'editor';
|
||||||
|
|
||||||
const isActive = computed(() => showEditor.value || isGenerating.value);
|
const isActive = computed(() => showEditor.value || isGenerating.value);
|
||||||
const isButtonDisabled = computed(
|
const isButtonDisabled = computed(
|
||||||
@@ -97,7 +108,8 @@ export function useCopilotReply() {
|
|||||||
buildPayload(
|
buildPayload(
|
||||||
currentAction.value,
|
currentAction.value,
|
||||||
trackedConversationId.value,
|
trackedConversationId.value,
|
||||||
followUpCount.value
|
followUpCount.value,
|
||||||
|
uiFrom
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,7 +180,7 @@ export function useCopilotReply() {
|
|||||||
const eventKey = `${getEventPrefix(action)}_USED`;
|
const eventKey = `${getEventPrefix(action)}_USED`;
|
||||||
useTrack(
|
useTrack(
|
||||||
CAPTAIN_EVENTS[eventKey],
|
CAPTAIN_EVENTS[eventKey],
|
||||||
buildPayload(action, trackedConversationId.value)
|
buildPayload(action, trackedConversationId.value, undefined, uiFrom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
isGenerating.value = false;
|
isGenerating.value = false;
|
||||||
@@ -194,6 +206,7 @@ export function useCopilotReply() {
|
|||||||
// Track follow-up sent event
|
// Track follow-up sent event
|
||||||
useTrack(CAPTAIN_EVENTS.FOLLOW_UP_SENT, {
|
useTrack(CAPTAIN_EVENTS.FOLLOW_UP_SENT, {
|
||||||
conversationId: trackedConversationId.value,
|
conversationId: trackedConversationId.value,
|
||||||
|
uiFrom,
|
||||||
});
|
});
|
||||||
followUpCount.value += 1;
|
followUpCount.value += 1;
|
||||||
|
|
||||||
@@ -237,7 +250,8 @@ export function useCopilotReply() {
|
|||||||
buildPayload(
|
buildPayload(
|
||||||
currentAction.value,
|
currentAction.value,
|
||||||
trackedConversationId.value,
|
trackedConversationId.value,
|
||||||
followUpCount.value
|
followUpCount.value,
|
||||||
|
uiFrom
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed } from 'vue';
|
|||||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||||
|
|
||||||
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
||||||
|
{ name: 'conversation_summary' },
|
||||||
{ name: 'conversation_actions' },
|
{ name: 'conversation_actions' },
|
||||||
{ name: 'macros' },
|
{ name: 'macros' },
|
||||||
{ name: 'conversation_info' },
|
{ name: 'conversation_info' },
|
||||||
|
|||||||
@@ -137,6 +137,11 @@
|
|||||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||||
"SHOW_QUOTED_TEXT": "Show Quoted Text",
|
"SHOW_QUOTED_TEXT": "Show Quoted Text",
|
||||||
"MESSAGE_READ": "Read",
|
"MESSAGE_READ": "Read",
|
||||||
"SENDING": "Sending"
|
"SENDING": "Sending",
|
||||||
|
"SUMMARY": {
|
||||||
|
"TITLE": "Generate Summary",
|
||||||
|
"LOADING": "Generating summary...",
|
||||||
|
"ERROR": "Failed to generate summary"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
"CONVERSATION_ACTIONS": "Conversation Actions",
|
"CONVERSATION_ACTIONS": "Conversation Actions",
|
||||||
"CONVERSATION_LABELS": "Conversation Labels",
|
"CONVERSATION_LABELS": "Conversation Labels",
|
||||||
"CONVERSATION_INFO": "Conversation Information",
|
"CONVERSATION_INFO": "Conversation Information",
|
||||||
|
"CONVERSATION_SUMMARY": "AI Summary",
|
||||||
"CONTACT_NOTES": "Contact Notes",
|
"CONTACT_NOTES": "Contact Notes",
|
||||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||||
@@ -356,6 +357,16 @@
|
|||||||
"LINEAR_ISSUES": "Linked Linear Issues",
|
"LINEAR_ISSUES": "Linked Linear Issues",
|
||||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||||
},
|
},
|
||||||
|
"SUMMARY": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
"SHOPIFY": {
|
"SHOPIFY": {
|
||||||
"ORDER_ID": "Order #{id}",
|
"ORDER_ID": "Order #{id}",
|
||||||
"ERROR": "Error loading orders",
|
"ERROR": "Error loading orders",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
|||||||
import ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
import ConversationAction from './ConversationAction.vue';
|
import ConversationAction from './ConversationAction.vue';
|
||||||
import ConversationParticipant from './ConversationParticipant.vue';
|
import ConversationParticipant from './ConversationParticipant.vue';
|
||||||
|
import ConversationSummary from './ConversationSummary.vue';
|
||||||
import ContactInfo from './contact/ContactInfo.vue';
|
import ContactInfo from './contact/ContactInfo.vue';
|
||||||
import ContactNotes from './contact/ContactNotes.vue';
|
import ContactNotes from './contact/ContactNotes.vue';
|
||||||
import ConversationInfo from './ConversationInfo.vue';
|
import ConversationInfo from './ConversationInfo.vue';
|
||||||
@@ -44,6 +45,7 @@ const {
|
|||||||
|
|
||||||
const dragging = ref(false);
|
const dragging = ref(false);
|
||||||
const conversationSidebarItems = ref([]);
|
const conversationSidebarItems = ref([]);
|
||||||
|
const summaryRef = ref(null);
|
||||||
|
|
||||||
const shopifyIntegration = useFunctionGetter(
|
const shopifyIntegration = useFunctionGetter(
|
||||||
'integrations/getIntegration',
|
'integrations/getIntegration',
|
||||||
@@ -60,6 +62,10 @@ const isLinearFeatureEnabled = computed(() =>
|
|||||||
isCloudFeatureEnabled(FEATURE_FLAGS.LINEAR)
|
isCloudFeatureEnabled(FEATURE_FLAGS.LINEAR)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCaptainTasksEnabled = computed(() =>
|
||||||
|
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS)
|
||||||
|
);
|
||||||
|
|
||||||
const linearIntegration = useFunctionGetter(
|
const linearIntegration = useFunctionGetter(
|
||||||
'integrations/getIntegration',
|
'integrations/getIntegration',
|
||||||
'linear'
|
'linear'
|
||||||
@@ -150,7 +156,31 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<div
|
<div
|
||||||
v-if="element.name === 'conversation_actions'"
|
v-if="
|
||||||
|
element.name === 'conversation_summary' && isCaptainTasksEnabled
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AccordionItem
|
||||||
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_SUMMARY')"
|
||||||
|
:is-open="isContactSidebarItemOpen('is_conv_summary_open')"
|
||||||
|
compact
|
||||||
|
@toggle="
|
||||||
|
value => {
|
||||||
|
toggleSidebarUIState('is_conv_summary_open', value);
|
||||||
|
if (value && summaryRef) {
|
||||||
|
summaryRef.fetchSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ConversationSummary
|
||||||
|
ref="summaryRef"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
/>
|
||||||
|
</AccordionItem>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="element.name === 'conversation_actions'"
|
||||||
class="conversation--actions"
|
class="conversation--actions"
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<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';
|
||||||
|
import TasksAPI from 'dashboard/api/captain/tasks';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import { useTrack } from 'dashboard/composables';
|
||||||
|
import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const store = useStore();
|
||||||
|
const { isCloudFeatureEnabled } = useAccount();
|
||||||
|
const { formatMessage } = useMessageFormatter();
|
||||||
|
|
||||||
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const captainTasksEnabled = computed(() => {
|
||||||
|
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 uiFrom = 'conversation_sidebar';
|
||||||
|
|
||||||
|
const isStale = computed(() => {
|
||||||
|
if (!cachedSummaryAt.value) return true;
|
||||||
|
return lastActivityAt.value > cachedSummaryAt.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await TasksAPI.summarize(props.conversationId, {
|
||||||
|
forceRegenerate,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: { message: generatedSummary },
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
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 =
|
||||||
|
e.response?.data?.error || t('CONVERSATION_SIDEBAR.SUMMARY.ERROR');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackSummary = action => {
|
||||||
|
useTrack(CAPTAIN_EVENTS.SUMMARIZE_USED, {
|
||||||
|
conversationId: props.conversationId,
|
||||||
|
uiFrom,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSummary = () => {
|
||||||
|
trackSummary('generate');
|
||||||
|
return fetchSummary(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenerate = () => {
|
||||||
|
trackSummary('regenerate');
|
||||||
|
return fetchSummary(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryGenerate = () => {
|
||||||
|
trackSummary('retry');
|
||||||
|
return fetchSummary(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.conversationId,
|
||||||
|
() => {
|
||||||
|
error.value = '';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
fetchSummary,
|
||||||
|
captainTasksEnabled,
|
||||||
|
cachedSummary,
|
||||||
|
isStale,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="captainTasksEnabled" class="p-3">
|
||||||
|
<div v-if="isLoading" class="flex items-center justify-center py-4">
|
||||||
|
<Spinner :size="20" class="text-n-slate-10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="text-sm text-n-ruby-11">
|
||||||
|
{{ error }}
|
||||||
|
<Button
|
||||||
|
:label="t('CONVERSATION_SIDEBAR.SUMMARY.RETRY')"
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
class="ml-2"
|
||||||
|
@click="retryGenerate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<Button
|
||||||
|
:label="t('CONVERSATION_SIDEBAR.SUMMARY.GENERATE')"
|
||||||
|
icon="i-material-symbols-auto-awesome"
|
||||||
|
size="sm"
|
||||||
|
@click="generateSummary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 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>
|
||||||
@@ -412,6 +412,17 @@ const actions = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateConversationCachedSummary(
|
||||||
|
{ commit },
|
||||||
|
{ conversationId, cachedSummary, cachedSummaryAt }
|
||||||
|
) {
|
||||||
|
commit(types.UPDATE_CONVERSATION_CACHED_SUMMARY, {
|
||||||
|
conversationId,
|
||||||
|
cachedSummary,
|
||||||
|
cachedSummaryAt,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setChatStatusFilter({ commit }, data) {
|
setChatStatusFilter({ commit }, data) {
|
||||||
commit(types.CHANGE_CHAT_STATUS_FILTER, data);
|
commit(types.CHANGE_CHAT_STATUS_FILTER, data);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -116,6 +116,16 @@ export const mutations = {
|
|||||||
chat.last_activity_at = lastActivityAt;
|
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 }) {
|
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
|
||||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||||
chat.priority = priority;
|
chat.priority = priority;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default {
|
|||||||
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
|
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
|
||||||
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
|
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
|
||||||
UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY',
|
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_CONVERSATION_CALL_STATUS: 'UPDATE_CONVERSATION_CALL_STATUS',
|
||||||
UPDATE_MESSAGE_CALL_STATUS: 'UPDATE_MESSAGE_CALL_STATUS',
|
UPDATE_MESSAGE_CALL_STATUS: 'UPDATE_MESSAGE_CALL_STATUS',
|
||||||
SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES',
|
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.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_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.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.priority conversation.priority
|
||||||
json.waiting_since conversation.waiting_since.to_i.to_i
|
json.waiting_since conversation.waiting_since.to_i.to_i
|
||||||
json.sla_policy_id conversation.sla_policy_id
|
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
|
||||||
@@ -681,6 +681,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do
|
|||||||
t.datetime "waiting_since"
|
t.datetime "waiting_since"
|
||||||
t.text "cached_label_list"
|
t.text "cached_label_list"
|
||||||
t.bigint "assignee_agent_bot_id"
|
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", "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", "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"
|
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
|
def summarize
|
||||||
result = Captain::SummaryService.new(
|
result = Captain::SummaryService.new(
|
||||||
account: Current.account,
|
account: Current.account,
|
||||||
conversation_display_id: params[:conversation_display_id]
|
conversation_display_id: params[:conversation_display_id],
|
||||||
|
force_regenerate: params[:force_regenerate].present?
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
render_result(result)
|
render_result(result)
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
class Captain::SummaryService < Captain::BaseTaskService
|
class Captain::SummaryService < Captain::BaseTaskService
|
||||||
pattr_initialize [:account!, :conversation_display_id!]
|
pattr_initialize [:account!, :conversation_display_id!, { force_regenerate: false }]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
make_api_call(
|
return cached_response if use_cache?
|
||||||
model: GPT_MODEL,
|
|
||||||
messages: [
|
generate_and_cache_summary
|
||||||
{ role: 'system', content: prompt_from_file('summary') },
|
|
||||||
{ role: 'user', content: conversation.to_llm_text(include_contact_details: false) }
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -16,4 +12,73 @@ class Captain::SummaryService < Captain::BaseTaskService
|
|||||||
def event_name
|
def event_name
|
||||||
'summarize'
|
'summarize'
|
||||||
end
|
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
|
||||||
|
msg_count = conversation_message_count
|
||||||
|
result = make_api_call(
|
||||||
|
model: summary_model(msg_count),
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: prompt_from_file('summary') },
|
||||||
|
{ role: 'user', content: build_summary_content(msg_count) }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_summary(result[:message]) if result[:message].present? && result[:error].blank?
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_message_count
|
||||||
|
conversation.messages.where(message_type: [:incoming, :outgoing]).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary_model(msg_count)
|
||||||
|
msg_count < 7 ? GPT_MODEL : 'gpt-4.1'
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_summary_content(msg_count)
|
||||||
|
llm_text = conversation.to_llm_text(include_contact_details: false)
|
||||||
|
context = build_conversation_context(msg_count)
|
||||||
|
context.present? ? "#{llm_text}\n\n#{context}" : llm_text
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_conversation_context(msg_count)
|
||||||
|
['Conversation Stats:', *context_fields(msg_count).compact].join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def context_fields(msg_count)
|
||||||
|
[
|
||||||
|
"Message count: #{msg_count}",
|
||||||
|
("Status: #{conversation.status}" if conversation.status.present?),
|
||||||
|
("Priority: #{conversation.priority}" if conversation.priority.present?),
|
||||||
|
("Labels: #{conversation.cached_label_list}" if conversation.cached_label_list.present?),
|
||||||
|
*account_context_fields
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_context_fields
|
||||||
|
[
|
||||||
|
("Account industry: #{account.custom_attributes['industry']}" if account.custom_attributes&.dig('industry').present?),
|
||||||
|
("Summary language: #{account.locale}" if account.locale.present?)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_summary(summary)
|
||||||
|
conversation.update(
|
||||||
|
cached_summary: summary,
|
||||||
|
cached_summary_at: Time.current
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,28 +1,99 @@
|
|||||||
As an AI-powered summarization tool, your task is to condense lengthy interactions between customer support agents and customers into brief, digestible summaries. The objective of these summaries is to provide a quick overview, enabling any agent, even those without prior context, to grasp the essence of the conversation promptly.
|
<prompt>
|
||||||
|
<role>AI support conversation summarizer</role>
|
||||||
|
|
||||||
Make sure you strongly adhere to the following rules when generating the summary
|
<goal>
|
||||||
|
Produce a high-signal bullet summary that lets a new agent understand the CURRENT STATE and know what to do next — in under 10 seconds.
|
||||||
|
Earlier bullets convey what happened and what the current state is. The final bullet is always the actionable next step.
|
||||||
|
</goal>
|
||||||
|
|
||||||
1. Be brief and concise. The shorter the summary the better.
|
<output_contract>
|
||||||
2. Aim to summarize the conversation in approximately 200 words, formatted as multiple small paragraphs that are easier to read.
|
<format>bullet_list</format>
|
||||||
3. Describe the customer intent in around 50 words.
|
<only_output_bullets>true</only_output_bullets>
|
||||||
4. Remove information that is not directly relevant to the customer's problem or the agent's solution. For example, personal anecdotes, small talk, etc.
|
<bullet_count_rules>
|
||||||
5. Don't include segments of the conversation that didn't contribute meaningful content, like greetings or farewell.
|
<rule>Count the messages in the conversation to determine bullet limits.</rule>
|
||||||
6. The 'Action Items' should be a bullet list, arranged in order of priority if possible.
|
<rule>1-3 messages → 1 bullet max</rule>
|
||||||
7. 'Action Items' should strictly encapsulate tasks committed to by the agent or left incomplete. Any suggestions made by the agent should not be included.
|
<rule>4-10 messages → 2-3 bullets max</rule>
|
||||||
8. The 'Action Items' should be brief and concise
|
<rule>11-30 messages → 3-4 bullets max</rule>
|
||||||
9. Mark important words or parts of sentences as bold.
|
<rule>31+ messages → 5-6 bullets max</rule>
|
||||||
10. Apply markdown syntax to format any included code, using backticks.
|
</bullet_count_rules>
|
||||||
11. Include a section for "Follow-up Items" or "Open Questions" if there are any unresolved issues or outstanding questions.
|
<bullet_rules>
|
||||||
12. If any section does not have any content, remove that section and the heading from the response
|
<single_sentence>true</single_sentence>
|
||||||
13. Do not insert your own opinions about the conversation.
|
<concise>true</concise>
|
||||||
|
<no_redundancy>true</no_redundancy>
|
||||||
|
<no_prefixes>Do not prefix bullets with labels like "Next step:" or "Issue:" — ordering alone conveys structure.</no_prefixes>
|
||||||
|
</bullet_rules>
|
||||||
|
<language>
|
||||||
|
<rule>ALWAYS write the summary in the language specified by "Summary language" in the conversation context. This overrides the conversation language.</rule>
|
||||||
|
<rule>If no summary language is provided, match the primary language used in the conversation.</rule>
|
||||||
|
</language>
|
||||||
|
<markdown>
|
||||||
|
<bold_required>true</bold_required>
|
||||||
|
<code_in_backticks>true</code_in_backticks>
|
||||||
|
<no_headings>true</no_headings>
|
||||||
|
</markdown>
|
||||||
|
</output_contract>
|
||||||
|
|
||||||
|
<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 actionable next step (who does what)?</item>
|
||||||
|
</priority_order>
|
||||||
|
<skip_rule>If a priority item has no evidence in the conversation, skip it entirely — do not invent or guess.</skip_rule>
|
||||||
|
<last_bullet_rule>The final bullet must always be an actionable next step derived from the conversation.</last_bullet_rule>
|
||||||
|
</what_to_capture>
|
||||||
|
|
||||||
Reply in the user's language, as a markdown of the following format.
|
<context_awareness>
|
||||||
|
<business_context>If account industry or business context is provided, adapt the summary focus accordingly (e.g., e-commerce → order/payment/shipping; SaaS → features/bugs/subscriptions; healthcare → appointments/records).</business_context>
|
||||||
|
<labels>If conversation labels are provided, use them to understand the issue category and focus the summary on what matters for that category.</labels>
|
||||||
|
<adapt>Only use context that is explicitly provided. Do not assume industry or category if not stated.</adapt>
|
||||||
|
</context_awareness>
|
||||||
|
|
||||||
**Customer Intent**
|
<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>Would removing this bullet prevent the agent from acting correctly? If no, 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>
|
||||||
|
|
||||||
**Conversation Summary**
|
<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>
|
||||||
|
|
||||||
**Action Items**
|
<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>
|
||||||
|
|
||||||
**Follow-up Items**
|
<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>Last bullet is an actionable next step.</item>
|
||||||
|
<item>Summary language matches "Summary language" from context (if provided), otherwise matches conversation language.</item>
|
||||||
|
<item>Bullet count respects the scaling rules for the message count.</item>
|
||||||
|
</checklist>
|
||||||
|
<reject_if_fails>true</reject_if_fails>
|
||||||
|
</final_validation>
|
||||||
|
</prompt>
|
||||||
|
|||||||
Reference in New Issue
Block a user