From 478fef5dbad38a2c89c19a94ff9fc6c6948aa489 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 26 Jan 2024 12:08:22 -0500 Subject: [PATCH 1/6] 20240126 @Mookse - yaml structures for multiple proxy-bots on openai --- inc/yaml/README.md | 27 ++++++++++++++++++++++ openai.yaml => inc/yaml/mylife_openai.yaml | 0 2 files changed, 27 insertions(+) create mode 100644 inc/yaml/README.md rename openai.yaml => inc/yaml/mylife_openai.yaml (100%) diff --git a/inc/yaml/README.md b/inc/yaml/README.md new file mode 100644 index 0000000..64dd9b0 --- /dev/null +++ b/inc/yaml/README.md @@ -0,0 +1,27 @@ +# AI Agent Communication + +## Providers + +### OpenAI + +This folder contains `.yaml` files for communicating with AI agents at OpenAI using a token-bearing schema. Each `.yaml` file represents + +## Action Schematas + +The following action schematas are available for external bots: + +- `mylife_openai.yaml`: This is the original action that maps to the openai gpt [`MyLife`](https://chat.openai.com/g/g-rEjoOt9hN-mylife). +- `mylife_biog-bot_openai.yaml`: This action is associated with the abilities of the [`MyLife Biographer Bot`](https://chat.openai.com/g/g-QGzfgKj6I-mylife-biographer-bot) to identify user, and + +## Structure + +The folder structure is as follows: + +. +├── mylife_openai.yaml +├── mylife_biog-bot_openai.yaml +└── README.md + +## Versioning + +Currently schemas are using `openapi v.3.0.0`, and each `.yaml` will be individually versioned, actual changes maintained in git repository. diff --git a/openai.yaml b/inc/yaml/mylife_openai.yaml similarity index 100% rename from openai.yaml rename to inc/yaml/mylife_openai.yaml From 84704c3678a20508d6f419d0c5f704346a39a4c7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 26 Jan 2024 13:13:33 -0500 Subject: [PATCH 2/6] 20240126 @Mookse wip --- inc/yaml/mylife_biographer-bot_openai.yaml | 93 ++++++++++++++++++++++ inc/yaml/mylife_openai.yaml | 4 +- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 inc/yaml/mylife_biographer-bot_openai.yaml diff --git a/inc/yaml/mylife_biographer-bot_openai.yaml b/inc/yaml/mylife_biographer-bot_openai.yaml new file mode 100644 index 0000000..f2cb9e4 --- /dev/null +++ b/inc/yaml/mylife_biographer-bot_openai.yaml @@ -0,0 +1,93 @@ +openapi: 3.0.0 +info: + title: MyLife GPT Webhook Receiver API + description: This API is for receiving webhooks from [MyLife's public Biographer Bot instance](https://chat.openai.com/g/g-QGzfgKj6I-mylife-biographer-bot). + version: 1.0.0 +servers: + - url: https://4c87-73-149-210-124.ngrok-free.app/api/v1 + description: Endpoint for receiving stories from the MyLife Biographer Bot instance. +security: + - bearerAuth: [] +paths: + /story: + post: + operationId: MyLifeBiographerStoryCreation + summary: MyLife Biographer Bot will access this endpoint to generate a . + description: Endpoint for handling incoming registration webhook data from the MyLife GPT service. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - registrationInterests + - contact + properties: + registrationInterests: + type: array + default: + - information + minItems: 1 + maxItems: 4 + items: + type: string + enum: + - membership + - volunteer + - donate + - information + description: Interests in MyLife, defaults to information. + contact: + type: object + required: + - humanName + - email + properties: + avatarName: + type: string + description: The avatar name desired by the person registering. + humanName: + type: string + description: The name, can be nickname or short name, of the person registering. + humanDateOfBirth: + type: string + format: date + description: The date of birth of the person registering; could be as little as a year. + email: + type: string + description: The email of the person registering. + city: + type: string + description: The city of the person registering. + state: + type: string + description: The state of the person registering. + country: + type: string + description: The country of the person registering. + description: The registration data sent from the MyLife GPT instance. + personalInterests: + type: array + default: [] + minItems: 0 + maxItems: 5 + items: + type: string + enum: + - archivist + - humanist + - ethicist + - technologist + - futurist + description: The driving personal interests of the person registering. + additionalInfo: + type: string + maxLength: 1024 + description: Additional information sent from the MyLife GPT instance. + responses: + "200": + description: Webhook registration data received successfully. +post: + operationId: MyLifeBiographerStoryCreation + x-openai-isConsequential: false diff --git a/inc/yaml/mylife_openai.yaml b/inc/yaml/mylife_openai.yaml index 0bcb815..98b4b67 100644 --- a/inc/yaml/mylife_openai.yaml +++ b/inc/yaml/mylife_openai.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: MyLife GPT Webhook Receiver API - description: This API is for receiving webhooks from MyLife's public GPT instance found at [](). + description: This API is for receiving webhooks from [MyLife's public GPT instance](https://chat.openai.com/g/g-rEjoOt9hN-mylife). version: 1.0.0 servers: - url: https://humanremembranceproject.org/api/v1 @@ -89,5 +89,5 @@ paths: "200": description: Webhook registration data received successfully. post: - operationId: blah2 + operationId: MyLifeGPTRegistration x-openai-isConsequential: false From ca96de649886cc4ca5be3b8448e979d84152f70e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 26 Jan 2024 13:39:29 -0500 Subject: [PATCH 3/6] 20240126 @Mookse - GPT biographer bot `.yaml ` first draft --- inc/yaml/mylife_biographer-bot_openai.yaml | 81 +++++----------------- 1 file changed, 17 insertions(+), 64 deletions(-) diff --git a/inc/yaml/mylife_biographer-bot_openai.yaml b/inc/yaml/mylife_biographer-bot_openai.yaml index f2cb9e4..4e81c1a 100644 --- a/inc/yaml/mylife_biographer-bot_openai.yaml +++ b/inc/yaml/mylife_biographer-bot_openai.yaml @@ -12,82 +12,35 @@ paths: /story: post: operationId: MyLifeBiographerStoryCreation - summary: MyLife Biographer Bot will access this endpoint to generate a . + summary: MyLife Biographer Bot will access this endpoint to generate a `bio-story` document in MyLife Cosmos. description: Endpoint for handling incoming registration webhook data from the MyLife GPT service. requestBody: required: true content: application/json: schema: + description: The `story` data sent by MyLife Biographer BOt. type: object required: - - registrationInterests - - contact + - assistantType + - memberKey + - storySummary properties: - registrationInterests: - type: array - default: - - information - minItems: 1 - maxItems: 4 - items: - type: string - enum: - - membership - - volunteer - - donate - - information - description: Interests in MyLife, defaults to information. - contact: - type: object - required: - - humanName - - email - properties: - avatarName: - type: string - description: The avatar name desired by the person registering. - humanName: - type: string - description: The name, can be nickname or short name, of the person registering. - humanDateOfBirth: - type: string - format: date - description: The date of birth of the person registering; could be as little as a year. - email: - type: string - description: The email of the person registering. - city: - type: string - description: The city of the person registering. - state: - type: string - description: The state of the person registering. - country: - type: string - description: The country of the person registering. - description: The registration data sent from the MyLife GPT instance. - personalInterests: - type: array - default: [] - minItems: 0 - maxItems: 5 - items: - type: string - enum: - - archivist - - humanist - - ethicist - - technologist - - futurist - description: The driving personal interests of the person registering. - additionalInfo: + assistantType: + default: MyLife-Biographer-Bot + description: The type of assistant that generated the story. + type: string + memberKey: + description: Visitor enters memberKey and it is kept in GPT memory and sent with each request so that MyLife knows partition. + maxLength: 256 + type: string + storySummary: + description: MyLife Biographer Bot summary of identified `story`. + maxLength: 20480 type: string - maxLength: 1024 - description: Additional information sent from the MyLife GPT instance. responses: "200": description: Webhook registration data received successfully. post: operationId: MyLifeBiographerStoryCreation - x-openai-isConsequential: false + x-openai-isConsequential: false \ No newline at end of file From 1046d5be863900097e1b194d9a6a8af89ab33a73 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 29 Jan 2024 08:59:12 -0500 Subject: [PATCH 4/6] 20240129 @Mookse - openai integration via actions for biographer bot --- inc/js/api-functions.mjs | 189 +++++++++++++++++++++ inc/js/core.mjs | 35 ++++ inc/js/functions.mjs | 48 ------ inc/js/globals.mjs | 3 + inc/js/mylife-agent-factory.mjs | 32 +++- inc/js/mylife-data-service.js | 41 ++++- inc/js/mylife-datamanager.mjs | 26 ++- inc/js/routes.mjs | 52 ++---- inc/yaml/mylife_biographer-bot_openai.yaml | 80 +++++++-- 9 files changed, 394 insertions(+), 112 deletions(-) create mode 100644 inc/js/api-functions.mjs diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs new file mode 100644 index 0000000..11c3c86 --- /dev/null +++ b/inc/js/api-functions.mjs @@ -0,0 +1,189 @@ +import chalk from "chalk" +/* variables */ +const mBotSecrets = JSON.parse(process.env.OPENAI_JWT_SECRETS) +/* private modular functions */ +async function _keyValidation(ctx){ // transforms ctx.state + if(ctx.params.mid === ':mid') ctx.params.mid = undefined + // ctx session alternatives to hitting DB every time? can try... + const _mbr_id = ctx.params.mid??ctx.request.body.memberKey + const _validated = + ( // session validation shorthand + ( ctx.session?.isAPIValidated??false ) + && _mbr_id?.length + && ( _mbr_id===ctx.session?.APIMemberKey??false ) + ) || ( // initial full validation + (_mbr_id?.length) + && process.env.MYLIFE_HOSTED_MBR_ID.includes(_mbr_id) + && await ctx.MyLife.testPartitionKey(_mbr_id) + ) + ctx.state.mbr_id = _mbr_id + ctx.state.assistantType = _tokenType(ctx) + ctx.state.isValidated = _validated + ctx.session.isAPIValidated = ctx.state.isValidated + ctx.session.APIMemberKey = ctx.state.mbr_id +} +function _tokenType(ctx){ + const _token = ctx.state.token + const _assistantType = mBotSecrets?.[_token]??'personal-avatar' + return _assistantType +} +function _tokenValidation(_token){ + return mBotSecrets?.[_token]?.length??false +} +/* public modular functions */ +async function keyValidation(ctx){ + console.log(chalk.yellowBright('keyValidation()'), ctx.request.body) + await _keyValidation(ctx) + if(!ctx.state.isValidated){ + ctx.status = 400 // Bad Request + ctx.body = { + success: false, + message: 'Invalid member.', + } + return + } + ctx.status = 200 // OK + if(ctx.method === 'HEAD') return + // @todo: determine how to instantiate avatar via Maht Factory--session? In any case, perhaps relegate to session + const _memberCore = await ctx.MyLife.datacore(ctx.state.mbr_id) + const { updates, interests, birth, birthDate, fullName, names, nickname } = _memberCore + const _birth = (Array.isArray(birth) && birth.length) + ? birth[0] + : birth??{} + _birth.date = birthDate??_birth.date??'' + _birth.place = _birth.place??'' + const _memberCoreData = { + mbr_id: ctx.state.mbr_id, + updates: updates??'', + interests: interests??'', + birthDate: _birth.date, + birthPlace: _birth.place, + fullName: fullName??names?.[0]??'', + preferredName: nickname??names?.[0].split(' ')[0]??'', + } + ctx.body = { + success: true, + message: 'Valid member.', + data: _memberCoreData, + } + console.log(chalk.yellowBright('keyValidation()'), _memberCoreData, ) + return +} +async function register(ctx){ + const _registrationData = ctx.request.body + const { + registrationInterests, + contact={}, // as to not elicit error destructuring + personalInterests, + additionalInfo + } = _registrationData + const { + avatarName, + humanName, + humanDateOfBirth, + email, + city, + state, + country, + } = contact + if (!humanName?.length || !email?.length){ + ctx.status = 400 // Bad Request + ctx.body = { + success: false, + message: 'Missing required contact information: humanName and/or email are required.', + } + return + } + // Email validation + if (!ctx.Globals.isValidEmail(contact.email)) { + ctx.status = 400 // Bad Request + ctx.body = { + success: false, + message: 'Invalid email format.', + } + return + } + // throttle requests? + // write to cosmos db + _registrationData.email = email // required at root for select + const _ = ctx.MyLife.registerCandidate(_registrationData) + const { mbr_id, ..._return } = _registrationData // abstract out the mbr_id + ctx.status = 200 + ctx.body = { + success: true, + message: 'Registration completed successfully.', + data: _return, + } + return +} +/** + * Functionality around story contributions and portrayals + * @param {Koa} ctx - Koa Context object + * @returns {Koa} Koa Context object + */ +async function story(ctx){ + await _keyValidation(ctx) // sets ctx.state.mbr_id and more + const { assistantType, mbr_id } = ctx.state + const { storySummary } = ctx.request?.body??{} + if(!storySummary?.length){ + ctx.status = 400 // Bad Request + ctx.body = { + success: false, + message: 'No story summary provided. Use `storySummary` field.', + } + return + } + // write to cosmos db + const _story = await ctx.MyLife.story(mbr_id, assistantType, storySummary) // @todo: remove await + console.log(chalk.yellowBright('story submitted:'), _story) + ctx.status = 200 // OK + ctx.body = { + success: true, + message: 'Story submitted successfully.', + story: _story, + } + return +} +/** + * Validates api token + * @modular + * @public + * @param {object} ctx Koa context object + * @param {function} next Koa next function + * @returns {function} Koa next function + */ +async function tokenValidation(ctx, next) { + try { + const authHeader = ctx.request.headers['authorization'] + if(!authHeader){ + ctx.status = 401 + ctx.body = { error: 'Authorization header is missing' } + return + } + const _token = authHeader.split(' ')[1] // Bearer TOKEN_VALUE + if(!_tokenValidation(_token)){ + ctx.status = 401 + ctx.body = { error: 'Authorization token failure' } + return + } + ctx.state.token = _token // **note:** keep first, as it is used in _tokenType() + ctx.state.assistantType = _tokenType(ctx) + await next() + } catch (error) { + ctx.status = 401 + const _error = { + name: error.name, + message: error.message, + stack: error.stack + } + ctx.body = { message: 'Unauthorized Access', error: _error } + return + } +} +/* exports */ +export { + keyValidation, + register, + story, + tokenValidation, +} \ No newline at end of file diff --git a/inc/js/core.mjs b/inc/js/core.mjs index f5f069a..cc025c0 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -279,6 +279,10 @@ class MyLife extends Organization { // form=server constructor(_Factory){ // no session presumed to exist super(_Factory) } + async datacore(_mbr_id){ + if(!_mbr_id || _mbr_id===this.mbr_id) throw new Error('datacore cannot be accessed') + return await this.factory.datacore(_mbr_id) + } /* public functions */ /** * Server MyLife _Maht instantiation uses this function to populate the most current alerts in the modular factory memoryspace. Currently only applicable to system types, but since this is implemented at the `core.mjs` scope, we can account @@ -299,6 +303,37 @@ class MyLife extends Organization { // form=server async registerCandidate(_candidate){ return await this.factory.registerCandidate(_candidate) } + /** + * Submits a story to MyLife via API. Unclear if can be dual-purposed for internal, or if internal still instantiates API context. + * @public + * @param {string} _mbr_id - Member id + * @param {string} _assistantType - String name of assistant type + * @param {string} _summary - String summary of story + * @returns {object} - The story document from Cosmos. + */ + async story(_mbr_id, _assistantType, storySummary){ + const id = this.globals.newGuid + const _story = { + assistantType: _assistantType, + being: 'story', + form: _assistantType, + id, + mbr_id: _mbr_id, + name: `${_assistantType}-story_${_mbr_id}_${id}`, + summary: storySummary, + } + const _storyCosmos = await this.factory.story(_story) + return this.globals.stripCosmosFields(_storyCosmos) + } + /** + * Tests partition key for member + * @public + * @param {string} _mbr_id member id + * @returns {boolean} returns true if partition key is valid + */ + async testPartitionKey(_mbr_id){ + return await this.factory.testPartitionKey(_mbr_id) + } /* getters/setters */ /** * Gets MyLife agent role, refers to server entity Maht/MyLife diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 5462e80..4ec538c 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -27,53 +27,6 @@ async function alerts(ctx){ ctx.body = await ctx.state.MemberSession.alerts(ctx.request.body) } } -async function api_register(ctx){ - const _registrationData = ctx.request.body - const { - registrationInterests, - contact={}, // as to not elicit error destructuring - personalInterests, - additionalInfo - } = _registrationData - const { - avatarName, - humanName, - humanDateOfBirth, - email, - city, - state, - country, - } = contact - if (!humanName?.length || !email?.length){ - ctx.status = 400 // Bad Request - ctx.body = { - success: false, - message: 'Missing required contact information: humanName and/or email are required.', - } - return - } - // Email validation - if (!ctx.Globals.isValidEmail(contact.email)) { - ctx.status = 400 // Bad Request - ctx.body = { - success: false, - message: 'Invalid email format.', - } - return - } - // throttle requests? - // write to cosmos db - _registrationData.email = email // required at root for select - const _ = ctx.MyLife.registerCandidate(_registrationData) - const { mbr_id, ..._return } = _registrationData // abstract out the mbr_id - ctx.status = 200 - ctx.body = { - success: true, - message: 'Registration completed successfully.', - data: _return, - } - return -} async function avatarListing(ctx){ ctx.state.title = `Avatars for ${ ctx.state.member.memberName }` ctx.state.avatars = [] @@ -278,7 +231,6 @@ export { about, activateBot, alerts, - api_register, avatarListing, bots, category, diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 5e61714..ff9e884 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -22,6 +22,9 @@ class Globals extends EventEmitter { isValidGuid(_str='') { return (typeof _str === 'string' && guid_regex.test(_str)) } + stripCosmosFields(_obj){ + return Object.fromEntries(Object.entries(_obj).filter(([k, v]) => !k.startsWith('_'))) + } sysId(_mbr_id){ if(!typeof _mbr_id==='string' || !_mbr_id.length || !_mbr_id.includes('|')) throw new Error('expected MyLife member id string') diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index bde924b..0f76775 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -5,7 +5,6 @@ import EventEmitter from 'events' import vm from 'vm' import util from 'util' import { Guid } from 'js-guid' // usage = Guid.newGuid().toString() -import Globals from './globals.mjs' import Dataservices from './mylife-data-service.js' import { Member, MyLife } from './core.mjs' import { @@ -41,7 +40,6 @@ const mExcludeProperties = { definitions: true, name: true } -const mGlobals = new Globals() const mOpenAI = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, organizationId: process.env.OPENAI_ORG_KEY, @@ -146,6 +144,16 @@ class AgentFactory extends EventEmitter{ async challengeAccess(_mbr_id, _passphrase){ return await mDataservices.challengeAccess(_mbr_id, _passphrase) } + async datacore(_mbr_id){ + const _core = await mDataservices.getItems( + 'core', + undefined, + undefined, + undefined, + _mbr_id, + ) + return _core?.[0]??{} + } async getAlert(_alert_id){ const _alert = _alerts.system.find(alert => alert.id === _alert_id) return _alert ? _alert : await mDataservices.getAlert(_alert_id) @@ -223,6 +231,24 @@ class AgentFactory extends EventEmitter{ async setBot(_bot){ return await this.dataservices.setBot(_bot) } + /** + * Submits a story to MyLife. Currently via API, but could be also work internally. + * @param {object} _story - Story object { assistantType, being, form, id, mbr_id, name, summary }. + * @returns {object} - The story document from Cosmos. + */ + async story(_story){ + return await this.dataservices.story(_story) + } + /** + * Tests partition key for member + * @public + * @param {string} _mbr_id member id + * @returns {boolean} returns true if partition key is valid + */ + async testPartitionKey(_mbr_id){ + if(!this.isMyLife) return false + return await mDataservices.testPartitionKey(_mbr_id) + } // getters/setters get alerts(){ // currently only returns system alerts return _alerts.system @@ -246,7 +272,7 @@ class AgentFactory extends EventEmitter{ return this.schemas.file } get globals(){ - return mGlobals + return this.dataservices.globals } /** * Returns whether or not the factory is the MyLife server, as various functions are not available to the server and some _only_ to the server. diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index e84c345..7f36294 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -90,9 +90,15 @@ class Dataservices { get embedder(){ return this.#PgvectorManager } + get globals(){ + return this.datamanager.globals + } get id(){ return this.partitionId.split('|')[1] } + get isMyLife(){ + return this.mbr_id===process.env?.MYLIFE_SERVER_MBR_ID??false + } get mbr_id(){ return this.partitionId } @@ -143,7 +149,10 @@ class Dataservices { */ async challengeAccess(_mbr_id,_passphrase){ // if possible (async) injected into session object // ask global data service (stored proc) for passphrase - return await this.datamanager.challengeAccess(_mbr_id,_passphrase) + return await this.datamanager.challengeAccess(_mbr_id, _passphrase) + } + async datacore(_mbr_id){ + return await this.getItem(_mbr_id) } async findRegistrationIdByEmail(_email){ /* pull record for email, returning id or new guid */ @@ -257,13 +266,15 @@ class Dataservices { * @public * @param {string} _id - The unique identifier for the item. * @param {string} _container_id - The container to use, overriding default: `Members`. + * @param {string} _mbr_id - The member id to use, overriding default. * @returns {Promise} The item corresponding to the provided ID. */ - async getItem(_id, _container_id) { + async getItem(_id, _container_id, _mbr_id=this.mbr_id) { try{ return await this.datamanager.getItem( _id, _container_id, + { partitionKey: _mbr_id, populateQuotaInfo: false, }, ) } catch(_error){ @@ -279,9 +290,10 @@ class Dataservices { * @param {array} [_selects=[]] - Fields to select; if empty, selects all fields. * @param {Array} [_paramsArray=[]] - Additional query parameters. * @param {string} _container_id - The container name to use, overriding default. + * @param {string} _mbr_id - The member id to use, overriding default. * @returns {Promise} An array of items matching the query parameters. */ - async getItems(_being, _selects=[], _paramsArray=[], _container_id) { // _params is array of objects { name: '${varName}' } + async getItems(_being, _selects=[], _paramsArray=[], _container_id, _mbr_id=this.mbr_id) { // _params is array of objects { name: '${varName}' } // @todo: incorporate date range functionality into this.getItems() const _prefix = 'u' _paramsArray.unshift({ name: '@being', value: _being }) // add primary parameter to array at beginning @@ -299,6 +311,10 @@ class Dataservices { return await this.datamanager.getItems( { query: _query, parameters: _paramsArray }, _container_id, + { + partitionKey: _mbr_id, + populateQuotaInfo: false, // set this to true to include quota information in the response headers + }, ) } catch(_error){ @@ -399,6 +415,25 @@ class Dataservices { } return _bot } + /** + * Submits a story to MyLife. Currently via API, but could be also work internally. + * @param {object} _story - Story object { assistantType, being, form, id, mbr_id, name, summary }. + * @returns {object} - The story document from Cosmos. + */ + async story(_story){ + if(!this.isMyLife) return + return await this.datamanager.pushItem(_story) + } + /** + * Tests partition key for member + * @public + * @param {string} _mbr_id member id + * @returns {boolean} returns true if partition key is valid + */ + async testPartitionKey(_mbr_id){ + if(!this.isMyLife) return false + return await this.datamanager.testPartitionKey(_mbr_id) + } } // exports export default Dataservices \ No newline at end of file diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 1e88be9..270fe11 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -1,9 +1,12 @@ /* imports */ // import { DefaultAzureCredential } from "@azure/identity" import { CosmosClient } from '@azure/cosmos' -import Config from './mylife-datasource-config.mjs' import chalk from 'chalk' import { _ } from 'ajv' +import Config from './mylife-datasource-config.mjs' +import Globals from './globals.mjs' +/* modular constants */ +const mGlobals = new Globals() // define class class Datamanager { #containers @@ -31,7 +34,7 @@ class Datamanager { } this.requestOptions = { partitionKey: this.#partitionId, - populateQuotaInfo: true, // set this to true to include quota information in the response headers + populateQuotaInfo: false, // set this to true to include quota information in the response headers } } // init function @@ -86,11 +89,11 @@ class Datamanager { async getItems(_querySpec, _container_id=this.containerDefault, _options=this.requestOptions ){ const { resources } = await this.#containers[_container_id] .items - .query(_querySpec,_options) + .query(_querySpec, _options) .fetchAll() return resources } - async patchItem(_id, _item, _container_id=this.containerDefault) { // patch or update, depends on whether it finds id or not, will only overwrite fields that are in _item + async patchItem(_id, _item, _container_id=this.containerDefault){ // patch or update, depends on whether it finds id or not, will only overwrite fields that are in _item // [Partial Document Update, includes node.js examples](https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update) if(!Array.isArray(_item)) _item = [_item] const { resource: _update } = await this.#containers[_container_id] @@ -98,7 +101,9 @@ class Datamanager { .patch(_item) // see below for filter-patch example return _update } - async pushItem(_item, _container_id=this.containerDefault) { // post or insert, depends on whether it finds id or not, will overwrite all existing fields + async pushItem(_item, _container_id=this.containerDefault){ + _item.id = _item?.id??this.globals.newGuid + _item.mbr_id = _item?.mbr_id??this.#partitionId const { resource: doc } = await this.#containers[_container_id] .items .upsert(_item) @@ -115,6 +120,17 @@ class Datamanager { .upsert(_candidate) return doc } + async testPartitionKey(_mbr_id){ + const { resource: _result } = await this.#containers['members'] + .scripts + .storedProcedure('testPartitionKey') + .execute(_mbr_id) // first parameter is partition key, second is passphrase, third is case sensitivity + return _result + } + /* getters/setters */ + get globals(){ + return mGlobals + } } // exports export default Datamanager diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 6778886..e7dedf2 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -4,7 +4,6 @@ import { about, activateBot, alerts, - api_register, avatarListing, bots, category, @@ -20,6 +19,12 @@ import { upload, _upload } from './functions.mjs' +import { + keyValidation, + register, + story, + tokenValidation, +} from './api-functions.mjs' // variables const _Router = new Router() const _memberRouter = new Router() @@ -37,12 +42,17 @@ _Router.post('/', chat) _Router.post('/challenge/:mid', challenge) _Router.post('/signup', signup) /* api webhook routes */ -_apiRouter.use(_tokenValidate) +_apiRouter.use(tokenValidation) _apiRouter.get('/alerts', alerts) _apiRouter.get('/alerts/:aid', alerts) -_apiRouter.post('/register', api_register) +//_apiRouter.get('/keyValidation', (ctx)=>{console.log('48', ctx.request)}) +//_apiRouter.get('/keyValidation/:mid', keyValidation) +//_apiRouter.head('/keyValidation/:mid', keyValidation) +_apiRouter.post('/keyValidation/:mid', keyValidation) +_apiRouter.post('/register', register) +_apiRouter.post('/story/:mid', story) /* member routes */ -_memberRouter.use(_memberValidate) +_memberRouter.use(memberValidation) _memberRouter.get('/', members) _memberRouter.get('/avatars', avatarListing) _memberRouter.get('/avatars/:aid', avatarListing) @@ -77,7 +87,7 @@ function connectRoutes(_Menu){ * @param {function} next Koa next function * @returns {function} Koa next function */ -async function _memberValidate(ctx, next) { +async function memberValidation(ctx, next) { // validation logic if(ctx.state.locked) { ctx.redirect( @@ -105,38 +115,6 @@ function status(ctx){ // currently returns reverse "locked" status, could send o function status_signup(ctx){ ctx.body = ctx.session.signup } -/** - * Validates api token - * @param {object} ctx Koa context object - * @param {function} next Koa next function - * @returns {function} Koa next function - */ -async function _tokenValidate(ctx, next) { - try { - const authHeader = ctx.request.headers['authorization'] - if(!authHeader){ - ctx.status = 401 - ctx.body = { error: 'Authorization header is missing' } - return - } - const _token = authHeader.split(' ')[1] // Bearer TOKEN_VALUE - if(_token!==process.env.OPENAI_JWT_SECRET){ - ctx.status = 401 - ctx.body = { error: 'Authorization token failure' } - return - } - await next() - } catch (error) { - ctx.status = 401 - const _error = { - name: error.name, - message: error.message, - stack: error.stack - } - ctx.body = { message: 'Unauthorized Access', error: _error } - return - } -} // exports export default function init(_Menu) { connectRoutes(_Menu) diff --git a/inc/yaml/mylife_biographer-bot_openai.yaml b/inc/yaml/mylife_biographer-bot_openai.yaml index 4e81c1a..db96763 100644 --- a/inc/yaml/mylife_biographer-bot_openai.yaml +++ b/inc/yaml/mylife_biographer-bot_openai.yaml @@ -4,16 +4,77 @@ info: description: This API is for receiving webhooks from [MyLife's public Biographer Bot instance](https://chat.openai.com/g/g-QGzfgKj6I-mylife-biographer-bot). version: 1.0.0 servers: - - url: https://4c87-73-149-210-124.ngrok-free.app/api/v1 + - url: https://humanremembranceproject.org/api/v1 description: Endpoint for receiving stories from the MyLife Biographer Bot instance. security: - bearerAuth: [] paths: - /story: + /keyValidation/{mid}: post: + x-openai-isConsequential: false + operationId: MyLifeKeyValidation + summary: MyLife Biographer Bot will access this endpoint to validate a `memberKey` in MyLife Cosmos. + description: Endpoint for handling incoming registration webhook data from the MyLife GPT service. + parameters: + - name: mid + in: path + required: true + description: The `memberKey` data to be sent by MyLife Biographer Bot. Visitor enters memberKey and it is kept in GPT memory and sent with each request so that MyLife knows partition. + schema: + maxLength: 256 + minLength: 40 + type: string + responses: + "200": + description: A successful response indicating the member key is valid + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: Valid member. + data: + type: object + properties: + mbr_id: + maxLength: 256 + minLength: 40 + type: string + updates: + type: string + interests: + type: string + birthDate: + type: string + format: date-time + birthPlace: + type: string + fullName: + type: string + preferredName: + type: string + "400": + description: Invalid member. Field `memberKey` is not valid, check again with member. + /story/{mid}: + post: + x-openai-isConsequential: false operationId: MyLifeBiographerStoryCreation summary: MyLife Biographer Bot will access this endpoint to generate a `bio-story` document in MyLife Cosmos. description: Endpoint for handling incoming registration webhook data from the MyLife GPT service. + parameters: + - name: mid + in: path + required: true + description: The `memberKey` data to be sent by MyLife Biographer Bot. Visitor enters memberKey and it is kept in GPT memory and sent with each request so that MyLife knows partition. + schema: + maxLength: 256 + minLength: 40 + type: string requestBody: required: true content: @@ -22,25 +83,12 @@ paths: description: The `story` data sent by MyLife Biographer BOt. type: object required: - - assistantType - - memberKey - storySummary properties: - assistantType: - default: MyLife-Biographer-Bot - description: The type of assistant that generated the story. - type: string - memberKey: - description: Visitor enters memberKey and it is kept in GPT memory and sent with each request so that MyLife knows partition. - maxLength: 256 - type: string storySummary: description: MyLife Biographer Bot summary of identified `story`. maxLength: 20480 type: string responses: "200": - description: Webhook registration data received successfully. -post: - operationId: MyLifeBiographerStoryCreation - x-openai-isConsequential: false \ No newline at end of file + description: Webhook registration data received successfully. \ No newline at end of file From 026d3b17bead74c8a0d7fbcce77ee8b6916397cb Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 29 Jan 2024 09:25:31 -0500 Subject: [PATCH 5/6] 20240128 @Mookse - updated sample .env --- .env-sample | 31 ------------------------------- sample.env | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 31 deletions(-) delete mode 100644 .env-sample create mode 100644 sample.env diff --git a/.env-sample b/.env-sample deleted file mode 100644 index 2296aa2..0000000 --- a/.env-sample +++ /dev/null @@ -1,31 +0,0 @@ -OPENAI_API_CHAT_TIMEOUT=22000 -OPENAI_API_CHAT_RESPONSE_PING_INTERVAL=890 # how often to ping openai service for assistant run status complete -OPENAI_API_KEY=sk-... # your individual openai API key -OPENAI_BASE_URL=https://api.openai.com/v1 -OPENAI_MAHT_GITHUB=https://github.com/MyLife-Services/mylife-maht.git -OPENAI_MAHT_GITHUB_BRANCH=base -OPENAI_MAHT_GPT_OVERRIDE=# localdev version [3.5-turbo]: asst_c8qcXkJS6urevqja5wcZ0za5 -OPENAI_MODEL_CORE_AVATAR=gpt-3.5-turbo -OPENAI_MODEL_CORE_BOT=gpt-3.5-turbo -OPENAI_ORG_KEY=org-... # use ours or use your own -PORT=3000 # KOA port -MYLIFE_ALLOW_INTELLIGENT_QUESTIONS=false -MYLIFE_DB_ALLOW_SAVE=true -MYLIFE_DB_ENDPOINT= # get from administrator -MYLIFE_DB_NAME= # get from administrator -MYLIFE_DB_RW= # get from administrator -MYLIFE_DB_RX= # get from administrator -MYLIFE_DB_CONTAINER_NAME= # get from administrator -MYLIFE_SERVER_MBR_ID= # get from administrator -MYLIFE_HOSTED_MBR_ID=[] # ["mbr_id's hosted by this server"] -MYLIFE_SESSION_KEY=iteration-number # type in own personal key, quoted or unquoted if no spaces... Restarting server will NOT refresh sessions (as of now) so must change this key to refresh server-level constants} -MYLIFE_SESSION_TIMEOUT_MS=900000 # session timeout in milliseconds -MYLIFE_VERSION=0.0 -MYLIFE_EMBEDDING_SERVER_URL=http://mylife-embedding-server -MYLIFE_EMBEDDING_SERVER_PORT=8000 -MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN={ chosen personally to match environment variable in embedding server } -MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT=1024 # 1MB = 1024 * 1024 -MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN=1024 # 10MB = 1024 * 1024 * 10 -MYLIFE_CONTRIBUTIONS_DB_CONTAINER_NAME= # get from administrator -MYLIFE_REGISTRATION_DB_CONTAINER_NAME= # get from administrator -MYLIFE_SYSTEM_DB_CONTAINER_NAME= # get from administrator \ No newline at end of file diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..04f7a4b --- /dev/null +++ b/sample.env @@ -0,0 +1,35 @@ +OPENAI_API_CHAT_TIMEOUT=59000 +OPENAI_API_CHAT_RESPONSE_PING_INTERVAL=890 +OPENAI_API_KEY=sk-... # add your key here +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_JWT_SECRETS={} # object with keys being JWT share with external AIs +OPENAI_MAHT_GITHUB=https://github.com/MyLife-Services/mylife-maht.git +OPENAI_MAHT_GITHUB_BRANCH=base +OPENAI_MAHT_GPT_OVERRIDE=asst_... # local dev override for engine so that local can use chaper model +# 2000 chars (not tokens) is the max; disallows large code pastes, or rather, converts them as appropriate to file(s); **also** converts large text pastes to files a human member can use as well, though functionality TODO +OPENAI_MAX_CONTEXT_WINDOW=2000 +OPENAI_MODEL_CORE_AVATAR=gpt-3.5-turbo +OPENAI_MODEL_CORE_BOT=gpt-3.5-turbo +OPENAI_ORG_KEY=org-dTYDMEBuP2yb2qtwCQJA4HHh # MyLife org ID +PORT=3000 +MYLIFE_ALLOW_INTELLIGENT_QUESTIONS=false # almost deprecated, leave false +MYLIFE_DB_ALLOW_SAVE=false # during local testing, so not write to db +MYLIFE_DB_CONTAINER_NAME=members +MYLIFE_DB_ENDPOINT=https://mylife.documents.azure.com:443/ +MYLIFE_DB_NAME=membership +MYLIFE_DB_RW=string # add your key here +MYLIFE_DB_RX=string # add your key here +MYLIFE_SERVER_MBR_ID=mylife|... # add your key here +MYLIFE_HOSTED_MBR_ID=[] # array of ids hosted on your server +MYLIFE_SESSION_KEY=0.0.2.0001 # random string for resetting sessions +MYLIFE_SESSION_TIMEOUT_MS=900000 +MYLIFE_SYSTEM_ALERT_CHECK_INTERVAL=120000 # how often to check for alerts in ms +MYLIFE_VERSION=0.0.2.0001 +MYLIFE_EMBEDDING_SERVER_URL= # temp deprecation +MYLIFE_EMBEDDING_SERVER_PORT=0 # temp deprecation +MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN= # temp deprecation +MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT= # temp deprecation +MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN= # temp deprecation +MYLIFE_CONTRIBUTIONS_DB_CONTAINER_NAME= # get from admin +MYLIFE_REGISTRATION_DB_CONTAINER_NAME= # get from admin +MYLIFE_SYSTEM_DB_CONTAINER_NAME= # get from admin \ No newline at end of file From eff3112c0536b5a2d69e2f0dd7d04d92099f082f Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 29 Jan 2024 09:41:56 -0500 Subject: [PATCH 6/6] 20240129 @Mookse - console log cleanup --- inc/js/api-functions.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs index 11c3c86..9c07e61 100644 --- a/inc/js/api-functions.mjs +++ b/inc/js/api-functions.mjs @@ -32,7 +32,6 @@ function _tokenValidation(_token){ } /* public modular functions */ async function keyValidation(ctx){ - console.log(chalk.yellowBright('keyValidation()'), ctx.request.body) await _keyValidation(ctx) if(!ctx.state.isValidated){ ctx.status = 400 // Bad Request @@ -66,7 +65,7 @@ async function keyValidation(ctx){ message: 'Valid member.', data: _memberCoreData, } - console.log(chalk.yellowBright('keyValidation()'), _memberCoreData, ) + console.log(chalk.yellowBright(`keyValidation():${_memberCoreData.mbr_id}`), _memberCoreData.fullName) return } async function register(ctx){