@nixxie-cms/ai-rag 1.0.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.
Files changed (65) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +163 -0
  3. package/dist/declarations/src/AiRagService.d.ts +50 -0
  4. package/dist/declarations/src/AiRagService.d.ts.map +1 -0
  5. package/dist/declarations/src/admin-page.d.ts +29 -0
  6. package/dist/declarations/src/admin-page.d.ts.map +1 -0
  7. package/dist/declarations/src/chunking.d.ts +8 -0
  8. package/dist/declarations/src/chunking.d.ts.map +1 -0
  9. package/dist/declarations/src/collection.d.ts +18 -0
  10. package/dist/declarations/src/collection.d.ts.map +1 -0
  11. package/dist/declarations/src/express.d.ts +36 -0
  12. package/dist/declarations/src/express.d.ts.map +1 -0
  13. package/dist/declarations/src/graphql.d.ts +23 -0
  14. package/dist/declarations/src/graphql.d.ts.map +1 -0
  15. package/dist/declarations/src/index.d.ts +39 -0
  16. package/dist/declarations/src/index.d.ts.map +1 -0
  17. package/dist/declarations/src/plugin.d.ts +53 -0
  18. package/dist/declarations/src/plugin.d.ts.map +1 -0
  19. package/dist/declarations/src/prompt.d.ts +14 -0
  20. package/dist/declarations/src/prompt.d.ts.map +1 -0
  21. package/dist/declarations/src/providers/AnthropicRagProvider.d.ts +16 -0
  22. package/dist/declarations/src/providers/AnthropicRagProvider.d.ts.map +1 -0
  23. package/dist/declarations/src/providers/GeminiRagProvider.d.ts +19 -0
  24. package/dist/declarations/src/providers/GeminiRagProvider.d.ts.map +1 -0
  25. package/dist/declarations/src/providers/OllamaRagProvider.d.ts +23 -0
  26. package/dist/declarations/src/providers/OllamaRagProvider.d.ts.map +1 -0
  27. package/dist/declarations/src/providers/OpenAiRagProvider.d.ts +17 -0
  28. package/dist/declarations/src/providers/OpenAiRagProvider.d.ts.map +1 -0
  29. package/dist/declarations/src/providers/ServiceRagProvider.d.ts +17 -0
  30. package/dist/declarations/src/providers/ServiceRagProvider.d.ts.map +1 -0
  31. package/dist/declarations/src/providers/index.d.ts +14 -0
  32. package/dist/declarations/src/providers/index.d.ts.map +1 -0
  33. package/dist/declarations/src/providers/types.d.ts +45 -0
  34. package/dist/declarations/src/providers/types.d.ts.map +1 -0
  35. package/dist/declarations/src/similarity.d.ts +12 -0
  36. package/dist/declarations/src/similarity.d.ts.map +1 -0
  37. package/dist/declarations/src/types.d.ts +319 -0
  38. package/dist/declarations/src/types.d.ts.map +1 -0
  39. package/dist/declarations/src/vector-store.d.ts +34 -0
  40. package/dist/declarations/src/vector-store.d.ts.map +1 -0
  41. package/dist/nixxie-cms-ai-rag.cjs.d.ts +2 -0
  42. package/dist/nixxie-cms-ai-rag.cjs.js +2507 -0
  43. package/dist/nixxie-cms-ai-rag.esm.js +2481 -0
  44. package/package.json +37 -0
  45. package/src/AiRagService.ts +640 -0
  46. package/src/admin-page.ts +135 -0
  47. package/src/chunking.ts +78 -0
  48. package/src/collection.ts +79 -0
  49. package/src/express.ts +212 -0
  50. package/src/graphql.ts +196 -0
  51. package/src/guard.ts +75 -0
  52. package/src/index.ts +102 -0
  53. package/src/plugin.ts +162 -0
  54. package/src/prompt.ts +62 -0
  55. package/src/providers/AnthropicRagProvider.ts +91 -0
  56. package/src/providers/GeminiRagProvider.ts +147 -0
  57. package/src/providers/OllamaRagProvider.ts +157 -0
  58. package/src/providers/OpenAiRagProvider.ts +108 -0
  59. package/src/providers/ServiceRagProvider.ts +44 -0
  60. package/src/providers/index.ts +67 -0
  61. package/src/providers/types.ts +44 -0
  62. package/src/semaphore.ts +26 -0
  63. package/src/similarity.ts +31 -0
  64. package/src/types.ts +346 -0
  65. package/src/vector-store.ts +136 -0
@@ -0,0 +1,147 @@
1
+ import type { RagProviderConfig } from '../types'
2
+ import type {
3
+ EmbeddingProvider,
4
+ GenerationProvider,
5
+ RagGenerateOptions,
6
+ RagGenerateResult,
7
+ RagMessage,
8
+ RagStreamChunk,
9
+ } from './types'
10
+
11
+ const DEFAULT_MODEL = 'gemini-2.0-flash'
12
+ const DEFAULT_EMBEDDING_MODEL = 'text-embedding-004'
13
+ const DEFAULT_BASE = 'https://generativelanguage.googleapis.com/v1beta'
14
+
15
+ /** Google Gemini provider over the public REST API — generation, streaming and embeddings. */
16
+ export class GeminiRagProvider implements GenerationProvider, EmbeddingProvider {
17
+ readonly name = 'gemini'
18
+ readonly defaultModel = DEFAULT_MODEL
19
+ private apiKey: string
20
+ private model: string
21
+ private embeddingModel: string
22
+ private base: string
23
+ private extra: Record<string, unknown>
24
+
25
+ constructor(config: RagProviderConfig) {
26
+ if (!config.apiKey) throw new Error('[@nixxie-cms/ai-rag] Gemini requires `apiKey`.')
27
+ this.apiKey = config.apiKey
28
+ this.model = config.model ?? DEFAULT_MODEL
29
+ this.embeddingModel = config.model ?? DEFAULT_EMBEDDING_MODEL
30
+ this.base = (config.baseUrl ?? DEFAULT_BASE).replace(/\/$/, '')
31
+ this.extra = config.extra ?? {}
32
+ }
33
+
34
+ private buildBody(messages: RagMessage[], options?: RagGenerateOptions) {
35
+ return {
36
+ contents: messages.map(m => ({
37
+ role: m.role === 'assistant' ? 'model' : 'user',
38
+ parts: [{ text: m.content }],
39
+ })),
40
+ ...(options?.system
41
+ ? { systemInstruction: { parts: [{ text: options.system }] } }
42
+ : {}),
43
+ generationConfig: {
44
+ ...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
45
+ ...(options?.maxTokens !== undefined ? { maxOutputTokens: options.maxTokens } : {}),
46
+ ...(options?.topP !== undefined ? { topP: options.topP } : {}),
47
+ },
48
+ ...this.extra,
49
+ ...(options?.extra ?? {}),
50
+ }
51
+ }
52
+
53
+ private extractText(data: any): string {
54
+ const parts = data?.candidates?.[0]?.content?.parts ?? []
55
+ return parts.map((p: any) => p.text ?? '').join('')
56
+ }
57
+
58
+ async generate(messages: RagMessage[], options?: RagGenerateOptions): Promise<RagGenerateResult> {
59
+ const model = options?.model ?? this.model
60
+ const res = await fetch(
61
+ `${this.base}/models/${model}:generateContent?key=${this.apiKey}`,
62
+ {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify(this.buildBody(messages, options)),
66
+ }
67
+ )
68
+ if (!res.ok) throw new Error(`[@nixxie-cms/ai-rag] Gemini generate failed (${res.status}): ${await res.text()}`)
69
+ const data: any = await res.json()
70
+ return {
71
+ text: this.extractText(data),
72
+ model,
73
+ usage: {
74
+ inputTokens: data?.usageMetadata?.promptTokenCount,
75
+ outputTokens: data?.usageMetadata?.candidatesTokenCount,
76
+ },
77
+ }
78
+ }
79
+
80
+ async *stream(
81
+ messages: RagMessage[],
82
+ options?: RagGenerateOptions
83
+ ): AsyncIterable<RagStreamChunk> {
84
+ const model = options?.model ?? this.model
85
+ const res = await fetch(
86
+ `${this.base}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`,
87
+ {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify(this.buildBody(messages, options)),
91
+ }
92
+ )
93
+ if (!res.ok || !res.body) {
94
+ throw new Error(`[@nixxie-cms/ai-rag] Gemini stream failed (${res.status}): ${await res.text()}`)
95
+ }
96
+ let usage: any
97
+ for await (const data of parseSseJson(res.body)) {
98
+ const text = this.extractText(data)
99
+ if (text) yield { delta: text }
100
+ if (data?.usageMetadata) usage = data.usageMetadata
101
+ }
102
+ yield {
103
+ done: true,
104
+ model,
105
+ usage: { inputTokens: usage?.promptTokenCount, outputTokens: usage?.candidatesTokenCount },
106
+ }
107
+ }
108
+
109
+ async embed(texts: string[], model?: string): Promise<number[][]> {
110
+ const m = model ?? this.embeddingModel
111
+ const res = await fetch(`${this.base}/models/${m}:batchEmbedContents?key=${this.apiKey}`, {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({
115
+ requests: texts.map(text => ({
116
+ model: `models/${m}`,
117
+ content: { parts: [{ text }] },
118
+ })),
119
+ }),
120
+ })
121
+ if (!res.ok) throw new Error(`[@nixxie-cms/ai-rag] Gemini embed failed (${res.status}): ${await res.text()}`)
122
+ const data: any = await res.json()
123
+ return (data.embeddings ?? []).map((e: any) => e.values as number[])
124
+ }
125
+ }
126
+
127
+ /** Parse a `text/event-stream` of JSON `data:` lines into objects. */
128
+ async function* parseSseJson(body: any): AsyncIterable<any> {
129
+ const decoder = new TextDecoder()
130
+ let buffer = ''
131
+ for await (const piece of body as AsyncIterable<Uint8Array>) {
132
+ buffer += typeof piece === 'string' ? piece : decoder.decode(piece, { stream: true })
133
+ let idx
134
+ while ((idx = buffer.indexOf('\n')) !== -1) {
135
+ const line = buffer.slice(0, idx).trim()
136
+ buffer = buffer.slice(idx + 1)
137
+ if (!line.startsWith('data:')) continue
138
+ const payload = line.slice(5).trim()
139
+ if (!payload || payload === '[DONE]') continue
140
+ try {
141
+ yield JSON.parse(payload)
142
+ } catch {
143
+ // Partial frame — ignore; the next chunk will complete it.
144
+ }
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,157 @@
1
+ import type { RagProviderConfig } from '../types'
2
+ import type {
3
+ EmbeddingProvider,
4
+ GenerationProvider,
5
+ RagGenerateOptions,
6
+ RagGenerateResult,
7
+ RagMessage,
8
+ RagStreamChunk,
9
+ } from './types'
10
+
11
+ const DEFAULT_MODEL = 'llama3.1'
12
+ const DEFAULT_EMBEDDING_MODEL = 'nomic-embed-text'
13
+ const DEFAULT_BASE = 'http://localhost:11434'
14
+
15
+ /**
16
+ * Ollama provider for local (or remote) open models — generation, streaming and embeddings.
17
+ * Runs any Ollama model family (Llama, Mistral, Phi, Gemma, Qwen, …). No API key needed for a
18
+ * local server; point `baseUrl` at your Ollama host if it isn't on localhost.
19
+ */
20
+ export class OllamaRagProvider implements GenerationProvider, EmbeddingProvider {
21
+ readonly name = 'ollama'
22
+ readonly defaultModel = DEFAULT_MODEL
23
+ private base: string
24
+ private model: string
25
+ private embeddingModel: string
26
+ private apiKey?: string
27
+ private extra: Record<string, unknown>
28
+
29
+ constructor(config: RagProviderConfig) {
30
+ this.base = (config.baseUrl ?? DEFAULT_BASE).replace(/\/$/, '')
31
+ this.model = config.model ?? DEFAULT_MODEL
32
+ this.embeddingModel = config.model ?? DEFAULT_EMBEDDING_MODEL
33
+ this.apiKey = config.apiKey
34
+ this.extra = config.extra ?? {}
35
+ }
36
+
37
+ private headers() {
38
+ return {
39
+ 'Content-Type': 'application/json',
40
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
41
+ }
42
+ }
43
+
44
+ private buildBody(messages: RagMessage[], options: RagGenerateOptions | undefined, stream: boolean) {
45
+ return {
46
+ model: options?.model ?? this.model,
47
+ stream,
48
+ messages: [
49
+ ...(options?.system ? [{ role: 'system', content: options.system }] : []),
50
+ ...messages.map(m => ({ role: m.role, content: m.content })),
51
+ ],
52
+ options: {
53
+ ...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
54
+ ...(options?.maxTokens !== undefined ? { num_predict: options.maxTokens } : {}),
55
+ ...(options?.topP !== undefined ? { top_p: options.topP } : {}),
56
+ },
57
+ ...this.extra,
58
+ ...(options?.extra ?? {}),
59
+ }
60
+ }
61
+
62
+ async generate(messages: RagMessage[], options?: RagGenerateOptions): Promise<RagGenerateResult> {
63
+ const res = await fetch(`${this.base}/api/chat`, {
64
+ method: 'POST',
65
+ headers: this.headers(),
66
+ body: JSON.stringify(this.buildBody(messages, options, false)),
67
+ })
68
+ if (!res.ok) throw new Error(`[@nixxie-cms/ai-rag] Ollama generate failed (${res.status}): ${await res.text()}`)
69
+ const data: any = await res.json()
70
+ return {
71
+ text: data?.message?.content ?? '',
72
+ model: data?.model ?? options?.model ?? this.model,
73
+ usage: { inputTokens: data?.prompt_eval_count, outputTokens: data?.eval_count },
74
+ }
75
+ }
76
+
77
+ async *stream(
78
+ messages: RagMessage[],
79
+ options?: RagGenerateOptions
80
+ ): AsyncIterable<RagStreamChunk> {
81
+ const res = await fetch(`${this.base}/api/chat`, {
82
+ method: 'POST',
83
+ headers: this.headers(),
84
+ body: JSON.stringify(this.buildBody(messages, options, true)),
85
+ })
86
+ if (!res.ok || !res.body) {
87
+ throw new Error(`[@nixxie-cms/ai-rag] Ollama stream failed (${res.status}): ${await res.text()}`)
88
+ }
89
+ let usage: any
90
+ // Ollama streams newline-delimited JSON objects.
91
+ for await (const obj of parseNdJson(res.body)) {
92
+ const delta = obj?.message?.content
93
+ if (delta) yield { delta }
94
+ if (obj?.done) usage = obj
95
+ }
96
+ yield {
97
+ done: true,
98
+ model: options?.model ?? this.model,
99
+ usage: { inputTokens: usage?.prompt_eval_count, outputTokens: usage?.eval_count },
100
+ }
101
+ }
102
+
103
+ async embed(texts: string[], model?: string): Promise<number[][]> {
104
+ const m = model ?? this.embeddingModel
105
+ // /api/embed accepts a batch; older servers only support /api/embeddings (single).
106
+ const res = await fetch(`${this.base}/api/embed`, {
107
+ method: 'POST',
108
+ headers: this.headers(),
109
+ body: JSON.stringify({ model: m, input: texts }),
110
+ })
111
+ if (res.ok) {
112
+ const data: any = await res.json()
113
+ if (Array.isArray(data?.embeddings)) return data.embeddings as number[][]
114
+ }
115
+ // Fallback: one request per text against the legacy endpoint.
116
+ const out: number[][] = []
117
+ for (const text of texts) {
118
+ const r = await fetch(`${this.base}/api/embeddings`, {
119
+ method: 'POST',
120
+ headers: this.headers(),
121
+ body: JSON.stringify({ model: m, prompt: text }),
122
+ })
123
+ if (!r.ok) throw new Error(`[@nixxie-cms/ai-rag] Ollama embed failed (${r.status}): ${await r.text()}`)
124
+ const d: any = await r.json()
125
+ out.push(d.embedding as number[])
126
+ }
127
+ return out
128
+ }
129
+ }
130
+
131
+ /** Parse newline-delimited JSON from a stream body. */
132
+ async function* parseNdJson(body: any): AsyncIterable<any> {
133
+ const decoder = new TextDecoder()
134
+ let buffer = ''
135
+ for await (const piece of body as AsyncIterable<Uint8Array>) {
136
+ buffer += typeof piece === 'string' ? piece : decoder.decode(piece, { stream: true })
137
+ let idx
138
+ while ((idx = buffer.indexOf('\n')) !== -1) {
139
+ const line = buffer.slice(0, idx).trim()
140
+ buffer = buffer.slice(idx + 1)
141
+ if (!line) continue
142
+ try {
143
+ yield JSON.parse(line)
144
+ } catch {
145
+ // Ignore partial lines.
146
+ }
147
+ }
148
+ }
149
+ const tail = buffer.trim()
150
+ if (tail) {
151
+ try {
152
+ yield JSON.parse(tail)
153
+ } catch {
154
+ /* ignore */
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,108 @@
1
+ import type { RagProviderConfig } from '../types'
2
+ import type {
3
+ EmbeddingProvider,
4
+ GenerationProvider,
5
+ RagGenerateOptions,
6
+ RagGenerateResult,
7
+ RagMessage,
8
+ RagStreamChunk,
9
+ } from './types'
10
+
11
+ const DEFAULT_MODEL = 'gpt-4o'
12
+ const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small'
13
+
14
+ function loadOpenAi(): any {
15
+ try {
16
+ return require('openai').default ?? require('openai')
17
+ } catch {
18
+ throw new Error(
19
+ '[@nixxie-cms/ai-rag] The OpenAI provider requires the openai package. Run: npm install openai'
20
+ )
21
+ }
22
+ }
23
+
24
+ /** OpenAI (GPT) provider — generation, streaming and embeddings. */
25
+ export class OpenAiRagProvider implements GenerationProvider, EmbeddingProvider {
26
+ readonly name = 'openai'
27
+ readonly defaultModel = DEFAULT_MODEL
28
+ private client: any
29
+ private model: string
30
+ private embeddingModel: string
31
+ private extra: Record<string, unknown>
32
+
33
+ constructor(config: RagProviderConfig) {
34
+ if (!config.apiKey) throw new Error('[@nixxie-cms/ai-rag] OpenAI requires `apiKey`.')
35
+ const OpenAI = loadOpenAi()
36
+ this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl })
37
+ this.model = config.model ?? DEFAULT_MODEL
38
+ this.embeddingModel = config.model ?? DEFAULT_EMBEDDING_MODEL
39
+ this.extra = config.extra ?? {}
40
+ }
41
+
42
+ private buildMessages(messages: RagMessage[], system?: string) {
43
+ return [
44
+ ...(system ? [{ role: 'system' as const, content: system }] : []),
45
+ ...messages.map(m => ({ role: m.role, content: m.content })),
46
+ ]
47
+ }
48
+
49
+ async generate(messages: RagMessage[], options?: RagGenerateOptions): Promise<RagGenerateResult> {
50
+ const res = await this.client.chat.completions.create({
51
+ model: options?.model ?? this.model,
52
+ messages: this.buildMessages(messages, options?.system),
53
+ ...(options?.maxTokens !== undefined ? { max_tokens: options.maxTokens } : {}),
54
+ ...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
55
+ ...(options?.topP !== undefined ? { top_p: options.topP } : {}),
56
+ ...this.extra,
57
+ ...(options?.extra ?? {}),
58
+ })
59
+ return {
60
+ text: res.choices?.[0]?.message?.content ?? '',
61
+ model: res.model ?? options?.model ?? this.model,
62
+ usage: {
63
+ inputTokens: res.usage?.prompt_tokens,
64
+ outputTokens: res.usage?.completion_tokens,
65
+ },
66
+ }
67
+ }
68
+
69
+ async *stream(
70
+ messages: RagMessage[],
71
+ options?: RagGenerateOptions
72
+ ): AsyncIterable<RagStreamChunk> {
73
+ const stream = await this.client.chat.completions.create({
74
+ model: options?.model ?? this.model,
75
+ messages: this.buildMessages(messages, options?.system),
76
+ stream: true,
77
+ stream_options: { include_usage: true },
78
+ ...(options?.maxTokens !== undefined ? { max_tokens: options.maxTokens } : {}),
79
+ ...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
80
+ ...(options?.topP !== undefined ? { top_p: options.topP } : {}),
81
+ ...this.extra,
82
+ ...(options?.extra ?? {}),
83
+ })
84
+ let usage: any
85
+ for await (const part of stream) {
86
+ const delta = part.choices?.[0]?.delta?.content
87
+ if (delta) yield { delta }
88
+ if (part.usage) usage = part.usage
89
+ }
90
+ yield {
91
+ done: true,
92
+ model: options?.model ?? this.model,
93
+ usage: { inputTokens: usage?.prompt_tokens, outputTokens: usage?.completion_tokens },
94
+ }
95
+ }
96
+
97
+ async embed(texts: string[], model?: string): Promise<number[][]> {
98
+ const res = await this.client.embeddings.create({
99
+ model: model ?? this.embeddingModel,
100
+ input: texts,
101
+ })
102
+ const items = (res.data ?? []) as Array<{ index?: number; embedding: number[] }>
103
+ return items
104
+ .slice()
105
+ .sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
106
+ .map(d => d.embedding)
107
+ }
108
+ }
@@ -0,0 +1,44 @@
1
+ import type { NixxieAiService } from '@nixxie-cms/core'
2
+ import type {
3
+ EmbeddingProvider,
4
+ GenerationProvider,
5
+ RagGenerateOptions,
6
+ RagGenerateResult,
7
+ RagMessage,
8
+ RagStreamChunk,
9
+ } from './types'
10
+
11
+ /**
12
+ * Adapts an existing `NixxieAiService` (e.g. `context.services.ai` from @nixxie-cms/ai)
13
+ * into a RAG provider. Streaming is emulated by emitting the full answer as one delta,
14
+ * since `NixxieAiService` has no streaming surface.
15
+ */
16
+ export class ServiceRagProvider implements GenerationProvider, EmbeddingProvider {
17
+ readonly name = 'service'
18
+ readonly defaultModel = 'service'
19
+
20
+ constructor(private service: NixxieAiService) {}
21
+
22
+ async generate(messages: RagMessage[], options?: RagGenerateOptions): Promise<RagGenerateResult> {
23
+ const res = await this.service.chat(messages, {
24
+ model: options?.model,
25
+ system: options?.system,
26
+ temperature: options?.temperature,
27
+ maxTokens: options?.maxTokens,
28
+ })
29
+ return { text: res.text, model: res.model, usage: res.usage }
30
+ }
31
+
32
+ async *stream(
33
+ messages: RagMessage[],
34
+ options?: RagGenerateOptions
35
+ ): AsyncIterable<RagStreamChunk> {
36
+ const res = await this.generate(messages, options)
37
+ if (res.text) yield { delta: res.text }
38
+ yield { done: true, model: res.model, usage: res.usage }
39
+ }
40
+
41
+ async embed(texts: string[]): Promise<number[][]> {
42
+ return this.service.embedMany(texts)
43
+ }
44
+ }
@@ -0,0 +1,67 @@
1
+ import type { RagEmbeddingConfig, RagGenerationConfig, RagProviderName } from '../types'
2
+ import { AnthropicRagProvider } from './AnthropicRagProvider'
3
+ import { GeminiRagProvider } from './GeminiRagProvider'
4
+ import { OllamaRagProvider } from './OllamaRagProvider'
5
+ import { OpenAiRagProvider } from './OpenAiRagProvider'
6
+ import { ServiceRagProvider } from './ServiceRagProvider'
7
+ import type { EmbeddingProvider, GenerationProvider } from './types'
8
+
9
+ /** Build the generation provider from config (default provider: anthropic). */
10
+ export function resolveGenerationProvider(config: RagGenerationConfig = {}): GenerationProvider {
11
+ if (config.service) return new ServiceRagProvider(config.service)
12
+ const provider: RagProviderName = config.provider ?? 'anthropic'
13
+ switch (provider) {
14
+ case 'anthropic':
15
+ return new AnthropicRagProvider(config)
16
+ case 'openai':
17
+ return new OpenAiRagProvider(config)
18
+ case 'gemini':
19
+ return new GeminiRagProvider(config)
20
+ case 'ollama':
21
+ return new OllamaRagProvider(config)
22
+ default: {
23
+ const exhaustive: never = provider
24
+ throw new Error(`[@nixxie-cms/ai-rag] Unknown generation provider: ${exhaustive}`)
25
+ }
26
+ }
27
+ }
28
+
29
+ /** Build the embedding provider from config (default provider: openai). */
30
+ export function resolveEmbeddingProvider(config: RagEmbeddingConfig = {}): EmbeddingProvider {
31
+ if (config.service) return new ServiceRagProvider(config.service)
32
+ const provider: RagProviderName = config.provider ?? 'openai'
33
+ switch (provider) {
34
+ case 'openai':
35
+ return new OpenAiRagProvider(config)
36
+ case 'gemini':
37
+ return new GeminiRagProvider(config)
38
+ case 'ollama':
39
+ return new OllamaRagProvider(config)
40
+ case 'anthropic':
41
+ throw new Error(
42
+ '[@nixxie-cms/ai-rag] Anthropic has no native embeddings endpoint. Use `openai`, ' +
43
+ '`gemini` or `ollama` for the `embedding` provider (you can still use Anthropic for generation).'
44
+ )
45
+ default: {
46
+ const exhaustive: never = provider
47
+ throw new Error(`[@nixxie-cms/ai-rag] Unknown embedding provider: ${exhaustive}`)
48
+ }
49
+ }
50
+ }
51
+
52
+ export {
53
+ AnthropicRagProvider,
54
+ OpenAiRagProvider,
55
+ GeminiRagProvider,
56
+ OllamaRagProvider,
57
+ ServiceRagProvider,
58
+ }
59
+ export type {
60
+ GenerationProvider,
61
+ EmbeddingProvider,
62
+ RagMessage,
63
+ RagGenerateOptions,
64
+ RagGenerateResult,
65
+ RagStreamChunk,
66
+ RagUsage,
67
+ } from './types'
@@ -0,0 +1,44 @@
1
+ /** A single chat turn handed to a provider. */
2
+ export type RagMessage = { role: 'user' | 'assistant'; content: string }
3
+
4
+ export type RagGenerateOptions = {
5
+ model?: string
6
+ system?: string
7
+ temperature?: number
8
+ maxTokens?: number
9
+ topP?: number
10
+ /** Provider-specific extras merged into the request body. */
11
+ extra?: Record<string, unknown>
12
+ }
13
+
14
+ export type RagUsage = { inputTokens?: number; outputTokens?: number }
15
+
16
+ export type RagGenerateResult = {
17
+ text: string
18
+ model: string
19
+ usage?: RagUsage
20
+ }
21
+
22
+ /** One streamed delta. The final chunk carries `done: true` and any usage. */
23
+ export type RagStreamChunk = {
24
+ delta?: string
25
+ done?: boolean
26
+ usage?: RagUsage
27
+ model?: string
28
+ }
29
+
30
+ /** A provider that can answer (and ideally stream) a chat. */
31
+ export interface GenerationProvider {
32
+ readonly name: string
33
+ readonly defaultModel: string
34
+ generate(messages: RagMessage[], options?: RagGenerateOptions): Promise<RagGenerateResult>
35
+ /** Optional native streaming. When absent, the service emulates it from `generate`. */
36
+ stream?(messages: RagMessage[], options?: RagGenerateOptions): AsyncIterable<RagStreamChunk>
37
+ }
38
+
39
+ /** A provider that can turn text into embedding vectors. */
40
+ export interface EmbeddingProvider {
41
+ readonly name: string
42
+ readonly defaultModel: string
43
+ embed(texts: string[], model?: string): Promise<number[][]>
44
+ }
@@ -0,0 +1,26 @@
1
+ /** A minimal counting semaphore used to cap concurrent generations. */
2
+ export class Semaphore {
3
+ private available: number
4
+ private waiters: Array<() => void> = []
5
+
6
+ constructor(permits: number) {
7
+ this.available = Math.max(1, permits)
8
+ }
9
+
10
+ /** Acquire a permit, resolving to a `release` function to call when done. */
11
+ async acquire(): Promise<() => void> {
12
+ if (this.available > 0) {
13
+ this.available--
14
+ return () => this.release()
15
+ }
16
+ await new Promise<void>(resolve => this.waiters.push(resolve))
17
+ this.available--
18
+ return () => this.release()
19
+ }
20
+
21
+ private release(): void {
22
+ this.available++
23
+ const next = this.waiters.shift()
24
+ if (next) next()
25
+ }
26
+ }
@@ -0,0 +1,31 @@
1
+ /** Dot product of two equal-length vectors. */
2
+ export function dot(a: number[], b: number[]): number {
3
+ let sum = 0
4
+ const n = Math.min(a.length, b.length)
5
+ for (let i = 0; i < n; i++) sum += a[i]! * b[i]!
6
+ return sum
7
+ }
8
+
9
+ /** Euclidean norm of a vector. */
10
+ export function norm(a: number[]): number {
11
+ return Math.sqrt(dot(a, a))
12
+ }
13
+
14
+ /**
15
+ * Cosine similarity mapped from [-1, 1] into [0, 1] so it can be used as a relevance
16
+ * score and compared against a `minScore` threshold. Returns 0 for a zero vector.
17
+ */
18
+ export function cosineSimilarity(a: number[], b: number[]): number {
19
+ const denom = norm(a) * norm(b)
20
+ if (denom === 0) return 0
21
+ const cos = dot(a, b) / denom
22
+ // Clamp for floating-point drift, then rescale to [0, 1].
23
+ return (Math.max(-1, Math.min(1, cos)) + 1) / 2
24
+ }
25
+
26
+ /** Pre-normalise a vector to unit length (lets retrieval use a plain dot product). */
27
+ export function normalize(a: number[]): number[] {
28
+ const n = norm(a)
29
+ if (n === 0) return a.slice()
30
+ return a.map(x => x / n)
31
+ }