Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Queue flag filters for staff inbox (agent 8).
|
||||
const (
|
||||
FlagNeedsHuman = "needs_human"
|
||||
FlagAIDraft = "ai_draft"
|
||||
)
|
||||
|
||||
// ApproveAIDraftInput is the staff approve payload for draft_only AI replies.
|
||||
type ApproveAIDraftInput struct {
|
||||
Body string `json:"body"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
// ApplyQueueFlag appends WHERE clauses for staff auto-assist filters.
|
||||
// Unknown flags are ignored (fail open on filter only — never broaden visibility).
|
||||
func ApplyQueueFlag(f *ListFilter, args *[]any, where *string) {
|
||||
if f == nil || args == nil || where == nil {
|
||||
return
|
||||
}
|
||||
flag := strings.ToLower(strings.TrimSpace(f.Flag))
|
||||
switch flag {
|
||||
case FlagAIDraft:
|
||||
*where += ` AND COALESCE(t.auto_reply_status, 'none') = 'ai_draft'`
|
||||
case FlagNeedsHuman:
|
||||
*where += ` AND COALESCE(t.auto_reply_status, 'none') IN ('handed_off','failed','skipped')`
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func findAIDraftMessage(msgs []Message, preferredID *uuid.UUID) *Message {
|
||||
if preferredID != nil {
|
||||
for i := range msgs {
|
||||
if msgs[i].ID == *preferredID {
|
||||
return &msgs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
m := &msgs[i]
|
||||
if !m.IsInternalNote {
|
||||
continue
|
||||
}
|
||||
src := strings.ToLower(strings.TrimSpace(m.AutoSource))
|
||||
if m.IsAutoReply && (src == "ai" || src == "") {
|
||||
return m
|
||||
}
|
||||
if src == "ai" {
|
||||
return m
|
||||
}
|
||||
}
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
m := &msgs[i]
|
||||
if m.IsInternalNote && m.IsAutoReply {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApproveAIDraft posts an edited (or original) AI draft as a public staff reply.
|
||||
// Enforces the same ticket visibility as other desk mutations via the caller.
|
||||
func (s *Service) ApproveAIDraft(ctx context.Context, agentUserID, ticketID uuid.UUID, in ApproveAIDraftInput) (Ticket, error) {
|
||||
t, err := s.GetAdmin(ctx, ticketID)
|
||||
if err != nil {
|
||||
return Ticket{}, err
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(t.AutoReplyStatus)) != AutoReplyAIDraft {
|
||||
return Ticket{}, ErrNoAIDraft
|
||||
}
|
||||
|
||||
body := strings.TrimSpace(in.Body)
|
||||
if body == "" {
|
||||
draft := findAIDraftMessage(t.Messages, t.AutoReplyMessageID)
|
||||
if draft == nil {
|
||||
return Ticket{}, ErrNoAIDraft
|
||||
}
|
||||
body = draft.Body
|
||||
}
|
||||
|
||||
out, err := s.ReplyAsAgent(ctx, agentUserID, ticketID, ReplyInput{
|
||||
Body: body,
|
||||
Status: in.Status,
|
||||
})
|
||||
if err != nil {
|
||||
return Ticket{}, err
|
||||
}
|
||||
_ = out
|
||||
|
||||
meta, _ := json.Marshal(map[string]any{
|
||||
"action": "approve_ai_draft",
|
||||
"by": agentUserID.String(),
|
||||
})
|
||||
_ = s.RecordAutoReplyOutcome(ctx, ticketID, AutoReplyAISent, true, nil, meta, ActivityAISent)
|
||||
|
||||
return s.GetAdmin(ctx, ticketID)
|
||||
}
|
||||
|
||||
// DiscardAIDraft marks the ticket as handed off, disables further auto, and keeps the internal note.
|
||||
func (s *Service) DiscardAIDraft(ctx context.Context, agentUserID, ticketID uuid.UUID) (Ticket, error) {
|
||||
t, err := s.GetAdmin(ctx, ticketID)
|
||||
if err != nil {
|
||||
return Ticket{}, err
|
||||
}
|
||||
status := strings.ToLower(strings.TrimSpace(t.AutoReplyStatus))
|
||||
draft := findAIDraftMessage(t.Messages, t.AutoReplyMessageID)
|
||||
if status != AutoReplyAIDraft && draft == nil {
|
||||
return Ticket{}, ErrNoAIDraft
|
||||
}
|
||||
|
||||
disabled := true
|
||||
if _, err := s.UpdateAdmin(ctx, ticketID, agentUserID, AdminUpdateInput{
|
||||
AutoReplyDisabled: &disabled,
|
||||
}); err != nil {
|
||||
return Ticket{}, err
|
||||
}
|
||||
|
||||
meta, _ := json.Marshal(map[string]any{
|
||||
"action": "discard_ai_draft",
|
||||
"by": agentUserID.String(),
|
||||
})
|
||||
if err := s.RecordAutoReplyOutcome(ctx, ticketID, AutoReplyHandedOff, true, t.AutoReplyMessageID, meta, ActivityHandedOff); err != nil {
|
||||
return Ticket{}, err
|
||||
}
|
||||
return s.GetAdmin(ctx, ticketID)
|
||||
}
|
||||
Reference in New Issue
Block a user