saico 2.2.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -300
- package/context.js +3 -1211
- package/dynamo.js +227 -0
- package/index.js +8 -5
- package/itask.js +16 -3
- package/msgs.js +1167 -0
- package/package.json +14 -2
- package/saico.js +617 -0
- package/sid.js +0 -248
package/dynamo.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DynamoDBAdapter — Generic DynamoDB access layer.
|
|
5
|
+
*
|
|
6
|
+
* Generalized from ../backend/aws.js. Provides CRUD, update, list-append,
|
|
7
|
+
* counter, and scan operations. All methods accept an optional `table`
|
|
8
|
+
* parameter that defaults to `this.defaultTable`.
|
|
9
|
+
*
|
|
10
|
+
* AWS SDK v3 packages are required only when this module is loaded.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { DynamoDBClient, UpdateItemCommand, QueryCommand, DeleteItemCommand,
|
|
14
|
+
GetItemCommand, PutItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');
|
|
15
|
+
const { DynamoDBDocumentClient, UpdateCommand } = require('@aws-sdk/lib-dynamodb');
|
|
16
|
+
const { unmarshall, marshall } = require('@aws-sdk/util-dynamodb');
|
|
17
|
+
|
|
18
|
+
class DynamoDBAdapter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} opt
|
|
21
|
+
* @param {string} opt.table - Default table name for all operations
|
|
22
|
+
* @param {string} [opt.region='us-east-1'] - AWS region
|
|
23
|
+
* @param {DynamoDBClient} [opt.client] - Injectable DynamoDB client (for testing)
|
|
24
|
+
*/
|
|
25
|
+
constructor(opt = {}) {
|
|
26
|
+
this.defaultTable = opt.table || null;
|
|
27
|
+
this._region = opt.region || 'us-east-1';
|
|
28
|
+
this._client = opt.client || new DynamoDBClient({ region: this._region });
|
|
29
|
+
this.__docClient = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get _docClient() {
|
|
33
|
+
if (!this.__docClient)
|
|
34
|
+
this.__docClient = DynamoDBDocumentClient.from(this._client);
|
|
35
|
+
return this.__docClient;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_table(table) {
|
|
39
|
+
const t = table || this.defaultTable;
|
|
40
|
+
if (!t) throw new Error('DynamoDBAdapter: no table specified');
|
|
41
|
+
return t;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_unmarshall(item) {
|
|
45
|
+
if (!item) return undefined;
|
|
46
|
+
if (Array.isArray(item))
|
|
47
|
+
return item.map(i => unmarshall(i));
|
|
48
|
+
return unmarshall(item);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_removeUndefined(obj) {
|
|
52
|
+
return JSON.parse(JSON.stringify(obj));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- Core CRUD ----
|
|
56
|
+
|
|
57
|
+
async put(item, table) {
|
|
58
|
+
const params = {
|
|
59
|
+
TableName: this._table(table),
|
|
60
|
+
Item: marshall(item, { removeUndefinedValues: true, convertClassInstanceToMap: true }),
|
|
61
|
+
};
|
|
62
|
+
await this._client.send(new PutItemCommand(params));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async get(key, value, table) {
|
|
66
|
+
const _key = {};
|
|
67
|
+
_key[key] = { S: value };
|
|
68
|
+
const params = {
|
|
69
|
+
TableName: this._table(table),
|
|
70
|
+
Key: _key,
|
|
71
|
+
};
|
|
72
|
+
const data = await this._client.send(new GetItemCommand(params));
|
|
73
|
+
return this._unmarshall(data.Item);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(key, value, table) {
|
|
77
|
+
const _key = {};
|
|
78
|
+
_key[key] = { S: value };
|
|
79
|
+
const params = {
|
|
80
|
+
TableName: this._table(table),
|
|
81
|
+
Key: _key,
|
|
82
|
+
};
|
|
83
|
+
const data = await this._client.send(new DeleteItemCommand(params));
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async query(index, key, value, table) {
|
|
88
|
+
const params = {
|
|
89
|
+
TableName: this._table(table),
|
|
90
|
+
IndexName: index,
|
|
91
|
+
KeyConditionExpression: '#k = :key',
|
|
92
|
+
ExpressionAttributeNames: { '#k': key },
|
|
93
|
+
ExpressionAttributeValues: { ':key': { S: value } },
|
|
94
|
+
};
|
|
95
|
+
const data = await this._client.send(new QueryCommand(params));
|
|
96
|
+
return this._unmarshall(data.Items);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getAll(table) {
|
|
100
|
+
const params = {
|
|
101
|
+
TableName: this._table(table),
|
|
102
|
+
};
|
|
103
|
+
const response = await this._client.send(new ScanCommand(params));
|
|
104
|
+
if (!response.Items || response.Items.length === 0)
|
|
105
|
+
return [];
|
|
106
|
+
return this._unmarshall(response.Items);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Update operations ----
|
|
110
|
+
|
|
111
|
+
async update(key, keyValue, setKey, item, table) {
|
|
112
|
+
return this.updatePath(key, keyValue, [], setKey, item, table);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async updatePath(key, keyValue, path, setKey, item, table) {
|
|
116
|
+
const _key = {};
|
|
117
|
+
_key[key] = keyValue;
|
|
118
|
+
path = path || [];
|
|
119
|
+
const res = await this._getItemPath(key, keyValue, path, table);
|
|
120
|
+
const sanitizedItem = this._removeUndefined(item);
|
|
121
|
+
const params = {
|
|
122
|
+
TableName: this._table(table),
|
|
123
|
+
Key: _key,
|
|
124
|
+
UpdateExpression: `SET ${res.path}#k = :e`,
|
|
125
|
+
ExpressionAttributeNames: { '#k': setKey.replace('.', '') },
|
|
126
|
+
ExpressionAttributeValues: { ':e': sanitizedItem },
|
|
127
|
+
ReturnValues: 'UPDATED_NEW',
|
|
128
|
+
};
|
|
129
|
+
return await this._docClient.send(new UpdateCommand(params));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async listAppend(key, keyValue, setKey, item, table) {
|
|
133
|
+
return this.listAppendPath(key, keyValue, [], setKey, item, table);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listAppendPath(key, keyValue, path, setKey, item, table) {
|
|
137
|
+
const _key = {};
|
|
138
|
+
_key[key] = keyValue;
|
|
139
|
+
path = path || [];
|
|
140
|
+
const res = await this._getItemPath(key, keyValue, path, table);
|
|
141
|
+
const sanitizedItem = this._removeUndefined(item);
|
|
142
|
+
const params = {
|
|
143
|
+
TableName: this._table(table),
|
|
144
|
+
Key: _key,
|
|
145
|
+
UpdateExpression:
|
|
146
|
+
`SET ${res.path}${setKey} = list_append(if_not_exists(${res.path}${setKey}, :emptyList), :e)`,
|
|
147
|
+
ConditionExpression: 'NOT contains(myList, :check_item)',
|
|
148
|
+
ExpressionAttributeValues: {
|
|
149
|
+
':emptyList': [],
|
|
150
|
+
':e': [sanitizedItem],
|
|
151
|
+
':check_item': sanitizedItem,
|
|
152
|
+
},
|
|
153
|
+
ReturnValues: 'UPDATED_NEW',
|
|
154
|
+
};
|
|
155
|
+
return await this._docClient.send(new UpdateCommand(params));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- Counter operations ----
|
|
159
|
+
|
|
160
|
+
async nextCounterId(counter, table) {
|
|
161
|
+
const params = {
|
|
162
|
+
TableName: table || 'counters',
|
|
163
|
+
Key: { CounterName: { S: counter } },
|
|
164
|
+
UpdateExpression: 'SET #val = if_not_exists(#val, :zero) + :incr',
|
|
165
|
+
ExpressionAttributeNames: { '#val': 'CounterValue' },
|
|
166
|
+
ExpressionAttributeValues: { ':incr': { N: '1' }, ':zero': { N: '0' } },
|
|
167
|
+
ReturnValues: 'UPDATED_NEW',
|
|
168
|
+
};
|
|
169
|
+
const result = await this._client.send(new UpdateItemCommand(params));
|
|
170
|
+
return String(result.Attributes.CounterValue.N);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getCounterValue(counter, table) {
|
|
174
|
+
const item = await this.get('CounterName', counter, table || 'counters');
|
|
175
|
+
if (!item) {
|
|
176
|
+
await this.put({ CounterName: counter, CounterValue: 0 }, table || 'counters');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
return Number(item.CounterValue || 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async setCounterValue(counter, value, table) {
|
|
183
|
+
await this.put(
|
|
184
|
+
{ CounterName: counter, CounterValue: Number(value) || 0 },
|
|
185
|
+
table || 'counters'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---- Utility ----
|
|
190
|
+
|
|
191
|
+
async countItems(table) {
|
|
192
|
+
let total = 0;
|
|
193
|
+
let lastEvaluatedKey;
|
|
194
|
+
do {
|
|
195
|
+
const params = {
|
|
196
|
+
TableName: this._table(table),
|
|
197
|
+
Select: 'COUNT',
|
|
198
|
+
ExclusiveStartKey: lastEvaluatedKey,
|
|
199
|
+
};
|
|
200
|
+
const response = await this._client.send(new ScanCommand(params));
|
|
201
|
+
total += response.Count || 0;
|
|
202
|
+
lastEvaluatedKey = response.LastEvaluatedKey;
|
|
203
|
+
} while (lastEvaluatedKey);
|
|
204
|
+
return total;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- Internal helpers ----
|
|
208
|
+
|
|
209
|
+
async _getItemPath(key, keyValue, path, table) {
|
|
210
|
+
let item = await this.get(key, keyValue, table);
|
|
211
|
+
let indexedPath = '';
|
|
212
|
+
for (const p of path) {
|
|
213
|
+
item = item[p.key];
|
|
214
|
+
indexedPath += p.key + (!p.skey ? '.' : '');
|
|
215
|
+
if (!p.skey)
|
|
216
|
+
continue;
|
|
217
|
+
const idx = item.findIndex(b => b[p.skey] == p.svalue);
|
|
218
|
+
if (idx < 0)
|
|
219
|
+
throw new Error('_getItemPath: cannot find item with key ' + p.skey + '=' + p.svalue);
|
|
220
|
+
item = item[idx];
|
|
221
|
+
indexedPath += '[' + idx + '].';
|
|
222
|
+
}
|
|
223
|
+
return { item, path: indexedPath };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { DynamoDBAdapter };
|
package/index.js
CHANGED
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
* - Storage persistence (Redis cache + optional DB backend)
|
|
13
13
|
*
|
|
14
14
|
* Main Components:
|
|
15
|
+
* - Saico: Master class (external users extend this)
|
|
15
16
|
* - Itask: Base task class for all tasks (supports states, cancellation, promises)
|
|
16
17
|
* - Context: Conversation context with message handling and tool calls
|
|
17
|
-
* - Sid: Session root task (extends Itask, always has a context)
|
|
18
18
|
* - Store: Storage abstraction layer (Redis + optional backends like DynamoDB)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const Itask = require('./itask.js');
|
|
22
|
-
const { Context, createContext } = require('./
|
|
23
|
-
const { Sid, createSid } = require('./sid.js');
|
|
22
|
+
const { Context, createContext } = require('./msgs.js');
|
|
24
23
|
const { Store, DynamoBackend } = require('./store.js');
|
|
24
|
+
const { Saico } = require('./saico.js');
|
|
25
|
+
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
25
26
|
|
|
26
27
|
// Wire up Context class reference in Itask to avoid circular dependency
|
|
27
28
|
Itask.Context = Context;
|
|
@@ -135,10 +136,13 @@ function createQ(prompt, parent, tag, token_limit, msgs, tool_handler, config =
|
|
|
135
136
|
|
|
136
137
|
// Export all components
|
|
137
138
|
module.exports = {
|
|
139
|
+
// Master class (external users extend this)
|
|
140
|
+
Saico,
|
|
141
|
+
DynamoDBAdapter,
|
|
142
|
+
|
|
138
143
|
// Core classes
|
|
139
144
|
Itask,
|
|
140
145
|
Context,
|
|
141
|
-
Sid,
|
|
142
146
|
Store,
|
|
143
147
|
DynamoBackend,
|
|
144
148
|
|
|
@@ -147,7 +151,6 @@ module.exports = {
|
|
|
147
151
|
|
|
148
152
|
// Factory functions
|
|
149
153
|
createTask,
|
|
150
|
-
createSid,
|
|
151
154
|
createContext,
|
|
152
155
|
|
|
153
156
|
// Legacy compatibility
|
package/itask.js
CHANGED
|
@@ -746,9 +746,22 @@ Itask.prototype.closeContext = async function closeContext(){
|
|
|
746
746
|
await this.context.close();
|
|
747
747
|
};
|
|
748
748
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
749
|
+
// Walk DOWN to find the deepest active descendant with a context
|
|
750
|
+
Itask.prototype.findDeepestContext = function findDeepestContext() {
|
|
751
|
+
let deepest = this.context ? { context: this.context, depth: 0 } : null;
|
|
752
|
+
const search = (task, depth) => {
|
|
753
|
+
for (const child of task.child) {
|
|
754
|
+
if (child._completed) continue;
|
|
755
|
+
if (child.context) {
|
|
756
|
+
if (!deepest || depth + 1 >= deepest.depth)
|
|
757
|
+
deepest = { context: child.context, depth: depth + 1 };
|
|
758
|
+
}
|
|
759
|
+
search(child, depth + 1);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
search(this, 0);
|
|
763
|
+
return deepest ? deepest.context : null;
|
|
764
|
+
};
|
|
752
765
|
|
|
753
766
|
// Reference to Context class (set by index.js to avoid circular dependency)
|
|
754
767
|
Itask.Context = null;
|