@nextsparkjs/plugin-ai 0.1.0-beta.1 → 0.1.0-beta.101
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/LICENSE +21 -0
- package/api/ai-history/[id]/route.ts +5 -2
- package/api/ai-history/docs.md +135 -0
- package/api/ai-history/presets.ts +56 -0
- package/api/embeddings/docs.md +137 -0
- package/api/embeddings/presets.ts +41 -0
- package/api/embeddings/route.ts +7 -2
- package/api/generate/docs.md +157 -0
- package/api/generate/presets.ts +88 -0
- package/api/generate/route.ts +15 -5
- package/docs/02-features/03-ai-history.md +8 -0
- package/docs/04-use-cases/01-content-generation.md +1 -0
- package/entities/ai-history/ai-history.config.ts +3 -41
- package/entities/ai-history/ai-history.fields.ts +20 -1
- package/entities/ai-history/migrations/001_ai_history_table.sql +13 -1
- package/entities/ai-history/migrations/002_ai_history_metas.sql +1 -1
- package/lib/ai-history-meta-service.ts +0 -28
- package/lib/ai-history-service.ts +5 -2
- package/lib/save-example.ts +77 -53
- package/package.json +7 -3
- package/tsconfig.tsbuildinfo +0 -1
package/api/generate/route.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
|
9
9
|
import { selectModel, calculateCost, validatePlugin, extractTokens, handleAIError } from '../../lib/core-utils'
|
|
10
10
|
import { getServerPluginConfig } from '../../lib/server-env'
|
|
11
11
|
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
12
|
+
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
12
13
|
import { generateText } from 'ai'
|
|
13
14
|
import { saveExampleSafely } from '../../lib/save-example'
|
|
14
15
|
import { z } from 'zod'
|
|
@@ -25,7 +26,7 @@ const GenerateRequestSchema = z.object({
|
|
|
25
26
|
saveExample: z.boolean().optional().default(false)
|
|
26
27
|
})
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const postHandler = async (request: NextRequest) => {
|
|
29
30
|
try {
|
|
30
31
|
// 1. Authentication
|
|
31
32
|
const authResult = await authenticateRequest(request)
|
|
@@ -79,20 +80,25 @@ export async function POST(request: NextRequest) {
|
|
|
79
80
|
|
|
80
81
|
// 8. Save example if requested (opt-in)
|
|
81
82
|
if (saveExample) {
|
|
83
|
+
const teamId = request.headers.get('x-team-id') || ''
|
|
82
84
|
await saveExampleSafely(
|
|
83
85
|
{
|
|
84
86
|
prompt,
|
|
85
87
|
response: result.text,
|
|
86
88
|
model: selectedModel.modelName,
|
|
87
89
|
status: 'completed',
|
|
90
|
+
operation: 'generate',
|
|
91
|
+
provider: selectedModel.provider,
|
|
88
92
|
metadata: {
|
|
89
93
|
tokens: tokens.total,
|
|
94
|
+
tokensInput: tokens.input,
|
|
95
|
+
tokensOutput: tokens.output,
|
|
90
96
|
cost,
|
|
91
|
-
provider: selectedModel.provider,
|
|
92
97
|
isLocal: selectedModel.isLocal
|
|
93
98
|
}
|
|
94
99
|
},
|
|
95
|
-
authResult.user!.id
|
|
100
|
+
authResult.user!.id,
|
|
101
|
+
teamId
|
|
96
102
|
)
|
|
97
103
|
}
|
|
98
104
|
|
|
@@ -119,10 +125,12 @@ export async function POST(request: NextRequest) {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
export const POST = withRateLimitTier(postHandler, 'write')
|
|
129
|
+
|
|
122
130
|
/**
|
|
123
131
|
* Get endpoint info
|
|
124
132
|
*/
|
|
125
|
-
|
|
133
|
+
const getHandler = async (): Promise<NextResponse> => {
|
|
126
134
|
const config = await getServerPluginConfig()
|
|
127
135
|
|
|
128
136
|
return NextResponse.json({
|
|
@@ -157,4 +165,6 @@ export async function GET(): Promise<NextResponse> {
|
|
|
157
165
|
anthropic: 'Add ANTHROPIC_API_KEY to contents/plugins/ai/.env'
|
|
158
166
|
}
|
|
159
167
|
})
|
|
160
|
-
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const GET = withRateLimitTier(getHandler, 'read')
|
|
@@ -81,6 +81,7 @@ import { AIHistoryService } from '@/contents/plugins/ai/lib/ai-history-service'
|
|
|
81
81
|
```typescript
|
|
82
82
|
const historyId = await AIHistoryService.startOperation({
|
|
83
83
|
userId: 'user-id',
|
|
84
|
+
teamId: 'team-id',
|
|
84
85
|
operation: 'generate',
|
|
85
86
|
model: 'gpt-4o-mini',
|
|
86
87
|
provider: 'openai',
|
|
@@ -133,6 +134,7 @@ await AIHistoryService.failOperation({
|
|
|
133
134
|
```typescript
|
|
134
135
|
{
|
|
135
136
|
userId: string // Required
|
|
137
|
+
teamId: string // Required - team context for the operation
|
|
136
138
|
operation: AIOperation // 'generate' | 'refine' | 'analyze' | 'chat' | 'completion' | 'other'
|
|
137
139
|
model: string // Model name (e.g., 'gpt-4o-mini')
|
|
138
140
|
provider?: AIProvider // 'openai' | 'anthropic' | 'ollama'
|
|
@@ -147,6 +149,7 @@ await AIHistoryService.failOperation({
|
|
|
147
149
|
```typescript
|
|
148
150
|
const historyId = await AIHistoryService.startOperation({
|
|
149
151
|
userId: session.user.id,
|
|
152
|
+
teamId: session.user.activeTeamId,
|
|
150
153
|
operation: 'generate',
|
|
151
154
|
model: 'llama3.2:3b',
|
|
152
155
|
provider: 'ollama'
|
|
@@ -225,6 +228,7 @@ try {
|
|
|
225
228
|
```typescript
|
|
226
229
|
const historyId = await AIHistoryService.startOperation({
|
|
227
230
|
userId: 'user-id',
|
|
231
|
+
teamId: 'team-id',
|
|
228
232
|
operation: 'analyze',
|
|
229
233
|
model: 'claude-3-5-haiku-20241022',
|
|
230
234
|
relatedEntityType: 'clients',
|
|
@@ -249,6 +253,7 @@ PATCH /api/v1/plugin/ai/ai-history/:id
|
|
|
249
253
|
// Generate product description
|
|
250
254
|
const historyId = await AIHistoryService.startOperation({
|
|
251
255
|
userId: session.user.id,
|
|
256
|
+
teamId: session.user.activeTeamId,
|
|
252
257
|
operation: 'generate',
|
|
253
258
|
model: 'gpt-4o-mini',
|
|
254
259
|
relatedEntityType: 'products',
|
|
@@ -265,6 +270,7 @@ const historyId = await AIHistoryService.startOperation({
|
|
|
265
270
|
// Analyze client data
|
|
266
271
|
const historyId = await AIHistoryService.startOperation({
|
|
267
272
|
userId: session.user.id,
|
|
273
|
+
teamId: session.user.activeTeamId,
|
|
268
274
|
operation: 'analyze',
|
|
269
275
|
model: 'claude-3-5-sonnet-20241022',
|
|
270
276
|
relatedEntityType: 'clients',
|
|
@@ -279,6 +285,7 @@ const historyId = await AIHistoryService.startOperation({
|
|
|
279
285
|
// Audit article content
|
|
280
286
|
const historyId = await AIHistoryService.startOperation({
|
|
281
287
|
userId: session.user.id,
|
|
288
|
+
teamId: session.user.activeTeamId,
|
|
282
289
|
operation: 'analyze',
|
|
283
290
|
model: 'gpt-4o',
|
|
284
291
|
relatedEntityType: 'articles',
|
|
@@ -384,6 +391,7 @@ export async function generateProductDescription(productId: string) {
|
|
|
384
391
|
// 1. Start operation tracking
|
|
385
392
|
const historyId = await AIHistoryService.startOperation({
|
|
386
393
|
userId: session.user.id,
|
|
394
|
+
teamId: session.user.activeTeamId,
|
|
387
395
|
operation: 'generate',
|
|
388
396
|
model: 'gpt-4o-mini',
|
|
389
397
|
provider: 'openai',
|
|
@@ -336,6 +336,7 @@ export async function generateAndSaveDescription(productId: string) {
|
|
|
336
336
|
// Start operation tracking
|
|
337
337
|
const historyId = await AIHistoryService.startOperation({
|
|
338
338
|
userId: session.user.id,
|
|
339
|
+
teamId: session.user.activeTeamId,
|
|
339
340
|
operation: 'generate',
|
|
340
341
|
model: 'gpt-4o-mini',
|
|
341
342
|
provider: 'openai',
|
|
@@ -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',
|
|
@@ -57,46 +58,7 @@ export const aiHistoryEntityConfig: EntityConfig = {
|
|
|
57
58
|
},
|
|
58
59
|
|
|
59
60
|
// ==========================================
|
|
60
|
-
// 4.
|
|
61
|
-
// ==========================================
|
|
62
|
-
permissions: {
|
|
63
|
-
actions: [
|
|
64
|
-
{
|
|
65
|
-
action: 'create',
|
|
66
|
-
label: 'Create AI history',
|
|
67
|
-
description: 'Can create AI history entries (typically via API)',
|
|
68
|
-
roles: ['owner', 'admin', 'member'],
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
action: 'read',
|
|
72
|
-
label: 'View AI history',
|
|
73
|
-
description: 'Can view AI history entries (users see their own via API filtering)',
|
|
74
|
-
roles: ['owner', 'admin', 'member'],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
action: 'list',
|
|
78
|
-
label: 'List AI history',
|
|
79
|
-
description: 'Can list AI history entries',
|
|
80
|
-
roles: ['owner', 'admin', 'member'],
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
action: 'update',
|
|
84
|
-
label: 'Edit AI history',
|
|
85
|
-
description: 'Can modify AI history entries (immutable for most users)',
|
|
86
|
-
roles: ['owner', 'admin'],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
action: 'delete',
|
|
90
|
-
label: 'Delete AI history',
|
|
91
|
-
description: 'Can delete AI history entries (users can delete their own)',
|
|
92
|
-
roles: ['owner', 'admin', 'member'],
|
|
93
|
-
dangerous: true,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
// ==========================================
|
|
99
|
-
// 5. INTERNATIONALIZATION
|
|
61
|
+
// 4. INTERNATIONALIZATION
|
|
100
62
|
// ==========================================
|
|
101
63
|
i18n: {
|
|
102
64
|
fallbackLocale: 'en',
|
|
@@ -308,6 +308,25 @@ export const aiHistoryFields: EntityField[] = [
|
|
|
308
308
|
readOnly: true,
|
|
309
309
|
},
|
|
310
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: 'updatedAt',
|
|
313
|
+
type: 'datetime',
|
|
314
|
+
required: false,
|
|
315
|
+
display: {
|
|
316
|
+
label: 'Updated At',
|
|
317
|
+
description: 'When the record was last updated',
|
|
318
|
+
showInList: false,
|
|
319
|
+
showInDetail: true,
|
|
320
|
+
showInForm: false,
|
|
321
|
+
order: 98,
|
|
322
|
+
columnWidth: 6,
|
|
323
|
+
},
|
|
324
|
+
api: {
|
|
325
|
+
searchable: false,
|
|
326
|
+
sortable: true,
|
|
327
|
+
readOnly: true,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
311
330
|
{
|
|
312
331
|
name: 'completedAt',
|
|
313
332
|
type: 'datetime',
|
|
@@ -318,7 +337,7 @@ export const aiHistoryFields: EntityField[] = [
|
|
|
318
337
|
showInList: false,
|
|
319
338
|
showInDetail: true,
|
|
320
339
|
showInForm: false,
|
|
321
|
-
order:
|
|
340
|
+
order: 99,
|
|
322
341
|
columnWidth: 6,
|
|
323
342
|
},
|
|
324
343
|
api: {
|
|
@@ -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
|
-- =============================================
|
|
@@ -15,6 +15,7 @@ DROP TABLE IF EXISTS "ai_history" CASCADE;
|
|
|
15
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,
|
|
@@ -41,6 +42,7 @@ CREATE TABLE "ai_history" (
|
|
|
41
42
|
|
|
42
43
|
-- Timestamps
|
|
43
44
|
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
45
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
44
46
|
"completedAt" TIMESTAMPTZ
|
|
45
47
|
);
|
|
46
48
|
|
|
@@ -49,6 +51,8 @@ CREATE TABLE "ai_history" (
|
|
|
49
51
|
-- =============================================
|
|
50
52
|
|
|
51
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");
|
|
52
56
|
CREATE INDEX IF NOT EXISTS idx_ai_history_entity ON "ai_history"("relatedEntityType", "relatedEntityId") WHERE "relatedEntityType" IS NOT NULL;
|
|
53
57
|
CREATE INDEX IF NOT EXISTS idx_ai_history_status ON "ai_history"(status);
|
|
54
58
|
CREATE INDEX IF NOT EXISTS idx_ai_history_operation ON "ai_history"(operation);
|
|
@@ -103,6 +107,13 @@ CREATE POLICY "Admins can manage all ai history" ON "ai_history"
|
|
|
103
107
|
)
|
|
104
108
|
);
|
|
105
109
|
|
|
110
|
+
-- =============================================
|
|
111
|
+
-- TRIGGER: Auto-update updatedAt
|
|
112
|
+
-- =============================================
|
|
113
|
+
CREATE TRIGGER ai_history_set_updated_at
|
|
114
|
+
BEFORE UPDATE ON "ai_history"
|
|
115
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
116
|
+
|
|
106
117
|
-- =============================================
|
|
107
118
|
-- TRIGGER: Auto-update completedAt
|
|
108
119
|
-- =============================================
|
|
@@ -152,6 +163,7 @@ COMMENT ON COLUMN "ai_history"."estimatedCost" IS 'Cost in USD based on model pr
|
|
|
152
163
|
COMMENT ON COLUMN "ai_history"."balanceAfter" IS 'User credit balance after this operation';
|
|
153
164
|
COMMENT ON COLUMN "ai_history"."errorMessage" IS 'Error message if operation failed';
|
|
154
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';
|
|
155
167
|
COMMENT ON COLUMN "ai_history"."completedAt" IS 'When the AI operation finished (success or failure)';
|
|
156
168
|
|
|
157
169
|
-- =============================================
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- =============================================
|
|
2
2
|
-- AI HISTORY METADATA SYSTEM
|
|
3
3
|
-- Plugin: AI
|
|
4
|
-
-- Entity:
|
|
4
|
+
-- Entity: ai_history
|
|
5
5
|
-- =============================================
|
|
6
6
|
--
|
|
7
7
|
-- This migration adds comprehensive metadata capabilities through ai_history_metas table
|
|
@@ -14,27 +14,6 @@ import { CreateMetaPayload } from '@nextsparkjs/core/types/meta.types';
|
|
|
14
14
|
* METADATA BY ENDPOINT (Standardized)
|
|
15
15
|
* ============================================
|
|
16
16
|
*
|
|
17
|
-
* analyze-brief (/api/v1/theme/content-buddy/analyze-brief):
|
|
18
|
-
* - briefText: Original brief text (for auditing/debugging)
|
|
19
|
-
* - briefLength: Length of brief text
|
|
20
|
-
* - extractedFieldsCount: Number of fields extracted
|
|
21
|
-
* - audiencesCount: Number of audience entities extracted
|
|
22
|
-
* - objectivesCount: Number of objective entities extracted
|
|
23
|
-
* - productsCount: Number of product entities extracted
|
|
24
|
-
* - totalChildEntities: Total child entities extracted
|
|
25
|
-
* - clientName: Extracted client name
|
|
26
|
-
* - clientIndustry: Extracted client industry
|
|
27
|
-
*
|
|
28
|
-
* generate-content (/api/v1/theme/content-buddy/generate-content):
|
|
29
|
-
* Core generation parameters (reproducibility):
|
|
30
|
-
* - temperature: AI model temperature used
|
|
31
|
-
* - tone: Content tone
|
|
32
|
-
* - brandVoice: Brand voice style
|
|
33
|
-
* - audience: Target audience description
|
|
34
|
-
* - topic: Content topic
|
|
35
|
-
* - postType: Type of post
|
|
36
|
-
* - language: Content language code (e.g., "en", "es")
|
|
37
|
-
*
|
|
38
17
|
* Content specifications (analytics):
|
|
39
18
|
* - wordCount: Target word count
|
|
40
19
|
* - keywords: Comma-separated keywords (e.g., "innovation,tech,ai")
|
|
@@ -54,13 +33,6 @@ import { CreateMetaPayload } from '@nextsparkjs/core/types/meta.types';
|
|
|
54
33
|
* - objectiveId: Marketing objective (if specified)
|
|
55
34
|
* - targetAudienceId: Specific audience segment (if specified)
|
|
56
35
|
*
|
|
57
|
-
* refine-content (/api/v1/theme/content-buddy/refine-content):
|
|
58
|
-
* - userInstruction: User's refinement instruction
|
|
59
|
-
* - sourceOperationId: UUID linking to source operation (for workflow tracking)
|
|
60
|
-
* - temperature: AI model temperature used
|
|
61
|
-
* - tone: Desired tone (if specified)
|
|
62
|
-
* - platform: Social media platform being refined for
|
|
63
|
-
*
|
|
64
36
|
* ============================================
|
|
65
37
|
* DEPRECATED METADATA (Use ai_history columns instead)
|
|
66
38
|
* ============================================
|
|
@@ -20,6 +20,7 @@ export type AIStatus = 'pending' | 'processing' | 'completed' | 'failed'
|
|
|
20
20
|
|
|
21
21
|
export interface StartOperationParams {
|
|
22
22
|
userId: string
|
|
23
|
+
teamId: string // Required - team context for the operation
|
|
23
24
|
operation: AIOperation
|
|
24
25
|
model: string
|
|
25
26
|
provider?: AIProvider
|
|
@@ -75,6 +76,7 @@ export class AIHistoryService {
|
|
|
75
76
|
static async startOperation(params: StartOperationParams): Promise<string> {
|
|
76
77
|
const {
|
|
77
78
|
userId,
|
|
79
|
+
teamId,
|
|
78
80
|
operation,
|
|
79
81
|
model,
|
|
80
82
|
provider = 'anthropic',
|
|
@@ -86,6 +88,7 @@ export class AIHistoryService {
|
|
|
86
88
|
const result = await queryOne<{ id: string }>(
|
|
87
89
|
`INSERT INTO "ai_history" (
|
|
88
90
|
"userId",
|
|
91
|
+
"teamId",
|
|
89
92
|
operation,
|
|
90
93
|
model,
|
|
91
94
|
provider,
|
|
@@ -93,9 +96,9 @@ export class AIHistoryService {
|
|
|
93
96
|
"relatedEntityId",
|
|
94
97
|
status,
|
|
95
98
|
"createdAt"
|
|
96
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
|
99
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
|
|
97
100
|
RETURNING id`,
|
|
98
|
-
[userId, operation, model, provider, relatedEntityType || null, relatedEntityId || null, 'pending']
|
|
101
|
+
[userId, teamId, operation, model, provider, relatedEntityType || null, relatedEntityId || null, 'pending']
|
|
99
102
|
)
|
|
100
103
|
|
|
101
104
|
if (!result) {
|
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
|
+
}
|