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:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
@@ -0,0 +1,221 @@
package support
import (
"context"
"errors"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// AutoReplyTimeout is the hard deadline for one LLM auto-reply attempt.
const AutoReplyTimeout = 25 * time.Second
// AutoClaim is a successful idempotency claim for posting one auto reply.
type AutoClaim struct {
TicketID uuid.UUID
CompanyID uuid.UUID
}
// ClaimAutoReplyAttempt marks the ticket as in-progress for auto-reply if no public
// auto message exists yet and auto is not disabled. Concurrent claims fail with
// ErrAutoReplyAlreadyPosted.
func (s *Service) ClaimAutoReplyAttempt(ctx context.Context, ticketID uuid.UUID) (AutoClaim, error) {
if s == nil || s.Pool == nil {
return AutoClaim{}, ErrAIAutoReplyDisabled
}
tx, err := s.Pool.Begin(ctx)
if err != nil {
return AutoClaim{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
var (
companyID uuid.UUID
disabled bool
status string
msgID *uuid.UUID
tStatus string
)
err = tx.QueryRow(ctx, `
SELECT company_id,
COALESCE(auto_reply_disabled, false),
COALESCE(auto_reply_status, 'none'),
auto_reply_message_id,
status
FROM support_tickets
WHERE id = $1
FOR UPDATE`, ticketID,
).Scan(&companyID, &disabled, &status, &msgID, &tStatus)
if errors.Is(err, pgx.ErrNoRows) {
return AutoClaim{}, ErrNotFound
}
if err != nil {
return AutoClaim{}, err
}
if disabled {
return AutoClaim{}, ErrAutoReplyDisabled
}
if tStatus == "closed" || tStatus == "resolved" {
return AutoClaim{}, ErrTicketClosed
}
if msgID != nil || status == "matched" || status == "ai_sent" || status == "ai_draft" {
return AutoClaim{}, ErrAutoReplyAlreadyPosted
}
var existing uuid.UUID
err = tx.QueryRow(ctx, `
SELECT id FROM support_messages
WHERE ticket_id = $1 AND company_id = $2
AND is_auto_reply = true AND is_internal_note = false
LIMIT 1`, ticketID, companyID,
).Scan(&existing)
if err == nil {
return AutoClaim{}, ErrAutoReplyAlreadyPosted
}
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return AutoClaim{}, err
}
now := time.Now().UTC()
tag, err := tx.Exec(ctx, `
UPDATE support_tickets
SET auto_reply_attempted_at = $2, updated_at = $2
WHERE id = $1 AND company_id = $3
AND auto_reply_message_id IS NULL
AND COALESCE(auto_reply_disabled, false) = false`,
ticketID, now, companyID,
)
if err != nil {
return AutoClaim{}, err
}
if tag.RowsAffected() == 0 {
return AutoClaim{}, ErrAutoReplyAlreadyPosted
}
if err := tx.Commit(ctx); err != nil {
return AutoClaim{}, err
}
return AutoClaim{TicketID: ticketID, CompanyID: companyID}, nil
}
// InsertAutoSystemMessage posts a labeled system auto-reply scoped to claim.CompanyID.
// Unique partial index prevents double public auto posts under races.
func (s *Service) InsertAutoSystemMessage(
ctx context.Context,
claim AutoClaim,
body, source string,
confidence float64,
refType string,
refID *uuid.UUID,
internal bool,
) (uuid.UUID, error) {
if s == nil || s.Pool == nil {
return uuid.Nil, ErrAIAutoReplyDisabled
}
body = sanitizeAutoBody(body)
if body == "" {
return uuid.Nil, ErrBodyRequired
}
if source != "kb" && source != "template" && source != "ai" {
source = "ai"
}
tx, err := s.Pool.Begin(ctx)
if err != nil {
return uuid.Nil, err
}
defer func() { _ = tx.Rollback(ctx) }()
var companyID uuid.UUID
var disabled bool
var msgID *uuid.UUID
err = tx.QueryRow(ctx, `
SELECT company_id, COALESCE(auto_reply_disabled, false), auto_reply_message_id
FROM support_tickets WHERE id = $1 FOR UPDATE`, claim.TicketID,
).Scan(&companyID, &disabled, &msgID)
if errors.Is(err, pgx.ErrNoRows) {
return uuid.Nil, ErrNotFound
}
if err != nil {
return uuid.Nil, err
}
if companyID != claim.CompanyID {
return uuid.Nil, ErrForbidden
}
if disabled {
return uuid.Nil, ErrAutoReplyDisabled
}
if msgID != nil && !internal {
return uuid.Nil, ErrAutoReplyAlreadyPosted
}
now := time.Now().UTC()
var refTypeArg any
if strings.TrimSpace(refType) == "" {
refTypeArg = nil
} else {
refTypeArg = strings.TrimSpace(refType)
}
var newID uuid.UUID
err = tx.QueryRow(ctx, `
INSERT INTO support_messages (
ticket_id, company_id, author_user_id, author_role, body, is_internal_note,
created_at, is_auto_reply, auto_source, auto_confidence, auto_ref_type, auto_ref_id
) VALUES ($1,$2,NULL,'system',$3,$4,$5,true,$6,$7,$8,$9)
RETURNING id`,
claim.TicketID, claim.CompanyID, body, internal, now, source, confidence, refTypeArg, refID,
).Scan(&newID)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return uuid.Nil, ErrAutoReplyAlreadyPosted
}
return uuid.Nil, err
}
status := "ai_draft"
if !internal {
status = "ai_sent"
if source == "kb" || source == "template" {
status = "matched"
}
_, err = tx.Exec(ctx, `
UPDATE support_tickets
SET auto_reply_status = $2,
auto_reply_message_id = $3,
status = CASE WHEN status = 'open' THEN 'pending' ELSE status END,
last_message_at = $4,
last_agent_message_at = $4,
updated_at = $4
WHERE id = $1 AND company_id = $5`,
claim.TicketID, status, newID, now, claim.CompanyID,
)
} else {
_, err = tx.Exec(ctx, `
UPDATE support_tickets
SET auto_reply_status = $2, updated_at = $3
WHERE id = $1 AND company_id = $4`,
claim.TicketID, status, now, claim.CompanyID,
)
}
if err != nil {
return uuid.Nil, err
}
if err := tx.Commit(ctx); err != nil {
return uuid.Nil, err
}
return newID, nil
}
func sanitizeAutoBody(body string) string {
body = strings.TrimSpace(strings.ReplaceAll(body, "\x00", ""))
if utf8.RuneCountInString(body) <= maxBodyLen {
return body
}
return string([]rune(body)[:maxBodyLen])
}