mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-14 17:10:11 +00:00
e7fa4cedad
Guard the handoff and resolve paths against double handoffs, swallowed reply errors, and a stale team snapshot. Also fail closed on turn-count errors, spend the image budget newest-first, fail boot on an empty assistant set, and keep deleted assistants recognized as AI so FAQ mining never treats their replies as human.
138 lines
7.7 KiB
SQL
138 lines
7.7 KiB
SQL
-- name: get-assistants
|
|
SELECT a.id, a.created_at, a.updated_at, a.user_id, u.first_name AS name, u.avatar_url,
|
|
a.description, a.instructions, a.guardrails, a.expectation, a.tone, a.response_length, a.max_turns, a.fallback_team_id, a.handoff_enabled, a.languages, a.enabled
|
|
FROM ai_assistants a
|
|
JOIN users u ON u.id = a.user_id AND u.deleted_at IS NULL
|
|
ORDER BY a.updated_at DESC;
|
|
|
|
-- name: get-assistant
|
|
SELECT a.id, a.created_at, a.updated_at, a.user_id, u.first_name AS name, u.avatar_url,
|
|
a.description, a.instructions, a.guardrails, a.expectation, a.tone, a.response_length, a.max_turns, a.fallback_team_id, a.handoff_enabled, a.languages, a.enabled
|
|
FROM ai_assistants a
|
|
JOIN users u ON u.id = a.user_id AND u.deleted_at IS NULL
|
|
WHERE a.id = $1;
|
|
|
|
-- name: get-assistant-by-user-id
|
|
SELECT a.id, a.created_at, a.updated_at, a.user_id, u.first_name AS name, u.avatar_url,
|
|
a.description, a.instructions, a.guardrails, a.expectation, a.tone, a.response_length, a.max_turns, a.fallback_team_id, a.handoff_enabled, a.languages, a.enabled
|
|
FROM ai_assistants a
|
|
JOIN users u ON u.id = a.user_id AND u.deleted_at IS NULL
|
|
WHERE a.user_id = $1;
|
|
|
|
-- name: get-assistant-user-ids
|
|
-- Includes deleted assistants: their old replies must still be recognized as AI-authored
|
|
-- (e.g. FAQ mining must never treat them as human answers).
|
|
SELECT id FROM users WHERE type = 'ai_assistant';
|
|
|
|
-- name: insert-assistant-user
|
|
INSERT INTO users (type, first_name, last_name, enabled)
|
|
VALUES ('ai_assistant', $1, '', true)
|
|
RETURNING id;
|
|
|
|
-- name: insert-assistant
|
|
INSERT INTO ai_assistants (user_id, description, instructions, guardrails, tone, response_length, max_turns, fallback_team_id, enabled, expectation, handoff_enabled, languages)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
RETURNING id;
|
|
|
|
-- name: update-assistant
|
|
UPDATE ai_assistants
|
|
SET description = $2, instructions = $3, guardrails = $4, tone = $5, response_length = $6, max_turns = $7, fallback_team_id = $8, enabled = $9, expectation = $10, handoff_enabled = $11, languages = $12, updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: get-assistant-expectation-by-user-id
|
|
SELECT expectation FROM ai_assistants WHERE user_id = $1;
|
|
|
|
-- name: update-assistant-user
|
|
UPDATE users SET first_name = $2, updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: soft-delete-assistant-user
|
|
UPDATE users SET deleted_at = now(), updated_at = now()
|
|
WHERE id = $1 AND type = 'ai_assistant';
|
|
|
|
-- name: delete-assistant
|
|
DELETE FROM ai_assistants WHERE id = $1;
|
|
|
|
-- name: get-assistant-tools
|
|
SELECT tool_id FROM ai_assistant_tools WHERE assistant_id = $1 ORDER BY tool_id;
|
|
|
|
-- name: get-all-assistant-tools
|
|
SELECT assistant_id, tool_id FROM ai_assistant_tools ORDER BY assistant_id, tool_id;
|
|
|
|
-- name: delete-assistant-tools
|
|
DELETE FROM ai_assistant_tools WHERE assistant_id = $1;
|
|
|
|
-- name: insert-assistant-tool
|
|
INSERT INTO ai_assistant_tools (assistant_id, tool_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (assistant_id, tool_id) DO NOTHING;
|
|
|
|
-- name: insert-ai-agent-event
|
|
INSERT INTO ai_agent_events (assistant_id, conversation_id, type) VALUES ($1, $2, $3);
|
|
|
|
-- name: get-assistant-window-stats
|
|
-- $1 = assistant user id (message sender), $2 = assistant id (events), $3 = window start, $4 = window end.
|
|
SELECT
|
|
(SELECT count(DISTINCT conversation_id) FROM conversation_messages WHERE sender_id = $1 AND type = 'outgoing' AND private = false AND created_at >= $3 AND created_at < $4 AND NOT COALESCE((meta->>'is_csat')::boolean, false)) AS conversations,
|
|
(SELECT count(*) FROM conversation_messages WHERE sender_id = $1 AND type = 'outgoing' AND private = false AND created_at >= $3 AND created_at < $4 AND NOT COALESCE((meta->>'is_csat')::boolean, false) AND NOT COALESCE((meta->>'is_confirmation')::boolean, false)) AS replies,
|
|
(SELECT count(DISTINCT conversation_id) FROM ai_agent_events WHERE assistant_id = $2 AND type = 'handoff' AND created_at >= $3 AND created_at < $4) AS handoffs,
|
|
(SELECT count(DISTINCT conversation_id) FROM ai_agent_events WHERE assistant_id = $2 AND type = 'resolve' AND created_at >= $3 AND created_at < $4) AS resolves,
|
|
(SELECT count(DISTINCT e.conversation_id) FROM ai_agent_events e JOIN conversations c ON c.id = e.conversation_id JOIN conversation_statuses s ON s.id = c.status_id
|
|
WHERE e.assistant_id = $2 AND e.type = 'resolve' AND e.created_at >= $3 AND e.created_at < $4 AND s.category <> 'resolved') AS reopened,
|
|
(SELECT count(*) FROM csat_responses cr WHERE cr.rating > 0 AND cr.created_at >= $3 AND cr.created_at < $4 AND EXISTS (
|
|
SELECT 1 FROM conversation_messages m WHERE m.conversation_id = cr.conversation_id AND m.sender_id = $1 AND m.type = 'outgoing' AND m.private = false AND NOT COALESCE((m.meta->>'is_csat')::boolean, false))) AS csat_count,
|
|
COALESCE((SELECT round(avg(cr.rating)::numeric, 2) FROM csat_responses cr WHERE cr.rating > 0 AND cr.created_at >= $3 AND cr.created_at < $4 AND EXISTS (
|
|
SELECT 1 FROM conversation_messages m WHERE m.conversation_id = cr.conversation_id AND m.sender_id = $1 AND m.type = 'outgoing' AND m.private = false AND NOT COALESCE((m.meta->>'is_csat')::boolean, false))), 0)::float8 AS csat_avg,
|
|
COALESCE((SELECT round((count(*) FILTER (WHERE cr.rating >= 4))::numeric / NULLIF(count(*), 0) * 100, 1) FROM csat_responses cr WHERE cr.rating > 0 AND cr.created_at >= $3 AND cr.created_at < $4 AND EXISTS (
|
|
SELECT 1 FROM conversation_messages m WHERE m.conversation_id = cr.conversation_id AND m.sender_id = $1 AND m.type = 'outgoing' AND m.private = false AND NOT COALESCE((m.meta->>'is_csat')::boolean, false))), 0)::float8 AS csat_positive;
|
|
|
|
-- name: count-ai-turns-since-assignment
|
|
-- Counts the assistant's public non-CSAT replies since the last assignment activity.
|
|
SELECT count(*) FROM conversation_messages
|
|
WHERE conversation_id = $1 AND sender_id = $2 AND type = 'outgoing' AND private = false
|
|
AND NOT COALESCE((meta->>'is_csat')::boolean, false)
|
|
AND NOT COALESCE((meta->>'is_confirmation')::boolean, false)
|
|
AND created_at > COALESCE((
|
|
SELECT max(created_at) FROM conversation_messages
|
|
WHERE conversation_id = $1 AND type = 'activity'
|
|
AND meta->>'activity_type' IN ('assigned_user_change', 'self_assign')
|
|
), to_timestamp(0));
|
|
|
|
-- name: get-recent-contact-conversations
|
|
SELECT c.uuid, c.reference_number, c.created_at, COALESCE(c.subject, '') AS subject, s.name AS status
|
|
FROM conversations c
|
|
JOIN conversation_statuses s ON s.id = c.status_id
|
|
WHERE c.contact_id = $1 AND c.id != $2 AND c.created_at >= now() - make_interval(days => $3)
|
|
ORDER BY c.created_at DESC
|
|
LIMIT $4;
|
|
|
|
-- name: insert-faq-suggestion
|
|
INSERT INTO ai_faq_suggestions (conversation_id, question, answer) VALUES ($1, $2, $3);
|
|
|
|
-- name: count-faq-suggestions-by-conversation
|
|
SELECT count(*) FROM ai_faq_suggestions WHERE conversation_id = $1;
|
|
|
|
-- name: get-faq-suggestions
|
|
SELECT s.id, s.created_at, s.updated_at, s.conversation_id, s.question, s.answer, s.status, s.reviewed_by_id, s.reviewed_at,
|
|
c.uuid AS conversation_uuid, c.reference_number
|
|
FROM ai_faq_suggestions s
|
|
JOIN conversations c ON c.id = s.conversation_id
|
|
WHERE ($1 = '' OR s.status = $1)
|
|
ORDER BY s.created_at DESC;
|
|
|
|
-- name: get-faq-suggestion
|
|
SELECT id, created_at, updated_at, conversation_id, question, answer, status, reviewed_by_id, reviewed_at
|
|
FROM ai_faq_suggestions WHERE id = $1;
|
|
|
|
-- name: reject-faq-suggestion-if-pending
|
|
UPDATE ai_faq_suggestions SET status = 'rejected', reviewed_by_id = $2, reviewed_at = now(), updated_at = now()
|
|
WHERE id = $1 AND status = 'pending';
|
|
|
|
-- name: revert-faq-suggestion-to-pending
|
|
UPDATE ai_faq_suggestions SET status = 'pending', reviewed_by_id = NULL, reviewed_at = NULL, updated_at = now()
|
|
WHERE id = $1 AND status = 'approved';
|
|
|
|
-- name: approve-faq-suggestion-if-pending
|
|
UPDATE ai_faq_suggestions SET status = 'approved', reviewed_by_id = $2, reviewed_at = now(), updated_at = now()
|
|
WHERE id = $1 AND status = 'pending';
|