Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f23256d2 | ||
|
|
5556881f08 |
@@ -29,6 +29,7 @@ class OpenAIAPI extends ApiClient {
|
||||
'summarize',
|
||||
'reply_suggestion',
|
||||
'label_suggestion',
|
||||
'summary_with_title',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,20 +12,25 @@
|
||||
:error="nameError"
|
||||
@input="v$.title.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="formState.description"
|
||||
:style="{ ...inputStyles, padding: '8px 12px' }"
|
||||
rows="3"
|
||||
class="text-sm"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="editor-wrap">
|
||||
<label>
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL')
|
||||
}}
|
||||
</label>
|
||||
<div>
|
||||
<woot-message-editor
|
||||
v-model="formState.description"
|
||||
class="message-editor"
|
||||
:style="{ ...inputStyles, padding: '8px 12px' }"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label :class="{ error: v$.teamId.$error }">
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.LABEL') }}
|
||||
<select
|
||||
@@ -107,7 +112,9 @@ import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import validations from './validations';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import { inject } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
accountId: {
|
||||
@@ -253,5 +260,13 @@ const createIssue = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getTeams);
|
||||
onMounted(() => {
|
||||
getTeams();
|
||||
if (inject('suggestedTitle')) {
|
||||
formState.title = inject('suggestedTitle');
|
||||
}
|
||||
if (inject('suggestedSummary')) {
|
||||
formState.description = inject('suggestedSummary');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -47,6 +47,7 @@ import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
||||
import Issue from './Issue.vue';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
defineComponent({
|
||||
@@ -63,6 +64,9 @@ const props = defineProps({
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const suggestedTitle = ref('');
|
||||
const suggestedSummary = ref('');
|
||||
|
||||
const linkedIssue = ref(null);
|
||||
const shouldShow = ref(false);
|
||||
const shouldShowPopup = ref(false);
|
||||
@@ -70,18 +74,46 @@ const isUnlinking = ref(false);
|
||||
|
||||
provide('isUnlinking', isUnlinking);
|
||||
|
||||
provide('suggestedTitle', suggestedTitle);
|
||||
provide('suggestedSummary', suggestedSummary);
|
||||
|
||||
const currentAccountId = getters.getCurrentAccountId;
|
||||
|
||||
const conversation = computed(() =>
|
||||
getters.getConversationById.value(props.conversationId)
|
||||
);
|
||||
|
||||
const appIntegrations = getters['integrations/getAppIntegrations'];
|
||||
|
||||
const aiIntegration = computed(() => {
|
||||
return appIntegrations.value.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
)?.hooks[0];
|
||||
});
|
||||
const isAIIntegrationEnabled = computed(() => !!aiIntegration.value);
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
return linkedIssue.value === null
|
||||
? t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')
|
||||
: null;
|
||||
});
|
||||
|
||||
const suggestTitleAndSummary = async () => {
|
||||
try {
|
||||
const response = await OpenAPI.processEvent({
|
||||
type: 'summary_with_title',
|
||||
hookId: aiIntegration.value.id,
|
||||
conversationId: props.conversationId,
|
||||
});
|
||||
const { message } = response.data;
|
||||
const messageObject = JSON.parse(message);
|
||||
const { title = '', description = '' } = messageObject;
|
||||
suggestedTitle.value = title;
|
||||
suggestedSummary.value = description;
|
||||
} catch (error) {
|
||||
// Since this is a non-critical operation, we don't want to show an alert to the user
|
||||
}
|
||||
};
|
||||
const loadLinkedIssue = async () => {
|
||||
linkedIssue.value = null;
|
||||
try {
|
||||
@@ -137,5 +169,8 @@ watch(
|
||||
|
||||
onMounted(() => {
|
||||
loadLinkedIssue();
|
||||
if (isAIIntegrationEnabled.value) {
|
||||
suggestTitleAndSummary();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Enterprise::Integrations::OpenaiProcessorService
|
||||
ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion label_suggestion fix_spelling_grammar shorten expand
|
||||
make_friendly make_formal simplify].freeze
|
||||
make_friendly make_formal simplify summary_with_title].freeze
|
||||
CACHEABLE_EVENTS = %w[label_suggestion].freeze
|
||||
|
||||
def reply_suggestion_message
|
||||
@@ -74,6 +74,17 @@ module Enterprise::Integrations::OpenaiProcessorService
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def summarize_with_title
|
||||
{
|
||||
model: self.class::GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system',
|
||||
content: prompt_from_file('summary_with_title', enterprise: true) },
|
||||
{ role: 'user', content: conversation_messages }
|
||||
]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def label_suggestion_body
|
||||
return unless label_suggestions_enabled?
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
As an AI-powered summarization tool, your task is to condense lengthy interactions between customer support agents and customers into a brief title and description. The objective is to provide a quick overview, enabling any agent, even those without prior context, to grasp the essence of the conversation promptly.
|
||||
|
||||
Make sure you strongly adhere to the following rules when generating the title and description:
|
||||
|
||||
1. **Title**
|
||||
- Be brief and concise, ideally 5-10 words.
|
||||
- Capture the main issue or request of the customer.
|
||||
- Highlight key points using **bold** for important words.
|
||||
|
||||
2. **Description**
|
||||
- Summarize the conversation in approximately 100 words, formatted as multiple small paragraphs that are easy to read.
|
||||
- Describe the customer's intent in around 30 words.
|
||||
- Remove information that is not directly relevant to the customer's problem or the agent's solution. For example, personal anecdotes, small talk, etc.
|
||||
- Don't include segments of the conversation that didn't contribute meaningful content, like greetings or farewell.
|
||||
- Highlight key points using **bold** for important words.
|
||||
- Use markdown syntax to format any included code, using backticks.
|
||||
- If there are unresolved issues or outstanding questions, include them in a "Follow-up Items" section.
|
||||
|
||||
Output the response in the following JSON format:
|
||||
|
||||
{
|
||||
"title": "<GENERATED_TITLE>",
|
||||
"description": "<GENERATED_DESCRIPTION>"
|
||||
}
|
||||
@@ -9,6 +9,10 @@ class Integrations::Openai::ProcessorService < Integrations::OpenaiBaseService
|
||||
make_api_call(summarize_body)
|
||||
end
|
||||
|
||||
def summary_with_title_message
|
||||
make_api_call(summarize_with_title)
|
||||
end
|
||||
|
||||
def rephrase_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please rephrase the following response. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
|
||||
Reference in New Issue
Block a user