@vida-global/apps-tools 1.0.1

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.
@@ -0,0 +1,345 @@
1
+ const _ = require('lodash');
2
+ const { logger } = require('@vida-global/core')
3
+ const { IllegalAppInvocationError } = require('../errors/illegalAppInvocationError');
4
+
5
+
6
+ class VidaApp {
7
+ functionHandler = null
8
+ hooksHandler = null
9
+ eventsHandler = null
10
+
11
+ constructor(
12
+ {
13
+ appId, appVersion, appManifest,
14
+ userContext,
15
+ providerManager,
16
+ }
17
+ ) {
18
+ this.appId = appId
19
+ this.appVersion = appVersion
20
+ this.manifest = appManifest
21
+
22
+ this.userContext = userContext
23
+
24
+ // this.manifest.installed = this.manifest.installed === undefined ? false : this.manifest.installed
25
+ // this.manifest.active = this.manifest.active === undefined ? false : this.manifest.active
26
+
27
+ this.isServer = true
28
+
29
+ this.providerManager = providerManager
30
+ this.providers = {}
31
+
32
+ this.functions = {}
33
+ this.hooks = {}
34
+ this.events = {}
35
+ }
36
+
37
+ id() {
38
+ return this.appId
39
+ }
40
+
41
+ version() {
42
+ return this.appVersion
43
+ }
44
+
45
+ context() {
46
+ return this.userContext
47
+ }
48
+
49
+ static async get(
50
+ appClass,
51
+ {
52
+ appId, appVersion, appManifest,
53
+ userContext,
54
+ providerManager,
55
+ },
56
+ {isServer = true}
57
+ ) {
58
+ const app = new appClass({
59
+ appId, appVersion, appManifest,
60
+ userContext,
61
+ providerManager,
62
+ })
63
+ await app._init({ isServer})
64
+ return app
65
+ }
66
+
67
+ async _init ({ isServer }) {
68
+ this.isServer = isServer
69
+
70
+ this.manifest.installed = await this.userContext.isAppInstalled(this.appId, this.appVersion)
71
+
72
+ await this.buildImplementationList()
73
+ await this.buildProviders()
74
+
75
+ if (!this.isServer) {
76
+ await this.checkActive()
77
+ }
78
+
79
+ if (this.isServer) {
80
+ if (this.functionHandler) {
81
+ this._functionHandler = new this.functionHandler(this)
82
+ }
83
+ if (this.hooksHandler) {
84
+ this._hooksHandler = new this.hooksHandler(this)
85
+ }
86
+ if (this.eventsHandler) {
87
+ this._eventsHandler = new this.eventsHandler(this)
88
+ }
89
+ }
90
+
91
+ try {
92
+ await this.initApp()
93
+ } catch (error) {
94
+ console.warn('Potential unhandled error in app init: ', error, this.appId, this.appVersion)
95
+ }
96
+ }
97
+
98
+ async initApp() {
99
+ // throw new Error('Not implemented')
100
+ }
101
+
102
+ isFunctionInApp(functionName) {
103
+ return !!this.functions[functionName]
104
+ }
105
+
106
+ isHookInApp(hookName) {
107
+ return !!this.hooks[hookName]
108
+ }
109
+
110
+ isEventInApp(eventName) {
111
+ return !!this.events[eventName]
112
+ }
113
+
114
+ async handleFunctionCall(functionName, functionArgs) {
115
+ if (!this.isServer) {
116
+ throw new IllegalAppInvocationError("Illegal invocation of handleFunctionCall method: this operation is not allowed in isServer mode.")
117
+ }
118
+ if (!(this.isFunctionInApp(functionName))) {
119
+ throw new Error(`Function "${functionName}" is not defined in the manifest.`);
120
+ }
121
+ if (!this._functionHandler) {
122
+ throw new Error(`No function handler defined in app ${this.appId}`)
123
+ }
124
+ logger.debug('Function call received', functionName, functionArgs)
125
+
126
+ const startTime = Date.now();
127
+ let result = await this._functionHandler[functionName](functionArgs);
128
+ result.functionName = functionName;
129
+ result.functionArgs = functionArgs;
130
+ result.responseTime = Date.now() - startTime;
131
+
132
+ return result
133
+ }
134
+
135
+ async handleHookCall(hookName, hookArgs) {
136
+ if (!this.isServer) {
137
+ throw new IllegalAppInvocationError("Illegal invocation of handleHookCall method: this operation is not allowed in isServer mode.")
138
+ }
139
+ if (!(this.isHookInApp(hookName))) {
140
+ throw new Error(`Hook "${hookName}" is not defined in the manifest.`);
141
+ }
142
+ if (!this._hooksHandler) {
143
+ throw new Error(`No hook handler defined in the app ${this.appId}`)
144
+ }
145
+ logger.debug('Hook call received', hookName, hookArgs)
146
+
147
+ const startTime = Date.now();
148
+ let result = await this._hooksHandler[hookName](hookArgs)
149
+ result.responseTime = Date.now() - startTime;
150
+
151
+ return result
152
+ }
153
+
154
+ async handleEvent(eventName, eventData) {
155
+ if (!this.isServer) {
156
+ throw new IllegalAppInvocationError("Illegal invocation of handleEvent method: this operation is not allowed in isServer mode.")
157
+ }
158
+ if (!(this.isEventInApp(eventName))) {
159
+ throw new Error(`Event "${eventName}" is not defined in the manifest.`);
160
+ }
161
+ if (!this._eventsHandler) {
162
+ throw new Error(`No event handler defined in the app ${this.appId}`)
163
+ }
164
+ logger.debug('Hook call received', eventName, eventData)
165
+
166
+ const startTime = Date.now();
167
+ let result = await this._eventsHandler[eventName](eventData)
168
+ result.responseTime = Date.now() - startTime;
169
+ return result
170
+ }
171
+
172
+ addProvider(providerType, providerName, providerInstance) {
173
+ if (!this.providers[providerType]) {
174
+ this.providers[providerType] = {}
175
+ }
176
+ this.providers[providerType][providerName] = providerInstance
177
+ }
178
+
179
+ async getProviderManifest(providerType, providerName) {
180
+ return await this.providers[providerType][providerName].getManifest()
181
+ }
182
+
183
+ async getCompositeManifest() {
184
+ logger.info(`Building composite manifest ${this.appId} ${this.appVersion}`)
185
+ await this.checkActive()
186
+ let compositeManifest = {...this.manifest}
187
+ const providerManifests = this.manifest.dependencies.providers
188
+ for (const [providerType, providers] of Object.entries(providerManifests)) {
189
+ for (const [providerName, vanillaProviderManifest] of Object.entries(providers)) {
190
+ const installedProviderManifest = await this.getProviderManifest(providerType, providerName)
191
+ compositeManifest.dependencies.providers[providerType][providerName] = _.merge(
192
+ _.cloneDeep(vanillaProviderManifest), installedProviderManifest
193
+ )
194
+ }
195
+ }
196
+ return compositeManifest
197
+ }
198
+
199
+ async serialize(){
200
+ return await this.getCompositeManifest()
201
+ }
202
+
203
+ async buildProviders(update= false) {
204
+ const providerManifest = this.manifest.dependencies.providers
205
+ for (const [providerType, providers] of Object.entries(providerManifest)) {
206
+ for (const [providerName, providerConfig] of Object.entries(providers)) {
207
+ const provider = await this.providerManager.buildProvider(
208
+ providerType, providerName, this.userContext, providerConfig,
209
+ this.id(), this.version()
210
+ )
211
+ if (update) {
212
+ provider.update(providerConfig)
213
+ }
214
+ this.addProvider(providerType, providerName, provider)
215
+ }
216
+ }
217
+ }
218
+
219
+ buildImplementationList() {
220
+ if (this.manifest) {
221
+ if (!this.manifest.implementations) {
222
+ this.manifest.implementations = {}
223
+ }
224
+
225
+ if (this.manifest.implementations.functions) {
226
+ for (const func of this.manifest.implementations.functions) {
227
+ this.functions[func.name] = func
228
+ }
229
+ }
230
+
231
+ if (this.manifest.implementations.hooks) {
232
+ for (const hook of this.manifest.implementations.hooks) {
233
+ this.hooks[hook.name] = hook
234
+ }
235
+ }
236
+
237
+ if (this.manifest.implementations.events) {
238
+ for (const event of this.manifest.implementations.events) {
239
+ this.events[event.name] = event
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ async update(manifest) {
246
+ return await this.install(manifest, true)
247
+ }
248
+
249
+ async reload(manifest) {
250
+ return await this.install(manifest, true)
251
+ }
252
+
253
+ async checkActive() {
254
+ this.manifest.active = true
255
+ for (const providers of Object.values(this.providers)) {
256
+ for (const providerInstance of Object.values(providers)) {
257
+ // Perform desired operations on each providerInstance
258
+ const isProviderConfigured = await providerInstance.isConfigured()
259
+ this.manifest.active = this.manifest.active && isProviderConfigured
260
+ }
261
+ }
262
+ }
263
+
264
+ async install(manifest, update = false) {
265
+ if (this.manifest.installed && !update) {
266
+ throw new Error('App is already installed')
267
+ }
268
+
269
+ this.manifest = manifest
270
+ this.manifest.installed = true
271
+ await this.buildImplementationList()
272
+ await this.buildProviders(true)
273
+ await this.checkActive()
274
+
275
+ if (!update) {
276
+ this.userContext.installApp(this.appId, this.appVersion)
277
+ }
278
+
279
+ return this.manifest
280
+ }
281
+
282
+ async getFunctions(onlyEnabled = false) {
283
+ let functions = (await this.getCompositeManifest()).implementations.functions;
284
+ if (onlyEnabled) {
285
+ const enabledFunctions = await this.userContext.getEnabledFunctions(this.appId, this.appVersion);
286
+ return functions.filter(func => {
287
+ return enabledFunctions.some(enabledFunc => enabledFunc === func.name);
288
+ })
289
+ } else {
290
+ return functions;
291
+ }
292
+ }
293
+
294
+ async getHooks() {
295
+ return this.manifest.implementations.hooks || []
296
+ }
297
+
298
+ async getAgentInstructions() {
299
+ const agent = this.userContext.get('agent')
300
+ const agentApps = agent.apps || []
301
+ const agentApp = _.find(agentApps, (app) => {
302
+ return app.appId === this.id() && app.version === this.version()
303
+ })
304
+ if (agentApp && agentApp.instructions) {
305
+ return agentApp.instructions
306
+ } else {
307
+ return this.manifest.description
308
+ }
309
+ }
310
+
311
+ isInstalled() {
312
+ if (this.isServer) {
313
+ throw new IllegalAppInvocationError("Illegal invocation of isInstalled method: this operation is not allowed in isServer mode.")
314
+ }
315
+ return this.manifest.installed
316
+ }
317
+
318
+ isActive() {
319
+ if (this.isServer) {
320
+ throw new IllegalAppInvocationError("Illegal invocation of isActive method: this operation is not allowed in isServer mode.")
321
+ }
322
+ return this.manifest.active
323
+ }
324
+
325
+ async uninstall() {
326
+ if (this.isServer) {
327
+ throw new IllegalAppInvocationError("Illegal invocation of uninstall method: this operation is not allowed in isServer mode.")
328
+ }
329
+
330
+ this.manifest.installed = false
331
+ this.manifest.active = false
332
+
333
+ // Uninstall all the user context and settings
334
+ await this.userContext.uninstallApp(this.appId, this.appVersion)
335
+
336
+ for (const providers of Object.values(this.providers)) {
337
+ for (const providerInstance of Object.values(providers)) {
338
+ await providerInstance.uninstall()
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+
345
+ module.exports = { VidaApp }
@@ -0,0 +1,8 @@
1
+ class IllegalAppInvocationError extends Error {
2
+ constructor(message) {
3
+ super(message)
4
+ }
5
+ }
6
+
7
+
8
+ module.exports = { IllegalAppInvocationError };
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ openapi: "3.0.0", // OpenAPI version
3
+ info: {
4
+ title: "VADER Endpoints",
5
+ description: "This documents the endpoints exposed by VADER and the configured applications.",
6
+ version: "1.0.0"
7
+ },
8
+ servers: [],
9
+ paths: {}
10
+ }
@@ -0,0 +1,150 @@
1
+ const _ = require("lodash")
2
+ const baseApiSpec = require('./baseApiSpec')
3
+
4
+ function generateOpenApiSpecPaths(appId, version, functions, hooks) {
5
+ const paths = {}
6
+ const contextSpecs = {
7
+ type: "object",
8
+ properties: {
9
+ user: {
10
+ type: "object",
11
+ description: "Agent user details",
12
+ example: {
13
+ id: 100,
14
+ username: "johndoe",
15
+ }
16
+ },
17
+ agent: {
18
+ type: "object",
19
+ description: "Agent configuration data",
20
+ example: {
21
+ id: 200,
22
+ name: "Cool Agent",
23
+ }
24
+ },
25
+ caller: {
26
+ type: "object",
27
+ description: "Caller data",
28
+ example: {
29
+ id: 300,
30
+ name: "Cool Caller",
31
+ number: "+1234567890",
32
+ }
33
+ },
34
+ meta: {
35
+ type: "object",
36
+ description: "Convo meta",
37
+ example: {
38
+ callId: "123asd456",
39
+ convoUUID: "74c28825-aed9-45a6-8534-2177f2a66993 ",
40
+ }
41
+ },
42
+ manifest: {
43
+ type: "object",
44
+ description: "App manifest",
45
+ example: null
46
+ },
47
+ },
48
+ required: ["user", "agent", "caller"],
49
+ }
50
+ const responseSpecs = {
51
+ "200": {
52
+ description: "Successful response",
53
+ content: {
54
+ "application/json": {
55
+ schema: {
56
+ type: "object",
57
+ additionalProperties: true,
58
+ },
59
+ },
60
+ },
61
+ },
62
+ "400": {
63
+ description: "Bad request",
64
+ },
65
+ "500": {
66
+ description: "Server error",
67
+ },
68
+ }
69
+
70
+ functions.forEach((func) => {
71
+ const functionName = func.name
72
+ const endpoint = `/invoke/app/function/${appId}/${version}/${functionName}`
73
+
74
+ paths[endpoint] = {
75
+ post: {
76
+ summary: func.description,
77
+ tags: [`${appId}/${version}`],
78
+ operationId: `${functionName}Operation`,
79
+ requestBody: {
80
+ required: true,
81
+ content: {
82
+ "application/json": {
83
+ schema: {
84
+ type: "object",
85
+ properties: {
86
+ userContext: contextSpecs,
87
+ functionArgs: func.parameters,
88
+ },
89
+ required: ['context', 'params'],
90
+ },
91
+ },
92
+ },
93
+ },
94
+ responses: responseSpecs,
95
+ },
96
+ }
97
+ })
98
+
99
+ hooks.forEach((hook) => {
100
+ const hookName = hook.name
101
+ const endpoint = `/invoke/app/hook/${appId}/${version}/${hookName}`
102
+
103
+ paths[endpoint] = {
104
+ post: {
105
+ summary: hook.description,
106
+ tags: [`${appId}/${version}`],
107
+ operationId: `${hookName}Operation`,
108
+ requestBody: {
109
+ required: true,
110
+ content: {
111
+ 'application/json': {
112
+ schema: {
113
+ type: 'object',
114
+ properties: {
115
+ userContext: contextSpecs,
116
+ parameters: hook.parameters,
117
+ }
118
+ }
119
+ }
120
+ }
121
+ },
122
+ responses: responseSpecs,
123
+ }
124
+ }
125
+ })
126
+
127
+ return paths
128
+ }
129
+
130
+
131
+ async function generateOpenApiSpec(appManager) {
132
+ let paths = {}
133
+ for (const {id, version} of appManager.apps) {
134
+ const manifest = await appManager.fetchVanillaAppManifest(id, version)
135
+ const appPaths = generateOpenApiSpecPaths(id,
136
+ version,
137
+ manifest.implementations.functions || [],
138
+ manifest.implementations.hooks || []);
139
+ paths = {...paths, ...appPaths}
140
+ }
141
+ const apiSpec = _.cloneDeep(baseApiSpec)
142
+ apiSpec.paths = paths
143
+ return apiSpec
144
+ }
145
+
146
+
147
+ module.exports = {
148
+ generateOpenApiSpec
149
+ }
150
+
@@ -0,0 +1,141 @@
1
+ const _ = require('lodash');
2
+ const { logger } = require('@vida-global/core');
3
+
4
+
5
+ class BaseProviderConfigurator {
6
+ constructor(userContext, redisClient, type, name,
7
+ globalProviderConfig, appId, appVersion) {
8
+ this.userContext = userContext;
9
+ this.redis = redisClient;
10
+ this.type = type;
11
+ this.name = name;
12
+ this.globalConfig = globalProviderConfig;
13
+ this.appId = appId;
14
+ this.appVersion = appVersion;
15
+ this.hasBuiltRoutes = false;
16
+ }
17
+
18
+ async _initialize() {
19
+ await this.initialize();
20
+ }
21
+
22
+ async initialize() {
23
+ throw Error("Not implemented")
24
+ }
25
+
26
+ buildRoutes (expressApp) {
27
+ logger.info('No routes to be built for base configurator')
28
+ }
29
+ }
30
+
31
+
32
+ class BaseProvider {
33
+ type = null
34
+ name = 'sample-provider'
35
+ configurator = BaseProviderConfigurator
36
+
37
+ PROVIDER_GLOBAL_CONFIG_KEY () {
38
+ return `vader:providers:${this.type}:${this.name}:config`
39
+ }
40
+
41
+ constructor(
42
+ userContext, redisClient, vanillaConfig = null,
43
+ appId = null, appVersion = null,
44
+ isServer = false
45
+ ) {
46
+ this.userContext = userContext;
47
+ this.redis = redisClient;
48
+ this.isServer = isServer;
49
+ this.config = vanillaConfig || {};
50
+ this.appId = appId;
51
+ this.appVersion = appVersion;
52
+ this.globalConfig = {};
53
+ }
54
+
55
+ static async get(providerCls, {
56
+ userContext, redisClient, vanillaConfig,
57
+ appId, appVersion,
58
+ isServer = false
59
+ }) {
60
+ const provider = new providerCls(
61
+ userContext, redisClient, vanillaConfig,
62
+ appId, appVersion,
63
+ isServer
64
+ );
65
+ await provider._initialize();
66
+ return provider
67
+ }
68
+
69
+ async getConfigurator() {
70
+ const configurator = new this.configurator(
71
+ this.userContext, this.redis, this.type, this.name,
72
+ this.globalConfig,
73
+ this.appId, this.appVersion,
74
+ )
75
+ await configurator._initialize()
76
+ return configurator
77
+ }
78
+
79
+ async _initialize() {
80
+ if (!this.isServer) {
81
+ // Read global config and add it to config
82
+ let globalConfig = await this.redis.get(
83
+ this.PROVIDER_GLOBAL_CONFIG_KEY()
84
+ )
85
+ globalConfig = globalConfig ? JSON.parse(globalConfig) : {}
86
+ this.globalConfig = globalConfig
87
+
88
+ if (this.userContext) {
89
+ let userProviderConfig = await this.userContext.getProviderConfig(
90
+ this.type, this.name, this.appId, this.appVersion
91
+ ) || {}
92
+ this.config = _.merge(_.cloneDeep(this.config), userProviderConfig)
93
+ }
94
+ } else {
95
+ if (this.userContext) {
96
+ let userProviderConfig = await this.userContext.getProviderConfig(
97
+ this.type, this.name, this.appId, this.appVersion
98
+ ) || {}
99
+ this.config = _.merge(_.cloneDeep(this.config), userProviderConfig)
100
+ }
101
+ }
102
+ await this.initialize();
103
+ }
104
+
105
+ async getProperty (name) {
106
+ return this.config.parameters.properties[name].value;
107
+ }
108
+
109
+ async saveConfig(config) {
110
+ await this.userContext.setProviderConfig(
111
+ this.type, this.name, config,
112
+ this.appId, this.appVersion
113
+ )
114
+ }
115
+
116
+ async update(config) {
117
+ throw Error("Not implemented")
118
+ }
119
+
120
+ async isConfigured() {
121
+ throw Error("Not implemented")
122
+ }
123
+
124
+ async getManifest() {
125
+ throw Error("Not implemented")
126
+ }
127
+
128
+ async initialize() {
129
+ throw new Error("Not implemented");
130
+ }
131
+
132
+ async uninstall () {
133
+ throw new Error("Not implemented");
134
+ }
135
+ }
136
+
137
+
138
+ module.exports = {
139
+ BaseProviderConfigurator,
140
+ BaseProvider
141
+ }