spine-framework 0.3.108 → 0.3.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/functions/_shared/agent-runner.ts +139 -70
- package/.framework/functions/_shared/pipeline-runner.ts +31 -1
- package/.framework/functions/ai-interactions.ts +240 -0
- package/.framework/functions/embeddings.ts +426 -10
- package/.framework/functions/eval-harness.ts +353 -0
- package/.framework/functions/observability.ts +146 -1
- package/.framework/migrations/027_embedding_lifecycle.sql +176 -0
- package/.framework/migrations/028_fix_message_data_permissions.sql +49 -0
- package/.framework/migrations/029_tighten_support_triage_prompt.sql +41 -0
- package/package.json +4 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
-- Migration 027: Embedding lifecycle — approved_at, revoked_at, content_hash
|
|
2
|
+
--
|
|
3
|
+
-- Adds three columns to the embeddings table so that:
|
|
4
|
+
-- 1. Only approved embeddings are returned by match_embeddings (hard filter, not prompt-based).
|
|
5
|
+
-- 2. Embeddings are invalidated automatically when their source item is unpublished or deleted.
|
|
6
|
+
-- 3. Re-embedding is skipped when content has not changed (content_hash deduplication).
|
|
7
|
+
--
|
|
8
|
+
-- Adds filter_item_types parameter to match_embeddings RPC so agents can restrict
|
|
9
|
+
-- retrieval to specific content types (kb_article, code_chunk, db_schema, etc.) at
|
|
10
|
+
-- the DB layer rather than relying on post-processing.
|
|
11
|
+
--
|
|
12
|
+
-- Adds helper functions revoke_embeddings_for_item and approve_embeddings_for_item
|
|
13
|
+
-- called by the items_embedding_sync trigger.
|
|
14
|
+
--
|
|
15
|
+
-- IMPORTANT: approved_at defaults to now() so all existing embeddings are treated
|
|
16
|
+
-- as approved. revoked_at defaults to NULL (not revoked).
|
|
17
|
+
|
|
18
|
+
-- ============================================
|
|
19
|
+
-- COLUMNS
|
|
20
|
+
-- ============================================
|
|
21
|
+
|
|
22
|
+
ALTER TABLE public.embeddings
|
|
23
|
+
ADD COLUMN IF NOT EXISTS approved_at timestamptz DEFAULT now(),
|
|
24
|
+
ADD COLUMN IF NOT EXISTS revoked_at timestamptz DEFAULT NULL,
|
|
25
|
+
ADD COLUMN IF NOT EXISTS content_hash text DEFAULT NULL;
|
|
26
|
+
|
|
27
|
+
-- Partial index for fast active-embedding lookups (approved and not revoked)
|
|
28
|
+
CREATE INDEX IF NOT EXISTS embeddings_active_idx
|
|
29
|
+
ON public.embeddings (account_id, document_id)
|
|
30
|
+
WHERE approved_at IS NOT NULL AND revoked_at IS NULL;
|
|
31
|
+
|
|
32
|
+
-- ============================================
|
|
33
|
+
-- HELPER FUNCTIONS
|
|
34
|
+
-- ============================================
|
|
35
|
+
|
|
36
|
+
-- Revoke all embeddings for a given item (called when item leaves published state)
|
|
37
|
+
CREATE OR REPLACE FUNCTION public.revoke_embeddings_for_item(p_item_id uuid)
|
|
38
|
+
RETURNS integer
|
|
39
|
+
LANGUAGE plpgsql
|
|
40
|
+
SECURITY DEFINER
|
|
41
|
+
SET search_path = public
|
|
42
|
+
AS $$
|
|
43
|
+
DECLARE
|
|
44
|
+
v_count integer;
|
|
45
|
+
BEGIN
|
|
46
|
+
UPDATE public.embeddings
|
|
47
|
+
SET revoked_at = now()
|
|
48
|
+
WHERE document_id = p_item_id::text
|
|
49
|
+
AND revoked_at IS NULL;
|
|
50
|
+
|
|
51
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
52
|
+
RETURN v_count;
|
|
53
|
+
END;
|
|
54
|
+
$$;
|
|
55
|
+
|
|
56
|
+
-- Approve all embeddings for a given item (called when item re-enters published state)
|
|
57
|
+
CREATE OR REPLACE FUNCTION public.approve_embeddings_for_item(p_item_id uuid)
|
|
58
|
+
RETURNS integer
|
|
59
|
+
LANGUAGE plpgsql
|
|
60
|
+
SECURITY DEFINER
|
|
61
|
+
SET search_path = public
|
|
62
|
+
AS $$
|
|
63
|
+
DECLARE
|
|
64
|
+
v_count integer;
|
|
65
|
+
BEGIN
|
|
66
|
+
UPDATE public.embeddings
|
|
67
|
+
SET approved_at = now(),
|
|
68
|
+
revoked_at = NULL
|
|
69
|
+
WHERE document_id = p_item_id::text;
|
|
70
|
+
|
|
71
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
72
|
+
RETURN v_count;
|
|
73
|
+
END;
|
|
74
|
+
$$;
|
|
75
|
+
|
|
76
|
+
GRANT EXECUTE ON FUNCTION public.revoke_embeddings_for_item(uuid) TO service_role;
|
|
77
|
+
GRANT EXECUTE ON FUNCTION public.approve_embeddings_for_item(uuid) TO service_role;
|
|
78
|
+
|
|
79
|
+
-- ============================================
|
|
80
|
+
-- ITEMS → EMBEDDINGS SYNC TRIGGER
|
|
81
|
+
-- ============================================
|
|
82
|
+
-- When an item's status changes, keep its embeddings in sync automatically.
|
|
83
|
+
-- published → approve (clear revoked_at, set approved_at)
|
|
84
|
+
-- anything → revoke (set revoked_at, leave approved_at intact for audit)
|
|
85
|
+
|
|
86
|
+
CREATE OR REPLACE FUNCTION public.sync_item_embeddings()
|
|
87
|
+
RETURNS TRIGGER
|
|
88
|
+
LANGUAGE plpgsql
|
|
89
|
+
SECURITY DEFINER
|
|
90
|
+
SET search_path = public
|
|
91
|
+
AS $$
|
|
92
|
+
BEGIN
|
|
93
|
+
-- Only act on status changes
|
|
94
|
+
IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
|
|
95
|
+
RETURN NEW;
|
|
96
|
+
END IF;
|
|
97
|
+
|
|
98
|
+
IF NEW.status = 'published' THEN
|
|
99
|
+
PERFORM public.approve_embeddings_for_item(NEW.id);
|
|
100
|
+
ELSE
|
|
101
|
+
PERFORM public.revoke_embeddings_for_item(NEW.id);
|
|
102
|
+
END IF;
|
|
103
|
+
|
|
104
|
+
RETURN NEW;
|
|
105
|
+
END;
|
|
106
|
+
$$;
|
|
107
|
+
|
|
108
|
+
DROP TRIGGER IF EXISTS items_embedding_sync ON public.items;
|
|
109
|
+
CREATE TRIGGER items_embedding_sync
|
|
110
|
+
AFTER UPDATE OF status ON public.items
|
|
111
|
+
FOR EACH ROW
|
|
112
|
+
EXECUTE FUNCTION public.sync_item_embeddings();
|
|
113
|
+
|
|
114
|
+
-- ============================================
|
|
115
|
+
-- UPDATED match_embeddings RPC
|
|
116
|
+
-- ============================================
|
|
117
|
+
-- Adds filter_item_types parameter and hard approved_at / revoked_at filter.
|
|
118
|
+
-- Backward compatible: existing callers that omit filter_item_types get the same
|
|
119
|
+
-- behaviour as before, except that revoked / unapproved embeddings are now excluded.
|
|
120
|
+
|
|
121
|
+
CREATE OR REPLACE FUNCTION public.match_embeddings(
|
|
122
|
+
query_embedding vector(1536),
|
|
123
|
+
match_count int DEFAULT 10,
|
|
124
|
+
filter_account_ids uuid[] DEFAULT NULL,
|
|
125
|
+
filter_vector_type text DEFAULT NULL,
|
|
126
|
+
similarity_threshold float DEFAULT 0.15,
|
|
127
|
+
filter_item_types text[] DEFAULT NULL
|
|
128
|
+
)
|
|
129
|
+
RETURNS TABLE (
|
|
130
|
+
id uuid,
|
|
131
|
+
document_id text,
|
|
132
|
+
content text,
|
|
133
|
+
metadata jsonb,
|
|
134
|
+
account_id uuid,
|
|
135
|
+
similarity float
|
|
136
|
+
)
|
|
137
|
+
LANGUAGE plpgsql
|
|
138
|
+
SECURITY DEFINER
|
|
139
|
+
SET search_path = public
|
|
140
|
+
AS $$
|
|
141
|
+
BEGIN
|
|
142
|
+
RETURN QUERY
|
|
143
|
+
SELECT
|
|
144
|
+
e.id,
|
|
145
|
+
e.document_id,
|
|
146
|
+
e.content,
|
|
147
|
+
e.metadata,
|
|
148
|
+
e.account_id,
|
|
149
|
+
1 - (e.embedding <=> query_embedding) AS similarity
|
|
150
|
+
FROM public.embeddings e
|
|
151
|
+
WHERE
|
|
152
|
+
-- ── Hard lifecycle filter — never negotiable ──────────────────────────
|
|
153
|
+
e.approved_at IS NOT NULL
|
|
154
|
+
AND e.revoked_at IS NULL
|
|
155
|
+
|
|
156
|
+
-- ── Account scope (NULL = no account filter — platform-wide search) ───
|
|
157
|
+
AND (filter_account_ids IS NULL OR e.account_id = ANY(filter_account_ids))
|
|
158
|
+
|
|
159
|
+
-- ── Vector type (NULL = all types) ────────────────────────────────────
|
|
160
|
+
AND (filter_vector_type IS NULL OR e.metadata->>'vector_type' = filter_vector_type)
|
|
161
|
+
|
|
162
|
+
-- ── Item type (NULL = all item types) ─────────────────────────────────
|
|
163
|
+
AND (filter_item_types IS NULL OR e.metadata->>'item_type' = ANY(filter_item_types))
|
|
164
|
+
|
|
165
|
+
-- ── Similarity gate ───────────────────────────────────────────────────
|
|
166
|
+
AND (1 - (e.embedding <=> query_embedding)) >= similarity_threshold
|
|
167
|
+
|
|
168
|
+
ORDER BY e.embedding <=> query_embedding
|
|
169
|
+
LIMIT match_count;
|
|
170
|
+
END;
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
-- Re-grant execute to all roles (replace = drop+create resets grants)
|
|
174
|
+
GRANT EXECUTE ON FUNCTION public.match_embeddings(vector, int, uuid[], text, float, text[]) TO authenticated;
|
|
175
|
+
GRANT EXECUTE ON FUNCTION public.match_embeddings(vector, int, uuid[], text, float, text[]) TO service_role;
|
|
176
|
+
GRANT EXECUTE ON FUNCTION public.match_embeddings(vector, int, uuid[], text, float, text[]) TO anon;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- Migration 028: Add AI metadata fields to support_reply/community_reply design_schema
|
|
2
|
+
--
|
|
3
|
+
-- Existing message rows were stamped with the old design_schema, which did not
|
|
4
|
+
-- declare permissions for AI metadata fields (message_type, confidence, retrieved_sources,
|
|
5
|
+
-- feedback, etc.). The permission sanitizer therefore stripped those fields from API
|
|
6
|
+
-- responses, hiding the AI label, confidence badge, sources, and feedback UI.
|
|
7
|
+
--
|
|
8
|
+
-- This migration updates both the type definitions and the stamped design_schema on
|
|
9
|
+
-- every existing message record so the new fields are readable (and feedback writable
|
|
10
|
+
-- by members) without loosening the permission model.
|
|
11
|
+
|
|
12
|
+
DO $$
|
|
13
|
+
DECLARE
|
|
14
|
+
ai_fields jsonb := '{
|
|
15
|
+
"message_type": { "data_type": "text", "label": "Message Type", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
16
|
+
"confidence": { "data_type": "number", "label": "AI Confidence", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
17
|
+
"retrieval_confidence": { "data_type": "number", "label": "Retrieval Confidence", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
18
|
+
"retrieved_sources": { "data_type": "json", "label": "Retrieved Sources", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
19
|
+
"agent_id": { "data_type": "text", "label": "Agent ID", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
20
|
+
"prompt_config_id": { "data_type": "text", "label": "Prompt Config ID", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
21
|
+
"sender_type": { "data_type": "text", "label": "Sender Type", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
22
|
+
"feedback": { "data_type": "text", "label": "Feedback", "system": true, "permissions": { "system_admin": ["read", "write"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read", "write"], "member": ["read", "write"] } },
|
|
23
|
+
"feedback_at": { "data_type": "datetime", "label": "Feedback At", "system": true, "permissions": { "system_admin": ["read", "write"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read", "write"], "member": ["read", "write"] } },
|
|
24
|
+
"escalated": { "data_type": "boolean", "label": "Escalated", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
25
|
+
"suggested_title": { "data_type": "text", "label": "Suggested Title", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } },
|
|
26
|
+
"tool_calls": { "data_type": "json", "label": "Tool Calls", "system": true, "permissions": { "system_admin": ["read"], "support_admin": ["read"], "support": ["read"], "member_admin": ["read"], "member": ["read"] } }
|
|
27
|
+
}'::jsonb;
|
|
28
|
+
BEGIN
|
|
29
|
+
-- Update type definitions
|
|
30
|
+
UPDATE types
|
|
31
|
+
SET design_schema = jsonb_set(
|
|
32
|
+
design_schema,
|
|
33
|
+
'{fields}',
|
|
34
|
+
COALESCE(design_schema->'fields', '{}'::jsonb) || ai_fields
|
|
35
|
+
)
|
|
36
|
+
WHERE slug IN ('support_reply', 'community_reply')
|
|
37
|
+
AND kind = 'message';
|
|
38
|
+
|
|
39
|
+
-- Update stamped design_schema on existing messages
|
|
40
|
+
UPDATE messages
|
|
41
|
+
SET design_schema = jsonb_set(
|
|
42
|
+
design_schema,
|
|
43
|
+
'{fields}',
|
|
44
|
+
COALESCE(design_schema->'fields', '{}'::jsonb) || ai_fields
|
|
45
|
+
)
|
|
46
|
+
WHERE type_id IN (
|
|
47
|
+
SELECT id FROM types WHERE slug IN ('support_reply', 'community_reply') AND kind = 'message'
|
|
48
|
+
);
|
|
49
|
+
END $$;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Migration 029: Tighten the support_triage prompt so the AI escalates when no relevant knowledge is retrieved.
|
|
2
|
+
--
|
|
3
|
+
-- The previous prompt told the agent to "respond helpfully and accurately" without
|
|
4
|
+
-- explicitly forbidding general-knowledge answers or clarifying questions. This led to
|
|
5
|
+
-- the AI asking questions even when the knowledge base was empty. This migration
|
|
6
|
+
-- updates both the prompt_config and the agent's system_prompt to enforce:
|
|
7
|
+
-- 1. answer only from retrieved knowledge
|
|
8
|
+
-- 2. escalate immediately when no relevant knowledge is retrieved
|
|
9
|
+
-- 3. never ask the customer clarifying questions
|
|
10
|
+
|
|
11
|
+
UPDATE prompt_configs
|
|
12
|
+
SET context_template = 'A customer has submitted the following support request:
|
|
13
|
+
|
|
14
|
+
{{user_message}}
|
|
15
|
+
|
|
16
|
+
You may only answer using the relevant information provided in this prompt under ''## Relevant Information''.
|
|
17
|
+
|
|
18
|
+
Rules:
|
|
19
|
+
- If no relevant information is provided, or if it is insufficient to answer confidently, you MUST escalate to a human agent.
|
|
20
|
+
- Do not ask the customer clarifying questions.
|
|
21
|
+
- Do not answer from general knowledge or make up information.
|
|
22
|
+
- Be concise, accurate, and empathetic.
|
|
23
|
+
|
|
24
|
+
If you escalate, write a brief message telling the customer that a human support agent will review their request and follow up shortly.',
|
|
25
|
+
output_mode = 'text',
|
|
26
|
+
updated_at = now()
|
|
27
|
+
WHERE slug = 'support_triage';
|
|
28
|
+
|
|
29
|
+
UPDATE ai_agents
|
|
30
|
+
SET system_prompt = 'You are a first-line support agent for a software platform. Your goal is to resolve customer issues accurately and efficiently on the first response, using only the knowledge retrieved for you.
|
|
31
|
+
|
|
32
|
+
Guidelines:
|
|
33
|
+
- Be concise and clear. Customers want answers, not essays.
|
|
34
|
+
- You may only answer using the retrieved knowledge base articles and prior cases provided in your context.
|
|
35
|
+
- If no relevant knowledge is retrieved, or if it is insufficient to answer confidently, escalate to a human agent immediately.
|
|
36
|
+
- Do not ask the customer clarifying questions.
|
|
37
|
+
- Never fabricate feature behaviour, make promises about the product, or answer from general knowledge.
|
|
38
|
+
- If you escalate, tell the customer that a human support agent will review their request and follow up shortly.
|
|
39
|
+
- Tone: professional but warm. Match the customer''s level of formality.',
|
|
40
|
+
updated_at = now()
|
|
41
|
+
WHERE name = 'Support Triage Agent';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-framework",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.110",
|
|
4
4
|
"description": "Multi-tenant, modular application platform for modern SaaS systems",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
".framework/migrations/024_messages_account_id.sql",
|
|
34
34
|
".framework/migrations/025_retype_threads_messages.sql",
|
|
35
35
|
".framework/migrations/026_fix_rls_policies.sql",
|
|
36
|
+
".framework/migrations/027_embedding_lifecycle.sql",
|
|
37
|
+
".framework/migrations/028_fix_message_data_permissions.sql",
|
|
38
|
+
".framework/migrations/029_tighten_support_triage_prompt.sql",
|
|
36
39
|
".framework/public/",
|
|
37
40
|
".framework/scripts/",
|
|
38
41
|
".framework/seeds/",
|