@nextsparkjs/plugin-ai 0.1.0-beta.74 → 0.1.0-beta.76
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/api/generate/route.ts +7 -2
- package/entities/ai-history/ai-history.config.ts +2 -1
- package/entities/ai-history/migrations/001_ai_history_table.sql +55 -52
- package/entities/ai-history/migrations/002_ai_history_metas.sql +29 -29
- package/lib/save-example.ts +77 -53
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +0 -1
package/api/generate/route.ts
CHANGED
|
@@ -80,20 +80,25 @@ const postHandler = async (request: NextRequest) => {
|
|
|
80
80
|
|
|
81
81
|
// 8. Save example if requested (opt-in)
|
|
82
82
|
if (saveExample) {
|
|
83
|
+
const teamId = request.headers.get('x-team-id') || ''
|
|
83
84
|
await saveExampleSafely(
|
|
84
85
|
{
|
|
85
86
|
prompt,
|
|
86
87
|
response: result.text,
|
|
87
88
|
model: selectedModel.modelName,
|
|
88
89
|
status: 'completed',
|
|
90
|
+
operation: 'generate',
|
|
91
|
+
provider: selectedModel.provider,
|
|
89
92
|
metadata: {
|
|
90
93
|
tokens: tokens.total,
|
|
94
|
+
tokensInput: tokens.input,
|
|
95
|
+
tokensOutput: tokens.output,
|
|
91
96
|
cost,
|
|
92
|
-
provider: selectedModel.provider,
|
|
93
97
|
isLocal: selectedModel.isLocal
|
|
94
98
|
}
|
|
95
99
|
},
|
|
96
|
-
authResult.user!.id
|
|
100
|
+
authResult.user!.id,
|
|
101
|
+
teamId
|
|
97
102
|
)
|
|
98
103
|
}
|
|
99
104
|
|
|
@@ -17,7 +17,8 @@ export const aiHistoryEntityConfig: EntityConfig = {
|
|
|
17
17
|
// ==========================================
|
|
18
18
|
// 1. BASIC IDENTIFICATION
|
|
19
19
|
// ==========================================
|
|
20
|
-
slug: 'ai-history',
|
|
20
|
+
slug: 'ai-history',
|
|
21
|
+
tableName: 'ai_history', // Explicit table name (underscores, not hyphens)
|
|
21
22
|
enabled: true,
|
|
22
23
|
names: {
|
|
23
24
|
singular: 'AI History',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- =============================================
|
|
2
2
|
-- AI HISTORY ENTITY MIGRATION - PLUGIN ENTITY
|
|
3
3
|
-- Plugin: AI
|
|
4
|
-
-- Entity:
|
|
4
|
+
-- Entity: ai_history
|
|
5
5
|
-- Generic AI interaction history for all plugins
|
|
6
6
|
-- Date: 2025-10-03
|
|
7
7
|
-- =============================================
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
-- MAIN AI_HISTORY TABLE
|
|
11
11
|
-- =============================================
|
|
12
12
|
|
|
13
|
-
DROP TABLE IF EXISTS "
|
|
13
|
+
DROP TABLE IF EXISTS "ai_history" CASCADE;
|
|
14
14
|
|
|
15
|
-
CREATE TABLE "
|
|
15
|
+
CREATE TABLE "ai_history" (
|
|
16
16
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
17
17
|
"userId" TEXT NOT NULL REFERENCES "users"(id) ON DELETE CASCADE,
|
|
18
|
+
"teamId" TEXT NOT NULL REFERENCES "teams"(id) ON DELETE CASCADE,
|
|
18
19
|
|
|
19
20
|
-- Generic polymorphic relationship (flexible for any future use case)
|
|
20
21
|
"relatedEntityType" TEXT,
|
|
@@ -49,46 +50,48 @@ CREATE TABLE "ai-history" (
|
|
|
49
50
|
-- INDEXES
|
|
50
51
|
-- =============================================
|
|
51
52
|
|
|
52
|
-
CREATE INDEX IF NOT EXISTS
|
|
53
|
-
CREATE INDEX IF NOT EXISTS
|
|
54
|
-
CREATE INDEX IF NOT EXISTS
|
|
55
|
-
CREATE INDEX IF NOT EXISTS
|
|
56
|
-
CREATE INDEX IF NOT EXISTS
|
|
57
|
-
CREATE INDEX IF NOT EXISTS
|
|
58
|
-
CREATE INDEX IF NOT EXISTS
|
|
59
|
-
CREATE INDEX IF NOT EXISTS
|
|
60
|
-
CREATE INDEX IF NOT EXISTS
|
|
61
|
-
CREATE INDEX IF NOT EXISTS
|
|
62
|
-
CREATE INDEX IF NOT EXISTS
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_user_id ON "ai_history"("userId");
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_team_id ON "ai_history"("teamId");
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_team_user ON "ai_history"("teamId", "userId");
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_entity ON "ai_history"("relatedEntityType", "relatedEntityId") WHERE "relatedEntityType" IS NOT NULL;
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_status ON "ai_history"(status);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_operation ON "ai_history"(operation);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_model ON "ai_history"(model);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_provider ON "ai_history"(provider);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_created_at ON "ai_history"("createdAt" DESC);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_user_status ON "ai_history"("userId", status);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_user_operation ON "ai_history"("userId", operation);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_tokens_input ON "ai_history"("tokensInput") WHERE "tokensInput" IS NOT NULL;
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_ai_history_tokens_output ON "ai_history"("tokensOutput") WHERE "tokensOutput" IS NOT NULL;
|
|
63
66
|
|
|
64
67
|
-- =============================================
|
|
65
68
|
-- ROW LEVEL SECURITY (enableRLS: true)
|
|
66
69
|
-- =============================================
|
|
67
70
|
|
|
68
71
|
-- Enable RLS
|
|
69
|
-
ALTER TABLE "
|
|
72
|
+
ALTER TABLE "ai_history" ENABLE ROW LEVEL SECURITY;
|
|
70
73
|
|
|
71
|
-
-- Policies for
|
|
72
|
-
DROP POLICY IF EXISTS "Users can view own ai history" ON "
|
|
73
|
-
CREATE POLICY "Users can view own ai history" ON "
|
|
74
|
+
-- Policies for ai_history table
|
|
75
|
+
DROP POLICY IF EXISTS "Users can view own ai history" ON "ai_history";
|
|
76
|
+
CREATE POLICY "Users can view own ai history" ON "ai_history"
|
|
74
77
|
FOR SELECT USING ("userId" = public.get_auth_user_id());
|
|
75
78
|
|
|
76
|
-
DROP POLICY IF EXISTS "Users can insert own ai history" ON "
|
|
77
|
-
CREATE POLICY "Users can insert own ai history" ON "
|
|
79
|
+
DROP POLICY IF EXISTS "Users can insert own ai history" ON "ai_history";
|
|
80
|
+
CREATE POLICY "Users can insert own ai history" ON "ai_history"
|
|
78
81
|
FOR INSERT WITH CHECK ("userId" = public.get_auth_user_id());
|
|
79
82
|
|
|
80
|
-
DROP POLICY IF EXISTS "Users can update own ai history" ON "
|
|
81
|
-
CREATE POLICY "Users can update own ai history" ON "
|
|
83
|
+
DROP POLICY IF EXISTS "Users can update own ai history" ON "ai_history";
|
|
84
|
+
CREATE POLICY "Users can update own ai history" ON "ai_history"
|
|
82
85
|
FOR UPDATE USING ("userId" = public.get_auth_user_id())
|
|
83
86
|
WITH CHECK ("userId" = public.get_auth_user_id());
|
|
84
87
|
|
|
85
|
-
DROP POLICY IF EXISTS "Users can delete own ai history" ON "
|
|
86
|
-
CREATE POLICY "Users can delete own ai history" ON "
|
|
88
|
+
DROP POLICY IF EXISTS "Users can delete own ai history" ON "ai_history";
|
|
89
|
+
CREATE POLICY "Users can delete own ai history" ON "ai_history"
|
|
87
90
|
FOR DELETE USING ("userId" = public.get_auth_user_id());
|
|
88
91
|
|
|
89
92
|
-- Admins can manage all ai history
|
|
90
|
-
DROP POLICY IF EXISTS "Admins can manage all ai history" ON "
|
|
91
|
-
CREATE POLICY "Admins can manage all ai history" ON "
|
|
93
|
+
DROP POLICY IF EXISTS "Admins can manage all ai history" ON "ai_history";
|
|
94
|
+
CREATE POLICY "Admins can manage all ai history" ON "ai_history"
|
|
92
95
|
FOR ALL USING (
|
|
93
96
|
EXISTS (
|
|
94
97
|
SELECT 1 FROM "users"
|
|
@@ -107,14 +110,14 @@ CREATE POLICY "Admins can manage all ai history" ON "ai-history"
|
|
|
107
110
|
-- =============================================
|
|
108
111
|
-- TRIGGER: Auto-update updatedAt
|
|
109
112
|
-- =============================================
|
|
110
|
-
CREATE TRIGGER
|
|
111
|
-
BEFORE UPDATE ON "
|
|
113
|
+
CREATE TRIGGER ai_history_set_updated_at
|
|
114
|
+
BEFORE UPDATE ON "ai_history"
|
|
112
115
|
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
113
116
|
|
|
114
117
|
-- =============================================
|
|
115
118
|
-- TRIGGER: Auto-update completedAt
|
|
116
119
|
-- =============================================
|
|
117
|
-
CREATE OR REPLACE FUNCTION
|
|
120
|
+
CREATE OR REPLACE FUNCTION update_ai_history_completed_at()
|
|
118
121
|
RETURNS TRIGGER AS $$
|
|
119
122
|
BEGIN
|
|
120
123
|
IF NEW.status IN ('completed', 'failed') AND OLD.status NOT IN ('completed', 'failed') THEN
|
|
@@ -124,16 +127,16 @@ BEGIN
|
|
|
124
127
|
END;
|
|
125
128
|
$$ LANGUAGE plpgsql;
|
|
126
129
|
|
|
127
|
-
CREATE TRIGGER
|
|
128
|
-
BEFORE UPDATE ON "
|
|
130
|
+
CREATE TRIGGER ai_history_completed_at
|
|
131
|
+
BEFORE UPDATE ON "ai_history"
|
|
129
132
|
FOR EACH ROW
|
|
130
|
-
EXECUTE FUNCTION
|
|
133
|
+
EXECUTE FUNCTION update_ai_history_completed_at();
|
|
131
134
|
|
|
132
135
|
-- =============================================
|
|
133
136
|
-- CONSTRAINT: Token Split Validation
|
|
134
137
|
-- =============================================
|
|
135
138
|
-- Ensures data integrity: tokensUsed = tokensInput + tokensOutput when both present
|
|
136
|
-
ALTER TABLE "
|
|
139
|
+
ALTER TABLE "ai_history"
|
|
137
140
|
ADD CONSTRAINT chk_tokens_sum
|
|
138
141
|
CHECK (
|
|
139
142
|
("tokensInput" IS NULL AND "tokensOutput" IS NULL)
|
|
@@ -144,24 +147,24 @@ CHECK (
|
|
|
144
147
|
-- =============================================
|
|
145
148
|
-- COMMENTS
|
|
146
149
|
-- =============================================
|
|
147
|
-
COMMENT ON TABLE "
|
|
148
|
-
COMMENT ON COLUMN "
|
|
149
|
-
COMMENT ON COLUMN "
|
|
150
|
-
COMMENT ON COLUMN "
|
|
151
|
-
COMMENT ON COLUMN "
|
|
152
|
-
COMMENT ON COLUMN "
|
|
153
|
-
COMMENT ON COLUMN "
|
|
154
|
-
COMMENT ON COLUMN "
|
|
155
|
-
COMMENT ON COLUMN "
|
|
156
|
-
COMMENT ON COLUMN "
|
|
157
|
-
COMMENT ON COLUMN "
|
|
158
|
-
COMMENT ON COLUMN "
|
|
159
|
-
COMMENT ON COLUMN "
|
|
160
|
-
COMMENT ON COLUMN "
|
|
161
|
-
COMMENT ON COLUMN "
|
|
162
|
-
COMMENT ON COLUMN "
|
|
163
|
-
COMMENT ON COLUMN "
|
|
164
|
-
COMMENT ON COLUMN "
|
|
150
|
+
COMMENT ON TABLE "ai_history" IS 'Generic audit trail for AI interactions with cost and performance metrics';
|
|
151
|
+
COMMENT ON COLUMN "ai_history"."userId" IS 'User who initiated the AI operation';
|
|
152
|
+
COMMENT ON COLUMN "ai_history"."relatedEntityType" IS 'Type of entity this operation relates to (e.g., ''contents'', ''products'', ''campaigns'')';
|
|
153
|
+
COMMENT ON COLUMN "ai_history"."relatedEntityId" IS 'ID of the related entity (polymorphic relationship)';
|
|
154
|
+
COMMENT ON COLUMN "ai_history".operation IS 'Type of AI operation: generate, refine, analyze, chat, completion, or other';
|
|
155
|
+
COMMENT ON COLUMN "ai_history".model IS 'AI model used (e.g., claude-3-5-sonnet-20241022, gpt-4)';
|
|
156
|
+
COMMENT ON COLUMN "ai_history".provider IS 'AI provider: anthropic, openai, etc.';
|
|
157
|
+
COMMENT ON COLUMN "ai_history".status IS 'Processing status: pending, processing, completed, or failed';
|
|
158
|
+
COMMENT ON COLUMN "ai_history"."tokensUsed" IS 'Total tokens consumed (input + output) - kept for backward compatibility';
|
|
159
|
+
COMMENT ON COLUMN "ai_history"."tokensInput" IS 'Input tokens (prompt) - for precise cost calculation with asymmetric pricing';
|
|
160
|
+
COMMENT ON COLUMN "ai_history"."tokensOutput" IS 'Output tokens (completion) - for precise cost calculation with asymmetric pricing';
|
|
161
|
+
COMMENT ON COLUMN "ai_history"."creditsUsed" IS 'Credits deducted for this operation';
|
|
162
|
+
COMMENT ON COLUMN "ai_history"."estimatedCost" IS 'Cost in USD based on model pricing';
|
|
163
|
+
COMMENT ON COLUMN "ai_history"."balanceAfter" IS 'User credit balance after this operation';
|
|
164
|
+
COMMENT ON COLUMN "ai_history"."errorMessage" IS 'Error message if operation failed';
|
|
165
|
+
COMMENT ON COLUMN "ai_history"."createdAt" IS 'When the AI operation was initiated';
|
|
166
|
+
COMMENT ON COLUMN "ai_history"."updatedAt" IS 'When the record was last updated';
|
|
167
|
+
COMMENT ON COLUMN "ai_history"."completedAt" IS 'When the AI operation finished (success or failure)';
|
|
165
168
|
|
|
166
169
|
-- =============================================
|
|
167
170
|
-- MIGRATION COMPLETE
|
|
@@ -171,6 +174,6 @@ DO $$
|
|
|
171
174
|
BEGIN
|
|
172
175
|
RAISE NOTICE 'AI History entity migration completed successfully!';
|
|
173
176
|
RAISE NOTICE 'Plugin: ai';
|
|
174
|
-
RAISE NOTICE 'Table created:
|
|
177
|
+
RAISE NOTICE 'Table created: ai_history';
|
|
175
178
|
RAISE NOTICE 'RLS policies enabled for user isolation';
|
|
176
179
|
END $$;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
-- =============================================
|
|
2
2
|
-- AI HISTORY METADATA SYSTEM
|
|
3
3
|
-- Plugin: AI
|
|
4
|
-
-- Entity:
|
|
4
|
+
-- Entity: ai_history
|
|
5
5
|
-- =============================================
|
|
6
6
|
--
|
|
7
|
-
-- This migration adds comprehensive metadata capabilities through
|
|
7
|
+
-- This migration adds comprehensive metadata capabilities through ai_history_metas table
|
|
8
8
|
--
|
|
9
9
|
-- Purpose: Flexible metadata storage for AI operations
|
|
10
10
|
-- - Better query performance (indexed key-value lookups)
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
-- AI_HISTORY_METAS TABLE
|
|
30
30
|
-- =============================================
|
|
31
31
|
|
|
32
|
-
CREATE TABLE IF NOT EXISTS public."
|
|
32
|
+
CREATE TABLE IF NOT EXISTS public."ai_history_metas" (
|
|
33
33
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
34
|
-
"entityId" TEXT NOT NULL REFERENCES public."
|
|
34
|
+
"entityId" TEXT NOT NULL REFERENCES public."ai_history"(id) ON DELETE CASCADE,
|
|
35
35
|
"metaKey" TEXT NOT NULL,
|
|
36
36
|
"metaValue" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
37
37
|
"dataType" TEXT DEFAULT 'json' CHECK ("dataType" IN ('string', 'number', 'boolean', 'json', 'array')),
|
|
@@ -41,54 +41,54 @@ CREATE TABLE IF NOT EXISTS public."ai-history_metas" (
|
|
|
41
41
|
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
42
42
|
|
|
43
43
|
-- Prevent duplicate keys per history record
|
|
44
|
-
CONSTRAINT
|
|
44
|
+
CONSTRAINT ai_history_metas_unique_key UNIQUE ("entityId", "metaKey")
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
-- Trigger for auto-updating updatedAt (using Better Auth's function)
|
|
48
|
-
CREATE TRIGGER
|
|
49
|
-
BEFORE UPDATE ON public."
|
|
48
|
+
CREATE TRIGGER ai_history_metas_set_updated_at
|
|
49
|
+
BEFORE UPDATE ON public."ai_history_metas"
|
|
50
50
|
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
51
51
|
|
|
52
52
|
-- Indexes for efficient queries
|
|
53
|
-
CREATE INDEX
|
|
54
|
-
CREATE INDEX
|
|
55
|
-
CREATE INDEX
|
|
53
|
+
CREATE INDEX idx_ai_history_metas_entity_id ON public."ai_history_metas"("entityId");
|
|
54
|
+
CREATE INDEX idx_ai_history_metas_key ON public."ai_history_metas"("metaKey");
|
|
55
|
+
CREATE INDEX idx_ai_history_metas_composite ON public."ai_history_metas"("entityId", "metaKey");
|
|
56
56
|
|
|
57
57
|
-- GIN index for JSONB queries (enables @>, ?, ?&, ?| operators)
|
|
58
|
-
CREATE INDEX
|
|
58
|
+
CREATE INDEX idx_ai_history_metas_value_gin ON public."ai_history_metas" USING GIN ("metaValue");
|
|
59
59
|
|
|
60
60
|
-- RLS Policies
|
|
61
|
-
ALTER TABLE public."
|
|
61
|
+
ALTER TABLE public."ai_history_metas" ENABLE ROW LEVEL SECURITY;
|
|
62
62
|
|
|
63
63
|
-- Cleanup existing policies
|
|
64
|
-
DROP POLICY IF EXISTS "AI history metas owner can do all" ON public."
|
|
65
|
-
DROP POLICY IF EXISTS "AI history metas inherit parent access" ON public."
|
|
64
|
+
DROP POLICY IF EXISTS "AI history metas owner can do all" ON public."ai_history_metas";
|
|
65
|
+
DROP POLICY IF EXISTS "AI history metas inherit parent access" ON public."ai_history_metas";
|
|
66
66
|
|
|
67
|
-
-- Policy: Inherit access from parent
|
|
67
|
+
-- Policy: Inherit access from parent ai_history record
|
|
68
68
|
CREATE POLICY "AI history metas inherit parent access"
|
|
69
|
-
ON public."
|
|
69
|
+
ON public."ai_history_metas"
|
|
70
70
|
FOR ALL TO authenticated
|
|
71
71
|
USING (
|
|
72
72
|
EXISTS (
|
|
73
|
-
SELECT 1 FROM public."
|
|
74
|
-
WHERE "
|
|
75
|
-
AND "
|
|
73
|
+
SELECT 1 FROM public."ai_history"
|
|
74
|
+
WHERE "ai_history".id = "ai_history_metas"."entityId"
|
|
75
|
+
AND "ai_history"."userId" = public.get_auth_user_id()
|
|
76
76
|
)
|
|
77
77
|
)
|
|
78
78
|
WITH CHECK (
|
|
79
79
|
EXISTS (
|
|
80
|
-
SELECT 1 FROM public."
|
|
81
|
-
WHERE "
|
|
82
|
-
AND "
|
|
80
|
+
SELECT 1 FROM public."ai_history"
|
|
81
|
+
WHERE "ai_history".id = "ai_history_metas"."entityId"
|
|
82
|
+
AND "ai_history"."userId" = public.get_auth_user_id()
|
|
83
83
|
)
|
|
84
84
|
);
|
|
85
85
|
|
|
86
86
|
-- Comments for documentation
|
|
87
|
-
COMMENT ON TABLE public."
|
|
88
|
-
COMMENT ON COLUMN public."
|
|
89
|
-
COMMENT ON COLUMN public."
|
|
90
|
-
COMMENT ON COLUMN public."
|
|
91
|
-
COMMENT ON COLUMN public."
|
|
87
|
+
COMMENT ON TABLE public."ai_history_metas" IS 'Flexible metadata storage for AI history records. Supports any key-value pairs with JSONB values.';
|
|
88
|
+
COMMENT ON COLUMN public."ai_history_metas"."metaValue" IS 'JSONB value allows storing any data structure (string, number, boolean, object, array)';
|
|
89
|
+
COMMENT ON COLUMN public."ai_history_metas"."dataType" IS 'Hint for parsing metaValue on client side (actual type is always JSONB in DB)';
|
|
90
|
+
COMMENT ON COLUMN public."ai_history_metas"."isPublic" IS 'Whether this meta is publicly accessible or private to owner';
|
|
91
|
+
COMMENT ON COLUMN public."ai_history_metas"."isSearchable" IS 'Whether this meta should be included in search operations';
|
|
92
92
|
|
|
93
93
|
-- =============================================
|
|
94
94
|
-- MIGRATION COMPLETE
|
|
@@ -97,7 +97,7 @@ COMMENT ON COLUMN public."ai-history_metas"."isSearchable" IS 'Whether this meta
|
|
|
97
97
|
DO $$
|
|
98
98
|
BEGIN
|
|
99
99
|
RAISE NOTICE 'AI History metadata migration completed successfully!';
|
|
100
|
-
RAISE NOTICE 'Created:
|
|
100
|
+
RAISE NOTICE 'Created: ai_history_metas table for flexible metadata';
|
|
101
101
|
RAISE NOTICE 'Performance: GIN indexes for fast JSONB queries';
|
|
102
|
-
RAISE NOTICE 'Security: RLS policies enabled on
|
|
102
|
+
RAISE NOTICE 'Security: RLS policies enabled on ai_history_metas';
|
|
103
103
|
END $$;
|
package/lib/save-example.ts
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AI
|
|
2
|
+
* AI History Saving - PostgreSQL Implementation
|
|
3
3
|
*
|
|
4
4
|
* Uses project's PostgreSQL connection with RLS support.
|
|
5
|
-
*
|
|
5
|
+
* Saves AI interactions to the ai_history table.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { mutateWithRLS, queryWithRLS } from '@nextsparkjs/core/lib/db'
|
|
9
9
|
import { sanitizePrompt, sanitizeResponse } from './sanitize'
|
|
10
10
|
|
|
11
11
|
export interface AIExampleData {
|
|
12
|
-
title?: string
|
|
13
12
|
prompt: string
|
|
14
13
|
response: string
|
|
15
14
|
model: string
|
|
16
15
|
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
17
|
-
|
|
16
|
+
operation?: 'generate' | 'refine' | 'analyze' | 'chat' | 'completion' | 'other'
|
|
17
|
+
provider?: string
|
|
18
18
|
metadata?: {
|
|
19
19
|
tokens?: number
|
|
20
|
+
tokensInput?: number
|
|
21
|
+
tokensOutput?: number
|
|
20
22
|
cost?: number
|
|
21
23
|
duration?: number
|
|
22
|
-
provider?: string
|
|
23
24
|
isLocal?: boolean
|
|
24
25
|
platforms?: number
|
|
25
26
|
imagesProcessed?: number
|
|
27
|
+
relatedEntityType?: string
|
|
28
|
+
relatedEntityId?: string
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -34,11 +37,12 @@ export interface SaveExampleResult {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
* Save AI
|
|
40
|
+
* Save AI interaction to ai_history table
|
|
38
41
|
*/
|
|
39
42
|
export async function saveAIExample(
|
|
40
43
|
data: AIExampleData,
|
|
41
|
-
userId: string
|
|
44
|
+
userId: string,
|
|
45
|
+
teamId: string
|
|
42
46
|
): Promise<SaveExampleResult> {
|
|
43
47
|
try {
|
|
44
48
|
// 1. Sanitize sensitive data
|
|
@@ -49,25 +53,35 @@ export async function saveAIExample(
|
|
|
49
53
|
sanitizedPrompt !== data.prompt ||
|
|
50
54
|
sanitizedResponse !== data.response
|
|
51
55
|
|
|
52
|
-
// 2.
|
|
53
|
-
const
|
|
56
|
+
// 2. Extract token info
|
|
57
|
+
const tokensInput = data.metadata?.tokensInput || 0
|
|
58
|
+
const tokensOutput = data.metadata?.tokensOutput || 0
|
|
59
|
+
const tokensUsed = data.metadata?.tokens || (tokensInput + tokensOutput)
|
|
54
60
|
|
|
55
|
-
// 3. Insert into
|
|
61
|
+
// 3. Insert into ai_history table with RLS
|
|
56
62
|
const query = `
|
|
57
|
-
INSERT INTO
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
INSERT INTO ai_history (
|
|
64
|
+
"userId", "teamId", operation, model, provider, status,
|
|
65
|
+
"tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
|
|
66
|
+
"relatedEntityType", "relatedEntityId",
|
|
67
|
+
"createdAt", "updatedAt"
|
|
68
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
|
|
60
69
|
RETURNING id
|
|
61
70
|
`
|
|
62
71
|
|
|
63
72
|
const params = [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
userId,
|
|
74
|
+
teamId,
|
|
75
|
+
data.operation || 'generate',
|
|
67
76
|
data.model,
|
|
77
|
+
data.provider || 'ollama',
|
|
68
78
|
data.status,
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
tokensUsed || null,
|
|
80
|
+
tokensInput || null,
|
|
81
|
+
tokensOutput || null,
|
|
82
|
+
data.metadata?.cost || null,
|
|
83
|
+
data.metadata?.relatedEntityType || null,
|
|
84
|
+
data.metadata?.relatedEntityId || null
|
|
71
85
|
]
|
|
72
86
|
|
|
73
87
|
const result = await mutateWithRLS<{ id: string }>(query, params, userId)
|
|
@@ -75,13 +89,14 @@ export async function saveAIExample(
|
|
|
75
89
|
if (result.rowCount === 0 || !result.rows[0]) {
|
|
76
90
|
return {
|
|
77
91
|
success: false,
|
|
78
|
-
error: 'Failed to insert
|
|
92
|
+
error: 'Failed to insert ai_history record'
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
console.log('[AI Plugin]
|
|
96
|
+
console.log('[AI Plugin] History saved to database:', {
|
|
83
97
|
id: result.rows[0].id,
|
|
84
|
-
|
|
98
|
+
operation: data.operation || 'generate',
|
|
99
|
+
model: data.model,
|
|
85
100
|
sanitizationApplied
|
|
86
101
|
})
|
|
87
102
|
|
|
@@ -92,7 +107,7 @@ export async function saveAIExample(
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
} catch (error) {
|
|
95
|
-
console.error('[AI Plugin] Save
|
|
110
|
+
console.error('[AI Plugin] Save history failed:', error)
|
|
96
111
|
return {
|
|
97
112
|
success: false,
|
|
98
113
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -101,29 +116,44 @@ export async function saveAIExample(
|
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
/**
|
|
104
|
-
* Get AI
|
|
119
|
+
* Get AI history from PostgreSQL
|
|
105
120
|
*/
|
|
106
121
|
export async function getAIExamples(params: {
|
|
107
122
|
userId?: string
|
|
123
|
+
teamId?: string
|
|
108
124
|
limit?: number
|
|
109
125
|
offset?: number
|
|
110
126
|
status?: string
|
|
127
|
+
operation?: string
|
|
111
128
|
}) {
|
|
112
129
|
try {
|
|
113
|
-
const { userId, limit = 50, offset = 0, status } = params
|
|
130
|
+
const { userId, teamId, limit = 50, offset = 0, status, operation } = params
|
|
114
131
|
|
|
115
132
|
let query = `
|
|
116
|
-
SELECT id,
|
|
117
|
-
|
|
133
|
+
SELECT id, "userId", "teamId", operation, model, provider, status,
|
|
134
|
+
"tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
|
|
135
|
+
"relatedEntityType", "relatedEntityId",
|
|
136
|
+
"createdAt", "updatedAt", "completedAt"
|
|
137
|
+
FROM ai_history
|
|
118
138
|
WHERE 1=1
|
|
119
139
|
`
|
|
120
140
|
const queryParams: (string | number)[] = []
|
|
121
141
|
|
|
142
|
+
if (teamId) {
|
|
143
|
+
queryParams.push(teamId)
|
|
144
|
+
query += ` AND "teamId" = $${queryParams.length}`
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
if (status) {
|
|
123
148
|
queryParams.push(status)
|
|
124
149
|
query += ` AND status = $${queryParams.length}`
|
|
125
150
|
}
|
|
126
151
|
|
|
152
|
+
if (operation) {
|
|
153
|
+
queryParams.push(operation)
|
|
154
|
+
query += ` AND operation = $${queryParams.length}`
|
|
155
|
+
}
|
|
156
|
+
|
|
127
157
|
query += ` ORDER BY "createdAt" DESC LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`
|
|
128
158
|
queryParams.push(limit, offset)
|
|
129
159
|
|
|
@@ -134,7 +164,7 @@ export async function getAIExamples(params: {
|
|
|
134
164
|
data: rows
|
|
135
165
|
}
|
|
136
166
|
} catch (error) {
|
|
137
|
-
console.error('[AI Plugin] Get
|
|
167
|
+
console.error('[AI Plugin] Get history failed:', error)
|
|
138
168
|
return {
|
|
139
169
|
success: false,
|
|
140
170
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
@@ -144,13 +174,16 @@ export async function getAIExamples(params: {
|
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
/**
|
|
147
|
-
* Get single AI
|
|
177
|
+
* Get single AI history record by ID
|
|
148
178
|
*/
|
|
149
179
|
export async function getAIExampleById(id: string, userId?: string) {
|
|
150
180
|
try {
|
|
151
181
|
const query = `
|
|
152
|
-
SELECT id,
|
|
153
|
-
|
|
182
|
+
SELECT id, "userId", "teamId", operation, model, provider, status,
|
|
183
|
+
"tokensUsed", "tokensInput", "tokensOutput", "estimatedCost",
|
|
184
|
+
"relatedEntityType", "relatedEntityId",
|
|
185
|
+
"createdAt", "updatedAt", "completedAt"
|
|
186
|
+
FROM ai_history
|
|
154
187
|
WHERE id = $1
|
|
155
188
|
`
|
|
156
189
|
|
|
@@ -159,7 +192,7 @@ export async function getAIExampleById(id: string, userId?: string) {
|
|
|
159
192
|
if (rows.length === 0) {
|
|
160
193
|
return {
|
|
161
194
|
success: false,
|
|
162
|
-
error: '
|
|
195
|
+
error: 'History record not found'
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
198
|
|
|
@@ -168,7 +201,7 @@ export async function getAIExampleById(id: string, userId?: string) {
|
|
|
168
201
|
data: rows[0]
|
|
169
202
|
}
|
|
170
203
|
} catch (error) {
|
|
171
|
-
console.error('[AI Plugin] Get
|
|
204
|
+
console.error('[AI Plugin] Get history by ID failed:', error)
|
|
172
205
|
return {
|
|
173
206
|
success: false,
|
|
174
207
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -177,18 +210,18 @@ export async function getAIExampleById(id: string, userId?: string) {
|
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
/**
|
|
180
|
-
* Delete AI
|
|
213
|
+
* Delete AI history record
|
|
181
214
|
*/
|
|
182
215
|
export async function deleteAIExample(id: string, userId: string) {
|
|
183
216
|
try {
|
|
184
|
-
const query = `DELETE FROM
|
|
217
|
+
const query = `DELETE FROM ai_history WHERE id = $1 RETURNING id`
|
|
185
218
|
|
|
186
219
|
const result = await mutateWithRLS<{ id: string }>(query, [id], userId)
|
|
187
220
|
|
|
188
221
|
if (result.rowCount === 0) {
|
|
189
222
|
return {
|
|
190
223
|
success: false,
|
|
191
|
-
error: '
|
|
224
|
+
error: 'History record not found or unauthorized'
|
|
192
225
|
}
|
|
193
226
|
}
|
|
194
227
|
|
|
@@ -197,7 +230,7 @@ export async function deleteAIExample(id: string, userId: string) {
|
|
|
197
230
|
id: result.rows[0].id
|
|
198
231
|
}
|
|
199
232
|
} catch (error) {
|
|
200
|
-
console.error('[AI Plugin] Delete
|
|
233
|
+
console.error('[AI Plugin] Delete history failed:', error)
|
|
201
234
|
return {
|
|
202
235
|
success: false,
|
|
203
236
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -206,32 +239,23 @@ export async function deleteAIExample(id: string, userId: string) {
|
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
/**
|
|
209
|
-
*
|
|
210
|
-
*/
|
|
211
|
-
function generateTitle(prompt: string): string {
|
|
212
|
-
const cleaned = prompt.replace(/\n/g, ' ').trim()
|
|
213
|
-
return cleaned.length > 50
|
|
214
|
-
? cleaned.substring(0, 50) + '...'
|
|
215
|
-
: cleaned
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Save example with automatic error handling (fire-and-forget)
|
|
242
|
+
* Save history with automatic error handling (fire-and-forget)
|
|
220
243
|
*/
|
|
221
244
|
export async function saveExampleSafely(
|
|
222
245
|
data: AIExampleData,
|
|
223
|
-
userId: string
|
|
246
|
+
userId: string,
|
|
247
|
+
teamId: string
|
|
224
248
|
): Promise<void> {
|
|
225
249
|
try {
|
|
226
|
-
const result = await saveAIExample(data, userId)
|
|
250
|
+
const result = await saveAIExample(data, userId, teamId)
|
|
227
251
|
|
|
228
252
|
if (!result.success) {
|
|
229
|
-
console.error('[AI Plugin]
|
|
253
|
+
console.error('[AI Plugin] History save failed:', result.error)
|
|
230
254
|
} else if (result.sanitizationApplied) {
|
|
231
|
-
console.warn('[AI Plugin] Sanitization was applied to saved
|
|
255
|
+
console.warn('[AI Plugin] Sanitization was applied to saved history')
|
|
232
256
|
}
|
|
233
257
|
} catch (error) {
|
|
234
258
|
// Silent fail - don't break the main request
|
|
235
|
-
console.error('[AI Plugin]
|
|
259
|
+
console.error('[AI Plugin] History save error:', error)
|
|
236
260
|
}
|
|
237
|
-
}
|
|
261
|
+
}
|