@rudderjs/ai 1.18.1 → 1.18.3
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/dist/budget-orm/index.d.ts +95 -1
- package/dist/budget-orm/index.d.ts.map +1 -1
- package/dist/budget-orm/index.js +176 -4
- package/dist/budget-orm/index.js.map +1 -1
- package/dist/conversation-orm/index.d.ts +115 -1
- package/dist/conversation-orm/index.d.ts.map +1 -1
- package/dist/conversation-orm/index.js +214 -4
- package/dist/conversation-orm/index.js.map +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +67 -4
- package/dist/doctor.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +5 -4
- package/dist/mcp/index.js.map +1 -1
- package/dist/memory-embedding/index.d.ts +120 -1
- package/dist/memory-embedding/index.d.ts.map +1 -1
- package/dist/memory-embedding/index.js +228 -4
- package/dist/memory-embedding/index.js.map +1 -1
- package/dist/memory-orm/index.d.ts +117 -1
- package/dist/memory-orm/index.d.ts.map +1 -1
- package/dist/memory-orm/index.js +186 -4
- package/dist/memory-orm/index.js.map +1 -1
- package/package.json +14 -6
|
@@ -1,2 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/budget-orm` — ORM-backed {@link BudgetStorage} for #A6 Phase 4.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `memoryBudgetStorage()` (which is
|
|
5
|
+
* single-process only). Persists per-user spend counters in a
|
|
6
|
+
* `BudgetUsage` table via the registered `@rudderjs/orm` adapter — works
|
|
7
|
+
* across queue workers, web processes, and horizontally-scaled deployments.
|
|
8
|
+
*
|
|
9
|
+
* Wire it into your AI middleware:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { withBudget } from '@gemstack/ai-sdk'
|
|
13
|
+
* import { ormBudgetStorage } from '@rudderjs/ai/budget-orm'
|
|
14
|
+
*
|
|
15
|
+
* const budgeted = withBudget({
|
|
16
|
+
* user: (ctx) => ctx.context as string,
|
|
17
|
+
* budget: () => ({ daily: 0.50, monthly: 10 }),
|
|
18
|
+
* storage: ormBudgetStorage(),
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The schema lives at {@link budgetUsagePrismaSchema} — copy it into your
|
|
23
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
24
|
+
* multi-file setup). The `@@unique([userId, period, periodKey])`
|
|
25
|
+
* constraint is the one load-bearing index — without it, the
|
|
26
|
+
* find-or-create path can race and produce duplicate rows.
|
|
27
|
+
*
|
|
28
|
+
* # Atomicity caveat
|
|
29
|
+
*
|
|
30
|
+
* `checkAndDebit` does a read-then-conditional-increment. The increment
|
|
31
|
+
* itself is atomic (`UPDATE col = col + n`), but the cap check sits
|
|
32
|
+
* between the read and the write. Under high concurrency for a single
|
|
33
|
+
* user (more than ~1 in-flight budgeted request at a time), total spend
|
|
34
|
+
* can briefly exceed `cap` by up to `costUsd × concurrency`. For typical
|
|
35
|
+
* apps this is a non-issue.
|
|
36
|
+
*
|
|
37
|
+
* Strict guarantees require a database transaction with serializable
|
|
38
|
+
* isolation or a Redis-backed counter — both planned as follow-ups. File
|
|
39
|
+
* an issue if you hit this in production.
|
|
40
|
+
*/
|
|
41
|
+
import { Model } from '@rudderjs/orm';
|
|
42
|
+
import { type BudgetCheckOptions, type BudgetCheckResult, type BudgetPeriod, type BudgetStorage } from '@gemstack/ai-sdk';
|
|
43
|
+
/**
|
|
44
|
+
* Model row backing {@link OrmBudgetStorage}. Exposed so apps that
|
|
45
|
+
* want admin views (e.g. "show me top spenders this month") can use
|
|
46
|
+
* `BudgetUsageRecord.where(...).get()` instead of routing every read
|
|
47
|
+
* through the {@link BudgetStorage} interface.
|
|
48
|
+
*
|
|
49
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
50
|
+
* without it, two concurrent first-writes for the same user/period
|
|
51
|
+
* create duplicate rows and the cap accounting silently drifts.
|
|
52
|
+
*/
|
|
53
|
+
export declare class BudgetUsageRecord extends Model {
|
|
54
|
+
static table: string;
|
|
55
|
+
static fillable: string[];
|
|
56
|
+
id: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
/** `'daily'` or `'monthly'`. */
|
|
59
|
+
period: string;
|
|
60
|
+
/** TZ-aware bucket key — `YYYY-MM-DD` (daily) or `YYYY-MM` (monthly). */
|
|
61
|
+
periodKey: string;
|
|
62
|
+
/** Cumulative USD spend in this period. */
|
|
63
|
+
spent: number;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
updatedAt: Date | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Production `BudgetStorage` backed by the registered `@rudderjs/orm`
|
|
69
|
+
* adapter. See the module JSDoc for setup + the atomicity caveat.
|
|
70
|
+
*/
|
|
71
|
+
export declare class OrmBudgetStorage implements BudgetStorage {
|
|
72
|
+
checkAndDebit(opts: BudgetCheckOptions): Promise<BudgetCheckResult>;
|
|
73
|
+
/** Apply the read-then-conditional-increment path on an existing row. */
|
|
74
|
+
private _applyIncrementPath;
|
|
75
|
+
reset(userId: string, period: BudgetPeriod, now?: Date, timezone?: string): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Convenience factory — returns a fresh {@link OrmBudgetStorage}
|
|
79
|
+
* instance. Prefer this over `new OrmBudgetStorage()` for symmetry with
|
|
80
|
+
* `memoryBudgetStorage()`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function ormBudgetStorage(): BudgetStorage;
|
|
83
|
+
/**
|
|
84
|
+
* Reference Prisma schema for `OrmBudgetStorage`. Copy into your
|
|
85
|
+
* `prisma/schema/<file>.prisma` (or paste alongside an existing model).
|
|
86
|
+
*
|
|
87
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
88
|
+
* without it the find-or-create path can race and produce duplicate
|
|
89
|
+
* rows, breaking cap accounting.
|
|
90
|
+
*
|
|
91
|
+
* SQLite stores `Float` as `REAL`; Postgres / MySQL as `DOUBLE
|
|
92
|
+
* PRECISION` / `DOUBLE`. All three give 15+ significant digits — more
|
|
93
|
+
* than enough for sub-cent budget tracking.
|
|
94
|
+
*/
|
|
95
|
+
export declare const budgetUsagePrismaSchema = "model BudgetUsage {\n id String @id @default(cuid())\n userId String\n /// 'daily' | 'monthly'\n period String\n /// YYYY-MM-DD (daily) or YYYY-MM (monthly), in the configured timezone\n periodKey String\n /// Cumulative USD spend in this period\n spent Float @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([userId, period, periodKey])\n @@index([userId])\n}\n";
|
|
2
96
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AACrC,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,kBAAkB,CAAA;AAIzB;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,OAAgB,KAAK,SAAmB;IACxC,OAAgB,QAAQ,WAA6C;IAE7D,EAAE,EAAS,MAAM,CAAA;IACjB,MAAM,EAAK,MAAM,CAAA;IACzB,gCAAgC;IACxB,MAAM,EAAK,MAAM,CAAA;IACzB,yEAAyE;IACjE,SAAS,EAAE,MAAM,CAAA;IACzB,2CAA2C;IACnC,KAAK,EAAM,MAAM,CAAA;IACjB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;CAC/B;AAID;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuDzE,yEAAyE;YAC3D,mBAAmB;IAsB3B,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQhG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,8bAenC,CAAA"}
|
package/dist/budget-orm/index.js
CHANGED
|
@@ -1,5 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/budget-orm` — ORM-backed {@link BudgetStorage} for #A6 Phase 4.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `memoryBudgetStorage()` (which is
|
|
5
|
+
* single-process only). Persists per-user spend counters in a
|
|
6
|
+
* `BudgetUsage` table via the registered `@rudderjs/orm` adapter — works
|
|
7
|
+
* across queue workers, web processes, and horizontally-scaled deployments.
|
|
8
|
+
*
|
|
9
|
+
* Wire it into your AI middleware:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { withBudget } from '@gemstack/ai-sdk'
|
|
13
|
+
* import { ormBudgetStorage } from '@rudderjs/ai/budget-orm'
|
|
14
|
+
*
|
|
15
|
+
* const budgeted = withBudget({
|
|
16
|
+
* user: (ctx) => ctx.context as string,
|
|
17
|
+
* budget: () => ({ daily: 0.50, monthly: 10 }),
|
|
18
|
+
* storage: ormBudgetStorage(),
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The schema lives at {@link budgetUsagePrismaSchema} — copy it into your
|
|
23
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
24
|
+
* multi-file setup). The `@@unique([userId, period, periodKey])`
|
|
25
|
+
* constraint is the one load-bearing index — without it, the
|
|
26
|
+
* find-or-create path can race and produce duplicate rows.
|
|
27
|
+
*
|
|
28
|
+
* # Atomicity caveat
|
|
29
|
+
*
|
|
30
|
+
* `checkAndDebit` does a read-then-conditional-increment. The increment
|
|
31
|
+
* itself is atomic (`UPDATE col = col + n`), but the cap check sits
|
|
32
|
+
* between the read and the write. Under high concurrency for a single
|
|
33
|
+
* user (more than ~1 in-flight budgeted request at a time), total spend
|
|
34
|
+
* can briefly exceed `cap` by up to `costUsd × concurrency`. For typical
|
|
35
|
+
* apps this is a non-issue.
|
|
36
|
+
*
|
|
37
|
+
* Strict guarantees require a database transaction with serializable
|
|
38
|
+
* isolation or a Redis-backed counter — both planned as follow-ups. File
|
|
39
|
+
* an issue if you hit this in production.
|
|
40
|
+
*/
|
|
41
|
+
import { Model } from '@rudderjs/orm';
|
|
42
|
+
import { periodKey as buildPeriodKey, } from '@gemstack/ai-sdk';
|
|
43
|
+
// ─── ORM Model ────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Model row backing {@link OrmBudgetStorage}. Exposed so apps that
|
|
46
|
+
* want admin views (e.g. "show me top spenders this month") can use
|
|
47
|
+
* `BudgetUsageRecord.where(...).get()` instead of routing every read
|
|
48
|
+
* through the {@link BudgetStorage} interface.
|
|
49
|
+
*
|
|
50
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
51
|
+
* without it, two concurrent first-writes for the same user/period
|
|
52
|
+
* create duplicate rows and the cap accounting silently drifts.
|
|
53
|
+
*/
|
|
54
|
+
export class BudgetUsageRecord extends Model {
|
|
55
|
+
static table = 'budgetUsage';
|
|
56
|
+
static fillable = ['userId', 'period', 'periodKey', 'spent'];
|
|
57
|
+
}
|
|
58
|
+
// ─── BudgetStorage adapter ────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Production `BudgetStorage` backed by the registered `@rudderjs/orm`
|
|
61
|
+
* adapter. See the module JSDoc for setup + the atomicity caveat.
|
|
62
|
+
*/
|
|
63
|
+
export class OrmBudgetStorage {
|
|
64
|
+
async checkAndDebit(opts) {
|
|
65
|
+
if (!Number.isFinite(opts.cap) || opts.cap < 0) {
|
|
66
|
+
throw new Error(`[ai-sdk] BudgetStorage: cap must be a non-negative finite number, got ${opts.cap}`);
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(opts.costUsd) || opts.costUsd < 0) {
|
|
69
|
+
throw new Error(`[ai-sdk] BudgetStorage: costUsd must be a non-negative finite number, got ${opts.costUsd}`);
|
|
70
|
+
}
|
|
71
|
+
const now = opts.now ?? new Date();
|
|
72
|
+
const key = buildPeriodKey(opts.period, now, opts.timezone);
|
|
73
|
+
const existing = await BudgetUsageRecord
|
|
74
|
+
.where('userId', opts.userId)
|
|
75
|
+
.where('period', opts.period)
|
|
76
|
+
.where('periodKey', key)
|
|
77
|
+
.first();
|
|
78
|
+
// ─── No row yet — first write for this period ─────────
|
|
79
|
+
if (!existing) {
|
|
80
|
+
// Pure-read on an empty bucket — still empty after.
|
|
81
|
+
if (opts.costUsd === 0) {
|
|
82
|
+
return { allowed: true, spent: 0, cap: opts.cap };
|
|
83
|
+
}
|
|
84
|
+
// Single debit larger than cap — refuse before creating the row,
|
|
85
|
+
// so we don't pollute storage with denied requests.
|
|
86
|
+
if (opts.costUsd > opts.cap) {
|
|
87
|
+
return { allowed: false, spent: 0, cap: opts.cap };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await BudgetUsageRecord.create({
|
|
91
|
+
userId: opts.userId,
|
|
92
|
+
period: opts.period,
|
|
93
|
+
periodKey: key,
|
|
94
|
+
spent: opts.costUsd,
|
|
95
|
+
});
|
|
96
|
+
return { allowed: true, spent: opts.costUsd, cap: opts.cap };
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
// Race: another caller created the row between our `first()` and
|
|
100
|
+
// `create()`. Re-read and fall through to the increment path.
|
|
101
|
+
// We deliberately don't sniff the error type — any create failure
|
|
102
|
+
// means the row may now exist; let the re-read decide.
|
|
103
|
+
const refetched = await BudgetUsageRecord
|
|
104
|
+
.where('userId', opts.userId)
|
|
105
|
+
.where('period', opts.period)
|
|
106
|
+
.where('periodKey', key)
|
|
107
|
+
.first();
|
|
108
|
+
if (!refetched)
|
|
109
|
+
throw e; // not a unique-constraint race; surface the original error
|
|
110
|
+
return this._applyIncrementPath(refetched, opts);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return this._applyIncrementPath(existing, opts);
|
|
114
|
+
}
|
|
115
|
+
/** Apply the read-then-conditional-increment path on an existing row. */
|
|
116
|
+
async _applyIncrementPath(row, opts) {
|
|
117
|
+
const current = Number(row.spent ?? 0);
|
|
118
|
+
// Pure read.
|
|
119
|
+
if (opts.costUsd === 0) {
|
|
120
|
+
return { allowed: true, spent: current, cap: opts.cap };
|
|
121
|
+
}
|
|
122
|
+
// Cap check — read-then-decide. Atomic under single-writer; under
|
|
123
|
+
// concurrent writers, see the module-level atomicity caveat.
|
|
124
|
+
if (current + opts.costUsd > opts.cap) {
|
|
125
|
+
return { allowed: false, spent: current, cap: opts.cap };
|
|
126
|
+
}
|
|
127
|
+
const updated = await BudgetUsageRecord.increment(row.id, 'spent', opts.costUsd);
|
|
128
|
+
const newSpent = Number(updated?.spent ?? current + opts.costUsd);
|
|
129
|
+
return { allowed: true, spent: newSpent, cap: opts.cap };
|
|
130
|
+
}
|
|
131
|
+
async reset(userId, period, now, timezone) {
|
|
132
|
+
const key = buildPeriodKey(period, now ?? new Date(), timezone);
|
|
133
|
+
await BudgetUsageRecord
|
|
134
|
+
.where('userId', userId)
|
|
135
|
+
.where('period', period)
|
|
136
|
+
.where('periodKey', key)
|
|
137
|
+
.deleteAll();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convenience factory — returns a fresh {@link OrmBudgetStorage}
|
|
142
|
+
* instance. Prefer this over `new OrmBudgetStorage()` for symmetry with
|
|
143
|
+
* `memoryBudgetStorage()`.
|
|
144
|
+
*/
|
|
145
|
+
export function ormBudgetStorage() {
|
|
146
|
+
return new OrmBudgetStorage();
|
|
147
|
+
}
|
|
148
|
+
// ─── Schema reference ─────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Reference Prisma schema for `OrmBudgetStorage`. Copy into your
|
|
151
|
+
* `prisma/schema/<file>.prisma` (or paste alongside an existing model).
|
|
152
|
+
*
|
|
153
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
154
|
+
* without it the find-or-create path can race and produce duplicate
|
|
155
|
+
* rows, breaking cap accounting.
|
|
156
|
+
*
|
|
157
|
+
* SQLite stores `Float` as `REAL`; Postgres / MySQL as `DOUBLE
|
|
158
|
+
* PRECISION` / `DOUBLE`. All three give 15+ significant digits — more
|
|
159
|
+
* than enough for sub-cent budget tracking.
|
|
160
|
+
*/
|
|
161
|
+
export const budgetUsagePrismaSchema = `model BudgetUsage {
|
|
162
|
+
id String @id @default(cuid())
|
|
163
|
+
userId String
|
|
164
|
+
/// 'daily' | 'monthly'
|
|
165
|
+
period String
|
|
166
|
+
/// YYYY-MM-DD (daily) or YYYY-MM (monthly), in the configured timezone
|
|
167
|
+
periodKey String
|
|
168
|
+
/// Cumulative USD spend in this period
|
|
169
|
+
spent Float @default(0)
|
|
170
|
+
createdAt DateTime @default(now())
|
|
171
|
+
updatedAt DateTime @updatedAt
|
|
172
|
+
|
|
173
|
+
@@unique([userId, period, periodKey])
|
|
174
|
+
@@index([userId])
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
5
177
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,2 +1,116 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/conversation-orm` - ORM-backed {@link ConversationStore}.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `MemoryConversationStore` (which is
|
|
5
|
+
* single-process, in-memory, and loses every thread on restart). Persists
|
|
6
|
+
* conversation threads and their messages via the registered `@rudderjs/orm`
|
|
7
|
+
* adapter - works across web processes, queue workers, and horizontally
|
|
8
|
+
* scaled deployments. Mirrors the `@rudderjs/ai/memory-orm` /
|
|
9
|
+
* `@rudderjs/ai/budget-orm` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Wire it as the conversation store:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { setConversationStore } from '@gemstack/ai-sdk'
|
|
15
|
+
* import { OrmConversationStore } from '@rudderjs/ai/conversation-orm'
|
|
16
|
+
*
|
|
17
|
+
* setConversationStore(new OrmConversationStore())
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The schema lives at {@link conversationOrmPrismaSchema} - copy it into your
|
|
21
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
22
|
+
* multi-file setup). On the native engine, add an equivalent migration; on
|
|
23
|
+
* Drizzle, define matching tables and register them via `tables: { ... }`.
|
|
24
|
+
*
|
|
25
|
+
* # Adapter coverage
|
|
26
|
+
*
|
|
27
|
+
* - Prisma - works out of the box; copy {@link conversationOrmPrismaSchema}.
|
|
28
|
+
* - Native - add a migration with the same columns.
|
|
29
|
+
* - Drizzle - define the two tables and register them on the `drizzle()`
|
|
30
|
+
* config.
|
|
31
|
+
*
|
|
32
|
+
* # Ordering & concurrency
|
|
33
|
+
*
|
|
34
|
+
* Messages carry a monotonic per-thread `position` so `load()` returns them
|
|
35
|
+
* in append order regardless of timestamp granularity. `append()` reads the
|
|
36
|
+
* current max position and assigns the next slots; like
|
|
37
|
+
* `OrmBudgetStorage.checkAndDebit`, the read-then-write is not isolated, so
|
|
38
|
+
* two concurrent appends to the SAME thread could collide on a position.
|
|
39
|
+
* Conversation threads are single-writer in practice (one user, one turn at
|
|
40
|
+
* a time), so this is a non-issue for typical apps. File an issue if you hit
|
|
41
|
+
* it; strict ordering needs a serializable transaction or a DB sequence.
|
|
42
|
+
*/
|
|
43
|
+
import { Model } from '@rudderjs/orm';
|
|
44
|
+
import type { AiMessage, ConversationStore, ConversationStoreMeta } from '@gemstack/ai-sdk';
|
|
45
|
+
/**
|
|
46
|
+
* Entry shape returned by {@link ConversationStore.list}. Derived from the
|
|
47
|
+
* engine's exported `ConversationStore` interface so it stays in lockstep
|
|
48
|
+
* without depending on the type being exported by name.
|
|
49
|
+
*/
|
|
50
|
+
type ConversationStoreListEntry = Awaited<ReturnType<ConversationStore['list']>>[number];
|
|
51
|
+
/**
|
|
52
|
+
* The thread row backing {@link OrmConversationStore}. Exposed so apps that
|
|
53
|
+
* want their own queries (admin views, analytics) can use
|
|
54
|
+
* `AiConversationRecord.where(...).get()` directly.
|
|
55
|
+
*
|
|
56
|
+
* `userId` / `agent` mirror {@link ConversationStoreMeta} - `userId` scopes
|
|
57
|
+
* `list()`, `agent` carries the thread-segregation key the auto-persist
|
|
58
|
+
* machinery uses to keep one user's threads per agent class apart.
|
|
59
|
+
*/
|
|
60
|
+
export declare class AiConversationRecord extends Model {
|
|
61
|
+
static table: string;
|
|
62
|
+
static fillable: string[];
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
userId: string | null;
|
|
66
|
+
agent: string | null;
|
|
67
|
+
createdAt: Date;
|
|
68
|
+
updatedAt: Date | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One message row in a thread. `content` and `toolCalls` are JSON-encoded
|
|
72
|
+
* strings (so a `string` content and a `ContentPart[]` content both
|
|
73
|
+
* round-trip through a portable `text` column); `position` orders them.
|
|
74
|
+
*/
|
|
75
|
+
export declare class AiConversationMessageRecord extends Model {
|
|
76
|
+
static table: string;
|
|
77
|
+
static fillable: string[];
|
|
78
|
+
id: string;
|
|
79
|
+
conversationId: string;
|
|
80
|
+
position: number;
|
|
81
|
+
role: string;
|
|
82
|
+
/** JSON-encoded `string | ContentPart[]`. */
|
|
83
|
+
content: string;
|
|
84
|
+
toolCallId: string | null;
|
|
85
|
+
/** JSON-encoded `ToolCall[]` or null. */
|
|
86
|
+
toolCalls: string | null;
|
|
87
|
+
createdAt: Date;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* {@link ConversationStore} implementation that persists rows to the
|
|
91
|
+
* registered ORM adapter. Designed for production use - the in-process
|
|
92
|
+
* `MemoryConversationStore` is for tests and dev.
|
|
93
|
+
*/
|
|
94
|
+
export declare class OrmConversationStore implements ConversationStore {
|
|
95
|
+
create(title?: string, meta?: ConversationStoreMeta): Promise<string>;
|
|
96
|
+
load(conversationId: string): Promise<AiMessage[]>;
|
|
97
|
+
append(conversationId: string, messages: AiMessage[]): Promise<void>;
|
|
98
|
+
setTitle(conversationId: string, title: string): Promise<void>;
|
|
99
|
+
list(userId?: string): Promise<ConversationStoreListEntry[]>;
|
|
100
|
+
delete(conversationId: string): Promise<void>;
|
|
101
|
+
/** Throw the same not-found error shape as `MemoryConversationStore`. */
|
|
102
|
+
private requireThread;
|
|
103
|
+
/** Next monotonic position for the thread (0 when empty). */
|
|
104
|
+
private nextPosition;
|
|
105
|
+
}
|
|
106
|
+
/** Convenience factory mirroring `ormBudgetStorage()` / `OrmUserMemory`. */
|
|
107
|
+
export declare function ormConversationStore(): OrmConversationStore;
|
|
108
|
+
/**
|
|
109
|
+
* Reference Prisma schema for `OrmConversationStore`. Copy into your
|
|
110
|
+
* `prisma/schema/<file>.prisma`. SQLite stores the `text` content as TEXT;
|
|
111
|
+
* Postgres as `text`. The `@@index` keeps `list()` (by user) and `load()`
|
|
112
|
+
* (by thread, ordered) cheap.
|
|
113
|
+
*/
|
|
114
|
+
export declare const conversationOrmPrismaSchema = "model AiConversation {\n id String @id @default(cuid())\n title String\n userId String?\n /// Thread-segregation key - the agent class name by default\n agent String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n}\n\nmodel AiConversationMessage {\n id String @id @default(cuid())\n conversationId String\n /// Monotonic per-thread ordering\n position Int\n role String\n /// JSON-encoded `string | ContentPart[]`\n content String\n toolCallId String?\n /// JSON-encoded `ToolCall[]` or null\n toolCalls String?\n createdAt DateTime @default(now())\n\n @@index([conversationId, position])\n}\n";
|
|
115
|
+
export {};
|
|
2
116
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAErC,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EACjB,qBAAqB,EAEtB,MAAM,kBAAkB,CAAA;AAEzB;;;;GAIG;AACH,KAAK,0BAA0B,GAAG,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAIxF;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,OAAgB,KAAK,SAAsB;IAC3C,OAAgB,QAAQ,WAA4C;IAE5D,EAAE,EAAS,MAAM,CAAA;IACjB,KAAK,EAAM,MAAM,CAAA;IACjB,MAAM,EAAK,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAM,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;CAC/B;AAED;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,OAAgB,KAAK,SAA6B;IAClD,OAAgB,QAAQ,WAA+E;IAE/F,EAAE,EAAc,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAQ,MAAM,CAAA;IACtB,IAAI,EAAY,MAAM,CAAA;IAC9B,6CAA6C;IACrC,OAAO,EAAS,MAAM,CAAA;IACtB,UAAU,EAAM,MAAM,GAAG,IAAI,CAAA;IACrC,yCAAyC;IACjC,SAAS,EAAO,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAO,IAAI,CAAA;CAC7B;AAID;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,iBAAiB;IACtD,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IASrE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAYlD,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAapE,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,EAAE,CAAC;IAO5D,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,yEAAyE;YAC3D,aAAa;IAK3B,6DAA6D;YAC/C,YAAY;CAO3B;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,IAAI,oBAAoB,CAE3D;AAID;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,+tBA2BvC,CAAA"}
|
|
@@ -1,5 +1,215 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/conversation-orm` - ORM-backed {@link ConversationStore}.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `MemoryConversationStore` (which is
|
|
5
|
+
* single-process, in-memory, and loses every thread on restart). Persists
|
|
6
|
+
* conversation threads and their messages via the registered `@rudderjs/orm`
|
|
7
|
+
* adapter - works across web processes, queue workers, and horizontally
|
|
8
|
+
* scaled deployments. Mirrors the `@rudderjs/ai/memory-orm` /
|
|
9
|
+
* `@rudderjs/ai/budget-orm` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Wire it as the conversation store:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { setConversationStore } from '@gemstack/ai-sdk'
|
|
15
|
+
* import { OrmConversationStore } from '@rudderjs/ai/conversation-orm'
|
|
16
|
+
*
|
|
17
|
+
* setConversationStore(new OrmConversationStore())
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The schema lives at {@link conversationOrmPrismaSchema} - copy it into your
|
|
21
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
22
|
+
* multi-file setup). On the native engine, add an equivalent migration; on
|
|
23
|
+
* Drizzle, define matching tables and register them via `tables: { ... }`.
|
|
24
|
+
*
|
|
25
|
+
* # Adapter coverage
|
|
26
|
+
*
|
|
27
|
+
* - Prisma - works out of the box; copy {@link conversationOrmPrismaSchema}.
|
|
28
|
+
* - Native - add a migration with the same columns.
|
|
29
|
+
* - Drizzle - define the two tables and register them on the `drizzle()`
|
|
30
|
+
* config.
|
|
31
|
+
*
|
|
32
|
+
* # Ordering & concurrency
|
|
33
|
+
*
|
|
34
|
+
* Messages carry a monotonic per-thread `position` so `load()` returns them
|
|
35
|
+
* in append order regardless of timestamp granularity. `append()` reads the
|
|
36
|
+
* current max position and assigns the next slots; like
|
|
37
|
+
* `OrmBudgetStorage.checkAndDebit`, the read-then-write is not isolated, so
|
|
38
|
+
* two concurrent appends to the SAME thread could collide on a position.
|
|
39
|
+
* Conversation threads are single-writer in practice (one user, one turn at
|
|
40
|
+
* a time), so this is a non-issue for typical apps. File an issue if you hit
|
|
41
|
+
* it; strict ordering needs a serializable transaction or a DB sequence.
|
|
42
|
+
*/
|
|
43
|
+
import { Model } from '@rudderjs/orm';
|
|
44
|
+
import { sanitizeConversation } from '@gemstack/ai-sdk';
|
|
45
|
+
// ─── ORM Models ───────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* The thread row backing {@link OrmConversationStore}. Exposed so apps that
|
|
48
|
+
* want their own queries (admin views, analytics) can use
|
|
49
|
+
* `AiConversationRecord.where(...).get()` directly.
|
|
50
|
+
*
|
|
51
|
+
* `userId` / `agent` mirror {@link ConversationStoreMeta} - `userId` scopes
|
|
52
|
+
* `list()`, `agent` carries the thread-segregation key the auto-persist
|
|
53
|
+
* machinery uses to keep one user's threads per agent class apart.
|
|
54
|
+
*/
|
|
55
|
+
export class AiConversationRecord extends Model {
|
|
56
|
+
static table = 'aiConversation';
|
|
57
|
+
static fillable = ['title', 'userId', 'agent', 'updatedAt'];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One message row in a thread. `content` and `toolCalls` are JSON-encoded
|
|
61
|
+
* strings (so a `string` content and a `ContentPart[]` content both
|
|
62
|
+
* round-trip through a portable `text` column); `position` orders them.
|
|
63
|
+
*/
|
|
64
|
+
export class AiConversationMessageRecord extends Model {
|
|
65
|
+
static table = 'aiConversationMessage';
|
|
66
|
+
static fillable = ['conversationId', 'position', 'role', 'content', 'toolCallId', 'toolCalls'];
|
|
67
|
+
}
|
|
68
|
+
// ─── ConversationStore adapter ────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* {@link ConversationStore} implementation that persists rows to the
|
|
71
|
+
* registered ORM adapter. Designed for production use - the in-process
|
|
72
|
+
* `MemoryConversationStore` is for tests and dev.
|
|
73
|
+
*/
|
|
74
|
+
export class OrmConversationStore {
|
|
75
|
+
async create(title, meta) {
|
|
76
|
+
const data = { title: title ?? 'New conversation' };
|
|
77
|
+
if (meta?.userId !== undefined)
|
|
78
|
+
data['userId'] = meta.userId;
|
|
79
|
+
if (meta?.agent !== undefined)
|
|
80
|
+
data['agent'] = meta.agent;
|
|
81
|
+
const created = await AiConversationRecord.create(data);
|
|
82
|
+
return created.id;
|
|
83
|
+
}
|
|
84
|
+
async load(conversationId) {
|
|
85
|
+
await this.requireThread(conversationId);
|
|
86
|
+
const rows = await AiConversationMessageRecord
|
|
87
|
+
.where('conversationId', conversationId)
|
|
88
|
+
.orderBy('position', 'ASC')
|
|
89
|
+
.get();
|
|
90
|
+
// A thread persisted mid-turn (crash between the assistant row and its
|
|
91
|
+
// tool-result rows) would otherwise replay into a provider 400. Drop the
|
|
92
|
+
// incomplete turns at the load boundary so the history is replay-safe.
|
|
93
|
+
return sanitizeConversation(rows.map(rowToMessage));
|
|
94
|
+
}
|
|
95
|
+
async append(conversationId, messages) {
|
|
96
|
+
await this.requireThread(conversationId);
|
|
97
|
+
if (messages.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
let position = await this.nextPosition(conversationId);
|
|
100
|
+
for (const message of messages) {
|
|
101
|
+
await AiConversationMessageRecord.create(messageToRow(conversationId, position, message));
|
|
102
|
+
position++;
|
|
103
|
+
}
|
|
104
|
+
await AiConversationRecord.where('id', conversationId).updateAll({ updatedAt: new Date() });
|
|
105
|
+
}
|
|
106
|
+
async setTitle(conversationId, title) {
|
|
107
|
+
const updated = await AiConversationRecord
|
|
108
|
+
.where('id', conversationId)
|
|
109
|
+
.updateAll({ title, updatedAt: new Date() });
|
|
110
|
+
if (!updated)
|
|
111
|
+
throw notFound(conversationId);
|
|
112
|
+
}
|
|
113
|
+
async list(userId) {
|
|
114
|
+
let q = AiConversationRecord.query();
|
|
115
|
+
if (userId != null)
|
|
116
|
+
q = q.where('userId', userId);
|
|
117
|
+
const rows = await q.orderBy('updatedAt', 'DESC').get();
|
|
118
|
+
return rows.map(rowToListEntry);
|
|
119
|
+
}
|
|
120
|
+
async delete(conversationId) {
|
|
121
|
+
await AiConversationMessageRecord.where('conversationId', conversationId).deleteAll();
|
|
122
|
+
await AiConversationRecord.where('id', conversationId).deleteAll();
|
|
123
|
+
}
|
|
124
|
+
/** Throw the same not-found error shape as `MemoryConversationStore`. */
|
|
125
|
+
async requireThread(conversationId) {
|
|
126
|
+
const thread = await AiConversationRecord.where('id', conversationId).first();
|
|
127
|
+
if (!thread)
|
|
128
|
+
throw notFound(conversationId);
|
|
129
|
+
}
|
|
130
|
+
/** Next monotonic position for the thread (0 when empty). */
|
|
131
|
+
async nextPosition(conversationId) {
|
|
132
|
+
const last = await AiConversationMessageRecord
|
|
133
|
+
.where('conversationId', conversationId)
|
|
134
|
+
.orderBy('position', 'DESC')
|
|
135
|
+
.first();
|
|
136
|
+
return last ? last.position + 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Convenience factory mirroring `ormBudgetStorage()` / `OrmUserMemory`. */
|
|
140
|
+
export function ormConversationStore() {
|
|
141
|
+
return new OrmConversationStore();
|
|
142
|
+
}
|
|
143
|
+
// ─── Schema reference ─────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Reference Prisma schema for `OrmConversationStore`. Copy into your
|
|
146
|
+
* `prisma/schema/<file>.prisma`. SQLite stores the `text` content as TEXT;
|
|
147
|
+
* Postgres as `text`. The `@@index` keeps `list()` (by user) and `load()`
|
|
148
|
+
* (by thread, ordered) cheap.
|
|
149
|
+
*/
|
|
150
|
+
export const conversationOrmPrismaSchema = `model AiConversation {
|
|
151
|
+
id String @id @default(cuid())
|
|
152
|
+
title String
|
|
153
|
+
userId String?
|
|
154
|
+
/// Thread-segregation key - the agent class name by default
|
|
155
|
+
agent String?
|
|
156
|
+
createdAt DateTime @default(now())
|
|
157
|
+
updatedAt DateTime @updatedAt
|
|
158
|
+
|
|
159
|
+
@@index([userId])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
model AiConversationMessage {
|
|
163
|
+
id String @id @default(cuid())
|
|
164
|
+
conversationId String
|
|
165
|
+
/// Monotonic per-thread ordering
|
|
166
|
+
position Int
|
|
167
|
+
role String
|
|
168
|
+
/// JSON-encoded \`string | ContentPart[]\`
|
|
169
|
+
content String
|
|
170
|
+
toolCallId String?
|
|
171
|
+
/// JSON-encoded \`ToolCall[]\` or null
|
|
172
|
+
toolCalls String?
|
|
173
|
+
createdAt DateTime @default(now())
|
|
174
|
+
|
|
175
|
+
@@index([conversationId, position])
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
179
|
+
function notFound(conversationId) {
|
|
180
|
+
return new Error(`[ai-sdk] Conversation "${conversationId}" not found.`);
|
|
181
|
+
}
|
|
182
|
+
function messageToRow(conversationId, position, m) {
|
|
183
|
+
return {
|
|
184
|
+
conversationId,
|
|
185
|
+
position,
|
|
186
|
+
role: m.role,
|
|
187
|
+
content: JSON.stringify(m.content),
|
|
188
|
+
toolCallId: m.toolCallId ?? null,
|
|
189
|
+
toolCalls: m.toolCalls ? JSON.stringify(m.toolCalls) : null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function rowToMessage(row) {
|
|
193
|
+
const out = {
|
|
194
|
+
role: row.role,
|
|
195
|
+
content: JSON.parse(row.content),
|
|
196
|
+
};
|
|
197
|
+
if (row.toolCallId != null)
|
|
198
|
+
out.toolCallId = row.toolCallId;
|
|
199
|
+
if (row.toolCalls != null)
|
|
200
|
+
out.toolCalls = JSON.parse(row.toolCalls);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
function rowToListEntry(row) {
|
|
204
|
+
const out = {
|
|
205
|
+
id: row.id,
|
|
206
|
+
title: row.title,
|
|
207
|
+
createdAt: row.createdAt,
|
|
208
|
+
};
|
|
209
|
+
if (row.updatedAt != null)
|
|
210
|
+
out.updatedAt = row.updatedAt;
|
|
211
|
+
if (row.agent != null)
|
|
212
|
+
out.agent = row.agent;
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
5
215
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {};
|
|
2
2
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":""}
|