@nextsparkjs/plugin-ai 0.1.0-beta.1
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/.env.example +79 -0
- package/README.md +529 -0
- package/api/README.md +65 -0
- package/api/ai-history/[id]/route.ts +112 -0
- package/api/embeddings/route.ts +129 -0
- package/api/generate/route.ts +160 -0
- package/docs/01-getting-started/01-introduction.md +237 -0
- package/docs/01-getting-started/02-installation.md +447 -0
- package/docs/01-getting-started/03-configuration.md +416 -0
- package/docs/02-features/01-text-generation.md +523 -0
- package/docs/02-features/02-embeddings.md +241 -0
- package/docs/02-features/03-ai-history.md +549 -0
- package/docs/03-advanced-usage/01-core-utilities.md +500 -0
- package/docs/04-use-cases/01-content-generation.md +453 -0
- package/entities/ai-history/ai-history.config.ts +123 -0
- package/entities/ai-history/ai-history.fields.ts +330 -0
- package/entities/ai-history/messages/en.json +56 -0
- package/entities/ai-history/messages/es.json +56 -0
- package/entities/ai-history/migrations/001_ai_history_table.sql +167 -0
- package/entities/ai-history/migrations/002_ai_history_metas.sql +103 -0
- package/lib/ai-history-meta-service.ts +379 -0
- package/lib/ai-history-service.ts +391 -0
- package/lib/ai-sdk.ts +7 -0
- package/lib/core-utils.ts +217 -0
- package/lib/plugin-env.ts +252 -0
- package/lib/sanitize.ts +122 -0
- package/lib/save-example.ts +237 -0
- package/lib/server-env.ts +104 -0
- package/package.json +23 -0
- package/plugin.config.ts +55 -0
- package/public/docs/login-404-error.png +0 -0
- package/tsconfig.json +47 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/types/ai.types.ts +51 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Plugin Environment Configuration (Server-Only)
|
|
3
|
+
*
|
|
4
|
+
* Uses centralized plugin environment loader from core
|
|
5
|
+
* Provides type-safe access to AI provider credentials and configuration
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getPluginEnv } from '@nextsparkjs/core/lib/plugins/env-loader'
|
|
9
|
+
|
|
10
|
+
interface AIPluginEnvConfig {
|
|
11
|
+
// AI provider credentials
|
|
12
|
+
ANTHROPIC_API_KEY?: string
|
|
13
|
+
OPENAI_API_KEY?: string
|
|
14
|
+
|
|
15
|
+
// Ollama configuration
|
|
16
|
+
OLLAMA_BASE_URL?: string
|
|
17
|
+
OLLAMA_DEFAULT_MODEL?: string
|
|
18
|
+
|
|
19
|
+
// AI provider selection
|
|
20
|
+
USE_LOCAL_AI?: string
|
|
21
|
+
DEFAULT_CLOUD_MODEL?: string
|
|
22
|
+
|
|
23
|
+
// Plugin configuration
|
|
24
|
+
AI_PLUGIN_ENABLED?: string
|
|
25
|
+
AI_PLUGIN_DEBUG?: string
|
|
26
|
+
AI_PLUGIN_DEFAULT_PROVIDER?: string
|
|
27
|
+
AI_PLUGIN_MAX_TOKENS?: string
|
|
28
|
+
AI_PLUGIN_DEFAULT_TEMPERATURE?: string
|
|
29
|
+
|
|
30
|
+
// Cost tracking
|
|
31
|
+
AI_PLUGIN_COST_TRACKING_ENABLED?: string
|
|
32
|
+
AI_PLUGIN_DAILY_COST_LIMIT?: string
|
|
33
|
+
AI_PLUGIN_MONTHLY_COST_LIMIT?: string
|
|
34
|
+
|
|
35
|
+
// Rate limiting
|
|
36
|
+
AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE?: string
|
|
37
|
+
AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class PluginEnvironment {
|
|
41
|
+
private static instance: PluginEnvironment
|
|
42
|
+
private config: AIPluginEnvConfig = {}
|
|
43
|
+
private loaded = false
|
|
44
|
+
|
|
45
|
+
private constructor() {
|
|
46
|
+
this.loadEnvironment()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static getInstance(): PluginEnvironment {
|
|
50
|
+
if (!PluginEnvironment.instance) {
|
|
51
|
+
PluginEnvironment.instance = new PluginEnvironment()
|
|
52
|
+
}
|
|
53
|
+
return PluginEnvironment.instance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private loadEnvironment(forceReload: boolean = false): void {
|
|
57
|
+
if (this.loaded && !forceReload) return
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Use centralized plugin env loader
|
|
61
|
+
const env = getPluginEnv('ai')
|
|
62
|
+
|
|
63
|
+
this.config = {
|
|
64
|
+
// AI provider credentials
|
|
65
|
+
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
|
|
66
|
+
OPENAI_API_KEY: env.OPENAI_API_KEY,
|
|
67
|
+
|
|
68
|
+
// Ollama configuration
|
|
69
|
+
OLLAMA_BASE_URL: env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
|
70
|
+
OLLAMA_DEFAULT_MODEL: env.OLLAMA_DEFAULT_MODEL || 'llama3.2:3b',
|
|
71
|
+
|
|
72
|
+
// AI provider selection
|
|
73
|
+
USE_LOCAL_AI: env.USE_LOCAL_AI || 'false',
|
|
74
|
+
DEFAULT_CLOUD_MODEL: env.DEFAULT_CLOUD_MODEL || 'claude-sonnet-4-5-20250929',
|
|
75
|
+
|
|
76
|
+
// Plugin configuration
|
|
77
|
+
AI_PLUGIN_ENABLED: env.AI_PLUGIN_ENABLED || 'true',
|
|
78
|
+
AI_PLUGIN_DEBUG: env.AI_PLUGIN_DEBUG || 'false',
|
|
79
|
+
AI_PLUGIN_DEFAULT_PROVIDER: env.AI_PLUGIN_DEFAULT_PROVIDER || 'anthropic',
|
|
80
|
+
AI_PLUGIN_MAX_TOKENS: env.AI_PLUGIN_MAX_TOKENS || '4000',
|
|
81
|
+
AI_PLUGIN_DEFAULT_TEMPERATURE: env.AI_PLUGIN_DEFAULT_TEMPERATURE || '0.7',
|
|
82
|
+
AI_PLUGIN_COST_TRACKING_ENABLED: env.AI_PLUGIN_COST_TRACKING_ENABLED || 'true',
|
|
83
|
+
AI_PLUGIN_DAILY_COST_LIMIT: env.AI_PLUGIN_DAILY_COST_LIMIT || '10.00',
|
|
84
|
+
AI_PLUGIN_MONTHLY_COST_LIMIT: env.AI_PLUGIN_MONTHLY_COST_LIMIT || '100.00',
|
|
85
|
+
AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE: env.AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE || '60',
|
|
86
|
+
AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE: env.AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE || '50000'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.logLoadedConfiguration()
|
|
90
|
+
this.loaded = true
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('[AI Plugin] Failed to load environment:', error)
|
|
93
|
+
this.loaded = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private logLoadedConfiguration(): void {
|
|
98
|
+
if (process.env.NODE_ENV === 'development') {
|
|
99
|
+
console.log('[AI Plugin] ℹ️ Plugin Environment Configuration:')
|
|
100
|
+
console.log(' → AI Provider Credentials:')
|
|
101
|
+
console.log(` - ANTHROPIC_API_KEY: ${this.config.ANTHROPIC_API_KEY ? '✓ set' : '✗ not set'}`)
|
|
102
|
+
console.log(` - OPENAI_API_KEY: ${this.config.OPENAI_API_KEY ? '✓ set' : '✗ not set'}`)
|
|
103
|
+
console.log(' → AI Provider Selection:')
|
|
104
|
+
console.log(` - USE_LOCAL_AI: ${this.config.USE_LOCAL_AI}`)
|
|
105
|
+
console.log(` - DEFAULT_CLOUD_MODEL: ${this.config.DEFAULT_CLOUD_MODEL}`)
|
|
106
|
+
console.log(' → Ollama Configuration:')
|
|
107
|
+
console.log(` - OLLAMA_BASE_URL: ${this.config.OLLAMA_BASE_URL}`)
|
|
108
|
+
console.log(` - OLLAMA_DEFAULT_MODEL: ${this.config.OLLAMA_DEFAULT_MODEL}`)
|
|
109
|
+
console.log(' → Plugin Settings:')
|
|
110
|
+
const pluginVars = [
|
|
111
|
+
'AI_PLUGIN_ENABLED', 'AI_PLUGIN_DEBUG', 'AI_PLUGIN_DEFAULT_PROVIDER',
|
|
112
|
+
'AI_PLUGIN_MAX_TOKENS', 'AI_PLUGIN_DEFAULT_TEMPERATURE',
|
|
113
|
+
'AI_PLUGIN_COST_TRACKING_ENABLED', 'AI_PLUGIN_DAILY_COST_LIMIT',
|
|
114
|
+
'AI_PLUGIN_MONTHLY_COST_LIMIT', 'AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE',
|
|
115
|
+
'AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE'
|
|
116
|
+
]
|
|
117
|
+
for (const v of pluginVars) {
|
|
118
|
+
const value = this.config[v as keyof AIPluginEnvConfig]
|
|
119
|
+
console.log(` - ${v}: ${value || 'default'}`)
|
|
120
|
+
}
|
|
121
|
+
console.log()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public getConfig(): AIPluginEnvConfig {
|
|
126
|
+
if (!this.loaded) {
|
|
127
|
+
this.loadEnvironment()
|
|
128
|
+
}
|
|
129
|
+
return this.config
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Helper methods
|
|
133
|
+
public getAnthropicApiKey(): string | undefined {
|
|
134
|
+
return this.getConfig().ANTHROPIC_API_KEY
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public getOpenAiApiKey(): string | undefined {
|
|
138
|
+
return this.getConfig().OPENAI_API_KEY
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public getOllamaBaseUrl(): string {
|
|
142
|
+
return this.getConfig().OLLAMA_BASE_URL || 'http://localhost:11434'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public getOllamaDefaultModel(): string {
|
|
146
|
+
return this.getConfig().OLLAMA_DEFAULT_MODEL || 'llama3.2:3b'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public isUseLocalAI(): boolean {
|
|
150
|
+
return this.getConfig().USE_LOCAL_AI === 'true'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public getDefaultCloudModel(): string {
|
|
154
|
+
return this.getConfig().DEFAULT_CLOUD_MODEL || 'claude-sonnet-4-5-20250929'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public getDefaultProvider(): string {
|
|
158
|
+
return this.getConfig().AI_PLUGIN_DEFAULT_PROVIDER || 'anthropic'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public getDefaultModel(): string {
|
|
162
|
+
const useLocal = this.isUseLocalAI()
|
|
163
|
+
const ollamaModel = this.getOllamaDefaultModel()
|
|
164
|
+
const cloudModel = this.getDefaultCloudModel()
|
|
165
|
+
console.log(`🔍 [getDefaultModel] USE_LOCAL_AI=${useLocal}, ollamaModel=${ollamaModel}, cloudModel=${cloudModel}`)
|
|
166
|
+
const selectedModel = useLocal ? ollamaModel : cloudModel
|
|
167
|
+
console.log(`🔍 [getDefaultModel] Returning: ${selectedModel}`)
|
|
168
|
+
return selectedModel
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public getMaxTokens(): number {
|
|
172
|
+
return parseInt(this.getConfig().AI_PLUGIN_MAX_TOKENS || '4000', 10)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public getDefaultTemperature(): number {
|
|
176
|
+
return parseFloat(this.getConfig().AI_PLUGIN_DEFAULT_TEMPERATURE || '0.7')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public isCostTrackingEnabled(): boolean {
|
|
180
|
+
return this.getConfig().AI_PLUGIN_COST_TRACKING_ENABLED === 'true'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public getDailyCostLimit(): number {
|
|
184
|
+
return parseFloat(this.getConfig().AI_PLUGIN_DAILY_COST_LIMIT || '10.00')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public getMonthlyCostLimit(): number {
|
|
188
|
+
return parseFloat(this.getConfig().AI_PLUGIN_MONTHLY_COST_LIMIT || '100.00')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public getRateLimitRequestsPerMinute(): number {
|
|
192
|
+
return parseInt(this.getConfig().AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE || '60', 10)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public getRateLimitTokensPerMinute(): number {
|
|
196
|
+
return parseInt(this.getConfig().AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE || '50000', 10)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public isPluginEnabled(): boolean {
|
|
200
|
+
return this.getConfig().AI_PLUGIN_ENABLED !== 'false'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public isDebugEnabled(): boolean {
|
|
204
|
+
return this.getConfig().AI_PLUGIN_DEBUG === 'true'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public validateEnvironment(): { valid: boolean; errors: string[] } {
|
|
208
|
+
const errors: string[] = []
|
|
209
|
+
const config = this.getConfig()
|
|
210
|
+
|
|
211
|
+
if (!config.USE_LOCAL_AI || config.USE_LOCAL_AI === 'false') {
|
|
212
|
+
if (!config.ANTHROPIC_API_KEY && !config.OPENAI_API_KEY) {
|
|
213
|
+
errors.push('Cloud AI is enabled but no API keys are configured (ANTHROPIC_API_KEY or OPENAI_API_KEY required)')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (config.AI_PLUGIN_ENABLED === 'false') {
|
|
218
|
+
errors.push('AI Plugin is disabled (AI_PLUGIN_ENABLED=false)')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
valid: errors.length === 0,
|
|
223
|
+
errors
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public reload(): void {
|
|
228
|
+
this.loaded = false
|
|
229
|
+
this.loadEnvironment(true)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const pluginEnv = PluginEnvironment.getInstance()
|
|
234
|
+
|
|
235
|
+
// Convenience exports
|
|
236
|
+
export const getAnthropicApiKey = () => pluginEnv.getAnthropicApiKey()
|
|
237
|
+
export const getOpenAiApiKey = () => pluginEnv.getOpenAiApiKey()
|
|
238
|
+
export const getOllamaBaseUrl = () => pluginEnv.getOllamaBaseUrl()
|
|
239
|
+
export const getOllamaDefaultModel = () => pluginEnv.getOllamaDefaultModel()
|
|
240
|
+
export const isUseLocalAI = () => pluginEnv.isUseLocalAI()
|
|
241
|
+
export const getDefaultCloudModel = () => pluginEnv.getDefaultCloudModel()
|
|
242
|
+
export const getDefaultProvider = () => pluginEnv.getDefaultProvider()
|
|
243
|
+
export const getDefaultModel = () => pluginEnv.getDefaultModel()
|
|
244
|
+
export const getMaxTokens = () => pluginEnv.getMaxTokens()
|
|
245
|
+
export const getDefaultTemperature = () => pluginEnv.getDefaultTemperature()
|
|
246
|
+
export const isCostTrackingEnabled = () => pluginEnv.isCostTrackingEnabled()
|
|
247
|
+
export const getDailyCostLimit = () => pluginEnv.getDailyCostLimit()
|
|
248
|
+
export const getMonthlyCostLimit = () => pluginEnv.getMonthlyCostLimit()
|
|
249
|
+
export const getRateLimitRequestsPerMinute = () => pluginEnv.getRateLimitRequestsPerMinute()
|
|
250
|
+
export const getRateLimitTokensPerMinute = () => pluginEnv.getRateLimitTokensPerMinute()
|
|
251
|
+
export const isPluginEnabled = () => pluginEnv.isPluginEnabled()
|
|
252
|
+
export const isDebugEnabled = () => pluginEnv.isDebugEnabled()
|
package/lib/sanitize.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data sanitization utilities for AI examples
|
|
3
|
+
* Removes sensitive information before storing examples
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_PATTERNS = {
|
|
7
|
+
// Email addresses
|
|
8
|
+
email: /[\w.-]+@[\w.-]+\.\w+/gi,
|
|
9
|
+
|
|
10
|
+
// API keys and tokens
|
|
11
|
+
apiKey: /[a-zA-Z0-9]{20,}/g,
|
|
12
|
+
bearerToken: /Bearer\s+[\w-]+\.[\w-]+\.[\w-]+/gi,
|
|
13
|
+
|
|
14
|
+
// Common secret patterns
|
|
15
|
+
password: /password['":\s=]+[\w!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/gi,
|
|
16
|
+
secret: /secret['":\s=]+[\w!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/gi,
|
|
17
|
+
|
|
18
|
+
// Credit card numbers (basic pattern)
|
|
19
|
+
creditCard: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
20
|
+
|
|
21
|
+
// Phone numbers (basic pattern)
|
|
22
|
+
phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
|
|
23
|
+
|
|
24
|
+
// URLs with credentials
|
|
25
|
+
urlWithCredentials: /https?:\/\/[^:]+:[^@]+@[^\s]+/gi,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const REPLACEMENTS = {
|
|
29
|
+
email: '[EMAIL_REDACTED]',
|
|
30
|
+
apiKey: '[API_KEY_REDACTED]',
|
|
31
|
+
bearerToken: 'Bearer [TOKEN_REDACTED]',
|
|
32
|
+
password: 'password: [REDACTED]',
|
|
33
|
+
secret: 'secret: [REDACTED]',
|
|
34
|
+
creditCard: '[CARD_REDACTED]',
|
|
35
|
+
phone: '[PHONE_REDACTED]',
|
|
36
|
+
urlWithCredentials: 'https://[CREDENTIALS_REDACTED]',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SanitizationResult {
|
|
40
|
+
sanitized: string
|
|
41
|
+
redactedCount: number
|
|
42
|
+
redactedTypes: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize text by removing sensitive information
|
|
47
|
+
*/
|
|
48
|
+
export function sanitizeText(text: string): SanitizationResult {
|
|
49
|
+
if (!text || typeof text !== 'string') {
|
|
50
|
+
return {
|
|
51
|
+
sanitized: text || '',
|
|
52
|
+
redactedCount: 0,
|
|
53
|
+
redactedTypes: []
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let sanitized = text
|
|
58
|
+
let redactedCount = 0
|
|
59
|
+
const redactedTypes: string[] = []
|
|
60
|
+
|
|
61
|
+
for (const [type, pattern] of Object.entries(SENSITIVE_PATTERNS)) {
|
|
62
|
+
const matches = sanitized.match(pattern)
|
|
63
|
+
if (matches && matches.length > 0) {
|
|
64
|
+
const replacement = REPLACEMENTS[type as keyof typeof REPLACEMENTS]
|
|
65
|
+
sanitized = sanitized.replace(pattern, replacement)
|
|
66
|
+
redactedCount += matches.length
|
|
67
|
+
redactedTypes.push(type)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
sanitized,
|
|
73
|
+
redactedCount,
|
|
74
|
+
redactedTypes
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize prompt before saving as example
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizePrompt(prompt: string): string {
|
|
82
|
+
return sanitizeText(prompt).sanitized
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sanitize response before saving as example
|
|
87
|
+
*/
|
|
88
|
+
export function sanitizeResponse(response: string): string {
|
|
89
|
+
return sanitizeText(response).sanitized
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if text contains sensitive information
|
|
94
|
+
*/
|
|
95
|
+
export function containsSensitiveInfo(text: string): boolean {
|
|
96
|
+
if (!text || typeof text !== 'string') {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const pattern of Object.values(SENSITIVE_PATTERNS)) {
|
|
101
|
+
if (pattern.test(text)) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get sanitization report for a text
|
|
111
|
+
*/
|
|
112
|
+
export function getSanitizationReport(text: string): {
|
|
113
|
+
hasSensitiveInfo: boolean
|
|
114
|
+
details: SanitizationResult
|
|
115
|
+
} {
|
|
116
|
+
const details = sanitizeText(text)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
hasSensitiveInfo: details.redactedCount > 0,
|
|
120
|
+
details
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Example Saving - PostgreSQL Implementation
|
|
3
|
+
*
|
|
4
|
+
* Uses project's PostgreSQL connection with RLS support.
|
|
5
|
+
* Self-contained plugin with direct DB access.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mutateWithRLS, queryWithRLS } from '@nextsparkjs/core/lib/db'
|
|
9
|
+
import { sanitizePrompt, sanitizeResponse } from './sanitize'
|
|
10
|
+
|
|
11
|
+
export interface AIExampleData {
|
|
12
|
+
title?: string
|
|
13
|
+
prompt: string
|
|
14
|
+
response: string
|
|
15
|
+
model: string
|
|
16
|
+
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
17
|
+
userId?: string
|
|
18
|
+
metadata?: {
|
|
19
|
+
tokens?: number
|
|
20
|
+
cost?: number
|
|
21
|
+
duration?: number
|
|
22
|
+
provider?: string
|
|
23
|
+
isLocal?: boolean
|
|
24
|
+
platforms?: number
|
|
25
|
+
imagesProcessed?: number
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SaveExampleResult {
|
|
30
|
+
success: boolean
|
|
31
|
+
id?: string
|
|
32
|
+
error?: string
|
|
33
|
+
sanitizationApplied?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save AI example to PostgreSQL database
|
|
38
|
+
*/
|
|
39
|
+
export async function saveAIExample(
|
|
40
|
+
data: AIExampleData,
|
|
41
|
+
userId: string
|
|
42
|
+
): Promise<SaveExampleResult> {
|
|
43
|
+
try {
|
|
44
|
+
// 1. Sanitize sensitive data
|
|
45
|
+
const sanitizedPrompt = sanitizePrompt(data.prompt)
|
|
46
|
+
const sanitizedResponse = sanitizeResponse(data.response)
|
|
47
|
+
|
|
48
|
+
const sanitizationApplied =
|
|
49
|
+
sanitizedPrompt !== data.prompt ||
|
|
50
|
+
sanitizedResponse !== data.response
|
|
51
|
+
|
|
52
|
+
// 2. Generate title if not provided
|
|
53
|
+
const title = data.title || generateTitle(sanitizedPrompt)
|
|
54
|
+
|
|
55
|
+
// 3. Insert into database with RLS
|
|
56
|
+
const query = `
|
|
57
|
+
INSERT INTO ai_example (
|
|
58
|
+
title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
|
|
59
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
60
|
+
RETURNING id
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
const params = [
|
|
64
|
+
title,
|
|
65
|
+
sanitizedPrompt,
|
|
66
|
+
sanitizedResponse,
|
|
67
|
+
data.model,
|
|
68
|
+
data.status,
|
|
69
|
+
userId,
|
|
70
|
+
JSON.stringify(data.metadata || {})
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const result = await mutateWithRLS<{ id: string }>(query, params, userId)
|
|
74
|
+
|
|
75
|
+
if (result.rowCount === 0 || !result.rows[0]) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: 'Failed to insert example'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('[AI Plugin] Example saved to database:', {
|
|
83
|
+
id: result.rows[0].id,
|
|
84
|
+
title,
|
|
85
|
+
sanitizationApplied
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
id: result.rows[0].id,
|
|
91
|
+
sanitizationApplied
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('[AI Plugin] Save example failed:', error)
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get AI examples from PostgreSQL
|
|
105
|
+
*/
|
|
106
|
+
export async function getAIExamples(params: {
|
|
107
|
+
userId?: string
|
|
108
|
+
limit?: number
|
|
109
|
+
offset?: number
|
|
110
|
+
status?: string
|
|
111
|
+
}) {
|
|
112
|
+
try {
|
|
113
|
+
const { userId, limit = 50, offset = 0, status } = params
|
|
114
|
+
|
|
115
|
+
let query = `
|
|
116
|
+
SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
|
|
117
|
+
FROM ai_example
|
|
118
|
+
WHERE 1=1
|
|
119
|
+
`
|
|
120
|
+
const queryParams: (string | number)[] = []
|
|
121
|
+
|
|
122
|
+
if (status) {
|
|
123
|
+
queryParams.push(status)
|
|
124
|
+
query += ` AND status = $${queryParams.length}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
query += ` ORDER BY "createdAt" DESC LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`
|
|
128
|
+
queryParams.push(limit, offset)
|
|
129
|
+
|
|
130
|
+
const rows = await queryWithRLS(query, queryParams, userId)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
data: rows
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('[AI Plugin] Get examples failed:', error)
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
141
|
+
data: []
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get single AI example by ID from PostgreSQL
|
|
148
|
+
*/
|
|
149
|
+
export async function getAIExampleById(id: string, userId?: string) {
|
|
150
|
+
try {
|
|
151
|
+
const query = `
|
|
152
|
+
SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
|
|
153
|
+
FROM ai_example
|
|
154
|
+
WHERE id = $1
|
|
155
|
+
`
|
|
156
|
+
|
|
157
|
+
const rows = await queryWithRLS(query, [id], userId)
|
|
158
|
+
|
|
159
|
+
if (rows.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: 'Example not found'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
data: rows[0]
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[AI Plugin] Get example by ID failed:', error)
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete AI example from PostgreSQL
|
|
181
|
+
*/
|
|
182
|
+
export async function deleteAIExample(id: string, userId: string) {
|
|
183
|
+
try {
|
|
184
|
+
const query = `DELETE FROM ai_example WHERE id = $1 RETURNING id`
|
|
185
|
+
|
|
186
|
+
const result = await mutateWithRLS<{ id: string }>(query, [id], userId)
|
|
187
|
+
|
|
188
|
+
if (result.rowCount === 0) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
error: 'Example not found or unauthorized'
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
id: result.rows[0].id
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('[AI Plugin] Delete example failed:', error)
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate title from prompt (helper)
|
|
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)
|
|
220
|
+
*/
|
|
221
|
+
export async function saveExampleSafely(
|
|
222
|
+
data: AIExampleData,
|
|
223
|
+
userId: string
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
const result = await saveAIExample(data, userId)
|
|
227
|
+
|
|
228
|
+
if (!result.success) {
|
|
229
|
+
console.error('[AI Plugin] Example save failed:', result.error)
|
|
230
|
+
} else if (result.sanitizationApplied) {
|
|
231
|
+
console.warn('[AI Plugin] Sanitization was applied to saved example')
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Silent fail - don't break the main request
|
|
235
|
+
console.error('[AI Plugin] Example save error:', error)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Only Environment Variable Access
|
|
3
|
+
*
|
|
4
|
+
* This module provides server-only access to plugin environment variables
|
|
5
|
+
* preventing client-side access to sensitive configuration
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pluginEnv } from './plugin-env'
|
|
9
|
+
|
|
10
|
+
// Server-only environment getters
|
|
11
|
+
export async function getServerAnthropicApiKey(): Promise<string | undefined> {
|
|
12
|
+
return pluginEnv.getAnthropicApiKey()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getServerOpenAiApiKey(): Promise<string | undefined> {
|
|
16
|
+
return pluginEnv.getOpenAiApiKey()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getServerOllamaBaseUrl(): Promise<string> {
|
|
20
|
+
return pluginEnv.getOllamaBaseUrl()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getServerOllamaDefaultModel(): Promise<string> {
|
|
24
|
+
return pluginEnv.getOllamaDefaultModel()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function isServerUseLocalAI(): Promise<boolean> {
|
|
28
|
+
return pluginEnv.isUseLocalAI()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getServerDefaultCloudModel(): Promise<string> {
|
|
32
|
+
return pluginEnv.getDefaultCloudModel()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getServerDefaultModel(): Promise<string> {
|
|
36
|
+
return pluginEnv.getDefaultModel()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function isServerPluginEnabled(): Promise<boolean> {
|
|
40
|
+
return pluginEnv.isPluginEnabled()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function isServerDebugEnabled(): Promise<boolean> {
|
|
44
|
+
return pluginEnv.isDebugEnabled()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getServerDefaultProvider(): Promise<string> {
|
|
48
|
+
return pluginEnv.getDefaultProvider()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getServerMaxTokens(): Promise<number> {
|
|
52
|
+
return pluginEnv.getMaxTokens()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getServerDefaultTemperature(): Promise<number> {
|
|
56
|
+
return pluginEnv.getDefaultTemperature()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function isServerCostTrackingEnabled(): Promise<boolean> {
|
|
60
|
+
return pluginEnv.isCostTrackingEnabled()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getServerDailyCostLimit(): Promise<number> {
|
|
64
|
+
return pluginEnv.getDailyCostLimit()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getServerMonthlyCostLimit(): Promise<number> {
|
|
68
|
+
return pluginEnv.getMonthlyCostLimit()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getServerRateLimitRequestsPerMinute(): Promise<number> {
|
|
72
|
+
return pluginEnv.getRateLimitRequestsPerMinute()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getServerRateLimitTokensPerMinute(): Promise<number> {
|
|
76
|
+
return pluginEnv.getRateLimitTokensPerMinute()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function validateServerPluginEnvironment(): Promise<{ valid: boolean; errors: string[] }> {
|
|
80
|
+
return pluginEnv.validateEnvironment()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Configuration object for API routes
|
|
84
|
+
export async function getServerPluginConfig() {
|
|
85
|
+
return {
|
|
86
|
+
anthropicApiKey: await getServerAnthropicApiKey(),
|
|
87
|
+
openaiApiKey: await getServerOpenAiApiKey(),
|
|
88
|
+
ollamaBaseUrl: await getServerOllamaBaseUrl(),
|
|
89
|
+
ollamaDefaultModel: await getServerOllamaDefaultModel(),
|
|
90
|
+
useLocalAI: await isServerUseLocalAI(),
|
|
91
|
+
defaultCloudModel: await getServerDefaultCloudModel(),
|
|
92
|
+
defaultModel: await getServerDefaultModel(),
|
|
93
|
+
pluginEnabled: await isServerPluginEnabled(),
|
|
94
|
+
debugEnabled: await isServerDebugEnabled(),
|
|
95
|
+
defaultProvider: await getServerDefaultProvider(),
|
|
96
|
+
maxTokens: await getServerMaxTokens(),
|
|
97
|
+
defaultTemperature: await getServerDefaultTemperature(),
|
|
98
|
+
costTrackingEnabled: await isServerCostTrackingEnabled(),
|
|
99
|
+
dailyCostLimit: await getServerDailyCostLimit(),
|
|
100
|
+
monthlyCostLimit: await getServerMonthlyCostLimit(),
|
|
101
|
+
rateLimitRequestsPerMinute: await getServerRateLimitRequestsPerMinute(),
|
|
102
|
+
rateLimitTokensPerMinute: await getServerRateLimitTokensPerMinute()
|
|
103
|
+
}
|
|
104
|
+
}
|