saico 2.2.2 → 2.3.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/context.js +3 -1181
- package/dynamo.js +227 -0
- package/index.js +7 -1
- package/itask.js +5 -1
- package/msgs.js +1213 -0
- package/package.json +14 -1
- package/saico.js +345 -0
- package/sid.js +1 -1
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
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const Itask = require('./itask.js');
|
|
22
|
-
const { Context, createContext } = require('./
|
|
22
|
+
const { Context, createContext } = require('./msgs.js');
|
|
23
23
|
const { Sid, createSid } = require('./sid.js');
|
|
24
24
|
const { Store, DynamoBackend } = require('./store.js');
|
|
25
|
+
const { Saico } = require('./saico.js');
|
|
26
|
+
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
25
27
|
|
|
26
28
|
// Wire up Context class reference in Itask to avoid circular dependency
|
|
27
29
|
Itask.Context = Context;
|
|
@@ -135,6 +137,10 @@ function createQ(prompt, parent, tag, token_limit, msgs, tool_handler, config =
|
|
|
135
137
|
|
|
136
138
|
// Export all components
|
|
137
139
|
module.exports = {
|
|
140
|
+
// Master class (external users extend this)
|
|
141
|
+
Saico,
|
|
142
|
+
DynamoDBAdapter,
|
|
143
|
+
|
|
138
144
|
// Core classes
|
|
139
145
|
Itask,
|
|
140
146
|
Context,
|
package/itask.js
CHANGED
|
@@ -78,7 +78,7 @@ function Itask(opt, states){
|
|
|
78
78
|
|
|
79
79
|
EventEmitter.call(this);
|
|
80
80
|
opt = opt || {};
|
|
81
|
-
this.id = makeId(10);
|
|
81
|
+
this.id = opt.id || makeId(10);
|
|
82
82
|
this.name = opt.name;
|
|
83
83
|
this.cancelable = !!opt.cancel;
|
|
84
84
|
this.info = opt.info || {};
|
|
@@ -746,6 +746,10 @@ Itask.prototype.closeContext = async function closeContext(){
|
|
|
746
746
|
await this.context.close();
|
|
747
747
|
};
|
|
748
748
|
|
|
749
|
+
// Overridable: contextless tasks can provide a state summary that bubbles up
|
|
750
|
+
// into the nearest ancestor context's _getStateSummary().
|
|
751
|
+
Itask.prototype.getStateSummary = function getStateSummary(){ return ''; };
|
|
752
|
+
|
|
749
753
|
// Reference to Context class (set by index.js to avoid circular dependency)
|
|
750
754
|
Itask.Context = null;
|
|
751
755
|
|