diff --git a/server/ai/subtitles/subtitles.go b/server/ai/subtitles/subtitles.go index 53447fb0..43f60449 100644 --- a/server/ai/subtitles/subtitles.go +++ b/server/ai/subtitles/subtitles.go @@ -57,6 +57,16 @@ func (s *Subtitles) FormatTextOnly() string { return strings.TrimSpace(result.String()) } +func (s *Subtitles) FormatVTT() string { + var result strings.Builder + s.storage.WriteToWebVTT(&result) + return result.String() +} + +func (s *Subtitles) IsEmpty() bool { + return s.storage.IsEmpty() +} + func formatDurationForLLM(dur time.Duration) string { dur = dur.Round(time.Second) hours := dur / time.Hour diff --git a/server/api_post.go b/server/api_post.go index 8be8c648..602ce699 100644 --- a/server/api_post.go +++ b/server/api_post.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/render" "github.com/mattermost/mattermost-plugin-ai/server/ai" + "github.com/mattermost/mattermost-plugin-ai/server/ai/subtitles" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" ) @@ -86,13 +87,30 @@ func (p *Plugin) handleSummarize(c *gin.Context) { } func (p *Plugin) handleTranscribe(c *gin.Context) { + userID := c.GetHeader("Mattermost-User-Id") post := c.MustGet(ContextPostKey).(*model.Post) channel := c.MustGet(ContextChannelKey).(*model.Channel) - if err := p.handleCallRecordingPost(post, channel); err != nil { + user, err := p.pluginAPI.User.Get(userID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + createdPost, err := p.newCallRecordingThread(user, post, channel) + if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } + + data := struct { + PostID string `json:"postid"` + ChannelID string `json:"channelid"` + }{ + PostID: createdPost.Id, + ChannelID: createdPost.ChannelId, + } + c.Render(http.StatusOK, render.JSON{Data: data}) } func (p *Plugin) handleStop(c *gin.Context) { @@ -107,7 +125,7 @@ func (p *Plugin) handleStop(c *gin.Context) { return } - if post.GetProp("llm_requester_user_id") != userID { + if post.GetProp(LLMRequesterUserID) != userID { c.AbortWithError(http.StatusForbidden, errors.New("only the original poster can stop the stream")) return } @@ -131,40 +149,94 @@ func (p *Plugin) handleRegenerate(c *gin.Context) { return } - if post.GetProp("llm_requester_user_id") != userID { + if post.GetProp(LLMRequesterUserID) != userID { c.AbortWithError(http.StatusForbidden, errors.New("only the original poster can regenerate")) return } - user, err := p.pluginAPI.User.Get(userID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) + if post.GetProp(NoRegen) != nil { + c.AbortWithError(http.StatusBadRequest, errors.New("taged no regen")) return } - threadData, err := p.getThreadAndMeta(post.RootId) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - threadData.cutoffAtPostID(post.Id) - - postToRegenerate := threadData.latestPost() - - context := p.MakeConversationContext(user, channel, postToRegenerate) - conversation, err := p.prompts.ChatCompletion(ai.PromptDirectMessageQuestion, context) + user, err := p.pluginAPI.User.Get(userID) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } - conversation.AppendConversation(ai.ThreadToBotConversation(p.botid, threadData.Posts)) - result, err := p.getLLM().ChatCompletion(conversation) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) + summaryPostIDProp := post.GetProp(ThreadIDProp) + refrencedRecordingPostProp := post.GetProp(ReferencedRecordingPostID) + var result *ai.TextStreamResult + switch { + case summaryPostIDProp != nil: + summaryPostID := summaryPostIDProp.(string) + siteURL := p.API.GetConfig().ServiceSettings.SiteURL + post.Message = summaryPostMessage(summaryPostID, *siteURL) + + result, err = p.summarizePost(summaryPostID, p.MakeConversationContext(user, channel, nil)) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not summarize post on regen")) + return + } + case refrencedRecordingPostProp != nil: + post.Message = "" + refrencedRecordingPostID := refrencedRecordingPostProp.(string) + referencedRecordingPost, err := p.pluginAPI.Post.GetPost(refrencedRecordingPostID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not get transcription post on regen")) + return + } + + reader, err := p.pluginAPI.File.Get(post.FileIds[0]) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not get transcription file on regen")) + return + } + transcription, err := subtitles.NewSubtitlesFromVTT(reader) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not parse transcription file on regen")) + return + } + + if transcription.IsEmpty() { + c.AbortWithError(http.StatusInternalServerError, errors.New("transcription is empty on regen")) + return + } + + channel, err := p.pluginAPI.Channel.Get(referencedRecordingPost.ChannelId) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not get channel of original recording on regen")) + return + } + + context := p.MakeConversationContext(user, channel, nil) + result, err = p.summarizeTranscription(transcription, context) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not summarize transcription on regen")) + } + default: + post.Message = "" + + threadData, err := p.getThreadAndMeta(post.Id) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + respondingToPostID, ok := post.GetProp(RespondingToProp).(string) + if !ok { + threadData.cutoffBeforePostID(post.Id) + } else { + threadData.cutoffAtPostID(respondingToPostID) + } + postToRegenerate := threadData.latestPost() + context := p.MakeConversationContext(user, channel, postToRegenerate) + + if result, err = p.continueConversation(threadData, context); err != nil { + c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "could not continue conversation on regen")) + return + } } - post.Message = "" - p.streamResultToPost(result, post) } diff --git a/server/conversation_context.go b/server/conversation_context.go index 9e606a93..c9c45c33 100644 --- a/server/conversation_context.go +++ b/server/conversation_context.go @@ -15,10 +15,10 @@ func (p *Plugin) MakeConversationContext(user *model.User, channel *model.Channe context.CompanyName = license.Customer.Company } - if channel != nil { + if channel != nil && (channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup) { team, err := p.pluginAPI.Team.Get(channel.TeamId) if err != nil { - p.pluginAPI.Log.Error("Unable to get team for context", "error", err.Error()) + p.pluginAPI.Log.Error("Unable to get team for context", "error", err.Error(), "team_id", channel.TeamId) } else { context.Team = team } diff --git a/server/meeting_summarization.go b/server/meeting_summarization.go index 1e50d922..59b16340 100644 --- a/server/meeting_summarization.go +++ b/server/meeting_summarization.go @@ -6,54 +6,30 @@ import ( "os/exec" "strings" + sq "github.com/Masterminds/squirrel" + "github.com/mattermost/mattermost-plugin-ai/server/ai" + "github.com/mattermost/mattermost-plugin-ai/server/ai/subtitles" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" ) -func (p *Plugin) handleCallRecordingPost(recordingPost *model.Post, channel *model.Channel) (reterr error) { - if len(recordingPost.FileIds) != 1 { - return errors.New("Unexpected number of files in calls post") - } +const ReferencedRecordingPostID = "referenced_recording_post_id" +const NoRegen = "no_regen" +func (p *Plugin) createTranscription(recordingFileID string) (*subtitles.Subtitles, error) { if p.ffmpegPath == "" { - return errors.New("ffmpeg not installed") - } - - rootId := recordingPost.Id - if recordingPost.RootId != "" { - rootId = recordingPost.RootId + return nil, errors.New("ffmpeg not installed") } - botPost := &model.Post{ - ChannelId: recordingPost.ChannelId, - RootId: rootId, - Message: "Transcribing meeting...", - } - if err := p.botCreatePost("", botPost); err != nil { - return err - } - - // Update to an error if we return one. - defer func() { - if reterr != nil { - botPost.Message = "Sorry! Somthing went wrong. Check the server logs for details." - if err := p.pluginAPI.Post.UpdatePost(botPost); err != nil { - p.API.LogError("Failed to update post in error handling handleCallRecordingPost", "error", err) - } - } - }() - - recordingFileID := recordingPost.FileIds[0] - recordingFileInfo, err := p.pluginAPI.File.GetInfo(recordingFileID) if err != nil { - return errors.Wrap(err, "unable to get calls file info") + return nil, errors.Wrap(err, "unable to get calls file info") } fileReader, err := p.pluginAPI.File.Get(recordingFileID) if err != nil { - return errors.Wrap(err, "unable to read calls file") + return nil, errors.Wrap(err, "unable to read calls file") } var cmd *exec.Cmd @@ -67,74 +43,134 @@ func (p *Plugin) handleCallRecordingPost(recordingPost *model.Post, channel *mod audioReader, err := cmd.StdoutPipe() if err != nil { - return errors.Wrap(err, "couldn't create stdout pipe") + return nil, errors.Wrap(err, "couldn't create stdout pipe") } errorReader, err := cmd.StderrPipe() if err != nil { - return errors.Wrap(err, "couldn't create stderr pipe") + return nil, errors.Wrap(err, "couldn't create stderr pipe") } if err := cmd.Start(); err != nil { - return errors.Wrap(err, "couldn't run ffmpeg") + return nil, errors.Wrap(err, "couldn't run ffmpeg") } transcriber := p.getTranscribe() // Limit reader should probably error out instead of just silently failing transcription, err := transcriber.Transcribe(io.LimitReader(audioReader, WhisperAPILimit)) if err != nil { - return err + return nil, errors.Wrap(err, "unable to transcribe") } - llmFormattedTranscription := transcription.FormatForLLM() errout, err := io.ReadAll(errorReader) if err != nil { - return errors.Wrap(err, "unable to read stderr from ffmpeg") + return nil, errors.Wrap(err, "unable to read stderr from ffmpeg") } if err := cmd.Wait(); err != nil { p.pluginAPI.Log.Debug("ffmpeg stderr: " + string(errout)) - return errors.Wrap(err, "error while waiting for ffmpeg") + return nil, errors.Wrap(err, "error while waiting for ffmpeg") } - transcriptFileInfo, err := p.pluginAPI.File.Upload(strings.NewReader(transcription.FormatTextOnly()), "transcript.txt", channel.Id) - if err != nil { - return errors.Wrap(err, "unable to upload transcript") + return transcription, nil +} + +func (p *Plugin) newCallRecordingThread(requestingUser *model.User, recordingPost *model.Post, channel *model.Channel) (*model.Post, error) { + if len(recordingPost.FileIds) != 1 { + return nil, errors.New("Unexpected number of files in calls post") + } + + siteURL := p.API.GetConfig().ServiceSettings.SiteURL + surePost := &model.Post{ + Message: fmt.Sprintf("Sure, I will summarize this recording: %s/_redirect/pl/%s\n", *siteURL, recordingPost.Id), + } + surePost.AddProp(NoRegen, "true") + if err := p.botDM(requestingUser.Id, surePost); err != nil { + return nil, err } - // Can not update a post to include file attachments. So we have to delete and re-create. - if err := p.pluginAPI.Post.DeletePost(botPost.Id); err != nil { - return errors.Wrap(err, "unable to delete bot post") + if err := p.summarizeCallRecording(surePost.Id, requestingUser, recordingPost, channel); err != nil { + return nil, err } - botPost.Id = "" - botPost.CreateAt = 0 - botPost.UpdateAt = 0 - botPost.EditAt = 0 - botPost.Message += "\nRefining transcription..." - botPost.FileIds = []string{transcriptFileInfo.Id} - if err := p.botCreatePost("", botPost); err != nil { + return surePost, nil +} + +func (p *Plugin) summarizeCallRecording(rootID string, requestingUser *model.User, recordingPost *model.Post, channel *model.Channel) error { + transcriptPost := &model.Post{ + RootId: rootID, + Message: "Processing audio into transcription. This will take some time...", + } + transcriptPost.AddProp(ReferencedRecordingPostID, recordingPost.Id) + if err := p.botDM(requestingUser.Id, transcriptPost); err != nil { return err } + go func() (reterr error) { + // Update to an error if we return one. + defer func() { + if reterr != nil { + transcriptPost.Message = "Sorry! Somthing went wrong. Check the server logs for details." + if err := p.pluginAPI.Post.UpdatePost(transcriptPost); err != nil { + p.API.LogError("Failed to update post in error handling handleCallRecordingPost", "error", err) + } + p.API.LogError("Error in call recording post", "error", reterr) + } + }() + + transcription, err := p.createTranscription(recordingPost.FileIds[0]) + if err != nil { + return errors.Wrap(err, "failed to create transcription") + } + + transcriptFileInfo, err := p.pluginAPI.File.Upload(strings.NewReader(transcription.FormatVTT()), "transcript.txt", channel.Id) + if err != nil { + return errors.Wrap(err, "unable to upload transcript") + } + + context := p.MakeConversationContext(requestingUser, channel, nil) + summaryStream, err := p.summarizeTranscription(transcription, context) + if err != nil { + return errors.Wrap(err, "unable to summarize transcription") + } + + if err := p.updatePostWithFile(transcriptPost, transcriptFileInfo); err != nil { + return errors.Wrap(err, "unable to update transcript post") + } + + if err := p.streamResultToPost(summaryStream, transcriptPost); err != nil { + return errors.Wrap(err, "unable to stream result to post") + } + + return nil + }() + + return nil +} + +func (p *Plugin) summarizeTranscription(transcription *subtitles.Subtitles, context ai.ConversationContext) (*ai.TextStreamResult, error) { + llmFormattedTranscription := transcription.FormatForLLM() tokens := p.getLLM().CountTokens(llmFormattedTranscription) + tokenLimitWithMargin := int(float64(p.getLLM().TokenLimit())*0.75) - ContextTokenMargin + if tokenLimitWithMargin < 0 { + tokenLimitWithMargin = ContextTokenMargin / 2 + } isChunked := false - if tokens > p.getLLM().TokenLimit()-ContextTokenMargin { - p.pluginAPI.Log.Debug("Transcription too long, summarizing in chunks.", "tokens", tokens, "limit", p.getLLM().TokenLimit()-ContextTokenMargin) - chunks := splitPlaintextOnSentences(llmFormattedTranscription, (p.getLLM().TokenLimit()-ContextTokenMargin)*4) + if tokens > tokenLimitWithMargin { + p.pluginAPI.Log.Debug("Transcription too long, summarizing in chunks.", "tokens", tokens, "limit", tokenLimitWithMargin) + chunks := splitPlaintextOnSentences(llmFormattedTranscription, tokenLimitWithMargin*4) summarizedChunks := make([]string, 0, len(chunks)) p.pluginAPI.Log.Debug("Split into chunks", "chunks", len(chunks)) for _, chunk := range chunks { - context := p.MakeConversationContext(nil, channel, nil) context.PromptParameters = map[string]string{"TranscriptionChunk": chunk} summarizeChunkPrompt, err := p.prompts.ChatCompletion(ai.PromptSummarizeChunk, context) if err != nil { - return err + return nil, errors.Wrap(err, "unable to get summarize chunk prompt") } summarizedChunk, err := p.getLLM().ChatCompletionNoStream(summarizeChunkPrompt) if err != nil { - return err + return nil, errors.Wrap(err, "unable to get summarized chunk") } summarizedChunks = append(summarizedChunks, summarizedChunk) @@ -142,44 +178,39 @@ func (p *Plugin) handleCallRecordingPost(recordingPost *model.Post, channel *mod llmFormattedTranscription = strings.Join(summarizedChunks, "\n\n") isChunked = true + p.pluginAPI.Log.Debug("Completed chunk summarization", "chunks", len(summarizedChunks), "tokens", p.getLLM().CountTokens(llmFormattedTranscription)) } - context := p.MakeConversationContext(nil, channel, nil) context.PromptParameters = map[string]string{"Transcription": llmFormattedTranscription, "IsChunked": fmt.Sprintf("%t", isChunked)} - summaryPrompt, err := p.prompts.ChatCompletion(ai.PromptMeetingSummaryOnly, context) + summaryPrompt, err := p.prompts.ChatCompletion(ai.PromptMeetingSummary, context) if err != nil { - return err - } - - keyPointsPrompt, err := p.prompts.ChatCompletion(ai.PromptMeetingKeyPoints, context) - if err != nil { - return err + return nil, errors.Wrap(err, "unable to get meeting summary prompt") } summaryStream, err := p.getLLM().ChatCompletion(summaryPrompt) if err != nil { - return err + return nil, errors.Wrap(err, "unable to get meeting summary") } - keyPointsStream, err := p.getLLM().ChatCompletion(keyPointsPrompt) - if err != nil { - return err - } - - botPost.Message = "" - template := []string{ - "# Meeting Summary\n", - "", - "\n## Key Discussion Points\n", - "", - "\n\n_Summary generated using AI, and may contain inaccuracies. Do not take this summary as absolute truth._", - } - if err := p.pluginAPI.Post.UpdatePost(botPost); err != nil { - return err - } + return summaryStream, nil +} - if err := p.multiStreamResultToPost(botPost, template, summaryStream, keyPointsStream); err != nil { - return err +func (p *Plugin) updatePostWithFile(post *model.Post, fileinfo *model.FileInfo) error { + if _, err := p.execBuilder(p.builder. + Update("FileInfo"). + Set("PostId", post.Id). + Set("ChannelId", post.ChannelId). + Where(sq.And{ + sq.Eq{"Id": fileinfo.Id}, + sq.Eq{"PostId": ""}, + })); err != nil { + return errors.Wrap(err, "unable to update file info") + } + + post.FileIds = []string{fileinfo.Id} + post.Message = "" + if err := p.pluginAPI.Post.UpdatePost(post); err != nil { + return errors.Wrap(err, "unable to update post") } return nil diff --git a/server/post_processing.go b/server/post_processing.go index f5475beb..0e20f403 100644 --- a/server/post_processing.go +++ b/server/post_processing.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sort" - "strings" "github.com/mattermost/mattermost-plugin-ai/server/ai" "github.com/mattermost/mattermost/server/public/model" @@ -16,7 +15,7 @@ type ThreadData struct { UsersByID map[string]*model.User } -func (t *ThreadData) cutoffAtPostID(postID string) { +func (t *ThreadData) cutoffBeforePostID(postID string) { for i, post := range t.Posts { if post.Id == postID { t.Posts = t.Posts[:i] @@ -25,7 +24,19 @@ func (t *ThreadData) cutoffAtPostID(postID string) { } } +func (t *ThreadData) cutoffAtPostID(postID string) { + for i, post := range t.Posts { + if post.Id == postID { + t.Posts = t.Posts[:i+1] + break + } + } +} + func (t *ThreadData) latestPost() *model.Post { + if len(t.Posts) == 0 { + return nil + } return t.Posts[len(t.Posts)-1] } @@ -78,10 +89,12 @@ func formatThread(data *ThreadData) string { return result } +const LLMRequesterUserID = "llm_requester_user_id" + func (p *Plugin) modifyPostForBot(requesterUserID string, post *model.Post) { post.UserId = p.botid post.Type = "custom_llmbot" // This must be the only place we add this type for security. - post.AddProp("llm_requester_user_id", requesterUserID) + post.AddProp(LLMRequesterUserID, requesterUserID) } func (p *Plugin) botCreatePost(requesterUserID string, post *model.Post) error { @@ -106,11 +119,11 @@ func (p *Plugin) botDM(userID string, post *model.Post) error { func (p *Plugin) streamResultToNewPost(requesterUserID string, stream *ai.TextStreamResult, post *model.Post) error { if err := p.botCreatePost(requesterUserID, post); err != nil { - return err + return errors.Wrap(err, "unable to create post") } if err := p.streamResultToPost(stream, post); err != nil { - return err + return errors.Wrap(err, "unable to stream result to post") } return nil @@ -204,7 +217,7 @@ type WorkerResult struct { Value string } -func (p *Plugin) multiStreamResultToPost(post *model.Post, messageTemplate []string, streams ...*ai.TextStreamResult) error { +/*func (p *Plugin) multiStreamResultToPost(post *model.Post, messageTemplate []string, streams ...*ai.TextStreamResult) error { if len(messageTemplate) < 2*len(streams) { return errors.New("bad multi stream template") } @@ -262,4 +275,4 @@ func (p *Plugin) multiStreamResultToPost(post *model.Post, messageTemplate []str }() return nil -} +}*/ diff --git a/server/service.go b/server/service.go index 13eb05f9..ac50e182 100644 --- a/server/service.go +++ b/server/service.go @@ -10,9 +10,9 @@ import ( ) const ( - WhisperAPILimit = 25 * 1000 * 1000 // 25 MB - ContextTokenMargin = 1000 - defaultSpellcheckLanguage = "English" + WhisperAPILimit = 25 * 1000 * 1000 // 25 MB + ContextTokenMargin = 1000 + RespondingToProp = "responding_to" ) func (p *Plugin) processUserRequestToBot(context ai.ConversationContext) error { @@ -20,7 +20,26 @@ func (p *Plugin) processUserRequestToBot(context ai.ConversationContext) error { return p.newConversation(context) } - return p.continueConversation(context) + threadData, err := p.getThreadAndMeta(context.Post.RootId) + if err != nil { + return err + } + + result, err := p.continueConversation(threadData, context) + if err != nil { + return err + } + + responsePost := &model.Post{ + ChannelId: context.Channel.Id, + RootId: context.Post.RootId, + } + responsePost.AddProp(RespondingToProp, context.Post.Id) + if err := p.streamResultToNewPost(context.RequestingUser.Id, result, responsePost); err != nil { + return err + } + + return nil } func (p *Plugin) newConversation(context ai.ConversationContext) error { @@ -72,23 +91,18 @@ func (p *Plugin) generateTitle(context ai.ConversationContext) error { return nil } -func (p *Plugin) continueConversation(context ai.ConversationContext) error { - threadData, err := p.getThreadAndMeta(context.Post.RootId) - if err != nil { - return err - } - +func (p *Plugin) continueConversation(threadData *ThreadData, context ai.ConversationContext) (*ai.TextStreamResult, error) { // Special handing for threads started by the bot in response to a summarization request. var result *ai.TextStreamResult originalThreadID, ok := threadData.Posts[0].GetProp(ThreadIDProp).(string) if ok && originalThreadID != "" && threadData.Posts[0].UserId == p.botid { threadPost, err := p.pluginAPI.Post.GetPost(originalThreadID) if err != nil { - return err + return nil, err } threadChannel, err := p.pluginAPI.Channel.Get(threadPost.ChannelId) if err != nil { - return err + return nil, err } if !p.pluginAPI.User.HasPermissionToChannel(context.Post.UserId, threadChannel.Id, model.PermissionReadChannel) || @@ -99,37 +113,29 @@ func (p *Plugin) continueConversation(context ai.ConversationContext) error { Message: "Sorry, you no longer have access to the original thread.", } if err := p.botCreatePost(context.RequestingUser.Id, responsePost); err != nil { - return err + return nil, err } - return nil + return nil, errors.New("user no longer has access to original thread") } result, err = p.continueThreadConversation(threadData, originalThreadID, context) if err != nil { - return err + return nil, err } } else { prompt, err := p.prompts.ChatCompletion(ai.PromptDirectMessageQuestion, context) if err != nil { - return err + return nil, err } prompt.AppendConversation(ai.ThreadToBotConversation(p.botid, threadData.Posts)) result, err = p.getLLM().ChatCompletion(prompt) if err != nil { - return err + return nil, err } } - responsePost := &model.Post{ - ChannelId: context.Channel.Id, - RootId: context.Post.RootId, - } - if err := p.streamResultToNewPost(context.RequestingUser.Id, result, responsePost); err != nil { - return err - } - - return nil + return result, nil } func (p *Plugin) continueThreadConversation(questionThreadData *ThreadData, originalThreadID string, context ai.ConversationContext) (*ai.TextStreamResult, error) { @@ -157,7 +163,7 @@ func (p *Plugin) continueThreadConversation(questionThreadData *ThreadData, orig const ThreadIDProp = "referenced_thread" // DM the user with a standard message. Run the inferance -func (p *Plugin) startNewSummaryThread(postIDToSummarize string, context ai.ConversationContext) (*model.Post, error) { +func (p *Plugin) summarizePost(postIDToSummarize string, context ai.ConversationContext) (*ai.TextStreamResult, error) { threadData, err := p.getThreadAndMeta(postIDToSummarize) if err != nil { return nil, err @@ -175,12 +181,30 @@ func (p *Plugin) startNewSummaryThread(postIDToSummarize string, context ai.Conv return nil, err } + return summaryStream, nil +} + +func summaryPostMessage(postIDToSummarize string, siteURL string) string { + return fmt.Sprintf("Sure, I will summarize this thread: %s/_redirect/pl/%s\n", siteURL, postIDToSummarize) +} + +func (p *Plugin) makeSummaryPost(postIDToSummarize string) *model.Post { siteURL := p.API.GetConfig().ServiceSettings.SiteURL post := &model.Post{ - Message: fmt.Sprintf("Sure, I will summarize this thread: %s/_redirect/pl/%s\n", *siteURL, postIDToSummarize), + Message: summaryPostMessage(postIDToSummarize, *siteURL), } post.AddProp(ThreadIDProp, postIDToSummarize) + return post +} + +func (p *Plugin) startNewSummaryThread(postIDToSummarize string, context ai.ConversationContext) (*model.Post, error) { + summaryStream, err := p.summarizePost(postIDToSummarize, context) + if err != nil { + return nil, err + } + + post := p.makeSummaryPost(postIDToSummarize) if err := p.streamResultToNewDM(summaryStream, context.RequestingUser.Id, post); err != nil { return nil, err } diff --git a/webapp/src/client.tsx b/webapp/src/client.tsx index fbde9443..7ccead2b 100644 --- a/webapp/src/client.tsx +++ b/webapp/src/client.tsx @@ -57,7 +57,7 @@ export async function doTranscribe(postid: string) { })); if (response.ok) { - return; + return response.json(); } throw new ClientError(Client4.url, { diff --git a/webapp/src/components/llmbot_post.tsx b/webapp/src/components/llmbot_post.tsx index 08780350..73b6368c 100644 --- a/webapp/src/components/llmbot_post.tsx +++ b/webapp/src/components/llmbot_post.tsx @@ -137,6 +137,7 @@ export const LLMBotPost = (props: Props) => { const requesterIsCurrentUser = (props.post.props?.llm_requester_user_id === currentUserId); const isThreadSummaryPost = (props.post.props?.referenced_thread && props.post.props?.referenced_thread !== ''); + const isNoShowRegen = (props.post.props?.no_regen && props.post.props?.no_regen !== ''); let permalinkView = null; if (PostMessagePreview) { // Ignore permalink if version does not exporrt PostMessagePreview @@ -150,6 +151,8 @@ export const LLMBotPost = (props: Props) => { } } + const showRegenerate = !generating && requesterIsCurrentUser && !isNoShowRegen; + return ( { {'Stop Generating'} } - { !generating && requesterIsCurrentUser && + { showRegenerate && { selectPost(result.postid, result.channelid); }; + const meetingSummary = async (postId: string) => { + const result = await doTranscribe(postId); + selectPost(result.postid, result.channelid); + }; + return ( } title='AI Actions' > summarizePost(post.id)}>{'Summarize Thread'} - doTranscribe(post.id)}>{'Summarize Meeting Audio'} + meetingSummary(post.id)}>{'Summarize Meeting Audio'} doReaction(post.id)}>{'React for me'} );