@geenius/ai 0.1.0
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/.changeset/config.json +11 -0
- package/.env.example +2 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc +7 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +15 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +61 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +18 -0
- package/SUPPORT.md +14 -0
- package/package.json +75 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/index.ts +8 -0
- package/packages/convex/src/mutations/messages.ts +29 -0
- package/packages/convex/src/queries/messages.ts +24 -0
- package/packages/convex/src/schema.ts +20 -0
- package/packages/convex/tsconfig.json +11 -0
- package/packages/convex/tsup.config.ts +17 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +60 -0
- package/packages/react/src/components/AILogTable.tsx +90 -0
- package/packages/react/src/components/ChatWindow.tsx +118 -0
- package/packages/react/src/components/GenerationCard.tsx +73 -0
- package/packages/react/src/components/ImageGenerator.tsx +103 -0
- package/packages/react/src/components/ModelSelector.tsx +44 -0
- package/packages/react/src/components/ModelTestRunner.tsx +148 -0
- package/packages/react/src/components/VoiceSelector.tsx +51 -0
- package/packages/react/src/components/index.ts +9 -0
- package/packages/react/src/hooks/index.ts +12 -0
- package/packages/react/src/hooks/useAI.ts +158 -0
- package/packages/react/src/hooks/useAILogs.ts +40 -0
- package/packages/react/src/hooks/useAIModels.ts +53 -0
- package/packages/react/src/hooks/useChat.ts +141 -0
- package/packages/react/src/hooks/useContentManager.ts +108 -0
- package/packages/react/src/hooks/useImageGeneration.ts +82 -0
- package/packages/react/src/hooks/useMemory.ts +161 -0
- package/packages/react/src/hooks/useModelTest.ts +126 -0
- package/packages/react/src/hooks/useRealtimeAudio.ts +203 -0
- package/packages/react/src/hooks/useSkills.ts +114 -0
- package/packages/react/src/hooks/useTextToSpeech.ts +99 -0
- package/packages/react/src/hooks/useTranscription.ts +119 -0
- package/packages/react/src/hooks/useVideoGeneration.ts +79 -0
- package/packages/react/src/index.ts +42 -0
- package/packages/react/src/pages/AILogsPage.tsx +98 -0
- package/packages/react/src/pages/ChatPage.tsx +42 -0
- package/packages/react/src/pages/ModelTestPage.tsx +33 -0
- package/packages/react/src/pages/index.ts +5 -0
- package/packages/react/tsconfig.json +26 -0
- package/packages/react/tsup.config.ts +22 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +45 -0
- package/packages/react-css/src/ai.css +857 -0
- package/packages/react-css/src/components/AILogTable.tsx +90 -0
- package/packages/react-css/src/components/ChatWindow.tsx +118 -0
- package/packages/react-css/src/components/GenerationCard.tsx +73 -0
- package/packages/react-css/src/components/ImageGenerator.tsx +103 -0
- package/packages/react-css/src/components/ModelSelector.tsx +44 -0
- package/packages/react-css/src/components/ModelTestRunner.tsx +148 -0
- package/packages/react-css/src/components/VoiceSelector.tsx +51 -0
- package/packages/react-css/src/components/index.ts +9 -0
- package/packages/react-css/src/hooks/index.ts +12 -0
- package/packages/react-css/src/hooks/useAI.ts +153 -0
- package/packages/react-css/src/hooks/useAILogs.ts +40 -0
- package/packages/react-css/src/hooks/useAIModels.ts +51 -0
- package/packages/react-css/src/hooks/useChat.ts +145 -0
- package/packages/react-css/src/hooks/useContentManager.ts +108 -0
- package/packages/react-css/src/hooks/useImageGeneration.ts +82 -0
- package/packages/react-css/src/hooks/useMemory.ts +161 -0
- package/packages/react-css/src/hooks/useModelTest.ts +122 -0
- package/packages/react-css/src/hooks/useRealtimeAudio.ts +203 -0
- package/packages/react-css/src/hooks/useSkills.ts +114 -0
- package/packages/react-css/src/hooks/useTextToSpeech.ts +99 -0
- package/packages/react-css/src/hooks/useTranscription.ts +119 -0
- package/packages/react-css/src/hooks/useVideoGeneration.ts +79 -0
- package/packages/react-css/src/index.ts +35 -0
- package/packages/react-css/src/pages/AILogsPage.tsx +98 -0
- package/packages/react-css/src/pages/ChatPage.tsx +42 -0
- package/packages/react-css/src/pages/ModelTestPage.tsx +33 -0
- package/packages/react-css/src/pages/index.ts +5 -0
- package/packages/react-css/src/styles.css +127 -0
- package/packages/react-css/tsconfig.json +26 -0
- package/packages/react-css/tsup.config.ts +2 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +71 -0
- package/packages/shared/src/__tests__/ai.test.ts +67 -0
- package/packages/shared/src/ai-client.ts +243 -0
- package/packages/shared/src/config.ts +235 -0
- package/packages/shared/src/content.ts +249 -0
- package/packages/shared/src/convex/helpers.ts +163 -0
- package/packages/shared/src/convex/index.ts +16 -0
- package/packages/shared/src/convex/schemas.ts +146 -0
- package/packages/shared/src/convex/validators.ts +136 -0
- package/packages/shared/src/index.ts +107 -0
- package/packages/shared/src/memory.ts +197 -0
- package/packages/shared/src/providers/base.ts +103 -0
- package/packages/shared/src/providers/elevenlabs.ts +155 -0
- package/packages/shared/src/providers/index.ts +28 -0
- package/packages/shared/src/providers/openai-compatible.ts +286 -0
- package/packages/shared/src/providers/registry.ts +113 -0
- package/packages/shared/src/providers/replicate-fal.ts +230 -0
- package/packages/shared/src/skills.ts +273 -0
- package/packages/shared/src/types.ts +501 -0
- package/packages/shared/tsconfig.json +25 -0
- package/packages/shared/tsup.config.ts +22 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +59 -0
- package/packages/solidjs/src/components/ChatWindow.tsx +78 -0
- package/packages/solidjs/src/components/GenerationCard.tsx +62 -0
- package/packages/solidjs/src/components/ModelTestRunner.tsx +119 -0
- package/packages/solidjs/src/components/index.ts +5 -0
- package/packages/solidjs/src/index.ts +32 -0
- package/packages/solidjs/src/pages/ChatPage.tsx +22 -0
- package/packages/solidjs/src/pages/ModelTestPage.tsx +22 -0
- package/packages/solidjs/src/pages/index.ts +4 -0
- package/packages/solidjs/src/primitives/createAI.ts +79 -0
- package/packages/solidjs/src/primitives/createChat.ts +100 -0
- package/packages/solidjs/src/primitives/createContentManager.ts +61 -0
- package/packages/solidjs/src/primitives/createImageGeneration.ts +46 -0
- package/packages/solidjs/src/primitives/createMemory.ts +127 -0
- package/packages/solidjs/src/primitives/createModelTest.ts +89 -0
- package/packages/solidjs/src/primitives/createSkills.ts +83 -0
- package/packages/solidjs/src/primitives/createTextToSpeech.ts +56 -0
- package/packages/solidjs/src/primitives/createVideoGeneration.ts +46 -0
- package/packages/solidjs/src/primitives/index.ts +8 -0
- package/packages/solidjs/tsconfig.json +27 -0
- package/packages/solidjs/tsup.config.ts +21 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +44 -0
- package/packages/solidjs-css/src/ai.css +857 -0
- package/packages/solidjs-css/src/components/ChatWindow.tsx +78 -0
- package/packages/solidjs-css/src/components/GenerationCard.tsx +62 -0
- package/packages/solidjs-css/src/components/ModelTestRunner.tsx +119 -0
- package/packages/solidjs-css/src/components/index.ts +5 -0
- package/packages/solidjs-css/src/index.ts +26 -0
- package/packages/solidjs-css/src/pages/ChatPage.tsx +22 -0
- package/packages/solidjs-css/src/pages/ModelTestPage.tsx +22 -0
- package/packages/solidjs-css/src/pages/index.ts +4 -0
- package/packages/solidjs-css/src/primitives/createAI.ts +79 -0
- package/packages/solidjs-css/src/primitives/createChat.ts +100 -0
- package/packages/solidjs-css/src/primitives/createContentManager.ts +61 -0
- package/packages/solidjs-css/src/primitives/createImageGeneration.ts +46 -0
- package/packages/solidjs-css/src/primitives/createMemory.ts +127 -0
- package/packages/solidjs-css/src/primitives/createModelTest.ts +89 -0
- package/packages/solidjs-css/src/primitives/createSkills.ts +83 -0
- package/packages/solidjs-css/src/primitives/createTextToSpeech.ts +56 -0
- package/packages/solidjs-css/src/primitives/createVideoGeneration.ts +46 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/styles.css +127 -0
- package/packages/solidjs-css/tsconfig.json +27 -0
- package/packages/solidjs-css/tsup.config.ts +2 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// @geenius-ai/shared — src/content.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content management types and helpers.
|
|
5
|
+
* Supports AI-powered content generation, rewriting, translation, and templating.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AIProviderType } from './types'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Content Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export type ContentType =
|
|
15
|
+
| 'text'
|
|
16
|
+
| 'email'
|
|
17
|
+
| 'social-post'
|
|
18
|
+
| 'product-description'
|
|
19
|
+
| 'blog-post'
|
|
20
|
+
| 'landing-page'
|
|
21
|
+
| 'notification'
|
|
22
|
+
| 'seo-meta'
|
|
23
|
+
| 'ad-copy'
|
|
24
|
+
| 'custom'
|
|
25
|
+
|
|
26
|
+
export type ContentAction =
|
|
27
|
+
| 'generate'
|
|
28
|
+
| 'rewrite'
|
|
29
|
+
| 'translate'
|
|
30
|
+
| 'summarize'
|
|
31
|
+
| 'expand'
|
|
32
|
+
| 'shorten'
|
|
33
|
+
| 'change-tone'
|
|
34
|
+
| 'extract'
|
|
35
|
+
| 'proofread'
|
|
36
|
+
| 'variations'
|
|
37
|
+
|
|
38
|
+
export type ContentTone =
|
|
39
|
+
| 'professional'
|
|
40
|
+
| 'casual'
|
|
41
|
+
| 'friendly'
|
|
42
|
+
| 'formal'
|
|
43
|
+
| 'persuasive'
|
|
44
|
+
| 'technical'
|
|
45
|
+
| 'humorous'
|
|
46
|
+
| 'empathetic'
|
|
47
|
+
|
|
48
|
+
export interface ContentTemplate {
|
|
49
|
+
id: string
|
|
50
|
+
name: string
|
|
51
|
+
type: ContentType
|
|
52
|
+
description?: string
|
|
53
|
+
systemPrompt: string
|
|
54
|
+
userPromptTemplate: string
|
|
55
|
+
/** Expected output fields */
|
|
56
|
+
outputFields?: ContentOutputField[]
|
|
57
|
+
/** Default tone */
|
|
58
|
+
tone?: ContentTone
|
|
59
|
+
/** Max tokens for generation */
|
|
60
|
+
maxTokens?: number
|
|
61
|
+
/** Tags for filtering */
|
|
62
|
+
tags?: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ContentOutputField {
|
|
66
|
+
name: string
|
|
67
|
+
type: 'string' | 'string[]' | 'number'
|
|
68
|
+
description?: string
|
|
69
|
+
required?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ContentItem {
|
|
73
|
+
id: string
|
|
74
|
+
type: ContentType
|
|
75
|
+
action: ContentAction
|
|
76
|
+
input: string
|
|
77
|
+
output: string
|
|
78
|
+
templateId?: string
|
|
79
|
+
model?: string
|
|
80
|
+
provider?: AIProviderType
|
|
81
|
+
tone?: ContentTone
|
|
82
|
+
language?: string
|
|
83
|
+
metadata?: Record<string, unknown>
|
|
84
|
+
tokens?: number
|
|
85
|
+
costUsd?: number
|
|
86
|
+
createdAt: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ContentSchema {
|
|
90
|
+
/** Zod/Yup schema for the content fields */
|
|
91
|
+
fields: ContentFieldMeta[]
|
|
92
|
+
/** Context for AI generation */
|
|
93
|
+
context?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ContentFieldMeta {
|
|
97
|
+
name: string
|
|
98
|
+
type: 'string' | 'number' | 'boolean' | 'array' | 'object'
|
|
99
|
+
label?: string
|
|
100
|
+
/** AI hint for content generation */
|
|
101
|
+
hint?: string
|
|
102
|
+
/** Max length */
|
|
103
|
+
maxLength?: number
|
|
104
|
+
/** Allowed values */
|
|
105
|
+
enum?: string[]
|
|
106
|
+
required?: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Content Generation Options
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
export interface ContentGenerateOptions {
|
|
114
|
+
action: ContentAction
|
|
115
|
+
/** Input text to transform, or prompt for generation */
|
|
116
|
+
input: string
|
|
117
|
+
type?: ContentType
|
|
118
|
+
tone?: ContentTone
|
|
119
|
+
/** Target language for translation */
|
|
120
|
+
language?: string
|
|
121
|
+
/** Number of variations to generate */
|
|
122
|
+
variations?: number
|
|
123
|
+
/** Template to use */
|
|
124
|
+
templateId?: string
|
|
125
|
+
/** Custom instructions */
|
|
126
|
+
instructions?: string
|
|
127
|
+
/** Max output length */
|
|
128
|
+
maxLength?: number
|
|
129
|
+
/** Model override */
|
|
130
|
+
model?: string
|
|
131
|
+
/** Schema context for structured generation */
|
|
132
|
+
schema?: ContentSchema
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ContentResult {
|
|
136
|
+
content: string
|
|
137
|
+
/** For multi-variation generation */
|
|
138
|
+
alternatives?: string[]
|
|
139
|
+
action: ContentAction
|
|
140
|
+
type: ContentType
|
|
141
|
+
model: string
|
|
142
|
+
tokens?: number
|
|
143
|
+
costUsd?: number
|
|
144
|
+
durationMs: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Built-in Templates
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export const BUILT_IN_TEMPLATES: Record<string, ContentTemplate> = {
|
|
152
|
+
'product-description': {
|
|
153
|
+
id: 'product-description',
|
|
154
|
+
name: 'Product Description',
|
|
155
|
+
type: 'product-description',
|
|
156
|
+
description: 'Generate a compelling product description',
|
|
157
|
+
systemPrompt: 'You are a skilled copywriter specializing in product descriptions. Write clear, persuasive descriptions that highlight key features and benefits.',
|
|
158
|
+
userPromptTemplate: 'Write a product description for: {{input}}\n\n{{#instructions}}Additional instructions: {{instructions}}{{/instructions}}',
|
|
159
|
+
outputFields: [
|
|
160
|
+
{ name: 'headline', type: 'string', required: true },
|
|
161
|
+
{ name: 'description', type: 'string', required: true },
|
|
162
|
+
{ name: 'bulletPoints', type: 'string[]' },
|
|
163
|
+
],
|
|
164
|
+
tone: 'persuasive',
|
|
165
|
+
maxTokens: 500,
|
|
166
|
+
tags: ['ecommerce', 'marketing'],
|
|
167
|
+
},
|
|
168
|
+
'email-subject': {
|
|
169
|
+
id: 'email-subject',
|
|
170
|
+
name: 'Email Subject Line',
|
|
171
|
+
type: 'email',
|
|
172
|
+
description: 'Generate email subject lines',
|
|
173
|
+
systemPrompt: 'You are an email marketing expert. Generate compelling subject lines that drive opens.',
|
|
174
|
+
userPromptTemplate: 'Generate 5 email subject lines for: {{input}}',
|
|
175
|
+
outputFields: [
|
|
176
|
+
{ name: 'subjects', type: 'string[]', required: true },
|
|
177
|
+
],
|
|
178
|
+
tone: 'persuasive',
|
|
179
|
+
maxTokens: 200,
|
|
180
|
+
tags: ['email', 'marketing'],
|
|
181
|
+
},
|
|
182
|
+
'social-post': {
|
|
183
|
+
id: 'social-post',
|
|
184
|
+
name: 'Social Media Post',
|
|
185
|
+
type: 'social-post',
|
|
186
|
+
description: 'Generate a social media post',
|
|
187
|
+
systemPrompt: 'You are a social media manager. Write engaging posts that drive engagement.',
|
|
188
|
+
userPromptTemplate: 'Write a social media post about: {{input}}\n\nPlatform: {{platform}}\nTone: {{tone}}',
|
|
189
|
+
tone: 'casual',
|
|
190
|
+
maxTokens: 300,
|
|
191
|
+
tags: ['social', 'marketing'],
|
|
192
|
+
},
|
|
193
|
+
'seo-meta': {
|
|
194
|
+
id: 'seo-meta',
|
|
195
|
+
name: 'SEO Meta Tags',
|
|
196
|
+
type: 'seo-meta',
|
|
197
|
+
description: 'Generate SEO meta title and description',
|
|
198
|
+
systemPrompt: 'You are an SEO expert. Generate optimized meta tags.',
|
|
199
|
+
userPromptTemplate: 'Generate SEO meta title (60 chars max) and meta description (155 chars max) for: {{input}}',
|
|
200
|
+
outputFields: [
|
|
201
|
+
{ name: 'title', type: 'string', required: true },
|
|
202
|
+
{ name: 'description', type: 'string', required: true },
|
|
203
|
+
],
|
|
204
|
+
tone: 'professional',
|
|
205
|
+
maxTokens: 200,
|
|
206
|
+
tags: ['seo'],
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Helpers
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build a prompt from a content template and variables.
|
|
216
|
+
*/
|
|
217
|
+
export function buildContentPrompt(
|
|
218
|
+
template: ContentTemplate,
|
|
219
|
+
vars: Record<string, string>,
|
|
220
|
+
): { systemPrompt: string; userPrompt: string } {
|
|
221
|
+
let userPrompt = template.userPromptTemplate
|
|
222
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
223
|
+
// Handle simple {{var}} replacement
|
|
224
|
+
userPrompt = userPrompt.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value)
|
|
225
|
+
}
|
|
226
|
+
// Remove unmatched conditionals {{#...}}...{{/...}}
|
|
227
|
+
userPrompt = userPrompt.replace(/\{\{#\w+\}\}.*?\{\{\/\w+\}\}/gs, '')
|
|
228
|
+
return { systemPrompt: template.systemPrompt, userPrompt: userPrompt.trim() }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Build a system prompt for a content action.
|
|
233
|
+
*/
|
|
234
|
+
export function buildActionSystemPrompt(action: ContentAction, tone?: ContentTone): string {
|
|
235
|
+
const toneStr = tone ? ` Use a ${tone} tone.` : ''
|
|
236
|
+
const prompts: Record<ContentAction, string> = {
|
|
237
|
+
generate: `You are a skilled content writer. Generate high-quality content as requested.${toneStr}`,
|
|
238
|
+
rewrite: `You are an expert editor. Rewrite the provided content to improve clarity, flow, and impact.${toneStr}`,
|
|
239
|
+
translate: `You are a professional translator. Translate the content accurately while preserving tone and meaning.`,
|
|
240
|
+
summarize: `You are an expert at summarization. Provide a clear, concise summary of the given content.`,
|
|
241
|
+
expand: `You are a content writer. Expand the given content with more detail, examples, and depth.${toneStr}`,
|
|
242
|
+
shorten: `You are an editor. Shorten the content while preserving the key message and important details.`,
|
|
243
|
+
'change-tone': `You are a skilled writer. Rewrite the content in the specified tone while preserving the meaning.${toneStr}`,
|
|
244
|
+
extract: `You are a data extraction expert. Extract the requested information from the provided content.`,
|
|
245
|
+
proofread: `You are a proofreader. Fix grammar, spelling, punctuation, and style issues. Return the corrected text.`,
|
|
246
|
+
variations: `You are a creative writer. Generate multiple distinct variations of the given content.${toneStr}`,
|
|
247
|
+
}
|
|
248
|
+
return prompts[action]
|
|
249
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @geenius-ai/shared — src/convex/helpers.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convex backend helper functions for AI operations.
|
|
5
|
+
* Apps use these inside their own Convex actions/mutations.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // convex/ai.ts
|
|
10
|
+
* import { action, internalMutation, query } from './_generated/server'
|
|
11
|
+
* import { generateTextArgs, saveLogArgs, listLogsArgs } from '@geenius-ai/shared/convex'
|
|
12
|
+
* import { performAICall, createLogEntry } from '@geenius-ai/shared/convex'
|
|
13
|
+
*
|
|
14
|
+
* export const generateText = action({
|
|
15
|
+
* args: generateTextArgs,
|
|
16
|
+
* handler: async (ctx, args) => {
|
|
17
|
+
* return await performAICall(ctx, args)
|
|
18
|
+
* },
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
AIConfig,
|
|
25
|
+
AIGenerateTextOptions,
|
|
26
|
+
AIGenerationResult,
|
|
27
|
+
AILogEntry,
|
|
28
|
+
AIRequestStatus,
|
|
29
|
+
AIGenerationType,
|
|
30
|
+
} from '../types'
|
|
31
|
+
import { defineAIConfig } from '../config'
|
|
32
|
+
import { resolveProviderForModel, generateTextWithRetries } from '../providers/registry'
|
|
33
|
+
import { extractPromptText } from '../providers/base'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Perform an AI text generation call with logging.
|
|
37
|
+
* This is the high-level function apps call from Convex actions.
|
|
38
|
+
*/
|
|
39
|
+
export async function performAICall(
|
|
40
|
+
ctx: { runMutation?: (fn: any, args: any) => Promise<any>; runQuery?: (fn: any, args: any) => Promise<any> },
|
|
41
|
+
args: AIGenerateTextOptions,
|
|
42
|
+
options: {
|
|
43
|
+
config?: AIConfig
|
|
44
|
+
saveLogFn?: any // Reference to the internal mutation for saving logs
|
|
45
|
+
getModelCostFn?: any // Reference to the internal query for model costs
|
|
46
|
+
getApiKey?: (envVar: string) => string | undefined
|
|
47
|
+
} = {},
|
|
48
|
+
): Promise<AIGenerationResult> {
|
|
49
|
+
const config = options.config ?? defineAIConfig()
|
|
50
|
+
const getApiKey = options.getApiKey ?? ((envVar: string) =>
|
|
51
|
+
(globalThis as any).process?.env?.[envVar]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const { model, messages, caller = 'unknown' } = args
|
|
55
|
+
const provider = resolveProviderForModel(config, model, getApiKey)
|
|
56
|
+
const promptInfo = extractPromptText(messages)
|
|
57
|
+
const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
|
58
|
+
const timestamp = Date.now()
|
|
59
|
+
|
|
60
|
+
let result: AIGenerationResult | undefined
|
|
61
|
+
let status: AIRequestStatus = 'success'
|
|
62
|
+
let errorMessage: string | undefined
|
|
63
|
+
let httpStatus = 200
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
result = await generateTextWithRetries(provider, args, config.retries)
|
|
67
|
+
return result
|
|
68
|
+
} catch (err) {
|
|
69
|
+
status = 'error'
|
|
70
|
+
errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
|
71
|
+
// Try to extract HTTP status from error message
|
|
72
|
+
const statusMatch = errorMessage.match(/\((\d{3})\)/)
|
|
73
|
+
if (statusMatch) httpStatus = parseInt(statusMatch[1]!, 10)
|
|
74
|
+
throw err
|
|
75
|
+
} finally {
|
|
76
|
+
const durationMs = Date.now() - timestamp
|
|
77
|
+
|
|
78
|
+
// Calculate costs
|
|
79
|
+
let inputCostUsd: number | undefined
|
|
80
|
+
let outputCostUsd: number | undefined
|
|
81
|
+
let totalCostUsd: number | undefined
|
|
82
|
+
|
|
83
|
+
const modelConfig = config.models.find(m => m.id === model)
|
|
84
|
+
if (modelConfig && result?.tokens) {
|
|
85
|
+
inputCostUsd = (result.tokens.prompt / 1000) * modelConfig.inputCostPer1k
|
|
86
|
+
outputCostUsd = (result.tokens.completion / 1000) * modelConfig.outputCostPer1k
|
|
87
|
+
totalCostUsd = inputCostUsd + outputCostUsd
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Save log if mutation reference provided
|
|
91
|
+
if (options.saveLogFn && ctx.runMutation) {
|
|
92
|
+
try {
|
|
93
|
+
await ctx.runMutation(options.saveLogFn, {
|
|
94
|
+
requestId,
|
|
95
|
+
model,
|
|
96
|
+
provider: provider.type,
|
|
97
|
+
caller,
|
|
98
|
+
type: 'text' as string,
|
|
99
|
+
timestamp,
|
|
100
|
+
durationMs,
|
|
101
|
+
systemPrompt: promptInfo.systemPrompt.substring(0, config.logging.maxPromptLength),
|
|
102
|
+
userPrompt: promptInfo.userPromptText.substring(0, config.logging.maxPromptLength),
|
|
103
|
+
hasImage: promptInfo.hasImage,
|
|
104
|
+
imageSizeBytes: promptInfo.imageSizeBytes || undefined,
|
|
105
|
+
temperature: args.temperature,
|
|
106
|
+
maxTokens: args.maxTokens,
|
|
107
|
+
requestBodySize: JSON.stringify(args).length,
|
|
108
|
+
status,
|
|
109
|
+
httpStatus,
|
|
110
|
+
responseContent: (result?.content ?? '').substring(0, config.logging.maxResponseLength),
|
|
111
|
+
responseSize: result?.content?.length ?? 0,
|
|
112
|
+
finishReason: result?.finishReason,
|
|
113
|
+
promptTokens: result?.tokens?.prompt,
|
|
114
|
+
completionTokens: result?.tokens?.completion,
|
|
115
|
+
totalTokens: result?.tokens?.total,
|
|
116
|
+
errorMessage,
|
|
117
|
+
inputCostUsd,
|
|
118
|
+
outputCostUsd,
|
|
119
|
+
totalCostUsd,
|
|
120
|
+
})
|
|
121
|
+
} catch (logErr) {
|
|
122
|
+
console.error('[geenius-ai] Failed to save log:', logErr)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build an AI log entry object (without saving).
|
|
130
|
+
* Useful for custom logging flows.
|
|
131
|
+
*/
|
|
132
|
+
export function createLogEntry(
|
|
133
|
+
args: Partial<AILogEntry> & { requestId: string; model: string; timestamp: number },
|
|
134
|
+
): AILogEntry {
|
|
135
|
+
return {
|
|
136
|
+
requestId: args.requestId,
|
|
137
|
+
model: args.model,
|
|
138
|
+
provider: args.provider ?? 'openai',
|
|
139
|
+
caller: args.caller ?? 'unknown',
|
|
140
|
+
type: args.type ?? 'text',
|
|
141
|
+
timestamp: args.timestamp,
|
|
142
|
+
durationMs: args.durationMs ?? 0,
|
|
143
|
+
systemPrompt: args.systemPrompt ?? '',
|
|
144
|
+
userPrompt: args.userPrompt ?? '',
|
|
145
|
+
hasImage: args.hasImage ?? false,
|
|
146
|
+
imageSizeBytes: args.imageSizeBytes,
|
|
147
|
+
temperature: args.temperature,
|
|
148
|
+
maxTokens: args.maxTokens,
|
|
149
|
+
requestBodySize: args.requestBodySize ?? 0,
|
|
150
|
+
status: args.status ?? 'success',
|
|
151
|
+
httpStatus: args.httpStatus ?? 200,
|
|
152
|
+
responseContent: args.responseContent ?? '',
|
|
153
|
+
responseSize: args.responseSize ?? 0,
|
|
154
|
+
finishReason: args.finishReason,
|
|
155
|
+
promptTokens: args.promptTokens,
|
|
156
|
+
completionTokens: args.completionTokens,
|
|
157
|
+
totalTokens: args.totalTokens,
|
|
158
|
+
errorMessage: args.errorMessage,
|
|
159
|
+
inputCostUsd: args.inputCostUsd,
|
|
160
|
+
outputCostUsd: args.outputCostUsd,
|
|
161
|
+
totalCostUsd: args.totalCostUsd,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @geenius-ai/shared — src/convex/index.ts
|
|
2
|
+
|
|
3
|
+
// Schemas
|
|
4
|
+
export { aiTables, aiLogsTable, aiModelCostsTable, aiProvidersTable, aiConversationsTable, aiMessagesTable } from './schemas'
|
|
5
|
+
|
|
6
|
+
// Validators
|
|
7
|
+
export {
|
|
8
|
+
generateTextArgs, generateImageArgs, generateAudioArgs,
|
|
9
|
+
transcribeAudioArgs, generateVideoArgs, testModelArgs,
|
|
10
|
+
saveLogArgs, listLogsArgs,
|
|
11
|
+
upsertModelArgs,
|
|
12
|
+
createConversationArgs, updateConversationArgs, sendMessageArgs,
|
|
13
|
+
} from './validators'
|
|
14
|
+
|
|
15
|
+
// Helpers
|
|
16
|
+
export { performAICall, createLogEntry } from './helpers'
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @geenius-ai/shared — src/convex/schemas.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convex table schemas for the AI component.
|
|
5
|
+
* Apps merge these into their schema.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // convex/schema.ts
|
|
10
|
+
* import { defineSchema } from 'convex/server'
|
|
11
|
+
* import { aiTables } from '@geenius-ai/shared/convex'
|
|
12
|
+
*
|
|
13
|
+
* export default defineSchema({
|
|
14
|
+
* ...aiTables,
|
|
15
|
+
* // your other tables
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { defineTable } from 'convex/server'
|
|
21
|
+
import { v } from 'convex/values'
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// AI Log Entry — every API call is recorded
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export const aiLogsTable = defineTable({
|
|
28
|
+
requestId: v.string(),
|
|
29
|
+
model: v.string(),
|
|
30
|
+
provider: v.string(),
|
|
31
|
+
caller: v.string(),
|
|
32
|
+
type: v.string(), // text, image, audio, video, transcription
|
|
33
|
+
timestamp: v.number(),
|
|
34
|
+
durationMs: v.number(),
|
|
35
|
+
systemPrompt: v.string(),
|
|
36
|
+
userPrompt: v.string(),
|
|
37
|
+
hasImage: v.boolean(),
|
|
38
|
+
imageSizeBytes: v.optional(v.number()),
|
|
39
|
+
temperature: v.optional(v.number()),
|
|
40
|
+
maxTokens: v.optional(v.number()),
|
|
41
|
+
requestBodySize: v.number(),
|
|
42
|
+
status: v.union(v.literal('success'), v.literal('error')),
|
|
43
|
+
httpStatus: v.number(),
|
|
44
|
+
responseContent: v.string(),
|
|
45
|
+
responseSize: v.number(),
|
|
46
|
+
finishReason: v.optional(v.string()),
|
|
47
|
+
promptTokens: v.optional(v.number()),
|
|
48
|
+
completionTokens: v.optional(v.number()),
|
|
49
|
+
totalTokens: v.optional(v.number()),
|
|
50
|
+
errorMessage: v.optional(v.string()),
|
|
51
|
+
inputCostUsd: v.optional(v.number()),
|
|
52
|
+
outputCostUsd: v.optional(v.number()),
|
|
53
|
+
totalCostUsd: v.optional(v.number()),
|
|
54
|
+
})
|
|
55
|
+
.index('by_timestamp', ['timestamp'])
|
|
56
|
+
.index('by_model', ['model', 'timestamp'])
|
|
57
|
+
.index('by_provider', ['provider', 'timestamp'])
|
|
58
|
+
.index('by_caller', ['caller', 'timestamp'])
|
|
59
|
+
.index('by_status', ['status', 'timestamp'])
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Model Cost Registry
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export const aiModelCostsTable = defineTable({
|
|
66
|
+
model: v.string(),
|
|
67
|
+
provider: v.string(),
|
|
68
|
+
displayName: v.optional(v.string()),
|
|
69
|
+
inputCostPer1k: v.number(),
|
|
70
|
+
outputCostPer1k: v.number(),
|
|
71
|
+
capabilities: v.optional(v.array(v.string())),
|
|
72
|
+
contextWindow: v.optional(v.number()),
|
|
73
|
+
isActive: v.boolean(),
|
|
74
|
+
updatedAt: v.number(),
|
|
75
|
+
})
|
|
76
|
+
.index('by_model', ['model'])
|
|
77
|
+
.index('by_provider', ['provider'])
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Provider Configuration
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
export const aiProvidersTable = defineTable({
|
|
84
|
+
name: v.string(),
|
|
85
|
+
type: v.string(),
|
|
86
|
+
baseUrl: v.string(),
|
|
87
|
+
apiKeyEnvVar: v.string(),
|
|
88
|
+
isActive: v.boolean(),
|
|
89
|
+
models: v.array(v.string()),
|
|
90
|
+
defaultModel: v.optional(v.string()),
|
|
91
|
+
updatedAt: v.number(),
|
|
92
|
+
})
|
|
93
|
+
.index('by_type', ['type'])
|
|
94
|
+
.index('by_name', ['name'])
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Chat Conversations
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export const aiConversationsTable = defineTable({
|
|
101
|
+
userId: v.string(),
|
|
102
|
+
title: v.string(),
|
|
103
|
+
model: v.string(),
|
|
104
|
+
systemPrompt: v.optional(v.string()),
|
|
105
|
+
status: v.union(v.literal('active'), v.literal('archived')),
|
|
106
|
+
favorite: v.boolean(),
|
|
107
|
+
tags: v.array(v.string()),
|
|
108
|
+
folderId: v.optional(v.string()),
|
|
109
|
+
messageCount: v.number(),
|
|
110
|
+
createdAt: v.number(),
|
|
111
|
+
updatedAt: v.number(),
|
|
112
|
+
})
|
|
113
|
+
.index('by_userId', ['userId'])
|
|
114
|
+
.index('by_userId_model', ['userId', 'model'])
|
|
115
|
+
.index('by_userId_status', ['userId', 'status'])
|
|
116
|
+
.index('by_userId_favorite', ['userId', 'favorite'])
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Chat Messages
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
export const aiMessagesTable = defineTable({
|
|
123
|
+
conversationId: v.id('aiConversations'),
|
|
124
|
+
userId: v.string(),
|
|
125
|
+
role: v.union(v.literal('system'), v.literal('user'), v.literal('assistant'), v.literal('tool')),
|
|
126
|
+
content: v.string(),
|
|
127
|
+
model: v.optional(v.string()),
|
|
128
|
+
tokens: v.optional(v.number()),
|
|
129
|
+
durationMs: v.optional(v.number()),
|
|
130
|
+
createdAt: v.number(),
|
|
131
|
+
})
|
|
132
|
+
.index('by_conversationId', ['conversationId'])
|
|
133
|
+
.index('by_conversationId_createdAt', ['conversationId', 'createdAt'])
|
|
134
|
+
.index('by_userId', ['userId'])
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Combined table export
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export const aiTables = {
|
|
141
|
+
aiLogs: aiLogsTable,
|
|
142
|
+
aiModelCosts: aiModelCostsTable,
|
|
143
|
+
aiProviders: aiProvidersTable,
|
|
144
|
+
aiConversations: aiConversationsTable,
|
|
145
|
+
aiMessages: aiMessagesTable,
|
|
146
|
+
}
|