mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
551 lines
18 KiB
Go
551 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/abhinavxd/libredesk/internal/ai"
|
|
aimodels "github.com/abhinavxd/libredesk/internal/ai/models"
|
|
"github.com/abhinavxd/libredesk/internal/aiagent"
|
|
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
|
|
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
"github.com/abhinavxd/libredesk/internal/stringutil"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
// maxTranscriptMessages bounds how many recent messages are fed to the LLM as context.
|
|
const maxTranscriptMessages = 50
|
|
|
|
// maxCopilotHistoryMessages bounds how many persisted copilot turns are fed back to the LLM.
|
|
const maxCopilotHistoryMessages = 50
|
|
|
|
type aiCompletionReq struct {
|
|
PromptKey string `json:"prompt_key"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type generateReplyReq struct {
|
|
ConversationUUID string `json:"conversation_uuid"`
|
|
Instruction string `json:"instruction"`
|
|
}
|
|
|
|
type copilotReq struct {
|
|
ConversationUUID string `json:"conversation_uuid"`
|
|
Message string `json:"message"`
|
|
AssistantID int `json:"assistant_id"`
|
|
}
|
|
|
|
type summarizeReq struct {
|
|
ConversationUUID string `json:"conversation_uuid"`
|
|
}
|
|
|
|
type suggestTagsReq struct {
|
|
ConversationUUID string `json:"conversation_uuid"`
|
|
}
|
|
|
|
type promptReq struct {
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type snippetReq struct {
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type snippetImportReq struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// handleAICompletion runs a stored prompt over the supplied content (reply-box actions).
|
|
func handleAICompletion(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req = aiCompletionReq{}
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
resp, err := app.ai.Completion(r.RequestCtx, req.PromptKey, req.Content)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(resp)
|
|
}
|
|
|
|
// handleGetAIPrompts returns the agent-facing prompts.
|
|
func handleGetAIPrompts(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
resp, err := app.ai.GetPrompts()
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(resp)
|
|
}
|
|
|
|
func handleGetAIPrompt(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
p, err := app.ai.GetPromptByID(id)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(p)
|
|
}
|
|
|
|
func handleCreateAIPrompt(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req promptReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
p, err := app.ai.CreatePrompt(req.Title, req.Content)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(p)
|
|
}
|
|
|
|
func handleUpdateAIPrompt(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req promptReq
|
|
)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
p, err := app.ai.UpdatePrompt(id, req.Title, req.Content)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(p)
|
|
}
|
|
|
|
func handleDeleteAIPrompt(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := app.ai.DeletePrompt(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleGetAIConfig returns the sanitized provider config for a type (completion/embedding).
|
|
func handleGetAIConfig(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
providerType := r.RequestCtx.UserValue("type").(string)
|
|
cfg, err := app.ai.GetProviderConfig(providerType)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(cfg)
|
|
}
|
|
|
|
// handleUpdateAIConfig updates the provider config for a type.
|
|
func handleUpdateAIConfig(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
providerType = r.RequestCtx.UserValue("type").(string)
|
|
req aimodels.ProviderConfig
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if err := app.ai.UpdateProviderConfig(providerType, req); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleTestAIConfig makes one live provider request with the submitted config.
|
|
func handleTestAIConfig(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
providerType = r.RequestCtx.UserValue("type").(string)
|
|
req aimodels.ProviderConfig
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if err := app.ai.TestProviderConfig(providerType, req); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleGetAITools returns all custom tools (auth secrets masked).
|
|
func handleGetAITools(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
tools, err := app.ai.GetTools()
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(tools)
|
|
}
|
|
|
|
func handleGetAITool(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
tool, err := app.ai.GetTool(id)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(tool)
|
|
}
|
|
|
|
func handleCreateAITool(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req aimodels.Tool
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
tool, err := app.ai.CreateTool(req)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(tool)
|
|
}
|
|
|
|
func handleUpdateAITool(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req aimodels.Tool
|
|
)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
tool, err := app.ai.UpdateTool(id, req)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(tool)
|
|
}
|
|
|
|
func handleDeleteAITool(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := app.ai.DeleteTool(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleGetAISnippets returns the knowledge base snippets.
|
|
func handleGetAISnippets(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
items, err := app.ai.GetKnowledgeBaseItems()
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(items)
|
|
}
|
|
|
|
func handleCreateAISnippet(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req snippetReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
item, err := app.ai.CreateKnowledgeBaseItem(req.Title, req.Content, aimodels.KnowledgeSourceManual, "", req.Enabled)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(item)
|
|
}
|
|
|
|
func handleUpdateAISnippet(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req snippetReq
|
|
)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
item, err := app.ai.UpdateKnowledgeBaseItem(id, req.Title, req.Content, req.Enabled)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(item)
|
|
}
|
|
|
|
// handleImportAISnippetFromURL fetches a page and adds its readable content as a snippet.
|
|
func handleImportAISnippetFromURL(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req snippetImportReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
item, err := app.ai.ImportKnowledgeBaseFromURL(r.RequestCtx, req.URL)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(item)
|
|
}
|
|
|
|
func handleDeleteAISnippet(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := app.ai.DeleteKnowledgeBaseItem(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleAIGenerateReply drafts a reply to a conversation using the agentic loop.
|
|
func handleAIGenerateReply(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req generateReplyReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
transcript := ""
|
|
var conv *cmodels.Conversation
|
|
if req.ConversationUUID != "" {
|
|
conv, err = enforceAIConversationAccess(r, req.ConversationUUID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
transcript = conversationTranscript(app, req.ConversationUUID)
|
|
}
|
|
resp, err := app.ai.GenerateReply(r.RequestCtx, transcript, req.Instruction, ai.ToolContext{}, generateReplyTools(app, user, conv))
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if strings.TrimSpace(resp) == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil))
|
|
}
|
|
return r.SendEnvelope(stringutil.Markdown2HTML(resp))
|
|
}
|
|
|
|
// handleAISummarizeConversation summarizes a conversation and posts the summary as the requesting agent's private note.
|
|
func handleAISummarizeConversation(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req summarizeReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if req.ConversationUUID == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if _, err := enforceAIConversationAccess(r, req.ConversationUUID); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
transcript := conversationTranscript(app, req.ConversationUUID)
|
|
if strings.TrimSpace(transcript) == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("ai.summarizeEmptyConversation"), nil))
|
|
}
|
|
summary, err := app.ai.Summarize(r.RequestCtx, transcript)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if strings.TrimSpace(summary) == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil))
|
|
}
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
note := "**" + app.i18n.T("ai.summaryNoteTitle") + "**\n\n" + summary
|
|
if _, err := app.conversation.SendPrivateNote(nil, auser.ID, req.ConversationUUID, stringutil.Markdown2HTML(note), nil); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleAISuggestTags suggests up to 3 existing tags for a conversation. Read-only: it never applies them.
|
|
func handleAISuggestTags(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req suggestTagsReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if req.ConversationUUID == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
if _, err := enforceAIConversationAccess(r, req.ConversationUUID); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
transcript := conversationTranscript(app, req.ConversationUUID)
|
|
if strings.TrimSpace(transcript) == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("ai.tagSuggestEmptyConversation"), nil))
|
|
}
|
|
|
|
suggestions, err := app.ai.SuggestTags(r.RequestCtx, transcript)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(suggestions)
|
|
}
|
|
|
|
// handleAICopilot answers an agent's copilot chat message.
|
|
func handleAICopilot(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
req copilotReq
|
|
)
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
req.Message = strings.TrimSpace(req.Message)
|
|
if req.Message == "" || req.ConversationUUID == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
conv, err := enforceAIConversationAccess(r, req.ConversationUUID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
persona := ""
|
|
if req.AssistantID > 0 {
|
|
assistant, err := app.aiAgent.GetAssistant(req.AssistantID)
|
|
if err != nil || !assistant.Enabled {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("ai.assistantUnavailable"), nil))
|
|
}
|
|
persona = aiagent.BuildCopilotPersona(assistant)
|
|
}
|
|
|
|
saved, err := app.ai.GetCopilotMessages(conv.ID, auser.ID, maxCopilotHistoryMessages)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
history := make([]aimodels.ChatMessage, 0, len(saved)+1)
|
|
for _, m := range saved {
|
|
history = append(history, aimodels.ChatMessage{Role: m.Role, Content: m.Content})
|
|
}
|
|
history = append(history, aimodels.ChatMessage{Role: aimodels.RoleUser, Content: req.Message})
|
|
|
|
convoContext := conversationTranscript(app, req.ConversationUUID)
|
|
resp, err := app.ai.Copilot(r.RequestCtx, convoContext, history, ai.ToolContext{}, copilotTools(app, user, conv), persona)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if strings.TrimSpace(resp) == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil))
|
|
}
|
|
// Persist the exchange only after a successful reply, so a failed or empty call leaves no orphaned turn.
|
|
if err := app.ai.SaveCopilotMessage(conv.ID, auser.ID, aimodels.RoleUser, req.Message); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if err := app.ai.SaveCopilotMessage(conv.ID, auser.ID, aimodels.RoleAssistant, resp); err != nil {
|
|
app.lo.Error("error saving copilot reply", "error", err)
|
|
}
|
|
return r.SendEnvelope(stringutil.Markdown2HTML(resp))
|
|
}
|
|
|
|
// handleGetCopilotMessages returns the requesting agent's persisted copilot chat for a conversation.
|
|
func handleGetCopilotMessages(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
uuid := string(r.RequestCtx.QueryArgs().Peek("conversation_uuid"))
|
|
if uuid == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
conv, err := enforceAIConversationAccess(r, uuid)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
msgs, err := app.ai.GetCopilotMessages(conv.ID, auser.ID, maxCopilotHistoryMessages)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
// Assistant turns are stored as markdown; the panel renders HTML.
|
|
for i := range msgs {
|
|
if msgs[i].Role == aimodels.RoleAssistant {
|
|
msgs[i].Content = stringutil.Markdown2HTML(msgs[i].Content)
|
|
}
|
|
}
|
|
return r.SendEnvelope(msgs)
|
|
}
|
|
|
|
// handleClearCopilotMessages clears the requesting agent's copilot chat for a conversation.
|
|
func handleClearCopilotMessages(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
uuid := string(r.RequestCtx.QueryArgs().Peek("conversation_uuid"))
|
|
if uuid == "" {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("errors.parsingRequest"), nil))
|
|
}
|
|
conv, err := enforceAIConversationAccess(r, uuid)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
if err := app.ai.ClearCopilotMessages(conv.ID, auser.ID); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// enforceAIConversationAccess checks the requesting agent can access the conversation whose transcript is being fed to the LLM.
|
|
func enforceAIConversationAccess(r *fastglue.Request, uuid string) (*cmodels.Conversation, error) {
|
|
app := r.Context.(*App)
|
|
auser := r.RequestCtx.UserValue("user").(amodels.User)
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return enforceConversationAccess(app, uuid, user)
|
|
}
|
|
|
|
// conversationTranscript builds a plaintext transcript of a conversation's public messages for use as AI context.
|
|
func conversationTranscript(app *App, uuid string) string {
|
|
private := false
|
|
msgs, err := app.conversation.GetAllConversationMessages(uuid, &private, []string{cmodels.MessageIncoming, cmodels.MessageOutgoing}, maxTranscriptMessages)
|
|
if err != nil {
|
|
app.lo.Error("error building conversation transcript for AI", "error", err)
|
|
return ""
|
|
}
|
|
return cmodels.Transcript(msgs, maxTranscriptMessages)
|
|
}
|