@proveanything/smartlinks 1.3.16 → 1.3.17
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/README.md +7 -1
- package/dist/api/ai.d.ts +435 -0
- package/dist/api/ai.js +248 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/docs/API_SUMMARY.md +496 -2
- package/dist/docs/ai.md +1565 -0
- package/docs/API_SUMMARY.md +496 -2
- package/docs/ai.md +1565 -0
- package/package.json +1 -1
package/docs/ai.md
ADDED
|
@@ -0,0 +1,1565 @@
|
|
|
1
|
+
# SmartLinks AI
|
|
2
|
+
|
|
3
|
+
Complete guide to using AI capabilities in the SmartLinks SDK, including chat completions, RAG (Retrieval-Augmented Generation), and voice integration.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Authentication](#authentication)
|
|
12
|
+
- [Chat Completions](#chat-completions)
|
|
13
|
+
- [RAG: Product Assistants](#rag-product-assistants)
|
|
14
|
+
- [Voice Integration](#voice-integration)
|
|
15
|
+
- [Podcast Generation](#podcast-generation)
|
|
16
|
+
- [Type Definitions](#type-definitions)
|
|
17
|
+
- [API Reference](#api-reference)
|
|
18
|
+
- [Usage Examples](#usage-examples)
|
|
19
|
+
- [Error Handling](#error-handling)
|
|
20
|
+
- [Rate Limiting](#rate-limiting)
|
|
21
|
+
- [Best Practices](#best-practices)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
SmartLinks AI provides four main capabilities:
|
|
28
|
+
|
|
29
|
+
1. **Chat Completions** - OpenAI-compatible text generation with streaming and tool calling
|
|
30
|
+
2. **RAG (Retrieval-Augmented Generation)** - Document-grounded Q&A for product assistants
|
|
31
|
+
3. **Voice Integration** - Voice-to-text and text-to-voice for hands-free interaction
|
|
32
|
+
4. **Podcast Generation** - NotebookLM-style multi-voice conversational podcasts from documents
|
|
33
|
+
|
|
34
|
+
### Key Features
|
|
35
|
+
|
|
36
|
+
- ✅ Full TypeScript support with type safety
|
|
37
|
+
- ✅ Streaming responses with async iterators
|
|
38
|
+
- ✅ Automatic rate limit handling
|
|
39
|
+
- ✅ Session management for conversations
|
|
40
|
+
- ✅ Voice input/output helpers
|
|
41
|
+
- ✅ Tool/function calling support
|
|
42
|
+
- ✅ Document indexing and retrieval
|
|
43
|
+
- ✅ Customizable assistant behavior
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { initializeApi, ai } from '@proveanything/smartlinks';
|
|
51
|
+
|
|
52
|
+
// Initialize the SDK
|
|
53
|
+
initializeApi({
|
|
54
|
+
baseURL: 'https://smartlinks.app/api/v1',
|
|
55
|
+
apiKey: process.env.SMARTLINKS_API_KEY // Required for admin endpoints
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Simple chat completion
|
|
59
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
60
|
+
model: 'google/gemini-2.5-flash',
|
|
61
|
+
messages: [
|
|
62
|
+
{ role: 'user', content: 'Hello!' }
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log(response.choices[0].message.content);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Authentication
|
|
72
|
+
|
|
73
|
+
### Admin Endpoints
|
|
74
|
+
|
|
75
|
+
Admin endpoints require an API key passed during initialization:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
initializeApi({
|
|
79
|
+
baseURL: 'https://smartlinks.app/api/v1',
|
|
80
|
+
apiKey: process.env.SMARTLINKS_API_KEY
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The SDK automatically includes the API key in the `Authorization: Bearer <token>` header.
|
|
85
|
+
|
|
86
|
+
### Public Endpoints
|
|
87
|
+
|
|
88
|
+
Public endpoints don't require an API key but are rate-limited by `userId`:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// No API key needed
|
|
92
|
+
const response = await ai.publicApi.chat({
|
|
93
|
+
productId: 'coffee-maker',
|
|
94
|
+
userId: 'user-123',
|
|
95
|
+
message: 'How do I clean this?'
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Chat Completions
|
|
102
|
+
|
|
103
|
+
OpenAI-compatible chat completions with streaming and tool calling support.
|
|
104
|
+
|
|
105
|
+
### Basic Chat
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
109
|
+
model: 'google/gemini-2.5-flash',
|
|
110
|
+
messages: [
|
|
111
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
112
|
+
{ role: 'user', content: 'What is the capital of France?' }
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(response.choices[0].message.content);
|
|
117
|
+
// Output: "The capital of France is Paris."
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Streaming Chat
|
|
121
|
+
|
|
122
|
+
Stream responses in real-time for better UX:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const stream = await ai.chat.completions.create('my-collection', {
|
|
126
|
+
model: 'google/gemini-2.5-flash',
|
|
127
|
+
messages: [
|
|
128
|
+
{ role: 'user', content: 'Write a short poem about coding' }
|
|
129
|
+
],
|
|
130
|
+
stream: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
for await (const chunk of stream) {
|
|
134
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
135
|
+
process.stdout.write(content);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Tool/Function Calling
|
|
140
|
+
|
|
141
|
+
Define tools (functions) that the AI can call:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const tools = [
|
|
145
|
+
{
|
|
146
|
+
type: 'function',
|
|
147
|
+
function: {
|
|
148
|
+
name: 'get_weather',
|
|
149
|
+
description: 'Get the current weather for a location',
|
|
150
|
+
parameters: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
location: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
description: 'City name'
|
|
156
|
+
},
|
|
157
|
+
unit: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
enum: ['celsius', 'fahrenheit']
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
required: ['location']
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
169
|
+
model: 'google/gemini-2.5-flash',
|
|
170
|
+
messages: [
|
|
171
|
+
{ role: 'user', content: 'What\'s the weather in Paris?' }
|
|
172
|
+
],
|
|
173
|
+
tools
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const toolCall = response.choices[0].message.tool_calls?.[0];
|
|
177
|
+
if (toolCall) {
|
|
178
|
+
console.log('Function:', toolCall.function.name);
|
|
179
|
+
console.log('Arguments:', JSON.parse(toolCall.function.arguments));
|
|
180
|
+
// { location: "Paris", unit: "celsius" }
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Available Models
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// List all available models
|
|
188
|
+
const models = await ai.models.list('my-collection');
|
|
189
|
+
|
|
190
|
+
models.data.forEach(model => {
|
|
191
|
+
console.log(`${model.name}`);
|
|
192
|
+
console.log(` Provider: ${model.provider}`);
|
|
193
|
+
console.log(` Context: ${model.contextWindow} tokens`);
|
|
194
|
+
console.log(` Pricing: $${model.pricing.input}/1M input tokens`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Get specific model info
|
|
198
|
+
const model = await ai.models.get('my-collection', 'google/gemini-2.5-flash');
|
|
199
|
+
console.log(model.capabilities); // ['text', 'vision', 'audio', 'code']
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Recommended Models:**
|
|
203
|
+
|
|
204
|
+
| Model | Use Case | Speed | Cost |
|
|
205
|
+
|-------|----------|-------|------|
|
|
206
|
+
| `google/gemini-2.5-flash-lite` | Simple Q&A | Fastest | Lowest |
|
|
207
|
+
| `google/gemini-2.5-flash` | General purpose | Fast | Low |
|
|
208
|
+
| `google/gemini-2.5-pro` | Complex reasoning | Slower | Higher |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## RAG: Product Assistants
|
|
213
|
+
|
|
214
|
+
Create intelligent product assistants that answer questions based on product documentation.
|
|
215
|
+
|
|
216
|
+
### Setup: Index Documents
|
|
217
|
+
|
|
218
|
+
First, index your product documentation:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Index a product manual from URL
|
|
222
|
+
const result = await ai.rag.indexDocument('my-collection', {
|
|
223
|
+
productId: 'coffee-maker-deluxe',
|
|
224
|
+
documentUrl: 'https://example.com/manuals/coffee-maker.pdf',
|
|
225
|
+
chunkSize: 500, // Tokens per chunk
|
|
226
|
+
overlap: 50, // Token overlap between chunks
|
|
227
|
+
provider: 'openai' // Embedding provider
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log(`Indexed ${result.chunks} chunks`);
|
|
231
|
+
console.log(`Dimensions: ${result.metadata.embeddingDimensions}`);
|
|
232
|
+
|
|
233
|
+
// Or index from text directly
|
|
234
|
+
await ai.rag.indexDocument('my-collection', {
|
|
235
|
+
productId: 'coffee-maker-deluxe',
|
|
236
|
+
text: 'Your product manual content here...',
|
|
237
|
+
metadata: {
|
|
238
|
+
source: 'manual',
|
|
239
|
+
version: '2.0'
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Configure Assistant
|
|
245
|
+
|
|
246
|
+
Customize the assistant's behavior:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
await ai.rag.configureAssistant('my-collection', {
|
|
250
|
+
productId: 'coffee-maker-deluxe',
|
|
251
|
+
systemPrompt: 'You are a helpful coffee maker assistant. Be concise and friendly.',
|
|
252
|
+
model: 'google/gemini-2.5-flash',
|
|
253
|
+
temperature: 0.7,
|
|
254
|
+
maxTokensPerResponse: 500,
|
|
255
|
+
rateLimitPerUser: 20,
|
|
256
|
+
allowedTopics: ['usage', 'cleaning', 'troubleshooting'],
|
|
257
|
+
customInstructions: {
|
|
258
|
+
tone: 'friendly',
|
|
259
|
+
additionalRules: 'Always include safety warnings when relevant.'
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Public Chat
|
|
265
|
+
|
|
266
|
+
Users can chat with the product assistant without authentication:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// First question
|
|
270
|
+
const response = await ai.publicApiApi.chat('my-collection', {
|
|
271
|
+
productId: 'coffee-maker-deluxe',
|
|
272
|
+
userId: 'user-123',
|
|
273
|
+
message: 'How do I descale my coffee maker?'
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
console.log('Answer:', response.message);
|
|
277
|
+
console.log('Used', response.context.chunksUsed, 'document sections');
|
|
278
|
+
console.log('Top similarity:', response.context.topSimilarity);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Conversation History
|
|
282
|
+
|
|
283
|
+
Maintain conversation context with sessions:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const sessionId = `session-${Date.now()}`;
|
|
287
|
+
|
|
288
|
+
// First question
|
|
289
|
+
const q1 = await ai.public.chat('my-collection', {
|
|
290
|
+
productId: 'coffee-maker-deluxe',
|
|
291
|
+
userId: 'user-123',
|
|
292
|
+
message: 'How do I clean it?',
|
|
293
|
+
sessionId
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Follow-up question (uses history)
|
|
297
|
+
const q2 = await ai.public.chat('my-collection', {
|
|
298
|
+
productId: 'coffee-maker-deluxe',
|
|
299
|
+
userId: 'user-123',
|
|
300
|
+
message: 'How often should I do that?',
|
|
301
|
+
sessionId
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Get full conversation history
|
|
305
|
+
const session = await ai.public.getSession('my-collection', sessionId);
|
|
306
|
+
console.log('Messages:', session.messages);
|
|
307
|
+
console.log('Total messages:', session.messageCount);
|
|
308
|
+
|
|
309
|
+
// Clear session when done
|
|
310
|
+
await ai.public.clearSession('my-collection', sessionId);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Session Management
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Get session statistics (admin)
|
|
317
|
+
const stats = await ai.sessions.stats('my-collection');
|
|
318
|
+
console.log('Total sessions:', stats.totalSessions);
|
|
319
|
+
console.log('Active sessions:', stats.activeSessions);
|
|
320
|
+
console.log('Total messages:', stats.totalMessages);
|
|
321
|
+
console.log('Rate-limited users:', stats.rateLimitedUsers);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Voice Integration
|
|
327
|
+
|
|
328
|
+
Enable voice input and output for hands-free interaction.
|
|
329
|
+
|
|
330
|
+
### Browser Voice Helpers
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Check if voice is supported
|
|
334
|
+
if (ai.voice.isSupported()) {
|
|
335
|
+
// Listen for voice input
|
|
336
|
+
const question = await ai.voice.listen('en-US');
|
|
337
|
+
console.log('User said:', question);
|
|
338
|
+
|
|
339
|
+
// Get answer from AI
|
|
340
|
+
const response = await ai.public.chat('my-collection', {
|
|
341
|
+
productId: 'coffee-maker-deluxe',
|
|
342
|
+
userId: 'user-123',
|
|
343
|
+
message: question
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Speak the answer
|
|
347
|
+
await ai.voice.speak(response.message, {
|
|
348
|
+
voice: 'alloy',
|
|
349
|
+
rate: 1.0
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Voice Assistant Class
|
|
355
|
+
|
|
356
|
+
Create a complete voice assistant:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
class ProductVoiceAssistant {
|
|
360
|
+
private collectionId: string;
|
|
361
|
+
private productId: string;
|
|
362
|
+
private userId: string;
|
|
363
|
+
private sessionId: string;
|
|
364
|
+
|
|
365
|
+
constructor(config: {
|
|
366
|
+
collectionId: string;
|
|
367
|
+
productId: string;
|
|
368
|
+
userId: string;
|
|
369
|
+
}) {
|
|
370
|
+
this.collectionId = config.collectionId;
|
|
371
|
+
this.productId = config.productId;
|
|
372
|
+
this.userId = config.userId;
|
|
373
|
+
this.sessionId = `voice-${Date.now()}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async ask(): Promise<string> {
|
|
377
|
+
// Listen for question
|
|
378
|
+
console.log('Listening...');
|
|
379
|
+
const question = await ai.voice.listen();
|
|
380
|
+
|
|
381
|
+
// Get answer
|
|
382
|
+
console.log('Processing...');
|
|
383
|
+
const response = await ai.public.chat(this.collectionId, {
|
|
384
|
+
productId: this.productId,
|
|
385
|
+
userId: this.userId,
|
|
386
|
+
message: question,
|
|
387
|
+
sessionId: this.sessionId
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Speak answer
|
|
391
|
+
console.log('Speaking...');
|
|
392
|
+
await ai.voice.speak(response.message);
|
|
393
|
+
|
|
394
|
+
return response.message;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async getRemainingQuestions(): Promise<number> {
|
|
398
|
+
const status = await ai.publicApiApi.getRateLimit(this.collectionId, this.userId);
|
|
399
|
+
return status.remaining;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Usage
|
|
404
|
+
const assistant = new ProductVoiceAssistant({
|
|
405
|
+
collectionId: 'my-collection',
|
|
406
|
+
productId: 'coffee-maker-deluxe',
|
|
407
|
+
userId: 'user-123'
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await assistant.ask(); // Voice question → Voice answer
|
|
411
|
+
const remaining = await assistant.getRemainingQuestions();
|
|
412
|
+
console.log(`${remaining} questions remaining`);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Gemini Live Integration
|
|
416
|
+
|
|
417
|
+
Generate ephemeral tokens for Gemini Live (multimodal voice):
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// Generate token for voice session
|
|
421
|
+
const token = await ai.publicApi.getToken('my-collection', {
|
|
422
|
+
settings: {
|
|
423
|
+
ttl: 3600, // 1 hour
|
|
424
|
+
voice: 'alloy',
|
|
425
|
+
language: 'en-US'
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
console.log('Token:', token.token);
|
|
430
|
+
console.log('Expires at:', new Date(token.expiresAt));
|
|
431
|
+
|
|
432
|
+
// Use token with Gemini Live API
|
|
433
|
+
// (See Google's Gemini documentation)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Podcast Generation
|
|
439
|
+
|
|
440
|
+
Generate NotebookLM-style multi-voice conversational podcasts from product documentation.
|
|
441
|
+
|
|
442
|
+
### Generate a Podcast
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
const podcast = await ai.podcast.generate('my-collection', {
|
|
446
|
+
productId: 'coffee-maker-deluxe',
|
|
447
|
+
duration: 5, // Target 5 minutes
|
|
448
|
+
style: 'casual', // 'casual' | 'professional' | 'educational' | 'entertaining'
|
|
449
|
+
voices: {
|
|
450
|
+
host1: 'nova', // Female voice
|
|
451
|
+
host2: 'onyx' // Male voice
|
|
452
|
+
},
|
|
453
|
+
includeAudio: true // Generate audio files
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
console.log('Podcast Title:', podcast.script.title);
|
|
457
|
+
console.log('Duration:', podcast.metadata.duration, 'seconds');
|
|
458
|
+
console.log('Download:', podcast.audio?.mixedUrl);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Available Voices
|
|
462
|
+
|
|
463
|
+
| Voice | Gender | Personality | Best For |
|
|
464
|
+
|-------|--------|-------------|----------|
|
|
465
|
+
| `alloy` | Neutral | Balanced, neutral | Professional podcasts |
|
|
466
|
+
| `echo` | Male | Clear, authoritative | Expert/teacher role |
|
|
467
|
+
| `fable` | Neutral | Warm, storytelling | Narrative content |
|
|
468
|
+
| `onyx` | Male | Deep, engaging | Main host, discussions |
|
|
469
|
+
| `nova` | Female | Friendly, enthusiastic | Co-host, questions |
|
|
470
|
+
| `shimmer` | Female | Bright, energetic | Entertaining content |
|
|
471
|
+
|
|
472
|
+
**Recommended Combinations:**
|
|
473
|
+
- **Casual**: Nova + Onyx - Friendly and engaging
|
|
474
|
+
- **Professional**: Alloy + Echo - Authoritative and clear
|
|
475
|
+
- **Educational**: Fable + Echo - Teaching style
|
|
476
|
+
- **Entertaining**: Shimmer + Onyx - High energy
|
|
477
|
+
|
|
478
|
+
### Access the Script
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// View the generated script
|
|
482
|
+
podcast.script.segments.forEach((segment, i) => {
|
|
483
|
+
const speaker = segment.speaker === 'host1' ? 'Host 1' : 'Host 2';
|
|
484
|
+
console.log(`${speaker}: ${segment.text}`);
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Check Generation Status
|
|
489
|
+
|
|
490
|
+
For long-running podcast generation, poll for status:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// Start generation
|
|
494
|
+
const podcast = await ai.podcast.generate('my-collection', {
|
|
495
|
+
productId: 'coffee-maker-deluxe',
|
|
496
|
+
duration: 10,
|
|
497
|
+
includeAudio: true
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Poll for status
|
|
501
|
+
const checkStatus = async () => {
|
|
502
|
+
const status = await ai.podcast.getStatus('my-collection', podcast.podcastId);
|
|
503
|
+
|
|
504
|
+
console.log(`Status: ${status.status} (${status.progress}%)`);
|
|
505
|
+
|
|
506
|
+
if (status.status === 'completed' && status.result) {
|
|
507
|
+
console.log('Podcast ready!');
|
|
508
|
+
console.log('Listen:', status.result.audio?.mixedUrl);
|
|
509
|
+
return true;
|
|
510
|
+
} else if (status.status === 'failed') {
|
|
511
|
+
console.error('Generation failed:', status.error);
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return false;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Check every 5 seconds
|
|
519
|
+
const interval = setInterval(async () => {
|
|
520
|
+
const done = await checkStatus();
|
|
521
|
+
if (done) clearInterval(interval);
|
|
522
|
+
}, 5000);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Text-to-Speech (TTS)
|
|
526
|
+
|
|
527
|
+
Generate custom audio from text:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
const audioBlob = await ai.tts.generate('my-collection', {
|
|
531
|
+
text: 'Welcome to our podcast about coffee makers!',
|
|
532
|
+
voice: 'nova',
|
|
533
|
+
speed: 1.0,
|
|
534
|
+
format: 'mp3'
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Create audio URL for playback
|
|
538
|
+
const audioUrl = URL.createObjectURL(audioBlob);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Type Definitions
|
|
544
|
+
|
|
545
|
+
### Core Types
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
/**
|
|
549
|
+
* Chat message with role and content
|
|
550
|
+
*/
|
|
551
|
+
interface ChatMessage {
|
|
552
|
+
role: 'system' | 'user' | 'assistant' | 'function' | 'tool';
|
|
553
|
+
content: string | ContentPart[];
|
|
554
|
+
name?: string;
|
|
555
|
+
function_call?: FunctionCall;
|
|
556
|
+
tool_calls?: ToolCall[];
|
|
557
|
+
tool_call_id?: string;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Chat completion request
|
|
562
|
+
*/
|
|
563
|
+
interface ChatCompletionRequest {
|
|
564
|
+
messages: ChatMessage[];
|
|
565
|
+
model?: string;
|
|
566
|
+
stream?: boolean;
|
|
567
|
+
tools?: ToolDefinition[];
|
|
568
|
+
tool_choice?: 'none' | 'auto' | 'required' | { type: 'function'; function: { name: string } };
|
|
569
|
+
temperature?: number; // 0-2, default: 0.7
|
|
570
|
+
max_tokens?: number;
|
|
571
|
+
top_p?: number;
|
|
572
|
+
frequency_penalty?: number;
|
|
573
|
+
presence_penalty?: number;
|
|
574
|
+
response_format?: { type: 'text' | 'json_object' };
|
|
575
|
+
user?: string;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Chat completion response
|
|
580
|
+
*/
|
|
581
|
+
interface ChatCompletionResponse {
|
|
582
|
+
id: string;
|
|
583
|
+
object: 'chat.completion';
|
|
584
|
+
created: number;
|
|
585
|
+
model: string;
|
|
586
|
+
choices: ChatCompletionChoice[];
|
|
587
|
+
usage: {
|
|
588
|
+
prompt_tokens: number;
|
|
589
|
+
completion_tokens: number;
|
|
590
|
+
total_tokens: number;
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Streaming chunk
|
|
596
|
+
*/
|
|
597
|
+
interface ChatCompletionChunk {
|
|
598
|
+
id: string;
|
|
599
|
+
object: 'chat.completion.chunk';
|
|
600
|
+
created: number;
|
|
601
|
+
model: string;
|
|
602
|
+
choices: Array<{
|
|
603
|
+
index: number;
|
|
604
|
+
delta: Partial<ChatMessage>;
|
|
605
|
+
finish_reason: string | null;
|
|
606
|
+
}>;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### RAG Types
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
/**
|
|
614
|
+
* Index document request
|
|
615
|
+
*/
|
|
616
|
+
interface IndexDocumentRequest {
|
|
617
|
+
productId: string;
|
|
618
|
+
text?: string; // Either text or documentUrl required
|
|
619
|
+
documentUrl?: string;
|
|
620
|
+
metadata?: Record<string, any>;
|
|
621
|
+
chunkSize?: number; // Default: 500
|
|
622
|
+
overlap?: number; // Default: 50
|
|
623
|
+
provider?: 'openai' | 'gemini';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Configure assistant request
|
|
628
|
+
*/
|
|
629
|
+
interface ConfigureAssistantRequest {
|
|
630
|
+
productId: string;
|
|
631
|
+
systemPrompt?: string;
|
|
632
|
+
model?: string;
|
|
633
|
+
maxTokensPerResponse?: number;
|
|
634
|
+
temperature?: number;
|
|
635
|
+
rateLimitPerUser?: number;
|
|
636
|
+
allowedTopics?: string[];
|
|
637
|
+
customInstructions?: Record<string, any>;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Public chat request
|
|
642
|
+
*/
|
|
643
|
+
interface PublicChatRequest {
|
|
644
|
+
productId: string;
|
|
645
|
+
userId: string;
|
|
646
|
+
message: string;
|
|
647
|
+
sessionId?: string;
|
|
648
|
+
stream?: boolean;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Public chat response
|
|
653
|
+
*/
|
|
654
|
+
interface PublicChatResponse {
|
|
655
|
+
message: string;
|
|
656
|
+
sessionId: string;
|
|
657
|
+
usage: {
|
|
658
|
+
prompt_tokens: number;
|
|
659
|
+
completion_tokens: number;
|
|
660
|
+
total_tokens: number;
|
|
661
|
+
};
|
|
662
|
+
context?: {
|
|
663
|
+
chunksUsed: number;
|
|
664
|
+
topSimilarity: number;
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Podcast Types
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
/**
|
|
673
|
+
* Podcast generation request
|
|
674
|
+
*/
|
|
675
|
+
interface GeneratePodcastRequest {
|
|
676
|
+
productId: string;
|
|
677
|
+
documentText?: string; // Optional if document already indexed
|
|
678
|
+
duration?: number; // Target duration in minutes (default: 10)
|
|
679
|
+
style?: 'casual' | 'professional' | 'educational' | 'entertaining';
|
|
680
|
+
voices?: {
|
|
681
|
+
host1?: string; // Voice name for first host
|
|
682
|
+
host2?: string; // Voice name for second host
|
|
683
|
+
};
|
|
684
|
+
includeAudio?: boolean; // Generate audio files (default: false)
|
|
685
|
+
language?: string; // Default: 'en-US'
|
|
686
|
+
customInstructions?: string;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Podcast script segment
|
|
691
|
+
*/
|
|
692
|
+
interface PodcastSegment {
|
|
693
|
+
speaker: 'host1' | 'host2';
|
|
694
|
+
text: string;
|
|
695
|
+
timestamp?: number; // Start time in seconds
|
|
696
|
+
duration?: number; // Segment duration
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Podcast script
|
|
701
|
+
*/
|
|
702
|
+
interface PodcastScript {
|
|
703
|
+
title: string;
|
|
704
|
+
description: string;
|
|
705
|
+
segments: PodcastSegment[];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Podcast generation response
|
|
710
|
+
*/
|
|
711
|
+
interface GeneratePodcastResponse {
|
|
712
|
+
success: boolean;
|
|
713
|
+
podcastId: string;
|
|
714
|
+
script: PodcastScript;
|
|
715
|
+
audio?: {
|
|
716
|
+
host1Url?: string; // URL to download host 1 audio
|
|
717
|
+
host2Url?: string; // URL to download host 2 audio
|
|
718
|
+
mixedUrl?: string; // URL to download mixed podcast
|
|
719
|
+
};
|
|
720
|
+
metadata: {
|
|
721
|
+
duration: number; // Actual duration in seconds
|
|
722
|
+
wordCount: number;
|
|
723
|
+
generatedAt: string;
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Podcast status
|
|
729
|
+
*/
|
|
730
|
+
interface PodcastStatus {
|
|
731
|
+
podcastId: string;
|
|
732
|
+
status: 'generating_script' | 'generating_audio' | 'mixing' | 'completed' | 'failed';
|
|
733
|
+
progress: number; // 0-100
|
|
734
|
+
estimatedTimeRemaining?: number; // Seconds
|
|
735
|
+
error?: string;
|
|
736
|
+
result?: GeneratePodcastResponse; // Available when completed
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* TTS request
|
|
741
|
+
*/
|
|
742
|
+
interface TTSRequest {
|
|
743
|
+
text: string;
|
|
744
|
+
voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
|
|
745
|
+
speed?: number; // 0.25 - 4.0, default: 1.0
|
|
746
|
+
format?: 'mp3' | 'opus' | 'aac' | 'flac'; // Default: mp3
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Error Types
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
/**
|
|
754
|
+
* API Error response
|
|
755
|
+
*/
|
|
756
|
+
interface AIError {
|
|
757
|
+
error: {
|
|
758
|
+
message: string;
|
|
759
|
+
type: string;
|
|
760
|
+
code: string;
|
|
761
|
+
param?: string;
|
|
762
|
+
resetAt?: string; // ISO 8601 timestamp (for rate limits)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Custom error class
|
|
768
|
+
*/
|
|
769
|
+
class SmartLinksAIError extends Error {
|
|
770
|
+
type: string;
|
|
771
|
+
code: string;
|
|
772
|
+
statusCode: number;
|
|
773
|
+
resetAt?: string;
|
|
774
|
+
|
|
775
|
+
isRateLimitError(): boolean;
|
|
776
|
+
isAuthError(): boolean;
|
|
777
|
+
isNotFoundError(): boolean;
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## API Reference
|
|
784
|
+
|
|
785
|
+
### Admin Endpoints
|
|
786
|
+
|
|
787
|
+
#### `ai.chat.completions.create(collectionId, request)`
|
|
788
|
+
|
|
789
|
+
Create a chat completion (OpenAI-compatible).
|
|
790
|
+
|
|
791
|
+
**Parameters:**
|
|
792
|
+
- `collectionId` (string) - Collection ID
|
|
793
|
+
- `request` (ChatCompletionRequest) - Request parameters
|
|
794
|
+
|
|
795
|
+
**Returns:** `Promise<ChatCompletionResponse | AsyncIterable<ChatCompletionChunk>>`
|
|
796
|
+
|
|
797
|
+
**Example:**
|
|
798
|
+
```typescript
|
|
799
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
800
|
+
model: 'google/gemini-2.5-flash',
|
|
801
|
+
messages: [{ role: 'user', content: 'Hello!' }]
|
|
802
|
+
});
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
#### `ai.models.list(collectionId)`
|
|
808
|
+
|
|
809
|
+
List available AI models.
|
|
810
|
+
|
|
811
|
+
**Returns:** `Promise<ModelList>`
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
#### `ai.models.get(collectionId, modelId)`
|
|
816
|
+
|
|
817
|
+
Get specific model information.
|
|
818
|
+
|
|
819
|
+
**Parameters:**
|
|
820
|
+
- `modelId` (string) - Model identifier (e.g., 'google/gemini-2.5-flash')
|
|
821
|
+
|
|
822
|
+
**Returns:** `Promise<AIModel>`
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
#### `ai.rag.indexDocument(collectionId, request)`
|
|
827
|
+
|
|
828
|
+
Index a document for RAG.
|
|
829
|
+
|
|
830
|
+
**Parameters:**
|
|
831
|
+
- `request` (IndexDocumentRequest) - Document and indexing parameters
|
|
832
|
+
|
|
833
|
+
**Returns:** `Promise<IndexDocumentResponse>`
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
#### `ai.rag.configureAssistant(collectionId, request)`
|
|
838
|
+
|
|
839
|
+
Configure AI assistant behavior.
|
|
840
|
+
|
|
841
|
+
**Parameters:**
|
|
842
|
+
- `request` (ConfigureAssistantRequest) - Assistant configuration
|
|
843
|
+
|
|
844
|
+
**Returns:** `Promise<ConfigureAssistantResponse>`
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
#### `ai.sessions.stats(collectionId)`
|
|
849
|
+
|
|
850
|
+
Get session statistics.
|
|
851
|
+
|
|
852
|
+
**Returns:** `Promise<SessionStatistics>`
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
#### `ai.rateLimit.reset(collectionId, userId)`
|
|
857
|
+
|
|
858
|
+
Reset rate limit for a user.
|
|
859
|
+
|
|
860
|
+
**Returns:** `Promise<{ success: boolean; userId: string }>`
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
#### `ai.podcast.generate(collectionId, request)`
|
|
865
|
+
|
|
866
|
+
Generate a NotebookLM-style conversational podcast.
|
|
867
|
+
|
|
868
|
+
**Parameters:**
|
|
869
|
+
- `request` (GeneratePodcastRequest) - Podcast generation parameters
|
|
870
|
+
|
|
871
|
+
**Returns:** `Promise<GeneratePodcastResponse>`
|
|
872
|
+
|
|
873
|
+
**Example:**
|
|
874
|
+
```typescript
|
|
875
|
+
const podcast = await ai.podcast.generate('my-collection', {
|
|
876
|
+
productId: 'coffee-maker-deluxe',
|
|
877
|
+
duration: 5,
|
|
878
|
+
style: 'casual',
|
|
879
|
+
voices: { host1: 'nova', host2: 'onyx' },
|
|
880
|
+
includeAudio: true
|
|
881
|
+
});
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
#### `ai.podcast.getStatus(collectionId, podcastId)`
|
|
887
|
+
|
|
888
|
+
Get podcast generation status.
|
|
889
|
+
|
|
890
|
+
**Parameters:**
|
|
891
|
+
- `podcastId` (string) - Podcast identifier
|
|
892
|
+
|
|
893
|
+
**Returns:** `Promise<PodcastStatus>`
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
#### `ai.tts.generate(collectionId, request)`
|
|
898
|
+
|
|
899
|
+
Generate text-to-speech audio.
|
|
900
|
+
|
|
901
|
+
**Parameters:**
|
|
902
|
+
- `request` (TTSRequest) - TTS parameters
|
|
903
|
+
|
|
904
|
+
**Returns:** `Promise<Blob>`
|
|
905
|
+
|
|
906
|
+
**Example:**
|
|
907
|
+
```typescript
|
|
908
|
+
const audioBlob = await ai.tts.generate('my-collection', {
|
|
909
|
+
text: 'Welcome to our podcast!',
|
|
910
|
+
voice: 'nova',
|
|
911
|
+
speed: 1.0
|
|
912
|
+
});
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
### Public Endpoints
|
|
918
|
+
|
|
919
|
+
#### `ai.publicApi.chat(collectionId, request)`
|
|
920
|
+
|
|
921
|
+
Chat with product assistant (no auth required).
|
|
922
|
+
|
|
923
|
+
**Parameters:**
|
|
924
|
+
- `request` (PublicChatRequest) - Chat parameters
|
|
925
|
+
|
|
926
|
+
**Returns:** `Promise<PublicChatResponse>`
|
|
927
|
+
|
|
928
|
+
**Rate Limited:** Yes (20 requests/hour per userId by default)
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
#### `ai.publicApi.getSession(collectionId, sessionId)`
|
|
933
|
+
|
|
934
|
+
Get conversation history.
|
|
935
|
+
|
|
936
|
+
**Returns:** `Promise<Session>`
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
#### `ai.publicApi.clearSession(collectionId, sessionId)`
|
|
941
|
+
|
|
942
|
+
Clear conversation history.
|
|
943
|
+
|
|
944
|
+
**Returns:** `Promise<{ success: boolean }>`
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
#### `ai.publicApi.getRateLimit(collectionId, userId)`
|
|
949
|
+
|
|
950
|
+
Check rate limit status.
|
|
951
|
+
|
|
952
|
+
**Returns:** `Promise<RateLimitStatus>`
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
|
|
956
|
+
#### `ai.publicApi.getToken(collectionId, request)`
|
|
957
|
+
|
|
958
|
+
Generate ephemeral token for Gemini Live.
|
|
959
|
+
|
|
960
|
+
**Returns:** `Promise<EphemeralTokenResponse>`
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
## Usage Examples
|
|
965
|
+
|
|
966
|
+
### Example 1: Product FAQ Bot
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
async function createProductFAQ() {
|
|
970
|
+
const collectionId = 'my-collection';
|
|
971
|
+
const productId = 'coffee-maker-deluxe';
|
|
972
|
+
|
|
973
|
+
// 1. Index product documentation
|
|
974
|
+
await ai.rag.indexDocument(collectionId, {
|
|
975
|
+
productId,
|
|
976
|
+
documentUrl: 'https://example.com/manual.pdf'
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// 2. Configure assistant
|
|
980
|
+
await ai.rag.configureAssistant(collectionId, {
|
|
981
|
+
productId,
|
|
982
|
+
systemPrompt: 'You are a coffee maker expert. Provide clear, step-by-step instructions.',
|
|
983
|
+
rateLimitPerUser: 30
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// 3. Answer user questions
|
|
987
|
+
const answer = await ai.publicApi.chat(collectionId, {
|
|
988
|
+
productId,
|
|
989
|
+
userId: 'user-123',
|
|
990
|
+
message: 'How do I make espresso?'
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
console.log(answer.message);
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### Example 2: Streaming Chatbot UI
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
async function streamingChatbot(userMessage: string) {
|
|
1001
|
+
const stream = await ai.chat.completions.create('my-collection', {
|
|
1002
|
+
model: 'google/gemini-2.5-flash',
|
|
1003
|
+
messages: [
|
|
1004
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
1005
|
+
{ role: 'user', content: userMessage }
|
|
1006
|
+
],
|
|
1007
|
+
stream: true
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
let fullResponse = '';
|
|
1011
|
+
|
|
1012
|
+
for await (const chunk of stream) {
|
|
1013
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
1014
|
+
fullResponse += content;
|
|
1015
|
+
|
|
1016
|
+
// Update UI in real-time
|
|
1017
|
+
updateChatUI(content);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return fullResponse;
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Example 3: Multi-Turn Conversation
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
async function chatConversation() {
|
|
1028
|
+
const collectionId = 'my-collection';
|
|
1029
|
+
const sessionId = `chat-${Date.now()}`;
|
|
1030
|
+
const userId = 'user-123';
|
|
1031
|
+
const productId = 'coffee-maker-deluxe';
|
|
1032
|
+
|
|
1033
|
+
// Question 1
|
|
1034
|
+
const a1 = await ai.publicApi.chat(collectionId, {
|
|
1035
|
+
productId,
|
|
1036
|
+
userId,
|
|
1037
|
+
message: 'How do I clean the machine?',
|
|
1038
|
+
sessionId
|
|
1039
|
+
});
|
|
1040
|
+
console.log('A1:', a1.message);
|
|
1041
|
+
|
|
1042
|
+
// Question 2 (references previous context)
|
|
1043
|
+
const a2 = await ai.publicApi.chat(collectionId, {
|
|
1044
|
+
productId,
|
|
1045
|
+
userId,
|
|
1046
|
+
message: 'How often should I do that?',
|
|
1047
|
+
sessionId
|
|
1048
|
+
});
|
|
1049
|
+
console.log('A2:', a2.message);
|
|
1050
|
+
|
|
1051
|
+
// Get full history
|
|
1052
|
+
const session = await ai.publicApi.getSession(collectionId, sessionId);
|
|
1053
|
+
console.log('Full conversation:', session.messages);
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Example 4: React Hook for Product Assistant
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
import { useState, useCallback } from 'react';
|
|
1061
|
+
import { ai } from '@proveanything/smartlinks';
|
|
1062
|
+
|
|
1063
|
+
export function useProductAssistant(
|
|
1064
|
+
collectionId: string,
|
|
1065
|
+
productId: string,
|
|
1066
|
+
userId: string
|
|
1067
|
+
) {
|
|
1068
|
+
const [loading, setLoading] = useState(false);
|
|
1069
|
+
const [error, setError] = useState<string | null>(null);
|
|
1070
|
+
const [rateLimit, setRateLimit] = useState({ remaining: 20, limit: 20 });
|
|
1071
|
+
|
|
1072
|
+
const ask = useCallback(async (message: string) => {
|
|
1073
|
+
setLoading(true);
|
|
1074
|
+
setError(null);
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
const response = await ai.publicApi.chat(collectionId, {
|
|
1078
|
+
productId,
|
|
1079
|
+
userId,
|
|
1080
|
+
message
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
setRateLimit(prev => ({
|
|
1084
|
+
...prev,
|
|
1085
|
+
remaining: prev.remaining - 1
|
|
1086
|
+
}));
|
|
1087
|
+
|
|
1088
|
+
return response.message;
|
|
1089
|
+
} catch (err: any) {
|
|
1090
|
+
setError(err.message);
|
|
1091
|
+
throw err;
|
|
1092
|
+
} finally {
|
|
1093
|
+
setLoading(false);
|
|
1094
|
+
}
|
|
1095
|
+
}, [collectionId, productId, userId]);
|
|
1096
|
+
|
|
1097
|
+
return { ask, loading, error, rateLimit };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Usage in component
|
|
1101
|
+
function ProductHelp() {
|
|
1102
|
+
const { ask, loading, rateLimit } = useProductAssistant(
|
|
1103
|
+
'my-collection',
|
|
1104
|
+
'coffee-maker',
|
|
1105
|
+
'user-123'
|
|
1106
|
+
);
|
|
1107
|
+
const [answer, setAnswer] = useState('');
|
|
1108
|
+
|
|
1109
|
+
const handleAsk = async () => {
|
|
1110
|
+
const response = await ask('How do I clean this?');
|
|
1111
|
+
setAnswer(response);
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
return (
|
|
1115
|
+
<div>
|
|
1116
|
+
<button onClick={handleAsk} disabled={loading}>
|
|
1117
|
+
{loading ? 'Asking...' : 'Ask Question'}
|
|
1118
|
+
</button>
|
|
1119
|
+
{answer && <p>{answer}</p>}
|
|
1120
|
+
<p>{rateLimit.remaining} questions remaining</p>
|
|
1121
|
+
</div>
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Example 5: Voice Q&A
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
async function voiceQA() {
|
|
1130
|
+
if (!ai.voice.isSupported()) {
|
|
1131
|
+
console.error('Voice not supported in this browser');
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
console.log('Speak your question...');
|
|
1136
|
+
|
|
1137
|
+
// Listen for voice input
|
|
1138
|
+
const question = await ai.voice.listen('en-US');
|
|
1139
|
+
console.log('You asked:', question);
|
|
1140
|
+
|
|
1141
|
+
// Get answer from AI
|
|
1142
|
+
const response = await ai.public.chat('my-collection', {
|
|
1143
|
+
productId: 'coffee-maker-deluxe',
|
|
1144
|
+
userId: 'user-123',
|
|
1145
|
+
message: question
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// Display answer
|
|
1149
|
+
console.log('Answer:', response.message);
|
|
1150
|
+
|
|
1151
|
+
// Speak answer
|
|
1152
|
+
await ai.voice.speak(response.message);
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
### Example 6: Generate Product Podcast
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
async function generateProductPodcast() {
|
|
1160
|
+
// Generate a casual 5-minute podcast about the coffee maker
|
|
1161
|
+
const podcast = await ai.podcast.generate('my-collection', {
|
|
1162
|
+
productId: 'coffee-maker-deluxe',
|
|
1163
|
+
duration: 5, // minutes
|
|
1164
|
+
style: 'casual', // Conversational style
|
|
1165
|
+
voices: {
|
|
1166
|
+
host1: 'nova', // Female voice
|
|
1167
|
+
host2: 'onyx' // Male voice
|
|
1168
|
+
},
|
|
1169
|
+
includeAudio: true
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
console.log('Podcast Title:', podcast.script.title);
|
|
1173
|
+
console.log('Duration:', podcast.metadata.duration, 'seconds');
|
|
1174
|
+
|
|
1175
|
+
// Display script
|
|
1176
|
+
console.log('\nScript:');
|
|
1177
|
+
podcast.script.segments.forEach((segment, i) => {
|
|
1178
|
+
const speaker = segment.speaker === 'host1' ? 'Host 1' : 'Host 2';
|
|
1179
|
+
console.log(`\n${speaker}: ${segment.text}`);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// Download audio
|
|
1183
|
+
if (podcast.audio?.mixedUrl) {
|
|
1184
|
+
console.log('\nDownload podcast:', podcast.audio.mixedUrl);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Example 7: Podcast with Progress Tracking
|
|
1190
|
+
|
|
1191
|
+
```typescript
|
|
1192
|
+
async function generatePodcastWithProgress() {
|
|
1193
|
+
// Start podcast generation
|
|
1194
|
+
const podcast = await ai.podcast.generate('my-collection', {
|
|
1195
|
+
productId: 'coffee-maker-deluxe',
|
|
1196
|
+
duration: 10,
|
|
1197
|
+
style: 'professional',
|
|
1198
|
+
includeAudio: true
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
const podcastId = podcast.podcastId;
|
|
1202
|
+
|
|
1203
|
+
// Poll for status
|
|
1204
|
+
const checkStatus = async () => {
|
|
1205
|
+
const status = await ai.podcast.getStatus('my-collection', podcastId);
|
|
1206
|
+
|
|
1207
|
+
console.log(`Status: ${status.status} (${status.progress}%)`);
|
|
1208
|
+
|
|
1209
|
+
if (status.status === 'completed' && status.result) {
|
|
1210
|
+
console.log('Podcast ready!');
|
|
1211
|
+
console.log('Listen:', status.result.audio?.mixedUrl);
|
|
1212
|
+
return true;
|
|
1213
|
+
} else if (status.status === 'failed') {
|
|
1214
|
+
console.error('Generation failed:', status.error);
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return false;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Check every 5 seconds
|
|
1222
|
+
const interval = setInterval(async () => {
|
|
1223
|
+
const done = await checkStatus();
|
|
1224
|
+
if (done) clearInterval(interval);
|
|
1225
|
+
}, 5000);
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
---
|
|
1230
|
+
|
|
1231
|
+
## Error Handling
|
|
1232
|
+
|
|
1233
|
+
### Error Codes
|
|
1234
|
+
|
|
1235
|
+
| Code | Type | HTTP Status | Description |
|
|
1236
|
+
|------|------|-------------|-------------|
|
|
1237
|
+
| `rate_limit_exceeded` | `rate_limit_error` | 429 | User exceeded rate limit |
|
|
1238
|
+
| `invalid_request` | `invalid_request_error` | 400 | Invalid parameters |
|
|
1239
|
+
| `authentication_error` | `authentication_error` | 401 | Invalid/missing API key |
|
|
1240
|
+
| `permission_denied` | `permission_error` | 403 | Insufficient permissions |
|
|
1241
|
+
| `not_found` | `not_found_error` | 404 | Resource not found |
|
|
1242
|
+
| `document_not_found` | `not_found_error` | 404 | Product document not indexed |
|
|
1243
|
+
| `server_error` | `server_error` | 500 | Internal server error |
|
|
1244
|
+
| `service_unavailable` | `server_error` | 503 | Service temporarily unavailable |
|
|
1245
|
+
|
|
1246
|
+
### Error Handling Pattern
|
|
1247
|
+
|
|
1248
|
+
```typescript
|
|
1249
|
+
import { SmartLinksAIError } from '@proveanything/smartlinks';
|
|
1250
|
+
|
|
1251
|
+
async function robustChat() {
|
|
1252
|
+
try {
|
|
1253
|
+
const response = await ai.publicApi.chat('my-collection', {
|
|
1254
|
+
productId: 'coffee-maker',
|
|
1255
|
+
userId: 'user-123',
|
|
1256
|
+
message: 'Help!'
|
|
1257
|
+
});
|
|
1258
|
+
return response.message;
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (error instanceof SmartLinksAIError) {
|
|
1261
|
+
switch (error.code) {
|
|
1262
|
+
case 'rate_limit_exceeded':
|
|
1263
|
+
console.error('Rate limit exceeded');
|
|
1264
|
+
console.log('Try again at:', new Date(error.resetAt!));
|
|
1265
|
+
break;
|
|
1266
|
+
case 'document_not_found':
|
|
1267
|
+
console.error('Product manual not indexed yet');
|
|
1268
|
+
break;
|
|
1269
|
+
case 'authentication_error':
|
|
1270
|
+
console.error('Invalid API key');
|
|
1271
|
+
break;
|
|
1272
|
+
case 'invalid_request':
|
|
1273
|
+
console.error('Invalid request:', error.message);
|
|
1274
|
+
break;
|
|
1275
|
+
default:
|
|
1276
|
+
console.error('API error:', error.message);
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1279
|
+
console.error('Unexpected error:', error);
|
|
1280
|
+
}
|
|
1281
|
+
throw error;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### Rate Limit Retry
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
async function chatWithRetry(
|
|
1290
|
+
request: PublicChatRequest,
|
|
1291
|
+
maxRetries = 3
|
|
1292
|
+
) {
|
|
1293
|
+
let retries = 0;
|
|
1294
|
+
|
|
1295
|
+
while (retries < maxRetries) {
|
|
1296
|
+
try {
|
|
1297
|
+
return await ai.publicApi.chat('my-collection', request);
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (error instanceof SmartLinksAIError && error.isRateLimitError()) {
|
|
1300
|
+
if (retries === maxRetries - 1) throw error;
|
|
1301
|
+
|
|
1302
|
+
const resetTime = new Date(error.resetAt!).getTime();
|
|
1303
|
+
const waitTime = resetTime - Date.now();
|
|
1304
|
+
|
|
1305
|
+
console.log(`Rate limited. Waiting ${waitTime}ms...`);
|
|
1306
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1307
|
+
|
|
1308
|
+
retries++;
|
|
1309
|
+
} else {
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
---
|
|
1318
|
+
|
|
1319
|
+
## Rate Limiting
|
|
1320
|
+
|
|
1321
|
+
### Rate Limit Overview
|
|
1322
|
+
|
|
1323
|
+
Public endpoints are rate-limited per `userId`:
|
|
1324
|
+
|
|
1325
|
+
| Endpoint Type | Default Limit | Window |
|
|
1326
|
+
|--------------|---------------|--------|
|
|
1327
|
+
| Public Chat | 20 requests | 1 hour |
|
|
1328
|
+
| Token Generation | 10 requests | 1 hour |
|
|
1329
|
+
| Admin Endpoints | Unlimited* | - |
|
|
1330
|
+
|
|
1331
|
+
*Admin endpoints use API key authentication and are not rate-limited by default.
|
|
1332
|
+
|
|
1333
|
+
### Rate Limit Headers
|
|
1334
|
+
|
|
1335
|
+
All API responses include rate limit information:
|
|
1336
|
+
|
|
1337
|
+
```
|
|
1338
|
+
X-RateLimit-Limit: 20
|
|
1339
|
+
X-RateLimit-Remaining: 15
|
|
1340
|
+
X-RateLimit-Reset: 1707300000000
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
### Checking Rate Limit
|
|
1344
|
+
|
|
1345
|
+
```typescript
|
|
1346
|
+
// Check rate limit status before making requests
|
|
1347
|
+
const status = await ai.publicApi.getRateLimit('my-collection', 'user-123');
|
|
1348
|
+
|
|
1349
|
+
console.log('Used:', status.used);
|
|
1350
|
+
console.log('Remaining:', status.remaining);
|
|
1351
|
+
console.log('Resets at:', new Date(status.resetAt));
|
|
1352
|
+
|
|
1353
|
+
if (status.remaining > 0) {
|
|
1354
|
+
// Safe to make request
|
|
1355
|
+
await ai.publicApi.chat(/* ... */);
|
|
1356
|
+
} else {
|
|
1357
|
+
// Show user when they can ask again
|
|
1358
|
+
console.log('Rate limit reached. Try again at:', status.resetAt);
|
|
1359
|
+
}
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
### Resetting Rate Limits (Admin)
|
|
1363
|
+
|
|
1364
|
+
```typescript
|
|
1365
|
+
// Reset rate limit for a specific user
|
|
1366
|
+
await ai.rateLimit.reset('my-collection', 'user-123');
|
|
1367
|
+
console.log('Rate limit reset for user-123');
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
---
|
|
1371
|
+
|
|
1372
|
+
## Best Practices
|
|
1373
|
+
|
|
1374
|
+
### 1. Choose the Right Model
|
|
1375
|
+
|
|
1376
|
+
```typescript
|
|
1377
|
+
// For simple Q&A (fast, cheap)
|
|
1378
|
+
await ai.chat.completions.create('my-collection', {
|
|
1379
|
+
model: 'google/gemini-2.5-flash-lite',
|
|
1380
|
+
messages: [...]
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// For general use (balanced)
|
|
1384
|
+
await ai.chat.completions.create('my-collection', {
|
|
1385
|
+
model: 'google/gemini-2.5-flash',
|
|
1386
|
+
messages: [...]
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// For complex reasoning (powerful)
|
|
1390
|
+
await ai.chat.completions.create('my-collection', {
|
|
1391
|
+
model: 'google/gemini-2.5-pro',
|
|
1392
|
+
messages: [...]
|
|
1393
|
+
});
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
### 2. Use Streaming for Long Responses
|
|
1397
|
+
|
|
1398
|
+
Improve perceived performance with streaming:
|
|
1399
|
+
|
|
1400
|
+
```typescript
|
|
1401
|
+
// Non-streaming: User waits for full response
|
|
1402
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
1403
|
+
messages: [...]
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// Streaming: User sees progress immediately
|
|
1407
|
+
const stream = await ai.chat.completions.create('my-collection', {
|
|
1408
|
+
stream: true,
|
|
1409
|
+
messages: [...]
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
for await (const chunk of stream) {
|
|
1413
|
+
updateUI(chunk.choices[0]?.delta?.content);
|
|
1414
|
+
}
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
### 3. Maintain Session Context
|
|
1418
|
+
|
|
1419
|
+
Keep conversations coherent with session IDs:
|
|
1420
|
+
|
|
1421
|
+
```typescript
|
|
1422
|
+
// Generate unique session ID per conversation
|
|
1423
|
+
const sessionId = `user-${userId}-product-${productId}`;
|
|
1424
|
+
|
|
1425
|
+
// All questions in same conversation use same sessionId
|
|
1426
|
+
await ai.public.chat('my-collection', {
|
|
1427
|
+
sessionId,
|
|
1428
|
+
message: 'First question',
|
|
1429
|
+
...
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
await ai.public.chat('my-collection', {
|
|
1433
|
+
sessionId,
|
|
1434
|
+
message: 'Follow-up question',
|
|
1435
|
+
...
|
|
1436
|
+
});
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
### 4. Handle Rate Limits Gracefully
|
|
1440
|
+
|
|
1441
|
+
Show clear feedback to users:
|
|
1442
|
+
|
|
1443
|
+
```typescript
|
|
1444
|
+
try {
|
|
1445
|
+
await ai.publicApi.chat('my-collection', {...});
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
if (error instanceof SmartLinksAIError && error.isRateLimitError()) {
|
|
1448
|
+
const resetTime = new Date(error.resetAt!);
|
|
1449
|
+
showNotification(
|
|
1450
|
+
`You've reached your question limit. ` +
|
|
1451
|
+
`Try again at ${resetTime.toLocaleTimeString()}`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### 5. Optimize Voice UX
|
|
1458
|
+
|
|
1459
|
+
Provide clear status updates:
|
|
1460
|
+
|
|
1461
|
+
```typescript
|
|
1462
|
+
async function voiceAssistant() {
|
|
1463
|
+
try {
|
|
1464
|
+
showStatus('Listening...');
|
|
1465
|
+
const question = await ai.voice.listen();
|
|
1466
|
+
|
|
1467
|
+
showStatus('Processing...');
|
|
1468
|
+
const answer = await ai.public.chat('my-collection', {
|
|
1469
|
+
message: question,
|
|
1470
|
+
...
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
showStatus('Speaking...');
|
|
1474
|
+
await ai.voice.speak(answer.message);
|
|
1475
|
+
|
|
1476
|
+
showStatus('Ready');
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
showStatus('Error', error.message);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
### 6. Chunk Large Documents
|
|
1484
|
+
|
|
1485
|
+
For better RAG performance, chunk documents appropriately:
|
|
1486
|
+
|
|
1487
|
+
```typescript
|
|
1488
|
+
// For technical manuals
|
|
1489
|
+
await ai.rag.indexDocument('my-collection', {
|
|
1490
|
+
productId: 'coffee-maker',
|
|
1491
|
+
documentUrl: '...',
|
|
1492
|
+
chunkSize: 500, // Smaller chunks for precise answers
|
|
1493
|
+
overlap: 50 // Overlap maintains context
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// For narrative content
|
|
1497
|
+
await ai.rag.indexDocument('my-collection', {
|
|
1498
|
+
productId: 'coffee-maker',
|
|
1499
|
+
documentUrl: '...',
|
|
1500
|
+
chunkSize: 1000, // Larger chunks for coherent responses
|
|
1501
|
+
overlap: 100
|
|
1502
|
+
});
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
### 7. Use System Prompts Effectively
|
|
1506
|
+
|
|
1507
|
+
Provide clear instructions:
|
|
1508
|
+
|
|
1509
|
+
```typescript
|
|
1510
|
+
await ai.chat.completions.create('my-collection', {
|
|
1511
|
+
messages: [
|
|
1512
|
+
{
|
|
1513
|
+
role: 'system',
|
|
1514
|
+
content: `You are a coffee maker expert assistant.
|
|
1515
|
+
- Be concise and clear
|
|
1516
|
+
- Use numbered lists for steps
|
|
1517
|
+
- Always mention safety precautions
|
|
1518
|
+
- If unsure, ask for clarification`
|
|
1519
|
+
},
|
|
1520
|
+
{ role: 'user', content: 'How do I descale?' }
|
|
1521
|
+
]
|
|
1522
|
+
});
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
### 8. Monitor Usage and Costs
|
|
1526
|
+
|
|
1527
|
+
Track usage for cost management:
|
|
1528
|
+
|
|
1529
|
+
```typescript
|
|
1530
|
+
const response = await ai.chat.completions.create('my-collection', {
|
|
1531
|
+
messages: [...]
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// Log token usage
|
|
1535
|
+
console.log('Usage:', response.usage);
|
|
1536
|
+
console.log('Prompt tokens:', response.usage.prompt_tokens);
|
|
1537
|
+
console.log('Completion tokens:', response.usage.completion_tokens);
|
|
1538
|
+
console.log('Total tokens:', response.usage.total_tokens);
|
|
1539
|
+
|
|
1540
|
+
// Calculate estimated cost
|
|
1541
|
+
const model = await ai.models.get('my-collection', response.model);
|
|
1542
|
+
const cost =
|
|
1543
|
+
(response.usage.prompt_tokens * model.pricing.input / 1_000_000) +
|
|
1544
|
+
(response.usage.completion_tokens * model.pricing.output / 1_000_000);
|
|
1545
|
+
console.log('Estimated cost: $', cost.toFixed(4));
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
---
|
|
1549
|
+
|
|
1550
|
+
## Related Documentation
|
|
1551
|
+
|
|
1552
|
+
- [API Summary](./API_SUMMARY.md) - Complete API reference
|
|
1553
|
+
- [Widgets](./widgets.md) - Embedding SmartLinks components
|
|
1554
|
+
- [Realtime](./realtime.md) - Realtime data updates
|
|
1555
|
+
- [iframe Responder](./iframe-responder.md) - iframe integration
|
|
1556
|
+
|
|
1557
|
+
---
|
|
1558
|
+
|
|
1559
|
+
## Support
|
|
1560
|
+
|
|
1561
|
+
For questions or issues:
|
|
1562
|
+
|
|
1563
|
+
- **Documentation:** https://smartlinks.app/docs
|
|
1564
|
+
- **GitHub:** https://github.com/Prove-Anything/smartlinks
|
|
1565
|
+
- **Email:** support@smartlinks.app
|