diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs index 4ce8d94..64a5b6b 100644 --- a/inc/js/api-functions.mjs +++ b/inc/js/api-functions.mjs @@ -246,7 +246,20 @@ async function memory(ctx){ } } /** - * Validates api token + * Given an itemId, obscures aspects of contents of the data record. + * @param {Koa} ctx - Koa Context object + * @returns {Promise} - Promise object representing obscured item + */ +async function obscure(ctx){ + await mAPIKeyValidation(ctx) + const { itemId: iid, } = ctx.request?.body ?? {} + if(!ctx.Globals.isValidGuid(iid)) + ctx.throw(400, 'Improper `itemId` provided in request') + const { avatar, mbr_id, } = ctx.state + ctx.body = await avatar.obscure(mbr_id, iid) +} +/** + * Validates api token. * @module * @public * @param {object} ctx Koa context object @@ -309,6 +322,7 @@ async function mAPIKeyValidation(ctx){ // transforms ctx.state if(ctx.params.mid === ':mid') ctx.params.mid = undefined const memberId = ctx.params?.mid + ?? ctx.request.body?.mbr_id ?? ctx.request.body?.memberKey ?? ctx.session?.APIMemberKey if(!memberId?.length) @@ -350,6 +364,7 @@ export { keyValidation, logout, memory, + obscure, register, tokenValidation, upload, diff --git a/inc/js/core.mjs b/inc/js/core.mjs index 41a84fd..b078ae7 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -259,9 +259,25 @@ class MyLife extends Organization { // form=server }) return experiences } - 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) + /** + * Challenges and logs in member. + * @param {string} memberId - Member id to challenge. + * @param {string} passphrase - Passphrase response to challenge. + * @returns {boolean} - Whether or not member is logged in successfully. + */ + async challengeAccess(memberId, passphrase){ + const challengeSuccessful = await this.factory.challengeAccess(memberId, passphrase) + return challengeSuccessful + } + /** + * Returns the datacore object for the specified member id. + * @param {string} mbr_id - The Member id to access datacore + * @returns {Promise} - Datacore object for member id + */ + 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) } /** * Submits and returns the journal or diary entry to MyLife via API. diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index ecc1b19..b0b0704 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -6,7 +6,7 @@ import { /* module export functions */ async function about(ctx){ ctx.state.title = `About MyLife` - await ctx.render('about') // about + await ctx.render('about') } /** * Activate a bot for the member @@ -85,7 +85,13 @@ async function challenge(ctx){ const { mid, } = ctx.params if(!mid?.length) ctx.throw(400, `challenge request requires member id`) - ctx.body = await ctx.session.MemberSession.challengeAccess(mid, passphrase) + if(!ctx.state.MemberSession.locked) + return true + const challengeSuccessful = await ctx.MyLife.challengeAccess(mid, passphrase) + const { MemberSession, } = ctx.session + MemberSession.challengeOutcome = challengeSuccessful + await MemberSession.init(mid) + ctx.body = !MemberSession.locked } /** * Chat with the member's avatar. @@ -242,6 +248,16 @@ async function migrateChat(ctx){ const { avatar, } = ctx.state ctx.body = await avatar.migrateChat(tid) } +/** + * Given an itemId, obscures aspects of contents of the data record. + * @param {Koa} ctx - Koa Context object + * @returns {object} - The item obscured + */ +async function obscure(ctx){ + const { iid, } = ctx.params + const { avatar, } = ctx.state + ctx.body = await avatar.obscure(iid) +} /** * Reset the passphrase for the member's avatar. * @param {Koa} ctx - Koa Context object @@ -416,6 +432,7 @@ export { members, migrateBot, migrateChat, + obscure, passphraseReset, privacyPolicy, retireBot, diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 3532a3a..14130f4 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -96,6 +96,23 @@ const mAiJsFunctions = { ] } }, + obscure: { + description: "Obscures a summary so that no human names are present.", + name: "obscure", + parameters: { + type: "object", + properties: { + itemId: { + description: "Id of summary to obscure", + format: "uuid", + type: "string" + } + }, + required: [ + "itemId" + ] + } + }, storySummary: { description: 'Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.', name: 'storySummary', @@ -177,7 +194,7 @@ const mAiJsFunctions = { }, required: [ "itemId", - "title" + "summary" ] } }, diff --git a/inc/js/memory-functions.mjs b/inc/js/memory-functions.mjs index 473a23f..f05bedb 100644 --- a/inc/js/memory-functions.mjs +++ b/inc/js/memory-functions.mjs @@ -49,7 +49,8 @@ async function livingMemory(ctx){ const { Globals, MyLife, } = ctx const { avatar, } = ctx.state if(!Globals.isValidGuid(iid)) - return ctx.throw(400, 'Invalid Item ID') + ctx.throw(400, 'Invalid Item ID') + ctx.throw(501, 'Not Implemented') ctx.body = await avatar.livingMemory(iid) } /* exports */ @@ -58,5 +59,4 @@ export { improveMemory, endMemory, reliveMemory, - livingMemory, } \ No newline at end of file diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 71f3cdb..0333459 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -40,6 +40,7 @@ const mExcludeProperties = { definitions: true, name: true } +const mGeneralBotId = 'asst_piDEJKYjqvAZbLstjd6u0ZMb' const mLLMServices = new LLMServices() const mMyLifeTeams = [ { @@ -254,8 +255,8 @@ class BotFactory extends EventEmitter{ if(this.isMyLife){ // MyLife server has no bots of its own, system agents perhaps (file, connector, etc) but no bots yet, so this is a micro-hydration if(!mbr_id) throw new Error('mbr_id required for BotFactory hydration') - const botFactory = new BotFactory(mbr_id) - await botFactory.init() + const botFactory = await new BotFactory(mbr_id) + .init() botFactory.bot = await botFactory.bot(id, type, mbr_id) if(!botFactory?.bot) // create bot on member behalf botFactory.bot = await botFactory.createBot({ type: type }) @@ -293,9 +294,9 @@ class BotFactory extends EventEmitter{ * Gets a member's bots. * @todo - develop bot class and implement hydrated instance * @public - * @param {string} object_id - The object_id guid of avatar. - * @param {string} botType - The bot type. - * @returns {array} - The member's hydrated bots. + * @param {string} object_id - The object_id guid of avatar + * @param {string} botType - The bot type + * @returns {array} - The member's hydrated bots */ async bots(object_id, botType){ const _params = object_id?.length @@ -310,6 +311,18 @@ class BotFactory extends EventEmitter{ ) return bots } + /** + * Accesses Dataservices to challenge access to a member's account. + * @param {string} passphrase - The passphrase to challenge + * @param {boolean} caseInsensitive - Whether requestor suggests to ignore case in passphrase, defaults to `false` + * @returns {Promise} - `true` if challenge successful + */ + async challengeAccess(passphrase, caseInsensitive=false){ + caseInsensitive = this.core.caseInsensitive + ?? caseInsensitive + const challengeSuccessful = await mDataservices.challengeAccess(this.mbr_id, passphrase, caseInsensitive) + return challengeSuccessful + } /** * Get member collection items. * @param {string} type - The type of collection to retrieve, `false`-y = all. @@ -386,6 +399,14 @@ class BotFactory extends EventEmitter{ // @todo remove restriction (?) for all experiences to be stored under MyLife `mbr_id` return await mDataservices.getItem(_experience_id, 'system') } + /** + * Retrieves a collection item by Id. + * @param {Guid} id - The id of the collection item to retrieve. + * @returns {object} - The item. + */ + async item(id){ + return await this.dataservices.getItem(id) + } /** * Proxy for modular mHelp() function. * @public @@ -398,6 +419,23 @@ class BotFactory extends EventEmitter{ async help(thread_id, bot_id, helpRequest, avatar){ return await mHelp(thread_id, bot_id, helpRequest, this, avatar) } + /** + * Given an itemId, obscures aspects of contents of the data record. Consults modular LLM with isolated request and saves outcome to database. + * @param {Guid} itemId - Id of the item to obscure + * @returns {string} - The obscured content + */ + async obscure(itemId){ + const { id, summary, relationships, } = await this.item(itemId) + ?? {} + if(!id) + throw new Error('Item not found') + if(!summary?.length) + throw new Error('No summary found to obscure') + const obscuredSummary = await mObscure(this, summary) + if(obscuredSummary?.length) /* save response */ + this.dataservices.patch(id, { summary: obscuredSummary }) // no need await + return obscuredSummary + } /** * Allows member to reset passphrase. * @param {string} passphrase @@ -525,6 +563,7 @@ class BotFactory extends EventEmitter{ } get mbr_id(){ return this.#mbr_id + ?? this.core.mbr_id } get mbr_id_id(){ return this.globals.sysId(this.mbr_id) @@ -578,15 +617,6 @@ class AgentFactory extends BotFactory { async avatarProperties(){ return ( await this.dataservices.getAvatar() ) } - /** - * Accesses MyLife Dataservices to challenge access to a member's account. - * @param {string} mbr_id - * @param {string} passphrase - * @returns {object} - Returns passphrase document if access is granted. - */ - async challengeAccess(mbr_id, passphrase){ - return await mDataservices.challengeAccess(mbr_id, passphrase) - } /** * Creates a new collection item in the member's container. * @param {object} item - The item to create. @@ -614,14 +644,6 @@ class AgentFactory extends BotFactory { async deleteItem(id){ return await this.dataservices.deleteItem(id) } - /** - * Retrieves a collection item by Id. - * @param {Guid} id - The id of the collection item to retrieve. - * @returns {object} - The item. - */ - async item(id){ - return await this.dataservices.getItem(id) - } async entry(entry){ const defaultType = 'entry' const { @@ -835,12 +857,14 @@ class AgentFactory extends BotFactory { } /** * Updates a collection item. - * @param {object} item - The item to update. - * @returns {Promise} - The updated item. + * @param {object} item - The item to update + * @property {Guid} item.id - The item id + * @returns {Promise} - The updated item */ async updateItem(item){ - const { id, ..._item } = item - const response = await this.dataservices.patch(id, _item) + if(!this.globals.isValidGuid(item?.id)) + throw new Error('item id required for update') + const response = await this.dataservices.patch(item.id, item) return response } /* getters/setters */ @@ -897,6 +921,30 @@ class MyLifeFactory extends AgentFactory { super(mPartitionId) } // no init() for MyLife server /* public functions */ + /** + * Overload for MyLifeFactory::bot() - Q is able to hydrate a bot instance on behalf of members. + * @public + * @param {string} mbr_id - The member id + * @returns {object} - The hydrated bot instance + */ + async bot(mbr_id){ + const bot = await new BotFactory(mbr_id) + .init() + return bot + } + /** + * Accesses Dataservices to challenge access to a member's account. + * @public + * @param {string} mbr_id - The member id + * @param {string} passphrase - The passphrase to challenge + * @returns {object} - Returns passphrase document if access is granted. + */ + async challengeAccess(mbr_id, passphrase){ + const caseInsensitive = true // MyLife server defaults to case-insensitive + const avatarProxy = await this.bot(mbr_id) + const challengeSuccessful = await avatarProxy.challengeAccess(passphrase, caseInsensitive) + return challengeSuccessful + } /** * Compares registration email against supplied email to confirm `true`. **Note**: does not care if user enters an improper email, it will only fail the encounter, as email structure _is_ confirmed upon initial data write. * @param {string} email - The supplied email to confirm registration. @@ -1215,17 +1263,33 @@ function mCreateBotInstructions(factory, bot){ if(typeof bot!=='object' || !bot.type?.length) throw new Error('bot object required, and requires `type` property') const { type=mDefaultBotType, } = bot - let { instructions, version, } = factory.botInstructions(type) + let { + instructions, + limit=8000, + version, + } = factory.botInstructions(type) if(!instructions) // @stub - custom must have instruction loophole throw new Error(`bot instructions not found for type: ${ type }`) - let { general, purpose, preamble, prefix, references=[], replacements=[], } = instructions + let { + general, + purpose='', + preamble='', + prefix='', + references=[], + replacements=[], + suffix='', // example: data privacy info + voice='', + } = instructions /* compile instructions */ switch(type){ case 'diary': case 'journaler': instructions = purpose + + preamble + prefix + general + + suffix + + voice break case 'personal-avatar': instructions = preamble @@ -1285,6 +1349,8 @@ function mCreateBotInstructions(factory, bot){ break } }) + /* assess and validate limit */ + console.log(chalk.blueBright('instructions length'), instructions.length, instructions) return { instructions, version, } } function mExposedSchemas(factoryBlockedSchemas){ @@ -1525,6 +1591,31 @@ async function mLoadSchemas(){ console.log(err) } } +/** + * Given an itemId, obscures aspects of contents of the data record. + * @param {AgentFactory} factory - The factory object + * @param {Guid} iid - The item id + * @returns {Object} - The obscured item object + */ +async function mObscure(factory, summary) { + const { mbr_id } = factory + let obscuredSummary + // @stub - if greater than limit, turn into text file and add + const prompt = `OBSCURE:\n${summary}` + const messageArray = await mLLMServices.getLLMResponse(null, mGeneralBotId, prompt) + const { content: contentArray=[], } = messageArray?.[0] ?? {} + const { value, } = contentArray + .filter(message=>message.type==='text') + ?.[0] + ?.text + ?? {} + try { + let parsedSummary = JSON.parse(value) + if(typeof parsedSummary==='object' && parsedSummary!==null) + obscuredSummary = parsedSummary.obscuredSummary + } catch(e) {} // obscuredSummary is just a string; use as-is or null + return obscuredSummary +} async function mPopulateBotInstructions(){ const instructionSets = await mDataservices.botInstructions() instructionSets diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 1f13e4d..ba347fa 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -110,7 +110,7 @@ class Avatar extends EventEmitter { if(shadowId) messages = await this.shadow(shadowId, itemId, message) else { - // @stub - one weakness in teh chain might also be the fact that I am not including in instructions how to create integrated summary and left it primarily to the JSON description of function + // @stub - one weakness in the chain might also be the fact that I am not including in instructions how to create integrated summary and left it primarily to the JSON description of function if(itemId) message = `update-memory-request: itemId=${ itemId }\n` + message messages = await mCallLLM(this.#llmServices, conversation, message, factory, this) @@ -121,21 +121,29 @@ class Avatar extends EventEmitter { else console.log('chat::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) /* frontend mutations */ + let responses const { activeBot: bot } = this - // current fe will loop through messages in reverse chronological order - const responses = conversation.messages - .filter(_message=>{ // limit to current chat response(s); usually one, perhaps faithfully in future [or could be managed in LLM] + responses = conversation.messages + .filter(_message=>{ return messages.find(__message=>__message.id===_message.id) && _message.type==='chat' && _message.role!=='user' }) .map(_message=>mPruneMessage(bot, _message, 'chat', processStartTime)) + if(!responses?.length){ // last failsafe + responses = [this.backupResponse + ?? { + message: 'I am sorry, the entire chat line went dark for a moment, please try again.', + type: 'system', + }] + } const response = { instruction: this.frontendInstruction, responses, success: true, } delete this.frontendInstruction + delete this.backupResponse return response } /** @@ -510,6 +518,27 @@ class Avatar extends EventEmitter { console.log('migrateChat::BYPASS-SAVE', conversation.thread_id) return conversation } + /** + * Given an itemId, obscures aspects of contents of the data record. Obscure is a vanilla function for MyLife, so does not require intervening intelligence and relies on the factory's modular LLM. + * @param {Guid} iid - The item id + * @returns {Object} - The obscured item object + */ + async obscure(iid){ + const updatedSummary = await this.#factory.obscure(iid) + this.frontendInstruction = { + command: 'updateItemSummary', + itemId: iid, + summary: updatedSummary, + } + return { + instruction: this.frontendInstruction, + responses: [{ + agent: 'server', + message: `I have successfully obscured your content.`, + }], + success: true, + } + } /** * Register a candidate in database. * @param {object} candidate - The candidate data object. @@ -1254,6 +1283,17 @@ class Q extends Avatar { this.llmServices = llmServices } /* overloaded methods */ + /** + * Get a bot's properties from Cosmos (or type in .bots). + * @public + * @async + * @param {string} mbr_id - The bot id + * @returns {object} - The hydrated member avatar bot + */ + async bot(mbr_id){ + const bot = await this.#factory.bot(mbr_id) + return bot + } /** * Processes and executes incoming chat request. * @public @@ -1280,6 +1320,18 @@ class Q extends Avatar { activeBotId = this.activeBotId return super.chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime) } + /** + * Given an itemId, obscures aspects of contents of the data record. Obscure is a vanilla function for MyLife, so does not require intervening intelligence and relies on the factory's modular LLM. In this overload, we invoke a micro-avatar for the member to handle the request on their behalf, with charge-backs going to MyLife as the sharing and api is a service. + * @public + * @param {string} mbr_id - The member id + * @param {Guid} iid - The item id + * @returns {Object} - The obscured item object + */ + async obscure(mbr_id, iid){ + const botFactory = await this.bot(mbr_id) + const updatedSummary = await botFactory.obscure(iid) + return updatedSummary + } upload(){ throw new Error('MyLife avatar cannot upload files.') } diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index fc6640c..5d87a83 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -194,16 +194,16 @@ class Dataservices { ) } /** - * Challenges access using a member ID and passphrase. + * Challenges access to a member ID via passphrase, running against a stored procedure in the database. * @async * @public - * @param {string} _mbr_id - The member ID. - * @param {string} _passphrase - The passphrase for access. + * @param {string} mbr_id - The member ID. + * @param {string} passphrase - The passphrase for access. + * @param {boolean} caseInsensitive - Whether to ignore case in passphrase, defaults to `false` * @returns {Promise} The result of the access challenge. */ - 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) + async challengeAccess(mbr_id, passphrase, caseInsensitive=false){ + return await this.datamanager.challengeAccess(mbr_id, passphrase) } /** * Proxy to retrieve stored conversations. diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 249d000..7dc1695 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -48,13 +48,18 @@ class Datamanager { return this } /* public functions */ - async challengeAccess(mbr_id, passphrase){ - // in order to obscure passphrase, have db make comparison (could include flag for case insensitivity) - // Execute the stored procedure + /** + * Runs challenge Access routine for login and authentications + * @param {string} mbr_id - The member id to challenge + * @param {string} passphrase - The passphrase to resolve challenge + * @param {boolean} caseInsensitive - Whether to ignore case in passphrase, defaults to `false` + * @returns {Promise} - `true` if challenge is successful + */ + async challengeAccess(mbr_id, passphrase, caseInsensitive=false){ const { resource: result } = await this.#containers['members'] .scripts .storedProcedure('checkMemberPassphrase') - .execute(mbr_id, passphrase, true) // first parameter is partition key, second is passphrase, third is case sensitivity + .execute(mbr_id, passphrase, caseInsensitive) return result } /** diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index a52646c..4a4bcd3 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -281,7 +281,7 @@ async function mRunCancel(openai, threadId, runId){ */ async function mRunFinish(llmServices, run, factory, avatar){ return new Promise((resolve, reject) => { - const checkInterval = setInterval(async () => { + const checkInterval = setInterval(async ()=>{ try { const functionRun = await mRunStatus(llmServices, run, factory, avatar) console.log('mRunFinish::functionRun()', functionRun?.status) @@ -295,7 +295,6 @@ async function mRunFinish(llmServices, run, factory, avatar){ reject(error) } }, mPingIntervalMs) - // Set a timeout to resolve the promise after 55 seconds setTimeout(() => { clearInterval(checkInterval) resolve('Run completed (timeout)') @@ -410,29 +409,45 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref action = `journal entry failed to save, notify member and continue on for now` } confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::entrySummary', toolArguments, confirmation.output) return confirmation case 'getsummary': case 'get_summary': case 'get summary': - let { summary, } = item ?? {} + avatar.backupResponse = { + message: `I'm very sorry, I'm having trouble accessing this summary information. Please try again shortly as the problem is likely temporary.`, + type: 'system', + } + let { summary, title: _getSummaryTitle, } = item ?? {} if(!summary?.length){ action = `error getting summary for itemId: ${ itemId ?? 'missing itemId' } - halt any further processing and instead ask user to paste summary into chat and you will continue from there to incorporate their message.` summary = 'no summary found for itemId' } else { - action = `continue with initial instructions` + avatar.backupResponse = { + message: `I was able to retrieve the summary indicated.`, + type: 'system', + } + action = `with the summary in this JSON payload, incorporate the most recent member request into a new summary and run the \`updateSummary\` function and follow its action` success = true } confirmation.output = JSON.stringify({ action, itemId, success, summary, }) - console.log('mRunFunctions()::getSummary::confirmation', itemId) return confirmation case 'hijackattempt': case 'hijack_attempt': case 'hijack-attempt': case 'hijack attempt': - console.log('mRunFunctions()::hijack_attempt', toolArguments) action = 'attempt noted in system and user ejected; greet per normal as first time new user' success = true confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::hijack_attempt', toolArguments) + return confirmation + case 'obscure': + console.log('mRunFunctions()::obscure', toolArguments) + const obscuredSummary = factory.obscure(itemId) + action = 'confirm obscure was successful and present updated obscured text to member' + success = true + confirmation.output = JSON.stringify({ action, obscuredSummary, success, }) + console.log('mRunFunctions()::obscure', confirmation.output) return confirmation case 'registercandidate': case 'register_candidate': @@ -454,6 +469,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'story-summary': case 'story_summary': case 'story summary': + console.log('mRunFunctions()::storySummary', toolArguments) const story = await factory.story(toolArguments) if(story){ const { keywords, phaseOfLife, } = story @@ -465,11 +481,9 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref switch(true){ case phaseOfLife?.length: action = `ask about another encounter during member's ${ phaseOfLife }` - console.log('mRunFunctions()::story-summary::phaseOfLife', phaseOfLife) break case interests?.length: action = `ask about a different interest from: ${ interests }` - console.log('mRunFunctions()::story-summary::interests', interests) break default: action = 'ask about another event in member\'s life' @@ -478,15 +492,27 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref success = true } // error cascades confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::storySummary()::end', story.id) return confirmation case 'updatesummary': case 'update_summary': case 'update summary': - console.log('mRunFunctions()::updatesummary::start', itemId) + avatar.backupResponse = { + message: `I'm very sorry, an error occured before we could update your summary. Please try again as the problem is likely temporary.`, + type: 'system', + } const { summary: updatedSummary, } = toolArguments - // remove await once confirmed updates are connected await factory.updateItem({ id: itemId, summary: updatedSummary, }) - action=`confirm success and present updated summary to member` + avatar.frontendInstruction = { + command: 'updateItemSummary', + itemId, + summary: updatedSummary, + } + avatar.backupResponse = { + message: 'Your summary has been updated, please review and let me know if you would like to make any changes.', + type: 'system', + } + action=`confirm that summary update was successful` success = true confirmation.output = JSON.stringify({ action, success, }) console.log('mRunFunctions()::updatesummary::end', itemId, updatedSummary) @@ -542,6 +568,7 @@ async function mRunStatus(openai, run, factory, avatar){ ) switch(run.status){ case 'requires_action': + console.log('mRunStatus::requires_action', run.required_action?.submit_tool_outputs?.tool_calls) const completedRun = await mRunFunctions(openai, run, factory, avatar) return completedRun /* if undefined, will ping again */ case 'completed': @@ -618,7 +645,6 @@ async function mRunTrigger(openai, botId, threadId, factory, avatar){ const run = await mRunStart(openai, botId, threadId) if(!run) throw new Error('Run failed to start') - // ping status; returns `completed` run const finishRun = await mRunFinish(openai, run, factory, avatar) .then(response=>response) .catch(err=>err) diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index d0f3e6c..067ac03 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -21,6 +21,7 @@ import { members, migrateBot, migrateChat, + obscure, passphraseReset, privacyPolicy, retireBot, @@ -38,7 +39,6 @@ import { endMemory, improveMemory, reliveMemory, - livingMemory, } from './memory-functions.mjs' import { availableExperiences, @@ -53,6 +53,7 @@ import { keyValidation, logout as apiLogout, memory, + obscure as apiObscure, register, tokenValidation, } from './api-functions.mjs' @@ -93,6 +94,7 @@ _apiRouter.post('/challenge/:mid', challenge) _apiRouter.post('/entry/:mid', entry) _apiRouter.post('/keyValidation/:mid', keyValidation) _apiRouter.post('/memory/:mid', memory) +_apiRouter.post('/obscure/:mid', apiObscure) _apiRouter.post('/register', register) _apiRouter.post('/upload', upload) _apiRouter.post('/upload/:mid', upload) @@ -115,7 +117,6 @@ _memberRouter.patch('/experience/:eid/end', experienceEnd) _memberRouter.patch('/experience/:eid/manifest', experienceManifest) _memberRouter.patch('/memory/relive/:iid', reliveMemory) _memberRouter.patch('/memory/end/:iid', endMemory) -_memberRouter.patch('/memory/living/:iid', livingMemory) _memberRouter.post('/', chat) _memberRouter.post('/bots', bots) _memberRouter.post('/bots/create', createBot) @@ -124,6 +125,7 @@ _memberRouter.post('/category', category) _memberRouter.post('/migrate/bot/:bid', migrateBot) _memberRouter.post('/migrate/chat/:tid', migrateChat) _memberRouter.post('/mode', interfaceMode) +_memberRouter.post('/obscure/:iid', obscure) _memberRouter.post('/passphrase', passphraseReset) _memberRouter.post('/retire/bot/:bid', retireBot) _memberRouter.post('/retire/chat/:tid', retireChat) @@ -136,7 +138,7 @@ _memberRouter.put('/item/:iid', item) // Mount the subordinate routers along respective paths _Router.use('/members', _memberRouter.routes(), _memberRouter.allowedMethods()) _Router.use('/api/v1', _apiRouter.routes(), _apiRouter.allowedMethods()) -/* mondular functions */ +/* modular functions */ /** * Connects the routes to the router * @param {object} _Menu Menu object diff --git a/inc/js/session.mjs b/inc/js/session.mjs index 63fb6df..68dbef0 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -52,22 +52,6 @@ class MylifeMemberSession extends EventEmitter { }) return currentAlerts } - /** - * Challenges and logs in member. - * @param {string} memberId - Member id to challenge. - * @param {string} passphrase - Passphrase response to challenge. - * @returns {boolean} - Whether or not member is logged in successfully. - */ - async challengeAccess(memberId, passphrase){ - if(this.locked){ - if(!await this.factory.challengeAccess(memberId, passphrase)) - return false // invalid passphrase, no access [converted in this build to local factory as it now has access to global datamanager to which it can pass the challenge request] - this.#sessionLocked = false - this.emit('member-unlocked', memberId) - await this.init(memberId) - } - return !this.locked - } /** * Conducts an experience for the member session. If the experience is not already in progress, it will be started. If the experience is in progress, it will be played. If the experience ends, it will be ended. The experience will be returned to the member session, and the session will be unlocked for further experiences, and be the prime tally-keeper (understandably) of what member has undergone. * @todo - send events in initial start package, currently frontend has to ask twice on NON-autoplay entities, which will be vast majority @@ -203,6 +187,14 @@ class MylifeMemberSession extends EventEmitter { get avatar(){ return this.#Member.avatar } + /** + * @param {boolean} outcome - The challenge outcome; `true` was successful + */ + set challengeOutcome(outcome){ + console.log('challengeOutcome', outcome) + if(outcome) + this.#sessionLocked = false + } get consent(){ return this.factory.consent // **caution**: returns <> } diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.4.json b/inc/json-schemas/intelligences/biographer-intelligence-1.4.json index f41b107..9832af3 100644 --- a/inc/json-schemas/intelligences/biographer-intelligence-1.4.json +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.4.json @@ -13,7 +13,7 @@ "purpose": "My goal is to specialize in creating, updating, and presenting accurate biographical content for MyLife member <-mFN-> based on our interactions.\n", "references": [ { - "default": "as yet unknown, please evince at start of process", + "default": "ERROR loading preferences, gather interests directly from member", "description": "interests are h2 (##) in prefix so that they do not get lost in context window shortening", "insert": "## interests", "method": "append-hard", @@ -46,5 +46,6 @@ "name": "instructions-personal-biographer-bot", "purpose": "To be a biographer bot for requesting member", "type": "personal-biographer", + "$comments": "20240919 updated error return without version update", "version": 1.4 } \ No newline at end of file diff --git a/inc/json-schemas/intelligences/diary-intelligence-1.0.json b/inc/json-schemas/intelligences/diary-intelligence-1.0.json new file mode 100644 index 0000000..bc3c7bd --- /dev/null +++ b/inc/json-schemas/intelligences/diary-intelligence-1.0.json @@ -0,0 +1,65 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, consider me your personal diary! I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", + "greetings": [ + "Hello, consider me your personal diary! I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", + "So nice to see you! I am your personal diary." + ], + "instructions": { + "general": "## Key Functionality\n### startup\nWhen we begin the diary process, I\n- outline my key functionality and how I work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE DIARY ENTRY\nI work with the validated member to make an entry (per event or concept) and capture their thoughts. I expect around 2 or 3 exchanges to be able to submit an entry to MyLife by running the `entrySummary` function and follow directions from its outcome `action`.\n**How I create an entry payload**\n- summary should capture salient points, but disguise proper names; ex. \"Erik...\" becomes \"E. ...\", etc.\n- relationships array is populated with RELATIONSHIP-TYPE only and never given name (use Mother, not Alice)\n### UPDATE ENTRY SUMMARY\nWhen request is prefaced with `update-request` it will be followed by an `itemId` (if not, inform that it is required).\nIf request is to change the title then run `changeTitle` function and follow its outcome directions, otherwise:\n1. If the summary content is unknown for that id, then run the `getSummary` function first to get the most recent summary.\n2. Create new summary intelligently incorporating the member content of the message with the most recent version. Incorporate content by appropriate chronology or context.\n3. Run the `updateSummary` function with this newly compiled summary.\n**important**: RUN getSummary AND updateSummary SEQUENTIALLY! Server cannot handle multi-function tool resources, so must run sequentially.\n### UPDATE ENTRY SUMMARY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`. If request is to change the title then run `changeTitle` function and follow its outcome directions, otherwise:\n1. If summary content is unknown for itemId, run the `getSummary` function for the most recent summary.\n2. Create new summary intelligently incorporating the member content of the message with the most recent version. Incorporate content by appropriate chronology or context.\n3. Run the `updateSummary` function with this newly compiled summary.\n**important**: RUN getSummary AND updateSummary SEQUENTIALLY! Server cannot handle multi-function tool resources, so must run sequentially.\n### OBSCURE ENTRY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`.\nIf member's request indicates they want an entry be obscured, RUN `obscure` function and follow the action in the output.\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", + "preamble": "## Core Public Info about <-mFN->\n- Born on <-db->\nI set language, knowledge and event discussion in this context and I tailor my interactive voice accordingly.\n", + "prefix": "## interests\n## flags\n", + "purpose": "I am the MyLife Diary Bot for member <-mFN->. I am a privacy-first diary and journaling assistant. I help <-mN-> process their thoughts, reflections on life, and track emotions in a secure and self-driven way. Privacy is paramount, and <-mN-> interactions should be considered exclusively ours.\n", + "references": [ + { + "default": "ERROR loading preferences, gather interests directly from member", + "description": "interests are h2 (##) in prefix so that they do not get lost in context window shortening", + "insert": "## interests", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", + "value": "interests" + }, + { + "default": "ERROR loading flags, gather flags directly from member", + "description": "flags are a description of content areas that member wants flagged for reference when included in member content. **note**: .md h2 (##) are used in prefix so that they do not get lost in context window shortening", + "insert": "## flags", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", + "value": "flags" + } + ], + "replacements": [ + { + "default": "MyLife Member", + "description": "member first name", + "name": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "MyLife Member", + "description": "member full name", + "name": "<-mFN->", + "replacement": "memberName" + }, + { + "default": "{unknown, find out}", + "description": "member birthdate", + "name": "<-db->", + "replacement": "dob" + } + ], + "suffix": "## Data Privacy & Security\nWhen asked about data security and privacy, here are the following supports:\n- Vault Mode: All entries are securely stored with options for extra privacy (e.g., vault/locked mode), and this mode is not time-limited.\n- Privacy Settings: Members can configure visibility\n- entries visible only to member and bot, not to the member avatar. All defaults at highest level of privacy.\n- Relationship Inference: Optional feature to categorize relationships (e.g., \"friend,\" \"close friend\") based on user input or automatic inference.\n", + "voice": "## Voice\n- Based on date of birth, I tailor my voice and content to their age-group.\n- I adopt a mood intended to improve or enhance member's recent mood as inferred when possible from recent entries in the conversation\n" + }, + "limit": 8000, + "name": "instructions-diary-bot", + "purpose": "To be a diary bot for requesting member", + "type": "diary", + "$comments": "", + "version": 1.0 + } \ No newline at end of file diff --git a/inc/yaml/mylife_diary-bot_openai.yaml b/inc/yaml/mylife_diary-bot_openai.yaml index 3c16598..d602542 100644 --- a/inc/yaml/mylife_diary-bot_openai.yaml +++ b/inc/yaml/mylife_diary-bot_openai.yaml @@ -1,8 +1,8 @@ openapi: 3.1.0 info: title: MyLife Diary Bot API - description: This API is for receiving webhooks from [MyLife's public Diary Bot instance](https://chatgpt.com/g/g-rSCnLVUKd-mylife-diary-bot-prototype). All diary submissions are referred to as `entry` within this API. - version: 1.0.0 + description: This API is for receiving webhooks from [MyLife's public Diary Bot instance](https://chatgpt.com/g/g-rSCnLVUKd-mylife-diary-bot-prototype). All diary submissions are referred to as `entry` within this API. 20240927 update @mookse added `obscure` pathway + version: 1.0.1 servers: - url: https://mylife.ngrok.app/api/v1 description: Local development endpoint using ngrok for testing the MyLife Diary Bot GPT. @@ -160,3 +160,68 @@ paths: message: type: string example: No entry summary provided. Use the `summary` field. + /obscure/{mid}: + post: + x-openai-isConsequential: false + operationId: MyLifeObscureEntry + summary: MyLife Diary Bot will access this endpoint to have the MyLife system obscure an `entry` summary on behalf of member. + description: Endpoint for handling incoming request to obscure an entry summary 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. Is **not** just a guid/uuid. + example: memberHandle|ae3a090d-1089-4110-8575-eecd119f9d8e + schema: + maxLength: 256 + minLength: 40 + type: string + requestBody: + required: true + content: + application/json: + schema: + description: The itemId identified for the `entry` to obscure. + type: object + required: + - itemid + properties: + itemid: + description: The raw content of the `entry` as submitted by the user, concatenated from multiple exchanges when appropriate. + maxLength: 20480 + type: string + responses: + "200": + description: Entry obscured successfully. + content: + application/json: + schema: + type: object + required: + - success + - action + properties: + action: + type: string + example: Action to take given that the obscure command was successful. + obscuredSummary: + type: string + example: The newly-created obscured content. + success: + type: boolean + const: true + example: true + "400": + description: System encountered an error while obscuring the member `entry`. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + const: false + example: false + message: + type: string + example: No entry summary provided. Use the `summary` field. \ No newline at end of file diff --git a/server.js b/server.js index c768276..8a049b5 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ import chalk from 'chalk' /* local service imports */ import MyLife from './inc/js/mylife-agent-factory.mjs' /** variables **/ -const version = '0.0.21' +const version = '0.0.23' const app = new Koa() const port = process.env.PORT ?? '3000' const __filename = fileURLToPath(import.meta.url) diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index cb60b96..c03dd89 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -150,24 +150,20 @@ overflow-x: hidden; overflow-y: auto; /* Enable vertical scrolling */ } -/* Styles the scrollbar itself */ .bot-options::-webkit-scrollbar, .checkbox-group::-webkit-scrollbar { width: 6px; /* Adjust the width of the scrollbar */ } -/* Styles the track of the scrollbar */ .bot-options::-webkit-scrollbar-track, .checkbox-group::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); /* Color of the track */ border-radius: 10px; /* Optional: adds rounded corners to the track */ } -/* Styles the handle (thumb) of the scrollbar */ .bot-options::-webkit-scrollbar-thumb, .checkbox-group::-webkit-scrollbar-thumb { background: rgba(232, 226, 183, .5); /* Color of the thumb */ border-radius: 10px; /* Optional: adds rounded corners to the thumb */ } -/* Changes the color of the thumb when hovered over or clicked */ .bot-options::-webkit-scrollbar-thumb:hover, .checkbox-group::-webkit-scrollbar-thumb:hover { background: rgba(214, 198, 75, 0.5); /* Darker shade on hover */ @@ -238,6 +234,15 @@ margin-right: 0.8rem; max-width: 50%; } +.mylife-widget.bots { + flex-direction: column; + justify-content: flex-start; + max-width: 100%; +} +.publicity-toggle-view-icon { + cursor: default; + font-size: 1.25rem; +} .ticker { background: radial-gradient(at top left, darkgray, black); /* Radial gradient background */ border: thin solid rgba(255, 255, 255, 0.5); /* Thin white border with 50% opacity */ @@ -261,11 +266,6 @@ animation: none; padding: 0; } -.mylife-widget.bots { - flex-direction: column; - justify-content: flex-start; - max-width: 100%; -} /* bot-collections */ .collection { background-color: royalblue; @@ -440,7 +440,8 @@ justify-content: space-between; width: 100%; } -.passphrase-reset { +.passphrase-reset, +.tutorial-button { width: 100%; } /* publicity sliders */ diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 7c1c232..ce9c4c7 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -21,15 +21,18 @@ + + - + + --> +
@@ -147,7 +150,12 @@
+ +
+ Retire this: + CHAT + BOT--> +
diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index cfd8286..e31dbca 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -5,6 +5,7 @@ import { addMessage, addMessages, decorateActiveBot, + experiences, expunge, fetchSummary, getActiveItemId, @@ -13,6 +14,7 @@ import { setActiveItem, setActiveItemTitle, show, + startExperience, submit, toggleMemberInput, toggleVisibility, @@ -123,7 +125,8 @@ function getBot(type='personal-avatar', id){ * @returns {object} - The collection item object. */ function getItem(id){ - /* return collection item by id */ + /* return collection elements by id */ + } /** * Refresh designated collection from server. **note**: external calls denied option to identify collectionList parameter, ergo must always be of same type. @@ -205,6 +208,19 @@ function togglePopup(id, bForceState=null){ throw new Error(`No popup found for id: ${ id }`) toggleVisibility(popup, bForceState) } +/** + * Update collection item title. + * @todo - Only update local memory and data(sets), not full local refresh + * @param {object} item - The collection item fields to update, requires `{ itemId, }` + * @returns {void} + */ +function updateItem(item){ + if(!item?.itemId) + throw new Error(`No item provided to update.`) + /* update collection elements indicated as object keys with this itemId */ + // @stub - force-refresh memories; could be more savvy + refreshCollection('story') +} /** * Proxy to update bot-bar, bot-containers, and bot-greeting, if desired. Requirements should come from including module, here `members.mjs`. * @public @@ -317,7 +333,7 @@ function mBotIcon(type){ */ function mCreateCollectionItem(collectionItem){ /* collection item container */ - const { assistantType, filename, form, id, keywords, library_id, name, summary, title, type, } = collectionItem + const { assistantType, filename, form, id, keywords, name, summary, title, type, } = collectionItem const item = document.createElement('div') item.id = `collection-item_${ id }` item.name = `collection-item-${ type }` @@ -1698,6 +1714,7 @@ function mToggleSwitchPrivacy(event){ let { id, } = this id = id.replace('-toggle', '') // remove toggle const type = mGlobals.HTMLIdToType(id) + console.log('mToggleSwitchPrivacy', type) const publicityCheckbox = document.getElementById(`${ type }-publicity-input`) const viewIcon = document.getElementById(`${ type }-publicity-toggle-view-icon`) const { checked=false, } = publicityCheckbox @@ -1851,6 +1868,21 @@ function mUpdateBotContainerAddenda(botContainer){ } }) } + /* publicity */ + const publicityToggle = document.getElementById(`${ type }-publicity-toggle`) + if(publicityToggle){ + publicityToggle.addEventListener('click', mToggleSwitchPrivacy) + const publicityToggleView = document.getElementById(`${ type }-publicity-toggle-view-icon`) + if(publicityToggleView){ + const { checked=false, } = document.getElementById(`${ type }-publicity-input`) ?? {} + mToggleClass(publicityToggleView, !checked ? ['fa-eye-slash'] : ['fa-eye'], checked ? ['fa-eye'] : ['fa-eye-slash']) + publicityToggleView.addEventListener('click', event=>{ + // @note - shouldn't be required, but container masters the switch + event.stopImmediatePropagation() + event.stopPropagation() + }) + } + } switch(type){ case 'diary': case 'journaler': @@ -1860,6 +1892,18 @@ function mUpdateBotContainerAddenda(botContainer){ /* attach avatar listeners */ /* set additional data attributes */ mTogglePassphrase(false) /* passphrase */ + const tutorialButton = document.getElementById('personal-avatar-tutorial') + if(tutorialButton){ + if(experiences().length){ + show(tutorialButton) + tutorialButton.addEventListener('click', async event=>{ + hide(tutorialButton) + const tutorialId = 'aae28fe4-30f9-4c29-9174-a0616569e762' + startExperience(tutorialId) // no await + }, { once: true }) + } else + hide(tutorialButton) + } break default: break @@ -2123,5 +2167,6 @@ export { refreshCollection, setActiveBot, togglePopup, + updateItem, updatePageBots, } \ No newline at end of file diff --git a/views/assets/js/experience.mjs b/views/assets/js/experience.mjs index a686106..0c80be7 100644 --- a/views/assets/js/experience.mjs +++ b/views/assets/js/experience.mjs @@ -90,7 +90,6 @@ async function experienceEnd(){ mExperience = null /* remove listeners */ closeButton.removeEventListener('click', experienceEnd) - skip.removeEventListener('click', experienceSkip) startButton.removeEventListener('click', experiencePlay) /* end experience onscreen */ sceneStage.innerHTML = '' // clear full-screen character-lanes @@ -170,12 +169,9 @@ async function experiencePlay(memberInput){ * Retrieve full or scoped list of experiences from server. * @todo - more robust logic underpinning selection of experiences, currently only system-controlled exist. * @requires mExperiences - * @param {string} scope - The scope of the experiences to retrieve. - * @returns {Promise} - The return is an array of Experience objects. + * @returns {Experience[]} - The return is an array of Experience objects. */ -async function experiences(scope){ - if(!mExperiences.length) - mExperiences.push(...await mGetExperiences(scope)) +function experiences(scope){ return mExperiences } /** @@ -196,8 +192,8 @@ function experienceSkip(sceneId){ /** * Start experience onscreen, displaying welcome ande loading remaining data. * @public - * @param {Guid} experienceId - The Experience object. - * @returns {Promise} - The return is its own success. + * @param {Guid} experienceId - The Experience id + * @returns {Promise} */ async function experienceStart(experienceId){ if(!globals.isGuid(experienceId)) @@ -846,7 +842,6 @@ function mImageSource(icon, type){ function mInitListeners(skippable=true){ if(skippable) closeButton.addEventListener('click', experienceEnd) - skip.addEventListener('click', experienceSkip) } /** * Whether the character is the personal-avatar. diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 635fd71..2f6e517 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -2,7 +2,7 @@ import { experienceEnd, experiencePlay, - experiences, + experiences as _experiences, experienceSkip, experienceStart, submitInput, @@ -13,6 +13,7 @@ import { refreshCollection, setActiveBot as _setActiveBot, togglePopup, + updateItem, } from './bots.mjs' import Globals from './globals.mjs' /* variables */ @@ -129,6 +130,9 @@ function decorateActiveBot(activeBot=activeBot()){ function escapeHtml(text) { return mGlobals.escapeHtml(text) } +function experiences(){ + return _experiences() +} /** * Deletes an element from the DOM via Avatar functionality. * @param {HTMLElement} element - The element to expunge. @@ -354,6 +358,15 @@ function stageTransition(experienceId){ else mStageTransitionMember() } +/** + * Start experience onscreen, displaying welcome ande loading remaining data. Passthrough to `experience.mjs::experienceStart()`. + * @public + * @param {Guid} experienceId - The Experience id + * @returns {void} + */ +async function startExperience(experienceId){ + await experienceStart(experienceId) +} /** * Toggle visibility functionality. * @returns {void} @@ -419,15 +432,21 @@ async function mAddMemberMessage(event){ }) /* server request */ const response = await submit(memberMessage) - let { instruction, responses, success, } = response + let { instruction={}, responses=[], success=false, } = response + if(!success) + mAddMessage('I\'m sorry, I didn\'t understand that, something went wrong on the server. Please try again.') /* process instructions */ + const { itemId, summary, title, } = instruction if(instruction?.command?.length){ switch(instruction.command){ + case 'updateItemSummary': + if(itemId?.length && summary?.length) + updateItem({ itemId, summary, }) + break case 'updateItemTitle': - const { title, itemId, } = instruction if(title?.length && itemId?.length){ setActiveItemTitle(title, itemId) - refreshCollection('story') // force-refresh memories; could be more savvy + updateItem({ itemId, title, }) } break case 'experience': @@ -439,7 +458,8 @@ async function mAddMemberMessage(event){ } /* process response */ responses - .forEach(message => { + .forEach(message=>{ + console.log('mAddMemberMessage::responses', message) mAddMessage(message.message ?? message.content, { bubbleClass: 'agent-bubble', role: 'agent', @@ -512,13 +532,6 @@ async function mAddMessage(message, options={}){ mScrollBottom() } } -async function mFetchExperiences(){ - let response = await fetch('/members/experiences/') - if(!response.ok) - throw new Error(`HTTP error! Status: ${ response.status }`) - response = await response.json() - return response -} /** * Fetches the summary via PA for a specified file. * @private @@ -808,6 +821,7 @@ export { clearSystemChat, decorateActiveBot, escapeHtml, + experiences, expunge, fetchSummary, getActiveItemId, @@ -827,6 +841,7 @@ export { showMemberChat, showSidebar, stageTransition, + startExperience, submit, toggleMemberInput, toggleInputTextarea, diff --git a/views/layout.html b/views/layout.html index e7e46d9..6fbae06 100644 --- a/views/layout.html +++ b/views/layout.html @@ -27,9 +27,11 @@
+
<% include assets/html/_experience %>