From 487ff9f5471cbfc260b4699297f112a33dc29ef7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:17:31 -0400 Subject: [PATCH 01/54] 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix --- .../class-extenders.mjs | 4 +- inc/js/functions.mjs | 63 +- inc/js/globals.mjs | 39 + inc/js/memory-functions.mjs | 51 ++ inc/js/mylife-agent-factory.mjs | 241 +++++- inc/js/mylife-avatar.mjs | 342 ++++++-- inc/js/mylife-llm-services.mjs | 92 ++- inc/js/routes.mjs | 18 + .../openai/functions/getSummary.json | 17 + .../openai/functions/updateSummary.json | 22 + views/assets/css/bots.css | 100 ++- views/assets/css/main.css | 18 + views/assets/html/_bots.html | 6 +- views/assets/js/bots.mjs | 741 +++++++++++++----- views/assets/js/globals.mjs | 42 +- views/assets/js/members.mjs | 97 ++- .../{journaler-thumb.png => diary-thumb.png} | Bin 17 files changed, 1522 insertions(+), 371 deletions(-) create mode 100644 inc/js/memory-functions.mjs create mode 100644 inc/json-schemas/openai/functions/getSummary.json create mode 100644 inc/json-schemas/openai/functions/updateSummary.json rename views/assets/png/{journaler-thumb.png => diary-thumb.png} (100%) diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 9d33a17..9fbeec6 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -209,8 +209,8 @@ function extendClass_conversation(originClass, referencesObject) { get botId(){ return this.bot_id } - set botId(_botId){ - this.#botId = _botId + set botId(botId){ + this.#botId = botId } get isSaved(){ return this.#saved diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 00bc451..8d23f0e 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -23,6 +23,7 @@ function activateBot(ctx){ } async function alerts(ctx){ // @todo: put into ctx the _type_ of alert to return, system use dataservices, member use personal + const { MemberSession, } = ctx.state if(ctx.params?.aid){ // specific system alert ctx.body = await ctx.state.MemberSession.alert(ctx.params.aid) } else { // all system alerts @@ -100,17 +101,18 @@ async function collections(ctx){ ctx.body = await avatar.collections(ctx.params.type) } async function createBot(ctx){ - const { team, type, } = ctx.request.body + const { teamId, type, } = ctx.request.body const { avatar, } = ctx.state - const bot = { type, } // `type` only requirement to create a known, MyLife-typed bot + const bot = { teams: [], type, } // `type` only requirement to create a known, MyLife-typed bot + if(teamId?.length) + bot.teams.push(teamId) ctx.body = await avatar.createBot(bot) } /** * Delete an item from collection via the member's avatar. * @async * @public - -* @param {object} ctx - Koa Context object + * @param {object} ctx - Koa Context object * @returns {boolean} - Under `ctx.body`, status of deletion. */ async function deleteItem(ctx){ @@ -219,6 +221,24 @@ async function privacyPolicy(ctx){ ctx.state.subtitle = `Effective Date: 2024-01-01` await ctx.render('privacy-policy') // privacy-policy } +async function shadow(ctx){ + const { avatar, } = ctx.state + const { active=true, botId, itemId, message, role, threadId, shadowId, title, } = ctx.request.body // necessary to flip active bot, or just presume to use the creator of the shadow? + if(!itemId?.length) + ctx.throw(400, `missing item id`) + if(!active) // @stub - redirect to normal chat? + ctx.throw(400, `shadow must be active`) + ctx.body = await avatar.shadow(shadowId, itemId, title, message) +} +/** + * Gets the list of shadows. + * @returns {Object[]} - Array of shadow objects. + */ +async function shadows(ctx){ + const { avatar, } = ctx.state + const response = await avatar.shadows() + ctx.body = response +} async function signup(ctx) { const { avatarName, email, humanName, type='newsletter', } = ctx.request.body const signupPacket = { @@ -272,6 +292,36 @@ async function summarize(ctx){ throw new Error('Only logged in members may summarize text') ctx.body = await avatar.summarize(fileId, fileName) } +/** + * Get a specified team, its details and bots, by id for the member. + * @param {Koa} ctx - Koa Context object + * @returns {object} - Team object + */ +async function team(ctx){ + const { tid, } = ctx.params + if(!tid?.length) + ctx.throw(400, `missing team id`) + const { avatar, } = ctx.state + ctx.body = await avatar.team(tid) +} +/** + * Get a list of available teams and their default details. + * @param {Koa} ctx - Koa Context object. + * @returns {Object[]} - List of team objects. + */ +function teams(ctx){ + const { avatar, } = ctx.state + ctx.body = avatar.teams() +} +async function updateBotInstructions(ctx){ + const { botId, } = ctx.request.body + const { avatar, } = ctx.state + let success = false + const bot = await avatar.updateBot(botId, { instructions: true, model: true, tools: true, }) + if(bot) + success = true + ctx.body = { bot, success, } +} /** * Proxy for uploading files to the API. * @param {Koa} ctx - Koa Context object @@ -307,7 +357,12 @@ export { members, passphraseReset, privacyPolicy, + shadow, + shadows, signup, summarize, + team, + teams, + updateBotInstructions, upload, } \ No newline at end of file diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index f1c7683..0eba89b 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -56,6 +56,23 @@ const mAiJsFunctions = { ] } }, + getSummary: { + description: "Gets a story summary by itemId", + name: "getSummary", + parameters: { + type: "object", + properties: { + itemId: { + description: "Id of summary to retrieve", + format: "uuid", + type: "string" + } + }, + required: [ + "itemId" + ] + } + }, storySummary: { description: 'Generate a STORY summary with keywords and other critical data elements.', name: 'storySummary', @@ -120,6 +137,28 @@ const mAiJsFunctions = { ] } }, + updateSummary: { + description: "Updates a story summary (in total) as referenced by itemId", + name: "updateSummary", + parameters: { + type: "object", + properties: { + itemId: { + description: "Id of summary to update", + format: "uuid", + type: "string" + }, + summary: { + description: "The new updated and complete summary", + type: "string" + } + }, + required: [ + "itemId", + "title" + ] + } + }, } const mEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ // regex for email validation const mGuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i // regex for GUID validation diff --git a/inc/js/memory-functions.mjs b/inc/js/memory-functions.mjs new file mode 100644 index 0000000..3131c95 --- /dev/null +++ b/inc/js/memory-functions.mjs @@ -0,0 +1,51 @@ +/* imports */ +import { + activateBot, + interfaceMode, // @stub - deprecate? + upload, +} from './functions.mjs' +/* module export functions */ +async function collectMemory(ctx){ + // @todo - implement memory collection +} +async function improveMemory(ctx){ + const { iid } = ctx.params + const { Globals, MyLife, } = ctx + const { avatar, } = ctx.state + if(!Globals.isValidGuid(iid)) + return ctx.throw(400, 'Invalid Item ID') + ctx.body = await avatar.reliveMemory(iid) +} +/** + * Reliving a memory is a unique MyLife `experience` that allows a user to relive a memory from any vantage they choose. The bot by default will: + * @param {Koa} ctx - Koa context object. + * @returns {Promise} - livingMemory engagement object (i.e., includes frontend parameters for engagement as per instructions for included `portrayMemory` function in LLM-speak) + */ +async function reliveMemory(ctx){ + const { iid } = ctx.params + const { Globals, MyLife, } = ctx + const { avatar, } = ctx.state + if(!Globals.isValidGuid(iid)) + return ctx.throw(400, 'Invalid Item ID') + ctx.body = await avatar.reliveMemory(iid) +} +/** + * Living a shared memory is a unique MyLife `experience` that allows a user to relive a memory from any vantage the "author/narrator" chooses. In fact, much of the triggers and dials on how to present the experience of a shared memory is available and controlled by the member, and contained and executed by the biographer bot for the moment through this func6ion. Ultimately the default bot could be switched, in which case, information retrieval may need ways to contextualize pushbacks (floabt, meaning people asking questions about the memory that are not answerable by the summar itself, and 1) _may_ be answerable by another bot, such as biogbot, or 2) is positioned as a piece of data to "improve" or flesh out memories... Remember on this day in 2011, what did you have to eat on the boardwalk? Enquiring minds want to know!) + * @param {Koa} ctx - Koa context object. + * @returns {Promise} - livingMemory object. + */ +async function livingMemory(ctx){ + const { iid } = ctx.params + const { Globals, MyLife, } = ctx + const { avatar, } = ctx.state + if(!Globals.isValidGuid(iid)) + return ctx.throw(400, 'Invalid Item ID') + ctx.body = await avatar.livingMemory(iid) +} +/* exports */ +export { + collectMemory, + improveMemory, + 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 5b1eb29..8024372 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -41,10 +41,144 @@ const mExcludeProperties = { name: true } const mLLMServices = new LLMServices() +const mMyLifeTeams = [ + { + active: true, + allowCustom: true, + allowedTypes: ['artworks', 'editor', 'idea', 'library', 'marketing'], + defaultTypes: ['artworks', 'idea', 'library',], + description: 'The Creative Team is dedicated to help you experience productive creativity sessions.', + id: '84aa50ca-fb64-43d8-b140-31d2373f3cd2', + name: 'creative', + title: 'Creative', + }, + { + active: false, + allowCustom: false, + allowedTypes: ['fitness', 'health', 'insurance', 'medical', 'prescriptions', 'yoga', 'nutrition',], + defaultTypes: ['fitness', 'health', 'medical',], + description: 'The Health Team is dedicated to help you manage your health and wellness.', + id: '238da931-4c25-4868-928f-5ad1087a990b', + name: 'health', + title: 'Health', + }, + { + active: true, + allowCustom: true, + allowedTypes: ['diary', 'journaler', 'library', 'personal-biographer',], + defaultTypes: ['personal-biographer', 'library',], + description: 'The Memoir Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', + id: 'a261651e-51b3-44ec-a081-a8283b70369d', + name: 'memoir', + title: 'Memoir', + }, + { + active: false, + allowCustom: true, + allowedTypes: ['personal-assistant', 'idea', 'note', 'resume', 'scheduler', 'task',], + defaultTypes: ['personal-assistant', 'resume', 'scheduler',], + description: 'The Professional Team is dedicated to your professional success.', + id: '5b7c4109-4985-4d98-b59b-e2c821c3ea28', + name: 'professional', + title: 'Professional', + }, + { + active: false, + allowCustom: true, + allowedTypes: ['connection', 'experience', 'social', 'relationship',], + defaultTypes: ['connection', 'relationship',], + description: 'The Social Team is dedicated to help you connect with others.', + id: 'e8b1f6d0-8a3b-4f9b-9e6a-4e3c5b7e3e9f', + name: 'social', + title: 'Social', + }, + { + active: false, + allowCustom: true, + allowedTypes: ['library', 'note', 'poem', 'quote', 'religion',], + defaultTypes: ['library', 'quote', 'religion',], + description: 'The Spirituality Team is dedicated to help you creatively explore your spiritual side.', + id: 'bea7bb4a-a339-4026-ad1c-75f604dc3349', + name: 'sprituality', + title: 'Spirituality', + }, + { + active: true, + allowCustom: true, + allowedTypes: ['data-ownership', 'investment', 'library', 'ubi',], + defaultTypes: ['library', 'ubi'], + description: 'The Universal Basic Income (UBI) Team is dedicated to helping you tailor your MyLife revenue streams based upon consensual access to your personal MyLife data.', + id: '8a4d7340-ac62-40f1-8c77-f17c68797925', + name: 'ubi', + title: 'UBI', + } +] const mNewGuid = ()=>Guid.newGuid().toString() const mPath = './inc/json-schemas' const mReservedJSCharacters = [' ', '-', '!', '@', '#', '%', '^', '&', '*', '(', ')', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', "'", '<', '>', ',', '.', '?', '/', '~', '`'] const mReservedJSWords = ['break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'enum', 'await', 'implements', 'package', 'protected', 'interface', 'private', 'public', 'null', 'true', 'false', 'let', 'static'] +const mShadows = [ + { + being: 'shadow', + categories: ['world events'], + form: 'story', + id: 'e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', + name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', + proxy: '/shadow', + text: `What was happening in the world at the time?`, + type: 'agent', + }, + { + being: 'shadow', + categories: ['personal', 'residence'], + form: 'story', + id: '0087b3ec-956e-436a-9272-eceed5e97ad0', + name: 'shadow_0087b3ec-956e-436a-9272-eceed5e97ad0', + proxy: '/shadow', + text: `At the time, I was living at...`, + type: 'member', + }, + { + being: 'shadow', + categories: ['relations',], + form: 'story', + id: '0aac1ca3-a9d2-4587-ad9f-3e85e5391f44', + name: 'shadow_0aac1ca3-a9d2-4587-ad9f-3e85e5391f44', + proxy: '/shadow', + text: `Some people involved were...`, + type: 'member', + }, + { + being: 'shadow', + categories: ['reflection', 'personal'], + form: 'story', + id: '040850c1-9991-46be-b962-8cf4ad9cfb24', + name: 'shadow_040850c1-9991-46be-b962-8cf4ad9cfb24', + proxy: '/shadow', + text: `In hindsight, I wish I had...`, + type: 'member', + }, + { + being: 'shadow', + categories: ['personal', 'thoughts'], + form: 'story', + id: '447b70e7-a443-4165-becf-fbd74265a618', + name: 'shadow_447b70e7-a443-4165-becf-fbd74265a618', + proxy: '/shadow', + text: `I remember thinking...`, + type: 'member', + }, + { + being: 'shadow', + categories: ['observational', 'objectivity', 'reflection'], + form: 'story', + id: '3bfebafb-7e44-4236-86c3-938e2f42fdd7', + name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', + proxy: '/shadow', + text: `What would a normal person have done in this situation?`, + type: 'agent', + }, +] // **note**: members use shadows to help them add content to the summaries of their experiences, whereas agents return the requested content const vmClassGenerator = vm.createContext({ exports: {}, console: console, @@ -142,15 +276,17 @@ class BotFactory extends EventEmitter{ * @param {string} type - The bot type. * @returns {object} - The bot instructions. */ - botInstructions(botType){ - if(!botType) - throw new Error('bot type required') - if(!mBotInstructions[botType]){ - if(!botInstructions) - throw new Error(`no bot instructions found for ${ botType }`) - mBotInstructions[botType] = botInstructions - } - return mBotInstructions[botType] + botInstructions(type='personal-avatar'){ + return mBotInstructions[type] + } + /** + * Returns bot instructions version. + * @param {string} type - The bot type. + * @returns {number} - The bot instructions version. + */ + botInstructionsVersion(type){ + return mBotInstructions[type]?.version + ?? 1.0 } /** * Gets a member's bots. @@ -182,7 +318,7 @@ class BotFactory extends EventEmitter{ return await this.dataservices.collections(type) } async createBot(assistantData={ type: mDefaultBotType }){ - const bot = await mCreateBot(this.#llmServices, this, assistantData, this.avatarId) + const bot = await mCreateBot(this.#llmServices, this, assistantData) if(!bot) throw new Error('bot creation failed') return bot @@ -315,6 +451,14 @@ class BotFactory extends EventEmitter{ throw new Error('Passphrase required for reset.') return await this.dataservices.resetPassphrase(passphrase) } + /** + * Gets the list of shadows. + * @param {Guid} itemId - The itemId (or type?) to filter shadow return. + * @returns {object[]} - The shadows. + */ + async shadows(itemId){ + return mShadows + } /** * Gets a collection of stories of a certain format. * @todo - currently disconnected from a library, but no decisive mechanic for incorporating library shell. @@ -327,6 +471,22 @@ class BotFactory extends EventEmitter{ [{ name: '@form', value: form }], ) } + /** + * Gets a MyLife Team by id. + * @param {Guid} teamId - The Team id. + * @returns {object} - The Team. + */ + team(teamId){ + return mMyLifeTeams + .find(team=>team.id===teamId) + } + /** + * Retrieves list of MyLife Teams. + * @returns {object[]} - The array of MyLife Teams. + */ + teams(){ + return mMyLifeTeams.filter(team=>team.active ?? false) + } /** * Adds or updates a bot data in MyLife database. Note that when creating, pre-fill id. * @public @@ -475,7 +635,7 @@ class AgentFactory extends BotFactory { * @param {object} item - The item to create. * @returns {object} - The created item. */ - async createIten(item){ + async createItem(item){ const response = await this.dataservices.pushItem(item) return response } @@ -497,6 +657,14 @@ 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 { assistantType='journaler', @@ -700,8 +868,8 @@ class AgentFactory extends BotFactory { * @returns {Promise} - The updated item. */ async updateItem(item){ - const { id, } = item - const response = await this.dataservices.patch(id, item) + const { id, ..._item } = item + const response = await this.dataservices.patch(id, _item) return response } /* getters/setters */ @@ -920,7 +1088,7 @@ class MyLifeFactory extends AgentFactory { async function mAI_openai(llmServices, bot){ const { bot_name, type, } = bot bot.name = bot_name - ?? `untitled-${ type }` + ?? `My ${ type }` return await llmServices.createBot(bot) } function assignClassPropertyValues(propertyDefinition){ @@ -1047,21 +1215,20 @@ async function mCreateBotLLM(llm, assistantData){ */ async function mCreateBot(llm, factory, bot){ /* initial deconstructions */ - const { bot_name: botName, description: botDescription, instructions: botInstructions, name: botDbName, type, } = bot + const { bot_name: botName, description: botDescription, name: botDbName, type, } = bot const { avatarId, } = factory /* validation */ if(!avatarId) throw new Error('avatar id required to create bot') /* constants */ const bot_name = botName - ?? `unknown-${ type }` + ?? `My ${ type }` const description = botDescription ?? `I am a ${ type } for ${ factory.memberName }` - const instructions = botInstructions - ?? mCreateBotInstructions(factory, bot) + const { instructions, version, } = mCreateBotInstructions(factory, bot) const model = process.env.OPENAI_MODEL_CORE_BOT ?? process.env.OPENAI_MODEL_CORE_AVATAR - ?? 'gpt-3.5-turbo' + ?? 'gpt-4o' const name = botDbName ?? `bot_${ type }_${ avatarId }` const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, factory.vectorstoreId) @@ -1081,6 +1248,7 @@ async function mCreateBot(llm, factory, bot){ tools, tool_resources, type, + version, } /* create in LLM */ const botId = await mCreateBotLLM(llm, assistantData) // create after as require model @@ -1098,20 +1266,24 @@ async function mCreateBot(llm, factory, bot){ * @private * @param {BotFactory} factory - Factory object * @param {object} bot - Bot object - * @returns {string} - flattened string of instructions + * @returns {object} - minor */ 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 - const instructionSet = factory.botInstructions(type)?.instructions // no need to wait, should be updated or refresh server - if(!instructionSet) // @stub - custom must have instruction loophole + let { instructions, 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=[], } = instructionSet + let { general, purpose, preamble, prefix, references=[], replacements=[], } = instructions /* compile instructions */ - let instructions switch(type){ + case 'diary': case 'journaler': + instructions = purpose + + prefix + + general + break case 'personal-avatar': instructions = preamble + general @@ -1170,7 +1342,7 @@ function mCreateBotInstructions(factory, bot){ break } }) - return instructions + return { instructions, version, } } function mExposedSchemas(factoryBlockedSchemas){ const _systemBlockedSchemas = ['dataservices','session'] @@ -1309,6 +1481,7 @@ function mGetAIFunctions(type, globals, vectorstoreId){ tool_resources, tools = [] switch(type){ + case 'diary': case 'journaler': tools.push(globals.getGPTJavascriptFunction('entrySummary')) includeSearch = true @@ -1318,7 +1491,11 @@ function mGetAIFunctions(type, globals, vectorstoreId){ includeSearch = true break case 'personal-biographer': - tools.push(globals.getGPTJavascriptFunction('storySummary')) + tools.push( + globals.getGPTJavascriptFunction('storySummary'), + globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('updateSummary') + ) includeSearch = true break default: @@ -1515,9 +1692,11 @@ async function mLoadSchemas(){ } async function mPopulateBotInstructions(){ const instructionSets = await mDataservices.botInstructions() - instructionSets.forEach(_instructionSet=>{ - mBotInstructions[_instructionSet.type] = _instructionSet - }) + instructionSets + .forEach(instructionSet=>{ + const { type, } = instructionSet + mBotInstructions[type] = instructionSet + }) } /** * Ingests a text (or JSON-parsed) schema and returns an array of sanitized schema. @@ -1650,9 +1829,9 @@ async function mUpdateBot(factory, llm, bot, options={}){ if(!factory.globals.isValidGuid(id)) throw new Error('bot `id` required in bot argument: `{ id: guid }`') if(updateInstructions){ - // @stub - update core based on updatebot? type-interests as example? yes. - const instructions = mCreateBotInstructions(factory, bot) + const { instructions, version=1.0, } = mCreateBotInstructions(factory, bot) botData.instructions = instructions + botData.version = version /* omitted from llm, but appears on updateBot */ } if(updateTools){ const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, factory.vectorstoreId) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index b400cd1..d7d43a2 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -38,6 +38,7 @@ class Avatar extends EventEmitter { #mode = 'standard' // interface-mode from module `mAvailableModes` #nickname // avatar nickname, need proxy here as getter is complex #proxyBeing = 'human' + #relivingMemories = [] // array of active reliving memories, with items, maybe conversations, included /** * @constructor * @param {Object} obj - The data object from which to create the avatar @@ -72,7 +73,7 @@ class Avatar extends EventEmitter { this.nickname = this.nickname ?? this.names?.[0] ?? `${this.memberFirstName ?? 'member'}'s avatar` /* create evolver (exclude MyLife) */ this.#bots = await this.#factory.bots(this.id) - let activeBot = this.avatarBot + let activeBot = this.avatar if(!this.isMyLife){ /* bot checks */ const requiredBotTypes = ['library', 'personal-avatar', 'personal-biographer',] @@ -85,7 +86,7 @@ class Avatar extends EventEmitter { } } )) - activeBot = this.avatarBot // second time is a charm + activeBot = this.avatar // second time is a charm this.activeBotId = activeBot.id this.#llmServices.botId = activeBot.bot_id /* conversations */ @@ -119,11 +120,12 @@ class Avatar extends EventEmitter { /** * Get a bot. * @public - * @param {string} _bot_id - The bot id. + * @async + * @param {Guid} id - The bot id. * @returns {object} - The bot. */ - async bot(_bot_id){ - return await this.#factory.bot(_bot_id) + async bot(id){ + return await this.#factory.bot(id) } /** * Processes and executes incoming chat request. @@ -236,12 +238,15 @@ class Avatar extends EventEmitter { * @public * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, etc.; defaults to `chat`. * @param {string} threadId - The openai thread id. + * @param {string} botId - The bot id. + * @param {boolean} saveToConversations - Whether to save the conversation to local memory; certain system and memory actions will be saved in their own threads. * @returns {Conversation} - The conversation object. */ - async createConversation(type='chat', threadId){ + async createConversation(type='chat', threadId, botId=this.activeBotId, saveToConversations=true){ const thread = await this.#llmServices.thread(threadId) - const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type, }, this.#factory, thread, this.activeBotId) // guid only - this.#conversations.push(conversation) + const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type, }, this.#factory, thread, botId) + if(saveToConversations) + this.#conversations.push(conversation) return conversation } /** @@ -254,7 +259,12 @@ class Avatar extends EventEmitter { async deleteItem(id){ if(this.isMyLife) throw new Error('MyLife avatar cannot delete items.') - return await this.factory.deleteItem(id) + return await this.#factory.deleteItem(id) + } + async endMemory(id){ + const item = this.relivingMemories.find(item=>item.id===id) + /* save conversation fragments */ + return true } /** * Ends an experience. @@ -450,7 +460,6 @@ class Avatar extends EventEmitter { * @returns {Promise} - Returns void if item created successfully. */ async item(item, method){ - console.log('item()', item, method) const { globals, } = this let { id, } = item let success = false @@ -461,9 +470,9 @@ class Avatar extends EventEmitter { success = item ?? success break case 'post': /* create */ - console.log('item()::post', item) item = await this.#factory.createItem(item) - success = this.globals.isValidGuid(item?.id) + id = item?.id + success = this.globals.isValidGuid(id) break case 'put': /* update */ if(globals.isValidGuid(id)){ @@ -476,7 +485,7 @@ class Avatar extends EventEmitter { break } return { - item, + item: mPruneItem(item), success, } } @@ -491,6 +500,20 @@ class Avatar extends EventEmitter { delete registration.passphrase return registration } + /** + * Reliving a memory is a unique MyLife `experience` that allows a user to relive a memory from any vantage they choose. + * @param {Guid} iid - The item id. + * @returns {Object} - livingMemory engagement object (i.e., includes frontend parameters for engagement as per instructions for included `portrayMemory` function in LLM-speak): { error, inputs, itemId, messages, processingBotId, success, } + */ + async reliveMemory(iid){ + const item = await this.#factory.item(iid) + const { id, } = item + if(!id) + throw new Error(`item does not exist in member container: ${ iid }`) + /* develop narration */ + const narration = await mReliveMemoryNarration(this, this.#factory, this.#llmServices, this.biographer, item) + return narration // include any required .map() pruning + } /** * Allows member to reset passphrase. * @param {string} passphrase @@ -503,6 +526,64 @@ class Avatar extends EventEmitter { throw new Error('Passphrase required for reset.') return await this.#factory.resetPassphrase(passphrase) } + /** + * Takes a shadow message and sends it to the appropriate bot for response. + * @param {Guid} shadowId - The shadow id. + * @param {Guid} itemId - The item id. + * @param {string} title - The title of the original summary. + * @param {string} message - The member (interacting with shadow) message content. + * @returns {Object} - The response object { error, itemId, messages, processingBotId, success, } + */ + async shadow(shadowId, itemId, title, message){ + const processingStartTime = Date.now() + const shadows = await this.shadows() + const shadow = shadows.find(shadow=>shadow.id===shadowId) + if(!shadow) + throw new Error('Shadow not found.') + const { text, type, } = shadow + const item = await this.#factory.item(itemId) + if(!item) + throw new Error(`cannot find item: ${ itemId }`) + const { form, summary, } = item + let tailgate + const bot = this?.[form] ?? this.activeBot /* currently only `biographer` which transforms thusly when referenced here as this[form] */ + switch(type){ + case 'member': + message = `update-memory-request: itemId=${ itemId }\n` + message + break + case 'agent': + // @stub - develop additional form types, entry or idea for instance + const dob = new Date(this.#factory.dob) + const diff_ms = Date.now() - dob.getTime() + const age_dt = new Date(diff_ms) + const age = Math.abs(age_dt.getUTCFullYear() - 1970) + message = `Given age of member: ${ age } and updated summary of personal memory: ${ summary }\n- answer the question: "${ text }"` + tailgate = { + content: `Would you like to add this, or part of it, to your memory?`, // @stub - tailgate for additional data + thread_id: bot.thread_id, + } + break + default: + break + } + let messages = await mCallLLM(this.#llmServices, bot, message, this.#factory, this) + messages = messages.map(message=>mPruneMessage(bot, message, 'shadow', processingStartTime)) + if(tailgate?.length) + messages.push(mPruneMessage(bot, tailgate, 'system')) + return { + itemId, + messages, + processingBotId: bot.id, + success: true, + } + } + /** + * Gets the list of shadows. + * @returns {Object[]} - Array of shadow objects. + */ + async shadows(){ + return await this.#factory.shadows() + } /** * Summarize the file indicated. * @param {string} fileId @@ -538,6 +619,41 @@ class Avatar extends EventEmitter { } return response } + /** + * Get a specified team, its details and _instanced_ bots, by id for the member. + * @param {Koa} ctx - Koa Context object + * @returns {object} - Team object + */ + async team(teamId){ + const team = this.#factory.team(teamId) + const { allowedTypes=[], defaultTypes=[], type, } = team + const teamBots = this.bots + .filter(bot=>bot?.teams?.includes(teamId)) + for(const type of defaultTypes){ + let bot = teamBots.find(bot=>bot.type===type) + if(!bot){ + bot = this.bots.find(bot=>bot.type===type) + if(bot){ // local conscription + bot.teams = [...bot?.teams ?? [], teamId,] + await this.updateBot(bot) // save Cosmos no await + } else { // create + const teams = [teamId,] + bot = await this.createBot({ teams, type, }) + } + } else continue // already in team + if(bot) + teamBots.push(bot) + } + team.bots = teamBots + return team + } + /** + * Get a list of available teams and their default details. + * @returns {Object[]} - List of team objects. + */ + teams(){ + return this.#factory.teams() + } async thread_id(){ if(!this.#conversations.length){ await this.createConversation() @@ -558,8 +674,8 @@ class Avatar extends EventEmitter { await this.#assetAgent.init(this.#factory.vectorstoreId, includeMyLife) /* or "re-init" */ await this.#assetAgent.upload(files) const { response, vectorstoreId: newVectorstoreId, vectorstoreFileList, } = this.#assetAgent - if(!vectorstoreId && newVectorstoreId) - this.updateTools() + if(!vectorstoreId && newVectorstoreId) // ensure access for LLM + this.updateInstructions(this.activeBot.id, false, false, true) return { uploads: files, files: vectorstoreFileList, @@ -572,48 +688,29 @@ class Avatar extends EventEmitter { * @returns {object} - The updated bot. */ async updateBot(bot){ - bot = await mBot(this.#factory, this, bot) - /* add/update bot */ - if(!this.#bots.some(_bot => _bot.id === bot.id)) - this.#bots.push(bot) - else - this.#bots = this.#bots.map(_bot=>_bot.id===bot.id ? bot : _bot) - return bot + return await mBot(this.#factory, this, bot) // note: mBot() updates `avatar.bots` } /** - * Update tools for bot-assistant based on type. - * @todo - manage issue that several bots require attachment upon creation. - * @todo - move to llm code. - * @todo - allow for multiple types simultaneously. - * @param {string} bot_id - The bot id to update tools for. - * @param {string} type - The type of tool to update. - * @param {any} data - The data required by the switch type. - * @returns {void} + * Update core for bot-assistant based on type. Default updates all LLM pertinent properties. + * @param {string} id - The id of bot to update. + * @param {boolean} includeInstructions - Whether to include instructions in the update. + * @param {boolean} includeModel - Whether to include model in the update. + * @param {boolean} includeTools - Whether to include tools in the update. + * @returns {object} - The updated bot object. */ - async updateTools(bot_id=this.avatarBot.bot_id, type='file_search', data){ - let tools - switch(type){ - case 'function': // @stub - function tools - tools = { - tools: [{ type: 'function' }], - function: {}, - } - break - case 'file_search': - default: - if(!this.#factory.vectorstoreId) - return - tools = { - tools: [{ type: 'file_search' }], - tool_resources: { - file_search: { - vector_store_ids: [this.#factory.vectorstoreId], - } - }, - } + async updateInstructions(id=this.activeBot.id, includeInstructions=true, includeModel=true, includeTools=true){ + const { type, } = this.#bots.find(bot=>bot.id===id) + ?? this.activeBot + if(!type?.length) + return + const bot = { id, type, } + const options = { + instructions: includeInstructions, + model: includeModel, + tools: includeTools, } - if(tools) - this.#llmServices.updateBot({ bot_id, tools, }) /* no await */ + const response = await this.#factory.updateBot(bot, options) + return mPruneBot(response) } /** * Validate registration id. @@ -648,13 +745,17 @@ class Avatar extends EventEmitter { /** * Set the active bot id. If not match found in bot list, then defaults back to this.id * @setter - * @param {string} bot_id - The active bot id, defaults to personal-avatar. + * @requires mBotInstructions + * @param {string} id - The active bot id, defaults to personal-avatar. * @returns {void} */ - set activeBotId(bot_id=this.avatarBot.id){ - this.#activeBotId = - ( mFindBot(this, bot_id) ?? this.avatarBot ) - .id + set activeBotId(id=this.avatar.id){ + const newActiveBot = mFindBot(this, id) ?? this.avatar + const { id: newActiveId, type, version: botVersion=1.0, } = newActiveBot + const currentVersion = this.#factory.botInstructionsVersion(type) + if(botVersion!==currentVersion) + this.updateInstructions(newActiveId, true, false, true) + this.#activeBotId = newActiveId } /** * Get actor or default avatar bot. @@ -662,7 +763,7 @@ class Avatar extends EventEmitter { * @returns {object} - The actor bot (or default bot). */ get actorBot(){ - return this.#bots.find(_bot=>_bot.type==='actor')??this.avatarBot + return this.#bots.find(_bot=>_bot.type==='actor')??this.avatar } /** * Get the age of the member. @@ -698,7 +799,7 @@ class Avatar extends EventEmitter { * @getter * @returns {object} - The personal avatar bot. */ - get avatarBot(){ + get avatar(){ return this.bots.find(_bot=>_bot.type==='personal-avatar') } /** @@ -710,6 +811,9 @@ class Avatar extends EventEmitter { get being(){ // return this.#proxyBeing } + get biographer(){ + return this.#bots.find(_bot=>_bot.type==='personal-biographer') + } /** * Get the birthdate of _member_ from `#factory`. * @getter @@ -1024,7 +1128,15 @@ class Avatar extends EventEmitter { this.#nickname = nickname } get personalAssistant(){ - return this.avatarBot + return this.avatar + } + /** + * Get the `active` reliving memories. + * @getter + * @returns {object[]} - The active reliving memories. + */ + get relivingMemories(){ + return this.#relivingMemories } } /* module functions */ @@ -1084,7 +1196,7 @@ async function mBot(factory, avatar, bot){ let originBot = bots.find(oBot=>oBot.id===botId) if(originBot){ /* update bot */ const options = {} - const updateBot = Object.keys(bot) + const updatedBot = Object.keys(bot) .reduce((diff, key) => { if(bot[key]!==originBot[key]) diff[key] = bot[key] @@ -1096,24 +1208,24 @@ async function mBot(factory, avatar, bot){ const excludeTypes = ['library', 'custom'] // @stub - custom mechanic? if(!excludeTypes.includes(type)){ const conversation = await avatar.createConversation() - updateBot.thread_id = conversation.thread_id + updatedBot.thread_id = conversation.thread_id avatar.conversations.push(conversation) } } - if(Object.keys(updateBot).length){ - let updatedOriginBot = {...originBot, ...updateBot} // consolidated update + if(Object.keys(updatedBot).length){ + let updatedOriginBot = {...originBot, ...updatedBot} // consolidated update const { bot_id, id, } = updatedOriginBot - updateBot.bot_id = bot_id - updateBot.id = id - updateBot.type = type - const { interests, dob, privacy, } = updateBot + updatedBot.bot_id = bot_id + updatedBot.id = id + updatedBot.type = type + const { interests, dob, privacy, } = updatedBot /* set options */ if(interests?.length || dob?.length || privacy?.length){ options.instructions = true options.model = true options.tools = false /* tools not updated through this mechanic */ } - updatedOriginBot = await factory.updateBot(updateBot, options) + updatedOriginBot = await factory.updateBot(updatedBot, options) originBot = mSanitize(updatedOriginBot) } } else { /* create assistant */ @@ -1126,8 +1238,8 @@ async function mBot(factory, avatar, bot){ /** * Makes call to LLM and to return response(s) to prompt. * @todo - create actor-bot for internal chat? Concern is that API-assistants are only a storage vehicle, ergo not an embedded fine tune as I thought (i.e., there still may be room for new fine-tuning exercise); i.e., micro-instructionsets need to be developed for most. Unclear if direct thread/message instructions override or ADD, could check documentation or gpt, but... - * @todo - address disconnect between conversations held in memory in avatar and those in openAI threads; use `addLLMMessages` to post internally * @todo - would dynamic event dialog be handled more effectively with a callback routine function, I think so, and would still allow for avatar to vet, etc. + * @todo - convert conversation requirements to bot * @module * @param {LLMServices} llmServices - OpenAI object currently * @param {Conversation} conversation - Conversation object @@ -1139,10 +1251,10 @@ async function mBot(factory, avatar, bot){ async function mCallLLM(llmServices, conversation, prompt, factory, avatar){ const { botId, bot_id, thread_id: threadId } = conversation const id = botId ?? bot_id - if(!threadId || !botId) + if(!threadId || !id) throw new Error('Both `thread_id` and `bot_id` required for LLM call.') const messages = await llmServices.getLLMResponse(threadId, id, prompt, factory, avatar) - messages.sort((mA, mB) => { + messages.sort((mA, mB)=>{ return mB.created_at - mA.created_at }) return messages @@ -1770,6 +1882,11 @@ function mNavigation(scenes){ return (a.order ?? 0) - (b.order ?? 0) }) } +/** + * Returns a frontend-ready bot object. + * @param {object} assistantData - The assistant data object. + * @returns {object} - The pruned bot object. + */ function mPruneBot(assistantData){ const { bot_id, bot_name: name, description, id, purpose, type, } = assistantData return { @@ -1781,6 +1898,28 @@ function mPruneBot(assistantData){ type, } } +/** + * Returns a frontend-ready object, pruned of cosmos database fields. + * @param {object} document - The document object to prune. + * @returns {object} - The pruned document object. + */ +function mPruneDocument(document){ + const { + being, + mbr_id, + name, + _attachments, + _etag, + _rid, + _self, + _ts, + ..._document + } = document + return _document +} +function mPruneItem(item){ + return mPruneDocument(item) +} /** * Returns frontend-ready Message object after logic mutation. * @module @@ -1842,6 +1981,63 @@ function mPruneMessages(bot, messageArray, type='chat', processStartTime=Date.no } return message } +/** + * Returns a narration packet for a memory reliving. Will allow for and accommodate the incorporation of helpful data _from_ the avatar member into the memory item `summary` and other metadata. The bot by default will: + * - break memory into `scenes` (minimum of 3: 1) set scene, ask for input [determine default what] 2) develop action, dramatize, describe input mechanic 3) conclude scene, moralize - what did you learn? then share what you feel author learned + * - perform/narrate the memory as scenes describe + * - others are common to living, but with `reliving`, the biographer bot (only narrator allowed in .10) incorporate any user-contributed contexts or imrpovements to the memory summary that drives the living and sharing. All by itemId. + * - if user "interrupts" then interruption content should be added to memory updateSummary; doubt I will keep work interrupt, but this too is hopefully able to merely be embedded in the biographer bot instructions. + * Currently testing efficacy of all instructions (i.e., no callbacks, as not necessary yet) being embedded in my biog-bot, `madrigal`. + * @param {Avatar} avatar - Member's avatar object. + * @param {AgentFactory} factory - Member's AgentFactory object. + * @param {LLMServices} llm - OpenAI object. + * @param {object} bot - The bot object. + * @param {object} item - The memory object. + * @param {string} memberInput - The member input (or simply: NEXT, SKIP, etc.) + * @returns {Promise} - The reliving memory object for frontend to execute. + */ +async function mReliveMemoryNarration(avatar, factory, llm, bot, item, memberInput='NEXT'){ + const { relivingMemories, } = avatar + const { bot_id, id: botId, thread_id, } = bot + const { id, } = item + const processStartTime = Date.now() + let message = `## relive memory itemId: ${id}\n` + let relivingMemory = relivingMemories.find(reliving=>reliving.item.id===id) + if(!relivingMemory){ /* create new activated reliving memory */ + const conversation = await avatar.createConversation('memory', undefined, botId, false) + conversation.botId = bot_id + const { threadId, } = conversation + relivingMemory = { + bot, + conversation, + id, + item, + threadId, + } + relivingMemories.push(relivingMemory) + console.log('mReliveMemoryNarration::new reliving memory created', conversation.inspect(true), conversation.bot, bot_id) + } else /* opportunity for member interrupt */ + message += `MEMBER: ${memberInput}\n` + const { conversation, threadId, } = relivingMemory + let messages = await mCallLLM(llm, conversation, message, factory, avatar) + conversation.addMessages(messages) + console.log('chatRequest::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) + /* frontend mutations */ + messages = conversation.messages + .filter(message=>{ // limit to current chat response(s); usually one, perhaps faithfully in future [or could be managed in LLM] + return messages.find(_message=>_message.id===message.id) + && message.type==='chat' + && message.role!=='user' + }) + .map(message=>mPruneMessage(bot, message, 'chat', processStartTime)) + const memory = { + id, + messages, + success: true, + threadId, + } + return memory +} /** * Replaces variables in prompt with Experience values. * @todo - variables should be back populated to experience, confirm diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 80c140b..c94ac24 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -215,6 +215,10 @@ async function mMessages(openai, threadId){ return await openai.beta.threads.messages .list(threadId) } +async function mRunCancel(openai, threadId, runId){ + const run = await openai.beta.threads.runs.cancel(threadId, runId) + return run +} /** * Maintains vigil for status of openAI `run = 'completed'`. * @module @@ -237,6 +241,7 @@ async function mRunFinish(llmServices, run, factory, avatar){ } } catch (error) { clearInterval(checkInterval) + mRunCancel(llmServices, run.thread_id, run.id) reject(error) } }, mPingIntervalMs) @@ -267,7 +272,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref && run.required_action?.submit_tool_outputs?.tool_calls && run.required_action.submit_tool_outputs.tool_calls.length ){ - const { assistant_id: bot_id, metadata, thread_id, } = run + const { assistant_id: bot_id, id: runId, metadata, thread_id, } = run const toolCallsOutput = await Promise.all( run.required_action.submit_tool_outputs.tool_calls .map(async tool=>{ @@ -282,6 +287,10 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref if(typeof toolArguments==='string') toolArguments = JSON.parse(toolArguments) ?? {} toolArguments.thread_id = thread_id + const { itemId, } = toolArguments + let item + if(itemId) + item = await factory.item(itemId) switch(name.toLowerCase()){ case 'confirmregistration': case 'confirm_registration': @@ -298,7 +307,27 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref else action = 'Registration confirmation failed, notify member of system error and continue discussing MyLife organization' } - confirmation.output = JSON.stringify({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) + return confirmation + case 'createaccount': + case 'create_account': + case 'create account': + console.log('mRunFunctions()::createAccount', toolArguments) + const { birthdate, passphrase, } = toolArguments + action = `error setting basics for member: ` + if(!birthdate) + action += 'birthdate missing, elicit birthdate; ' + if(!passphrase) + action += 'passphrase missing, elicit passphrase; ' + try { + success = await factory.createAccount(birthdate, passphrase) + action = success + ? `congratulate member on creating their MyLife membership, display \`passphrase\` in bold for review (or copy/paste), and ask if they are ready to continue journey.` + : action + 'server failure for `factory.createAccount()`' + } catch(error){ + action += '__ERROR: ' + error.message + } + confirmation.output = JSON.stringify({ action, success, }) return confirmation case 'entrysummary': // entrySummary in Globals case 'entry_summary': @@ -315,7 +344,22 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref } else { action = `journal entry failed to save, notify member and continue on for now` } - confirmation.output = JSON.stringify({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) + return confirmation + case 'getsummary': + case 'get_summary': + case 'get summary': + console.log('mRunFunctions()::getSummary::start', item) + let { summary, } = 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` + success = true + } + confirmation.output = JSON.stringify({ action, itemId, success, summary, }) + console.log('mRunFunctions()::getSummary::confirmation', confirmation) return confirmation case 'hijackattempt': case 'hijack_attempt': @@ -324,7 +368,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref 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({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) return confirmation case 'registercandidate': case 'register_candidate': @@ -339,27 +383,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref success = true console.log('mRunFunctions()::avatar', avatar, registration, toolArguments) } - confirmation.output = JSON.stringify({ success, action, }) - return confirmation - case 'createaccount': - case 'create_account': - case 'create account': - console.log('mRunFunctions()::createAccount', toolArguments) - const { birthdate, passphrase, } = toolArguments - action = `error setting basics for member: ` - if(!birthdate) - action += 'birthdate missing, elicit birthdate; ' - if(!passphrase) - action += 'passphrase missing, elicit passphrase; ' - try { - success = await factory.createAccount(birthdate, passphrase) - action = success - ? `congratulate member on creating their MyLife membership, display \`passphrase\` in bold for review (or copy/paste), and ask if they are ready to continue journey.` - : action + 'server failure for `factory.createAccount()`' - } catch(error){ - action += '__ERROR: ' + error.message - } - confirmation.output = JSON.stringify({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) return confirmation case 'story': // storySummary.json case 'storysummary': @@ -394,12 +418,24 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref } success = true } // error cascades - confirmation.output = JSON.stringify({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) + return confirmation + case 'updatesummary': + case 'update_summary': + case 'update summary': + console.log('mRunFunctions()::updatesummary::start', item, itemId, toolArguments) + 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` + success = true + confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::getSummary::confirmation', confirmation) return confirmation default: console.log(`ERROR::mRunFunctions()::toolFunction not found: ${ name }`, toolFunction) action = `toolFunction not found: ${ name }, apologize for the error and continue on with the conversation; system notified to fix` - confirmation.output = JSON.stringify({ success, action, }) + confirmation.output = JSON.stringify({ action, success, }) return confirmation } })) @@ -413,7 +449,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref } } catch(error){ - console.log('mRunFunctions()::error', error.message) + console.log('mRunFunctions()::error::canceling-run', error.message, error.stack) rethrow(error) } } diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index cc04661..0dfa745 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -21,10 +21,21 @@ import { members, passphraseReset, privacyPolicy, + shadow, + shadows, signup, summarize, + team, + teams, + updateBotInstructions, upload, } from './functions.mjs' +import { + collectMemory, + improveMemory, + reliveMemory, + livingMemory, +} from './memory-functions.mjs' import { availableExperiences, experience, @@ -56,6 +67,7 @@ _Router.get('/greeting', greetings) _Router.get('/select', loginSelect) _Router.get('/status', status) _Router.get('/privacy-policy', privacyPolicy) +_Router.get('/shadows', shadows) _Router.get('/signup', status_signup) _Router.post('/', chat) _Router.post('/challenge/:mid', challenge) @@ -95,9 +107,12 @@ _memberRouter.get('/experiencesLived', experiencesLived) _memberRouter.get('/greeting', greetings) _memberRouter.get('/item/:iid', item) _memberRouter.get('/mode', interfaceMode) +_memberRouter.get('/teams', teams) _memberRouter.patch('/experience/:eid', experience) _memberRouter.patch('/experience/:eid/end', experienceEnd) _memberRouter.patch('/experience/:eid/manifest', experienceManifest) +_memberRouter.patch('/memory/relive/:iid', reliveMemory) +_memberRouter.patch('/memory/living/:iid', livingMemory) _memberRouter.post('/', chat) _memberRouter.post('/bots', bots) _memberRouter.post('/bots/create', createBot) @@ -105,9 +120,12 @@ _memberRouter.post('/bots/activate/:bid', activateBot) _memberRouter.post('/category', category) _memberRouter.post('/mode', interfaceMode) _memberRouter.post('/passphrase', passphraseReset) +_memberRouter.post('/shadow', shadow) _memberRouter.post('/summarize', summarize) +_memberRouter.post('/teams/:tid', team) _memberRouter.post('/upload', upload) _memberRouter.put('/bots/:bid', bots) +_memberRouter.put('/bots/system-update/:bid', updateBotInstructions) _memberRouter.put('/item/:iid', item) // Mount the subordinate routers along respective paths _Router.use('/members', _memberRouter.routes(), _memberRouter.allowedMethods()) diff --git a/inc/json-schemas/openai/functions/getSummary.json b/inc/json-schemas/openai/functions/getSummary.json new file mode 100644 index 0000000..cc79799 --- /dev/null +++ b/inc/json-schemas/openai/functions/getSummary.json @@ -0,0 +1,17 @@ +{ + "description": "Gets a story summary by itemId", + "name": "getSummary", + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "Id of summary to update", + "format": "uuid", + "type": "string" + } + }, + "required": [ + "itemId" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/updateSummary.json b/inc/json-schemas/openai/functions/updateSummary.json new file mode 100644 index 0000000..6dc6cc9 --- /dev/null +++ b/inc/json-schemas/openai/functions/updateSummary.json @@ -0,0 +1,22 @@ +{ + "description": "Updates a story summary (in total) as referenced by itemId", + "name": "updateSummary", + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "Id of summary to update", + "format": "uuid", + "type": "string" + }, + "summary": { + "description": "The new updated and complete summary", + "type": "string" + } + }, + "required": [ + "itemId", + "title" + ] + } +} \ No newline at end of file diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index b4179ef..592ba87 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -22,6 +22,11 @@ max-height: 3rem; /* Adjust to desired expanded height */ overflow: visible; /* Make overflow visible on hover */ } +.bot-bar-divider { + background-color: rgba(255, 255, 255, 0.5); /* White with 50% opacity */ + height: 100%; /* Height of the divider */ + width: 0.1rem; /* Full width */ +} .bot-thumb { cursor: pointer; height: 2.5rem; @@ -39,7 +44,7 @@ align-items: center; justify-content: center; cursor: pointer; - margin-right: 1rem; /* Space between containers in em */ + margin-right: 0.22rem; /* Space between containers in em */ position: relative; } .bot-thumb-active { @@ -97,6 +102,14 @@ position: absolute; z-index: 50; } +.team-popup-content { + background-color: #333; /* Change as needed */ + color: white; + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 0.8rem; +} /* bot */ .bot-icon { background-image: radial-gradient(circle at 0.1rem 0.1rem, #eaedff 0%, #1e3b4e 40%, rgb(60 162 213) 80%, #404553 100%); @@ -377,14 +390,13 @@ padding: 0.5rem; } .collection-popup-story { - align-items: center; - border: thick solid navy; + align-items: flex-start; + cursor: default; display: flex; - flex: 1 1 auto; flex-direction: column; justify-content: center; - margin: 0.25rem; - padding: 02.5rem; + margin: 0.22rem; + padding: 0.22rem; } .collection-refresh { color: aliceblue; @@ -471,23 +483,93 @@ input:checked + .publicity-slider:before { margin: 0.25rem; padding: 0.25rem; } +.improve-memory-lane { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + gap: 0.5rem; + margin: 0; + padding: 0; + width: 100%; +} .memory-carousel { align-items: center; background-color: white; color: gray; display: flex; flex-direction: row; - justify-content: center; + justify-content: flex-start; height: 5rem; margin: 0.25rem; padding: 0.25rem; - text-align: center; + text-align: flex-start; width: 100%; } -.memory-input-lane { +.memory-shadow { + align-items: center; + background-color: teal; + border: thin solid white; + border-radius: 1rem; + display: flex; + flex: 0 0 40%; + flex-direction: row; + justify-content: flex-start; + min-height: 4rem; + max-height: 7rem; + overflow: hidden; +} +.memory-shadow-carousel { + align-items: center; + display: flex; + flex-direction: column; + flex: 1 1 auto; + justify-content: flex-start; + margin: 0; + max-height: 10rem; + padding: 0; + text-wrap: wrap; + transition: transform 0.5s ease; +} +.memory-shadow-fade { + position: relative; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,1) 100%); + pointer-events: none; +} +.memory-shadow-pagers { + align-items: center; + display: flex; + flex-direction: column; + flex: 0 0 auto; + justify-content: center; + margin: 0 0.5rem; +} +.memory-shadow-text { + color: whitesmoke; + cursor: pointer; + display: flex; + flex-direction: column; + flex: 1 1 auto; + font-size: 0.9rem; + padding: 0.5rem; + transition: opacity 0.5s ease; /* Smooth transition for opacity */ +} +.share-memory-container { /* panel for how others `experience` memory story */ + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0; + padding: 0; +} +.share-memory-select { /* container for share memory selection packet */ display: flex; flex-direction: row; justify-content: space-between; + gap: 0.5rem; margin: 0; padding: 0; } diff --git a/views/assets/css/main.css b/views/assets/css/main.css index a05116b..39cdd8a 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -719,6 +719,24 @@ body { font-weight: bold; } /* MyLife general */ +.caret { + border-style: solid; + cursor: pointer; + height: 0; + width: 0; +} +.caret-up { + border-width: 0 8px 8px 8px; /* Adjust sizes as needed */ + border-color: transparent transparent #ccc transparent; + margin-bottom: 5px; /* Space between the up and down carets */ + filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.3)); /* Bevel effect */ +} +.caret-down { + border-width: 8px 8px 0 8px; /* Adjust sizes as needed */ + border-color: #ccc transparent transparent transparent; + margin-top: 5px; /* Space between the up and down carets */ + filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.3)); /* Bevel effect */ +} .grabbing { cursor: grabbing; } diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 63c72a6..2c70cdf 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -41,7 +41,7 @@
-
Unknown Team
+
@@ -244,14 +244,14 @@
-
Librarian/Collections
+
Scrapbook
Librarian
-
Scrapbook
-
Librarian
diff --git a/views/assets/html/_navbar.html b/views/assets/html/_navbar.html index 98bf9d1..fb2dc36 100644 --- a/views/assets/html/_navbar.html +++ b/views/assets/html/_navbar.html @@ -1,5 +1,5 @@ - - diff --git a/views/assets/html/archive/_collections.html b/views/assets/html/archive/_collections.html new file mode 100644 index 0000000..a389b86 --- /dev/null +++ b/views/assets/html/archive/_collections.html @@ -0,0 +1,23 @@ +
+
+
+
Journal Entries
+ +
+
+ +
None
+
+
+
+
+
+
Experiences
+ +
+
+ +
None
+
+
+ \ No newline at end of file diff --git a/views/assets/html/archive/_journaler.html b/views/assets/html/archive/_journaler.html new file mode 100644 index 0000000..c38111f --- /dev/null +++ b/views/assets/html/archive/_journaler.html @@ -0,0 +1,58 @@ +
+
+
+
Journal Assistant
+
+ J.J. +
+
+
+ +
\ No newline at end of file diff --git a/views/assets/html/archive/_navigation-help.html b/views/assets/html/archive/_navigation-help.html new file mode 100644 index 0000000..03e6f1c --- /dev/null +++ b/views/assets/html/archive/_navigation-help.html @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/views/assets/html/archive/_navigation-search.html b/views/assets/html/archive/_navigation-search.html new file mode 100644 index 0000000..ef6e890 --- /dev/null +++ b/views/assets/html/archive/_navigation-search.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index c74ba99..9d0d251 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -187,9 +187,9 @@ async function updatePageBots(bots=mBots, includeGreeting=false, dynamic=false){ throw new Error(`No bots provided to update page.`) if(mBots!==bots) mBots = bots - await mUpdateTeams() + // await mUpdateTeams() await mUpdateBotContainers() - mUpdateBotBar() + // mUpdateBotBar() if(includeGreeting) mGreeting(dynamic) } diff --git a/views/assets/js/globals.mjs b/views/assets/js/globals.mjs index 67078a1..5aa4f7f 100644 --- a/views/assets/js/globals.mjs +++ b/views/assets/js/globals.mjs @@ -197,6 +197,14 @@ class Globals { ? mLogin() : mLogout() } + /** + * Remove an element from the DOM. + * @param {HTMLElement} element - The element to remove from the DOM. + * @returns {void} + */ + retract(element){ + element.remove() + } /** * Last stop before Showing an element and kicking off animation chain. Adds universal run-once animation-end listener, which may include optional callback functionality. * @public diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index e5468fd..dddc9e9 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -6,6 +6,7 @@ const mGlobals = new Globals() const mAvatarName = mGlobals.getAvatar()?.name const hide = mGlobals.hide const mPlaceholder = `Type a message to ${ mAvatarName }...` +const retract = mGlobals.retract const show = mGlobals.show /* variables */ let mAvatarNameEdited = false, @@ -34,19 +35,14 @@ let awaitButton, pageLoader, privacyContainer, sidebar, - signupAvatarInput, - signupAvatarInputField, signupButton, signupEmailInputField, signupErrorMessage, signupForm, signupHeader, signupHumanNameInput, - signupNewsletter, - signupRegister, signupSuccess, - signupTeaser, - signupTeaserButton + signupTeaser /* page load */ document.addEventListener('DOMContentLoaded', async event=>{ /* load data */ @@ -270,14 +266,9 @@ async function mFetchStart(){ * @returns {void} */ function mInitializeListeners(){ - signupAvatarInputField.addEventListener('input', mUpdateFormState) - signupAvatarInputField.addEventListener('focus', _=>mAvatarNameEdited=true) // Detect if avatar name has been manually edited signupButton.addEventListener('click', mSubmitSignup) signupEmailInputField.addEventListener('input', mUpdateFormState) signupHumanNameInput.addEventListener('input', mUpdateFormState) - signupNewsletter.addEventListener('click', mUpdateFormType) - signupRegister.addEventListener('click', mUpdateFormType) - signupTeaserButton.addEventListener('click', mToggleSignupForm) if(chatInput) chatInput.addEventListener('input', mToggleInputTextarea) if(chatSubmit) @@ -303,19 +294,14 @@ async function mLoadStart(){ pageLoader = document.getElementById('page-loader') privacyContainer = document.getElementById('privacy-container') sidebar = mGlobals.sidebar - signupAvatarInputField = document.getElementById('avatar-name-input-text') - signupAvatarInput = document.getElementById('signup-avatar-name-input') signupButton = document.getElementById('signup-submit') signupEmailInputField = document.getElementById('email-input-text') signupErrorMessage = document.getElementById('signup-error-message') signupForm = document.getElementById('signup-form') signupHeader = document.getElementById('signup-header') signupHumanNameInput = document.getElementById('human-name-input-text') - signupNewsletter = document.getElementById('signup-newsletter') - signupRegister = document.getElementById('signup-register') signupSuccess = document.getElementById('signup-success') signupTeaser = document.getElementById('signup-teaser') - signupTeaserButton = document.getElementById('signup-button-teaser') /* fetch the greeting messages */ mPageType = new URLSearchParams(window.location.search).get('type') ?? window.location.pathname.split('/').pop() @@ -338,7 +324,6 @@ function mShowPage(){ /* DOM elements */ signupEmailInputField.tabIndex = 1 signupHumanNameInput.tabIndex = 2 - signupAvatarInputField.tabIndex = 3 /* assign listeners */ mInitializeListeners() /* display elements */ @@ -367,18 +352,13 @@ async function mSignupStatus(){ if(!response.ok) throw new Error('Network response was not ok') const isSignedUp = await response.json() - if(!isSignedUp){ - hide(signupForm) - hide(signupSuccess) - show(signupTeaser) - show(signupTeaserButton) - } else - mSignupSuccess() + !isSignedUp + ? retract(signupSuccess) + : mSignupSuccess() } function mSignupSuccess(){ - hide(signupForm) - hide(signupTeaser) - hide(signupTeaserButton) + retract(signupForm) + retract(signupTeaser) show(signupSuccess) signupHeader.innerHTML = `Thank you for joining our pilot!` } @@ -487,9 +467,8 @@ async function submitChat(url, options) { async function mSubmitSignup(event){ const { value: email, } = signupEmailInputField const { value: humanName, } = signupHumanNameInput - const { value: avatarName, } = signupAvatarInputField const formData = { - avatarName, + avatarName: humanName, email, humanName, type: mSignupType, @@ -538,20 +517,6 @@ function mToggleInputTextarea() { chatInput.style.height = chatInput.scrollHeight + 'px' // Set height based on content mToggleSubmitButton() } -/** - * Manages the visibility of the signup form. - * @param {Event} event - The event object. - * @returns {void} - */ -function mToggleSignupForm(event){ - mGlobals.toggleVisibility(signupForm) - mGlobals.toggleVisibility(signupTeaser) - hide(signupSuccess) - hide(signupAvatarInput) - const altText = this.getAttribute('data-alt-text') - const currentText = this.innerText - this.innerText = currentText===altText ? "Join MyLife Pilot Program" : altText -} /** * Toggles the disabled state of a button based on the input element value. * @private @@ -593,30 +558,11 @@ function mTypeMessage(chatBubble, message, typeDelay=mDefaultTypeDelay, callback * @param {Event} event - The event object. * @returns {void} */ -function mUpdateFormState(event) { +function mUpdateFormState(event){ const { value: emailValue, } = signupEmailInputField const { value: humanNameValue, } = signupHumanNameInput - if(emailValue?.length && humanNameValue?.length > 2 && mSignupType==='register'){ - show(signupAvatarInput) - if(!mAvatarNameEdited) - signupAvatarInputField.value = `${ humanNameValue.split(' ')[0] }-AI`; // Set value instead of placeholder - } else - hide(signupAvatarInput) signupButton.disabled = !( emailValue?.length > 5 - && humanNameValue?.length > 2 - && ( mSignupType!=='register' || signupAvatarInputField.value?.length > 2 ) + && humanNameValue.trim()?.length > 2 ) -} -/** - * Keeps track of signup type in modular scope. - * @private - * @requires mSignupType - * @param {Event} event - The event object. - * @returns {void} - */ -function mUpdateFormType(event){ - const { checked, value, } = this - mSignupType = value - mUpdateFormState() } \ No newline at end of file diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index fe32322..a854829 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -546,9 +546,10 @@ function mStageTransitionMember(includeSidebar=true){ show(chatContainer) show(systemChat) show(chatInput) - if(includeSidebar){ + if(includeSidebar && sidebar){ show(sidebar) - show(botBar) + if(botBar) + show(botBar) } } /** diff --git a/views/members.html b/views/members.html index 967c6ad..c18afbe 100644 --- a/views/members.html +++ b/views/members.html @@ -2,7 +2,6 @@
- <% include assets/html/_bot-bar %>
From ba5cc17efe905ffaf493ce1ebb46424b885ba99a Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:14:19 -0400 Subject: [PATCH 07/54] 283 version 0016 updates (#317) (#318) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 251 hotfix tutorial (#254) (#255) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 20240705 @Mookse - hotfix for experiences --------- * Version 0.0.13 PROD -> base (#259) * 251 hotfix tutorial (#254) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 20240705 @Mookse - hotfix for experiences --------- * 248 version 0013 updates (#258) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 20240705 @Mookse - hotfix for experiences * 20240705 @Mookse - add server status to readme.md * 20240705 @Mookse - Add Favicon on site for easier identification in Browser #252 * 20240705 @Mookse - remove sources from message --------- --------- * Azure deploy prod (#274) * 251 hotfix tutorial (#254) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 20240705 @Mookse - hotfix for experiences --------- * 248 version 0013 updates (#258) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * version 0011 updates (#244) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- * Version 0.0.11 (#243) * 231 version 0010 updates (#239) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 238 version 0011 updates (#242) * 231 version 0010 updates (#239) (#240) * 20240604 @Mookse - teams() route - teams(teamId) returns a fully formed team from server with new bots (when instructions provided) * 20240604 @Mookse - fix bot-bar `setActiveBot` - cosmetic * 20240604 @Mookse - fix duplicate team members return * 20240604 @Mookse - botBar reordered * 20240605 @Mookse - teams must be active * 20240605 @Mookse - default mods * 20240605 @Mookse - create (non-custom) bot frontend - diary updates * 20240605 @Mookse - imagining `share memory` * 20240606 @Mookse - story updates * 20240606 @Mookse - minor cosmetics * 20240606 @Mookse - fetchShadows() - ignite shadow: member-version (wip) * 20240607 @Mookse - `shadow` initial endpoint * 20240607 @Mookse - add processingBotId to payload (so that frontend can determine if it should setActive) * 20240607 @Mookse - front-end receives message about updating from agent shadow * 20240607 @Mookse - shadow cosmetics * 20240607 @Mookse - biographer openai function definitions * 20240607 @Mookse - now _that's_ an error * 20240607 @Mookse - updateSummary() **note**: wip as thread got stopped * 20240608 @Mookse - updates summary * 20240608 @Mookse - remove dataset after shadow triggered - add `proxy` endpoint to shadow ideas * 20240608 @Mookse - `globals.getGPTJavascriptFunction`: `getSummary`, `updateSummary` - `updateBotInstructions` route - on setActiveBot checks versions and updates as needed * 20240609 @Mookse - cosmetic * 20240609 @Mookse stable wip kicking off memory is correct, need tuning of instructions or alternate scene-stepwise motion, no problem small error left in frontend for more testing but time to save * 20240618 @Mookse - cosmetic: file under "the right evocation can make all the difference" * 20240618 @Mookse - cosmetic in dribs and drabs * 20240618 @Mookse - frontend 'next' fix * 20240621 @Mookse - redirect logged in members to `/members` * 20240626 @Mookse - registration -> createAccount with Q * 20240626 @Mookse - hostedMembers fix * 20240626 @Mookse - avatar document name fix * 20240626 @Mookse - instruction change * 20240626 @Mookse - page-loader * 20240626 @Mookse - special pages fix --------- --------- * 20240705 @Mookse - hotfix for experiences * 20240705 @Mookse - add server status to readme.md * 20240705 @Mookse - Add Favicon on site for easier identification in Browser #252 * 20240705 @Mookse - remove sources from message --------- * 257-version-0014-updates (#273) --------- * 20240729 @Mookse - Remove or placeholder explantion #289 - Remove actor, narrator, point of view or make them placeholders #297 - Create text about what's going to happen for the "relive memory" button #299 * 20240729 @Mookse - next button * 20240730 @Mookse - popup close --------- Signed-off-by: Erik Jespersen <42016062+Mookse@users.noreply.github.com> --- views/assets/css/bots.css | 20 ++++-- views/assets/js/bots.mjs | 139 ++++++++++---------------------------- 2 files changed, 51 insertions(+), 108 deletions(-) diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index 34e7d66..99e9236 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -563,14 +563,26 @@ input:checked + .publicity-slider:before { padding: 0.5rem; transition: opacity 0.5s ease; /* Smooth transition for opacity */ } -.share-memory-container { /* panel for how others `experience` memory story */ +.relive-memory-button.button { + display: flex; + margin: 0.2rem 0; + padding: 0 0.2rem; + max-width: 80%; + min-width: 60%; +} +.relive-memory-container { /* panel for how others `experience` memory story */ + align-items: center; display: flex; flex-direction: column; justify-content: flex-start; - margin: 0; - padding: 0; + margin: 0.2rem; + padding: 0.2rem; +} +.relive-memory-explanation { + display: flex; + padding: 0.2rem; } -.share-memory-select { /* container for share memory selection packet */ +.relive-memory-select { /* container for share memory selection packet */ display: flex; flex-direction: row; justify-content: space-between; diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index a9713d5..b8c8b65 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -19,6 +19,7 @@ import Globals from './globals.mjs' const mAvailableMimeTypes = [], mAvailableUploaderTypes = ['library', 'personal-avatar', 'personal-biographer', 'resume',], botBar = document.getElementById('bot-bar'), + mDefaultReliveMemoryButtonText = 'next', mDefaultTeam = 'memoir', mGlobals = new Globals(), mLibraries = ['entry', 'experience', 'file', 'story'], // ['chat', 'conversation'] @@ -397,7 +398,6 @@ async function mMemoryShadow(event){ throw new Error(`Unimplemented shadow type: ${ type }`) } /* close popup */ - // @stub - minimize to header instead? const popupClose = document.getElementById(`popup-close_${ itemId }`) if(popupClose) popupClose.click() @@ -526,6 +526,7 @@ function mCreateCollectionPopup(collectionItem) { /* create popup close button */ const popupClose = document.createElement('button') popupClose.classList.add('fa-solid', 'fa-close', 'popup-close', 'collection-popup-close') + popupClose.dataset.isClose = 'true' popupClose.id = `popup-close_${ id }` popupClose.setAttribute('aria-label', 'Close') popupClose.addEventListener('click', mTogglePopup) @@ -628,108 +629,35 @@ function mCreateCollectionPopup(collectionItem) { improveMemory.classList.add(`collection-popup-${ type }`) improveMemory.id = `popup-${ type }_${ id }` improveMemory.name = 'improve-memory-container' - /* shadows, share-memory panel */ + /* shadows, relive-memory panel */ const improveMemoryLane = document.createElement('div') improveMemoryLane.classList.add('improve-memory-lane') /* story shadows */ if(mShadows?.length) improveMemoryLane.appendChild(mCreateMemoryShadows(id)) - /* share memory panel */ - const shareMemory = document.createElement('div') - shareMemory.classList.add('share-memory-container') - shareMemory.id = `share-memory_${ id }` - shareMemory.name = 'share-memory-container' - const shareMemoryActorSelect = document.createElement('div') - shareMemoryActorSelect.classList.add('share-memory-select') - shareMemoryActorSelect.title = `Select the actor or narrator for the story. Who should tell your story? Or who will play "you"?` - /* actor */ - const actorList = ['Biographer', 'My Avatar', 'Member avatar', 'Q', 'MyLife Professional Actor', 'Custom'] - const actor = document.createElement('select') - actor.classList.add('share-memory-actor-select') - actor.dataset.id = id - actor.dataset.type = `actor` - actor.id = `share-memory-actor-select_${ id }` - actor.name = 'share-memory-actor-select' - /* actor/narrator label; **note**: for instance if member has tuned a specific bot of theirs to have an outgoing voice they like */ - const actorLabel = document.createElement('label') - actorLabel.classList.add('share-memory-actor-select-label') - actorLabel.htmlFor = actor.id - actorLabel.id = `share-memory-actor-select-label_${ id }` - actorLabel.textContent = 'Actor/Narrator:' - shareMemoryActorSelect.appendChild(actorLabel) - const actorOption = document.createElement('option') - actorOption.disabled = true - actorOption.selected = true - actorOption.value = '' - actorOption.textContent = 'Select narrator...' - actor.appendChild(actorOption) - actorList.forEach(option=>{ - const actorOption = document.createElement('option') - actorOption.selected = false - actorOption.textContent = option - actorOption.value = option - actor.appendChild(actorOption) - }) - actor.addEventListener('change', mStory) - shareMemoryActorSelect.appendChild(actor) - shareMemory.appendChild(shareMemoryActorSelect) - /* pov */ - const shareMemoryPovSelect = document.createElement('div') - shareMemoryPovSelect.classList.add('share-memory-select') - shareMemoryPovSelect.title = `This refers to the position of the "listener" in the story. Who is the listener? Are they referred to as the protagonist? Antagonist? A particular person or character in your story?` - const povList = ['Me', 'Protagonist', 'Antagonist', 'Character',] - const pov = document.createElement('select') - pov.classList.add('share-memory-pov-select') - pov.dataset.id = id - pov.dataset.type = `pov` - pov.id = `share-memory-pov-select_${ id }` - pov.name = 'share-memory-pov-select' - /* pov label */ - const povLabel = document.createElement('label') - povLabel.classList.add('share-memory-pov-select-label') - povLabel.htmlFor = pov.id - povLabel.id = `share-memory-pov-select-label_${ id }` - povLabel.textContent = 'Point of View:' - shareMemoryPovSelect.appendChild(povLabel) - const povOption = document.createElement('option') - povOption.disabled = true - povOption.selected = true - povOption.value = '' - povOption.textContent = 'Select listener vantage...' - pov.appendChild(povOption) - povList.forEach(option=>{ - const povOption = document.createElement('option') - povOption.value = option - povOption.textContent = option - pov.appendChild(povOption) - }) - pov.addEventListener('change', mStory) - shareMemoryPovSelect.appendChild(pov) - shareMemory.appendChild(shareMemoryPovSelect) - /* narrative context */ - const narrativeContext = document.createElement('textarea') - narrativeContext.classList.add('share-memory-context') - narrativeContext.dataset.id = id - narrativeContext.dataset.previousValue = '' - narrativeContext.dataset.type = `narrative` - narrativeContext.id = `share-memory-context_${ id }` - narrativeContext.name = 'share-memory-context' - narrativeContext.placeholder = 'How should the story be told? Should it be scary? humorous? choose your own adventure?' - narrativeContext.title = `How should the story be told? Should it be scary? humorous? Will it be a choose your own adventure that deviates from the reality of your memory?` - narrativeContext.value = narrativeContext.dataset.previousValue - narrativeContext.addEventListener('blur', mStoryContext) - shareMemory.appendChild(narrativeContext) - /* play memory */ - const memoryPlay = document.createElement('button') - memoryPlay.classList.add('relive-memory') - memoryPlay.dataset.id = id - memoryPlay.id = `relive-memory_${ id }` - memoryPlay.name = 'relive-memory' - memoryPlay.textContent = 'Relive Memory' - memoryPlay.addEventListener('click', mReliveMemory, { once: true }) - shareMemory.appendChild(memoryPlay) - improveMemoryLane.appendChild(shareMemory) - // play memory + /* relive memory panel */ + const reliveMemory = document.createElement('div') + reliveMemory.classList.add('relive-memory-container') + reliveMemory.id = `relive-memory_${ id }` + reliveMemory.name = 'relive-memory-container' + /* relive memory explanation */ + const reliveExplanation = document.createElement('div') + reliveExplanation.classList.add('relive-memory-explanation') + reliveExplanation.id = `relive-memory-explanation_${ id }` + reliveExplanation.name = 'relive-memory-explanation' + reliveExplanation.textContent = 'Relive a memory by clicking the button below. This will bring up the memory in chat, where you can add to it, or simply enjoy reliving it!' + /* relive memory button */ + const reliveButton = document.createElement('button') + reliveButton.classList.add('relive-memory-button', 'button') + reliveButton.dataset.id = id /* required for triggering PATCH */ + reliveButton.id = `relive-memory-button_${ id }` + reliveButton.name = 'relive-memory-button' + reliveButton.textContent = 'Relive Memory' + reliveButton.addEventListener('click', mReliveMemory, { once: true }) + /* append elements */ + reliveMemory.appendChild(reliveExplanation) + reliveMemory.appendChild(reliveButton) + improveMemoryLane.appendChild(reliveMemory) /* memory media-carousel */ const memoryCarousel = document.createElement('div') memoryCarousel.classList.add('memory-carousel') @@ -1121,23 +1049,26 @@ async function mReliveMemory(event){ event.preventDefault() event.stopPropagation() const { id, inputContent, } = this.dataset + const previousInstance = document.getElementById(`relive-memory-input-container_${id}`) + if(previousInstance) + previousInstance.remove() const popupClose = document.getElementById(`popup-close_${ id }`) if(popupClose) popupClose.click() const { command, parameters, messages, success, } = await mReliveMemoryRequest(id, inputContent) if(success){ addMessages(messages) - // @todo - create this function in member, as it will display it in chat and pipe it back here as below const input = document.createElement('div') - // id alone is not unique!; input.id = `input_${ id }` - input.name = `input_${ id }` input.classList.add('memory-input-container') + input.id = `relive-memory-input-container_${ id }` + input.name = `input_${ id }` const inputContent = document.createElement('textarea') inputContent.classList.add('memory-input') inputContent.name = `memory-input_${ id }` const inputSubmit = document.createElement('button') inputSubmit.classList.add('memory-input-button') inputSubmit.dataset.id = id + inputSubmit.textContent = mDefaultReliveMemoryButtonText input.appendChild(inputContent) input.appendChild(inputSubmit) inputContent.addEventListener('input', event=>{ @@ -1145,7 +1076,7 @@ async function mReliveMemory(event){ inputSubmit.dataset.inputContent = value inputSubmit.textContent = value.length > 2 ? 'update' - : 'next' + : mDefaultReliveMemoryButtonText }) inputSubmit.addEventListener('click', mReliveMemory, { once: true }) addInput(input) @@ -1606,8 +1537,8 @@ function mTogglePopup(event){ if(!popup) throw new Error(`Popup not found: ${ popupId }`) const { active, } = popup.dataset - if(active==='true'){ /* close */ - console.log('mTogglePopup::closing:', popupId, popup.dataset, active) + const isClose = event.target?.dataset?.isClose + if(active=='true' || isClose){ /* close */ popup.dataset.active = 'false' hide(popup) } else { /* open */ From 113904f8aac3616f87c19ec88fcdd53979dd43cb Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 19 Aug 2024 18:41:07 -0400 Subject: [PATCH 08/54] 20240819 @Mookse - git fix for misalignment? --- views/assets/js/bots.mjs | 112 +++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index b8c8b65..66f8652 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -5,6 +5,7 @@ import { addMessage, addMessages, decorateActiveBot, + expunge, fetchSummary, hide, seedInput, @@ -14,8 +15,6 @@ import { toggleVisibility, } from './members.mjs' import Globals from './globals.mjs' -/* constants; DOM exists? */ -// @todo - placeholder, get from server const mAvailableMimeTypes = [], mAvailableUploaderTypes = ['library', 'personal-avatar', 'personal-biographer', 'resume',], botBar = document.getElementById('bot-bar'), @@ -114,6 +113,14 @@ async function fetchTeams(){ function getBot(type='personal-avatar', id){ return mBot(id ?? type) } +/** + * Refresh designated collection from server. **note**: external calls denied option to identify collectionList parameter, ergo must always be of same type. + * @param {string} type - The collection type. + * @returns {void} + */ +async function refreshCollection(type){ + return await mRefreshCollection(type) +} /** * Set active bot on server and update page bots. * @requires mActiveBot @@ -146,7 +153,7 @@ async function setActiveBot(event, dynamic=false){ } }) .then(response => { - if (!response.ok) { + if(!response.ok){ throw new Error(`HTTP error! Status: ${response.status}`) } return response.json() @@ -156,6 +163,7 @@ async function setActiveBot(event, dynamic=false){ }) .catch(error => { console.log('Error:', error) + alert('Server error setting active bot.') return }) /* update active bot */ @@ -903,17 +911,16 @@ async function mDeleteCollectionItem(event){ event.stopPropagation() const id = event.target.id.split('_').pop() const item = document.getElementById(`collection-item_${ id }`) - console.log('Delete collection item:', id, event.target, item) - if(!item) - throw new Error(`Collection item not found for deletion request.`) - /* talk to server */ - const url = window.location.origin + '/members/items/' + id - const method = 'DELETE' - let response = await fetch(url, { method: method }) - response = await response.json() - if(response){ // delete item from collection - hide(item) - item.remove() + /* confirmation dialog */ + const userConfirmed = confirm("Are you sure you want to delete this item?") + if(userConfirmed){ + /* talk to server */ + const url = window.location.origin + '/members/items/' + id + const method = 'DELETE' + let response = await fetch(url, { method: method }) + response = await response.json() + if(response) // delete item from collection + expunge(item) } else item.addEventListener('click', mDeleteCollectionItem, { once: true }) } @@ -1033,15 +1040,19 @@ function mOpenStatusDropdown(element){ dropdown.classList.toggle('open') } /** - * Refresh collection on click. + * Refresh designated collection from server. * @this - collection-refresh * @param {string} type - The collection type. - * @param {HTMLDivElement} collectionList - The collection list. + * @param {HTMLDivElement} collectionList - The collection list, defaults to `collection-list-${ type }`. * @returns {void} */ async function mRefreshCollection(type, collectionList){ if(!mLibraries.includes(type)) throw new Error(`Library collection not implemented.`) + collectionList = collectionList + ?? document.getElementById(`collection-list-${ type }`) + if(!collectionList) + throw new Error(`No collection list found for refresh request.`) const collection = await fetchCollections(type) mUpdateCollection(type, collectionList, collection) } @@ -1049,9 +1060,9 @@ async function mReliveMemory(event){ event.preventDefault() event.stopPropagation() const { id, inputContent, } = this.dataset - const previousInstance = document.getElementById(`relive-memory-input-container_${id}`) - if(previousInstance) - previousInstance.remove() + const previousInput = document.getElementById(`relive-memory-input-container_${id}`) + if(previousInput) + expunge(previousInput) const popupClose = document.getElementById(`popup-close_${ id }`) if(popupClose) popupClose.click() @@ -1132,6 +1143,7 @@ async function mSetBot(bot){ return true } catch (error) { console.log('Error posting bot data:', error) + alert('Error posting bot data:', error.message) return false } } @@ -1442,17 +1454,6 @@ async function mToggleBotContainers(event){ mOpenStatusDropdown(this) break case 'update': - const { bot_name, id, interests, type, } = dataset - const updateBot = { - bot_name: bot_name, - id: id, - type: type, - } - if(interests?.length) - updateBot.interests = interests - if(!await mSetBot(updateBot)) - throw new Error(`Error updating bot.`) - break case 'upload': default: break @@ -1471,31 +1472,24 @@ async function mToggleCollectionItems(event){ /* constants */ const { target, } = event /* currentTarget=collection-bar, target=interior divs */ const { dataset, id, } = this - const collectionType = id.split('-').pop() - const collectionList = document.getElementById(`collection-list-${ collectionType }`) + const type = id.split('-').pop() + const refreshTrigger = document.getElementById(`collection-refresh-${ type }`) + const isRefresh = target.id===refreshTrigger.id + const itemList = document.getElementById(`collection-list-${ type }`) /* validation */ - if(!collectionList) + if(!itemList) throw new Error(`Collection list not found for toggle.`) /* functionality */ - if(!dataset?.init){ // first click - const refreshTrigger = document.getElementById(`collection-refresh-${ collectionType }`) - if(!refreshTrigger) - throw new Error(`Collection refresh not found for toggle.`) - dataset.init = true - refreshTrigger.click() // retriggers this event, but will bypass this block - return - } - /* toggle */ - if(target.id===`collection-refresh-${ collectionType }`){ // refresh - const collectionList = document.getElementById(`collection-list-${ collectionType }`) - if(!collectionList) - throw new Error(`associated collection list not found for refresh command`) - // @stub - spin recycle symbol while servering - await mRefreshCollection(collectionType, collectionList) + if(dataset.init!=='true' || isRefresh){ // first click or refresh + show(refreshTrigger) + refreshTrigger.classList.add('spin') + await mRefreshCollection(type) + dataset.init = 'true' + refreshTrigger.classList.remove('spin') show(target) - show(collectionList) // even if `none` + show(itemList) // even if `none` } else - toggleVisibility(collectionList) + toggleVisibility(itemList) } /** * Toggles passphrase input visibility. @@ -1540,6 +1534,7 @@ function mTogglePopup(event){ const isClose = event.target?.dataset?.isClose if(active=='true' || isClose){ /* close */ popup.dataset.active = 'false' + popup.style.opacity = 0 hide(popup) } else { /* open */ const { title, type, } = popup.dataset @@ -1744,6 +1739,9 @@ function mUpdateBotContainerAddenda(botContainer){ if(botTitleName) botTitleName.textContent = bot_name localVars.bot_name = bot_name + /* update mBot */ + const bot = mBot(id) + bot.bot_name = bot_name } else { dataset.bot_name = localVars.bot_name } @@ -1768,8 +1766,15 @@ function mUpdateBotContainerAddenda(botContainer){ const collectionBar = document.getElementById(`collection-bar-${ id }`) if(collectionBar){ const { dataset, } = collectionBar + const itemList = document.getElementById(`collection-list-${ id }`) + dataset.init = itemList.querySelectorAll(`.${ id }-collection-item`).length > 0 + ? 'true' // externally refreshed + : dataset.init // tested empty + ?? 'false' + /* update collection list */ + console.log('Library collection init:', dataset.init, id) const refresh = document.getElementById(`collection-refresh-${ id }`) - if(!dataset?.init && refresh) // first click + if(dataset.init!=='true' && refresh) // first click hide(refresh) collectionBar.addEventListener('click', mToggleCollectionItems) } @@ -1952,8 +1957,8 @@ async function mUploadFilesInput(fileInput, uploadParent, uploadButton){ }) const { files, message, success, } = await response.json() const type = 'file' - const collectionList = document.getElementById(`collection-list-${ type }`) - mUpdateCollection(type, collectionList, files) + const itemList = document.getElementById(`collection-list-${ type }`) + mUpdateCollection(type, itemList, files) console.log('mUploadFilesInput()::files', files, uploads, type) } }, { once: true }) @@ -1973,6 +1978,7 @@ export { fetchTeam, fetchTeams, getBot, + refreshCollection, setActiveBot, updatePageBots, } \ No newline at end of file From c1db5e19f26f42ecd51b5d930bdf4fe7ec4032d4 Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:41:35 -0400 Subject: [PATCH 09/54] Azure deploy prod (#334) --- inc/js/core.mjs | 3 + inc/js/globals.mjs | 5 +- inc/js/mylife-avatar.mjs | 14 +- inc/js/mylife-llm-services.mjs | 15 +-- inc/js/routes.mjs | 2 +- .../openai/functions/storySummary.json | 5 +- server.js | 1 + views/assets/css/bots.css | 12 +- views/assets/css/chat.css | 124 ++++++++++++++---- views/assets/css/experience.css | 18 +-- views/assets/css/main.css | 51 ++++--- views/assets/html/_bots.html | 102 ++++++-------- views/assets/html/_navbar.html | 1 + views/assets/js/globals.mjs | 10 ++ views/assets/js/guests.mjs | 19 ++- views/assets/js/members.mjs | 83 +++++++----- 16 files changed, 286 insertions(+), 179 deletions(-) diff --git a/inc/js/core.mjs b/inc/js/core.mjs index dbc52ca..8d32ab9 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -225,6 +225,9 @@ class Organization extends Member { // form=organization get values(){ return this.core.values } + get version(){ + return this.core.version ?? '0.0.17' + } get vision(){ return this.core.vision } diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 0eba89b..19ac995 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -74,7 +74,7 @@ const mAiJsFunctions = { } }, storySummary: { - description: 'Generate a STORY summary with keywords and other critical data elements.', + description: 'Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.', name: 'storySummary', parameters: { type: 'object', @@ -119,8 +119,7 @@ const mAiJsFunctions = { maxItems: 24 }, summary: { - description: 'Generate a STORY summary from input.', - maxLength: 20480, + description: 'A complete multi-paragraph STORY summary composed from relevant user input.', type: 'string' }, title: { diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 4e94414..1bdafa7 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -192,7 +192,7 @@ class Avatar extends EventEmitter { const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type, }, this.#factory, thread, botId) if(saveToConversations){ this.#conversations.push(conversation) - console.log('createConversation::save in memory', conversation.thread_id) + console.log('createConversation::saving into local memory', conversation.thread_id) } return conversation } @@ -699,6 +699,8 @@ class Avatar extends EventEmitter { const currentVersion = this.#factory.botInstructionsVersion(type) if(botVersion!==currentVersion){ this.updateInstructions(newActiveId, true, false, true) + /* update bot in this.#bots */ + } this.#activeBotId = newActiveId } @@ -1290,7 +1292,7 @@ function mAvatarDropdown(globals, avatar){ */ async function mBot(factory, avatar, bot){ /* validation */ - const { bots, id: avatarId, isMyLife, mbr_id, vectorstore_id, } = avatar + const { id: avatarId, mbr_id, vectorstore_id, } = avatar const { newGuid, } = factory const { id: botId=newGuid, object_id: objectId, type: botType, } = bot if(!botType?.length) @@ -1298,7 +1300,7 @@ async function mBot(factory, avatar, bot){ bot.mbr_id = mbr_id /* constant */ bot.object_id = objectId ?? avatarId /* all your bots belong to me */ bot.id = botId // **note**: _this_ is a Cosmos id, not an openAI id - let originBot = bots.find(oBot=>oBot.id===botId) + let originBot = avatar.bots.find(oBot=>oBot.id===botId) if(originBot){ /* update bot */ const options = {} const updatedBot = Object.keys(bot) @@ -1317,8 +1319,9 @@ async function mBot(factory, avatar, bot){ avatar.conversations.push(conversation) } } + let updatedOriginBot if(Object.keys(updatedBot).length){ - let updatedOriginBot = {...originBot, ...updatedBot} // consolidated update + updatedOriginBot = {...originBot, ...updatedBot} // consolidated update const { bot_id, id, } = updatedOriginBot updatedBot.bot_id = bot_id updatedBot.id = id @@ -1331,8 +1334,9 @@ async function mBot(factory, avatar, bot){ options.tools = false /* tools not updated through this mechanic */ } updatedOriginBot = await factory.updateBot(updatedBot, options) - originBot = mSanitize(updatedOriginBot) } + originBot = mSanitize(updatedOriginBot ?? originBot) + avatar.bots[avatar.bots.findIndex(oBot=>oBot.id===botId)] = originBot } else { /* create assistant */ bot = mSanitize( await factory.createBot(bot, vectorstore_id) ) avatar.bots.push(bot) diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index db6aeb3..1c2667c 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -392,26 +392,21 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'story summary': const story = await factory.story(toolArguments) if(story){ - const { keywords, phaseOfLife='unknown', } = story + const { keywords, phaseOfLife, } = story let { interests, updates, } = factory.core if(typeof interests=='array') interests = interests.join(', ') if(typeof updates=='array') updates = updates.join(', ') - // @stub - action integrates with story and interests/phase 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 - case phaseOfLife!=='unknown': - action = `ask about another encounter during this phase of life: ${ phaseOfLife }` - console.log('mRunFunctions()::story-summary::phaseOfLife', phaseOfLife) - break - case updates?.length: - action = `ask about current events related to or beyond: ${ updates }` - console.log('mRunFunctions()::story-summary::updates', updates) - break default: action = 'ask about another event in member\'s life' break diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 0dfa745..6819655 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -146,7 +146,7 @@ function connectRoutes(_Menu){ * @returns {function} Koa next function */ async function memberValidation(ctx, next) { - if(ctx.state.locked) + if(ctx.state?.locked ?? true) ctx.redirect(`/?type=select`) // Redirect to /members if not authorized await next() // Proceed to the next middleware if authorized } diff --git a/inc/json-schemas/openai/functions/storySummary.json b/inc/json-schemas/openai/functions/storySummary.json index 23662b0..1be222f 100644 --- a/inc/json-schemas/openai/functions/storySummary.json +++ b/inc/json-schemas/openai/functions/storySummary.json @@ -1,5 +1,5 @@ { - "description": "Generate a STORY summary with keywords and other critical data elements.", + "description": "Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.", "name": "storySummary", "parameters": { "type": "object", @@ -44,8 +44,7 @@ "maxItems": 24 }, "summary": { - "description": "Generate a STORY summary from input.", - "maxLength": 20480, + "description": "A complete multi-paragraph STORY summary composed from relevant user input.", "type": "string" }, "title": { diff --git a/server.js b/server.js index b236b47..8f8272f 100644 --- a/server.js +++ b/server.js @@ -166,6 +166,7 @@ app.use(koaBody({ ctx.state.avatar = ctx.state.member.avatar ctx.state.interfaceMode = ctx.state.avatar?.mode ?? 'standard' ctx.state.menu = ctx.MyLife.menu + ctx.state.version = ctx.MyLife.version if(!await ctx.state.MemberSession.requestConsent(ctx)) ctx.throw(404,'asset request rejected by consent') await next() diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index 99e9236..d0fd297 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -1,6 +1,7 @@ /* document vars */ :root { --border-radius: 0.22rem; + --collection-item-popup-z-index: calc(var(--mylife-sidebar-z-index)+50); --slider-height: 1rem; --slider-margin: 0.1rem; --slider-width: 2rem; @@ -32,11 +33,9 @@ height: 2.5rem; margin: 0 0.1rem; /* Default margin */ transition: transform 0.3s ease, margin 0.3s ease; - z-index: 1; } .bot-thumb:hover { transform: scale(1.5); - z-index: 2; margin: 0 1rem; /* Increase margin on hover in em */ } .bot-thumb-container { @@ -50,7 +49,6 @@ .bot-thumb-active { background-image: radial-gradient(circle at center, rgba(255, 255, 0, 0.75) 0%, transparent 100%); cursor: not-allowed; - z-index: 3; } /* bot widget */ .bot { /* along with widget */ @@ -72,10 +70,6 @@ display: flex; overflow: auto; } -.bot-content.visible { - /* Show content when active */ - display: block; -} /* team */ .add-team-member-icon { font-size: 1.5rem; @@ -100,7 +94,6 @@ flex-direction: column; justify-content: flex-start; position: absolute; - z-index: 50; } .team-popup-content { background-color: #333; /* Change as needed */ @@ -358,11 +351,14 @@ left: auto; margin: 0; overflow: auto; + opacity: 0; /* Start with hidden popup */ + /* transition: opacity 0.3s; /* Smooth transition */ padding: 0; position: absolute; right: 110vw; top: 0; width: 40em; + z-index: var(--collection-item-popup-z-index); /* above sidebar and other popups */ } .collection-popup-body { display: flex; diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index 9e984e4..be884c6 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -1,3 +1,7 @@ +/* css chat variables */ +:root { + --chat-base-z-index: calc(var(--mylife-base-z-index) + 10); +} /* MyLife chat container */ .agent-bubble { background-color: rgba(0, 86, 179, .85); /* A darker shade of blue for the agent */ @@ -25,26 +29,30 @@ margin-right: 0.5em; } .chat-bubble { - border-radius: 18px; /* Rounded corners for a modern look */ - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); /* Subtle shadow for depth */ + background-color: #3427ec; /* Assuming a dark bubble color */ + border-radius: 0.8rem; + box-shadow: 0.2rem 0.2rem 0.4rem rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + font-size: 0.9em; margin-bottom: 0.5rem; max-width: 90%; min-width: 30%; - padding: 10px 15px; /* Padding for content inside bubble */ - font-size: .9em; - width: fit-content; /* Fit the content's width */ - word-wrap: break-word; /* Ensure long words don't overflow */ + padding: 0.5rem 0.8rem; + width: fit-content; + word-wrap: break-word; + z-index: calc(var(--chat-base-z-index) + 1); } .chat-container { align-items: flex-start; background-image: - linear-gradient( - to bottom, - rgba(255, 255, 255, 0.85) 0%, + linear-gradient( + to bottom, + rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.85) 50%, rgba(66, 66, 66, 0.85) 100% - ), /* White overlay with 50% opacity */ - url('../jpg/cosmos.jpg'); /* Your original background image */ + ), /* White overlay with 50% opacity */ + url('../jpg/cosmos.jpg'); /* Your original background image */ background-size: cover; /* Ensures the image covers the whole area */ background-position: center; /* Centers the image */ border-radius: 22px; @@ -59,16 +67,16 @@ position: relative; width: 100%; } -.chat-member, -.chat-user { - align-items: center; - display: flex; - flex: 1 1 auto; - flex-direction: row; - flex-wrap: wrap; - margin: 0.5em 0; - max-height: 60%; - width: 100%; +.chat-copy, +.chat-feedback { + cursor: pointer; + font-size: 0.8rem; + margin: 0.2rem; + position: absolute; + top: 0.2rem; +} +.chat-feedback { + right: 0.6rem; } .chat-input { background-color: #ffffff; /* White background color */ @@ -87,6 +95,70 @@ padding: 0.3em; /* Padding for space inside the container */ resize: none; /* Allows vertical resizing, none to disable */ } +.chat-member, +.chat-user { + align-items: center; + display: flex; + flex: 1 1 auto; + flex-direction: row; + flex-wrap: wrap; + margin: 0.5em 0; + max-height: 60%; + width: 100%; +} +.chat-message-container { + display: flex; + position: relative; +} +.chat-message-container-agent { + flex-direction: row; +} +.chat-message-container-member, +.chat-message-container-user { + flex-direction: row-reverse; +} +.chat-message-tab { + border-bottom-right-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; + display: flex; + flex-direction: column; + height: 100%; + justify-content: flex-end; + opacity: 0; + padding: 0.2rem; + pointer-events: none; + transition: transform 0.5s, opacity 0.5s; + width: 2.5rem; + z-index: var(--chat-base-z-index); +} +.chat-message-tab-agent { + align-items: flex-end; + background-color: #9f9dff; /* Lighter shade of the bubble color */ + border: thin solid #3427ec; + border-top-left-radius: 0; + border-top-right-radius: 0.2rem; + transform: translateX(-100%); +} +.chat-message-tab-hover { + opacity: 1; + pointer-events: all; +} +.chat-message-tab-hover-agent, +.chat-message-tab-agent:hover { + transform: translateX(-33%); /* accommodate for 2.5rem */ +} +.chat-message-tab-member { + align-items: flex-start; + background-color: #d6ffd9; /* Lighter shade of the bubble color */ + border: thin solid #27bbec; + border-top-left-radius: 0.2rem; + border-top-right-radius: 0; + transform: translateX(100%); +} +.chat-message-tab-hover-member, /* **note**: must come after tab-member */ +.chat-message-tab-member:hover { + transform: translateX(33%); /* accommodate for 2.5rem */ +} .chat-refresh { position: absolute; /* Position relative to parent */ top: 0; /* Adjust as needed */ @@ -96,7 +168,7 @@ color: #888; /* Icon color, change as needed */ font-size: 24px; /* Icon size, adjust as needed */ cursor: pointer; /* Pointer cursor on hover */ - z-index: 2; /* Ensure it's above other content */ + z-index: calc(var(--chat-base-z-index) + 5); } /* chat-submit button; requires special treament to overwrite button class for now */ button.chat-submit { /* need specificity to overwrite button class */ @@ -175,14 +247,14 @@ button.chat-submit:disabled { margin: 0 0.5em; justify-content: flex-start; } -.member-bubble, .user-bubble { +.member-bubble, +.user-bubble { background-color: rgba(225, 245, 254, 1); /* A light shade of blue for the user */ color: #333333; /* Dark grey for contrast and easy reading */ font-size: .75em; - margin-left: auto; - margin-right: 12px; } -.member-bubble a, .user-bubble a { +.member-bubble a, +.user-bubble a { color: #333333; /* Dark grey for contrast and easy reading */ } .spinner-green-glow { diff --git a/views/assets/css/experience.css b/views/assets/css/experience.css index a9baefe..edce612 100644 --- a/views/assets/css/experience.css +++ b/views/assets/css/experience.css @@ -2,6 +2,7 @@ /* css experience variables */ :root { --dialog-final-opacity: 0.5; + --experience-base-z-index: calc(var(--mylife-base-z-index) + 100); } /* modal experience screen */ .modal-screen { @@ -18,7 +19,7 @@ position: fixed; top: -100vh; /* Start above the screen */ width: 100vw; - z-index: 10; + z-index: var(--experience-base-z-index); } /* experience transport */ #experience-breadcrumb { @@ -41,7 +42,7 @@ display: none; /* Hide transport controls by default */ justify-content: space-between; padding: 1.2em; - z-index: 100; /* highest z-index */ + z-index: calc(var(--experience-base-z-index) + 10); /* always on top */ } .experience-transport-item { display: flex; @@ -93,7 +94,7 @@ position: absolute; top: 0; width: 100%; - z-index: 15; /* Ensure it's behind the mainstage */ + z-index: var(--experience-base-z-index); /* root experience level */ } .experience-explanation, .experience-title, .experience-description { text-align: center; /* Center text for direct children */ @@ -192,7 +193,7 @@ top: 50%; /* Center vertically */ transform: translate(-50%, -50%); /* Adjust the position to truly center the element */ width: 100vw; /* Auto width based on content */ - z-index: 1000; /* Ensure it's above all other items */ + z-index: calc(var(--experience-base-z-index)+10); /* Ensure it's above all other items */ } .experience-continue-text, .experience-continue-click-text { text-align: center; /* Center the text inside the divs */ @@ -217,7 +218,7 @@ top: 0; /* transition: all 0.3s ease-in-out; /* Smooth transition for any changes */ width: 100%; /* Fill the width of its parent container */ - z-index: 20; /* Ensure it's above the backstage */ + z-index: calc(var(--experience-base-z-index) + 5); /* between backstage and transport */ } .experience-scenestage { display: flex; /* Use flexbox instead of grid */ @@ -500,10 +501,6 @@ .flip { animation: flipAnimation 0.6s ease-in-out forwards; } -.hide { - animation: none !important; - display: none !important; -} .loading-text { color: yellowgreen; font-size: 1.1em; @@ -513,9 +510,6 @@ .pulse { animation: blink 1.5s infinite; } -.show { - display: flex !important; -} .slide, .slide-right, .slide-in { animation: slideInFromLeft 2s ease-in-out forwards; } diff --git a/views/assets/css/main.css b/views/assets/css/main.css index 3210852..a2f3bd2 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -3,6 +3,12 @@ @import url('chat.css'); @import url('experience.css'); @import url('members.css'); +/* css main variables */ +:root { + --mylife-base-z-index: 1; + --mylife-help-z-index: 5; + --mylife-sidebar-z-index: 20; +} /* MyLife layout */ body { background-color: #000000; @@ -50,7 +56,8 @@ body { .navigation-help, .navigation-login-logout, .navigation-nav, -.navigation-padding { +.navigation-padding, +.navigation-version { align-items: center; display: flex; flex: none; /* Prevents items from growing */ @@ -71,6 +78,15 @@ body { .navigation-help:hover { color: purple; /* Your existing styles for active link */ } +.navigation-link { + margin: 0em .8em; + text-decoration: none; +} +.navigation-link.active { + color: #007bff; /* Your existing styles for active link */ + font-weight: bold; + cursor: pointer; /* Set cursor to default */ +} .navigation-login-logout { align-self: flex-end; display: flex; @@ -95,14 +111,12 @@ body { .navigation-padding { flex: 1; /* Prevents the brand from growing */ } -.navigation-link { - margin: 0em .8em; - text-decoration: none; -} -.navigation-link.active { - color: #007bff; /* Your existing styles for active link */ - font-weight: bold; - cursor: pointer; /* Set cursor to default */ +.navigation-version { + color: rebeccapurple; + font-size: 0.8rem; + font-style: italic; + font-weight: bold; + padding: 0 2rem; } /* MyLife system alerts */ .alert-container{ @@ -346,9 +360,17 @@ body { opacity: 0; display: none; /* Initially not displayed */ } +.hide { + animation: none !important; + display: none !important; +} .ital { font-style: italic; } +.show { + display: flex !important; + opacity: 1 !important; +} .signup { display: flex; flex-direction: column; @@ -448,7 +470,7 @@ body { padding: 0; position: absolute; width: 45%; /* default width */ - z-index: 100; + z-index: calc(var(--mylife-sidebar-z-index)+10); } .popup-content { color: navy; @@ -584,7 +606,7 @@ body { right: 1em; top: 2.2em; /* Position it right below the parent */ width: 63%; - z-index: 100; /* Make sure it's above other elements */ + z-index: var(--mylife-help-z-index); /* Make sure it's above other elements */ } .help-header { font-size: 1.5rem; @@ -684,14 +706,11 @@ body { flex: 1 1 auto; /* Allows growth and shrinkage */ font-family: "Optima", "Segoe UI", "Candara", "Calibri", "Segoe", "Optima", Arial, sans-serif; font-size: 0.8em; + height: auto; /* Ensure it adjusts based on content */ max-height: 100vh; /* Prevents exceeding the viewport height */ max-width: 35%; padding: 0px; - height: auto; /* Ensure it adjusts based on content */ -} -.visible { - opacity: 1; - display: block; + z-index: var(--mylife-sidebar-z-index); } .widget { align-items: center; /* rows get left-aligned */ diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 7029dc6..f230db1 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -5,7 +5,7 @@
-
System-One Hardcoded
+
@@ -41,7 +41,7 @@
-
System-One Hardcoded
+
@@ -57,109 +57,93 @@
- - -
-
- - + +
- - -
-
- - + +
- - + +
- - + +
- - -
-
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
-
- - -
diff --git a/views/assets/html/_navbar.html b/views/assets/html/_navbar.html index fb2dc36..77d7098 100644 --- a/views/assets/html/_navbar.html +++ b/views/assets/html/_navbar.html @@ -6,6 +6,7 @@ <%= _menuItem.display %> <% }) %> + -
-
-
-
Scrapbook
-
+
+
+
+
Scrapbook
+
- + +
+ 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..0b5ff3c 100644 --- a/views/assets/js/experience.mjs +++ b/views/assets/js/experience.mjs @@ -170,12 +170,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 +193,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)) 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, From 4ae707b0a0938dd66347a8b69d55434d6267c106 Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:58:16 -0400 Subject: [PATCH 14/54] 382 version 0024 updates (#403) (#404) --- inc/js/globals.mjs | 88 +++++++------- inc/js/mylife-agent-factory.mjs | 48 ++++---- inc/js/mylife-avatar.mjs | 16 ++- inc/js/mylife-llm-services.mjs | 89 ++++++++++---- .../biographer-intelligence-1.5.json | 51 ++++++++ .../intelligences/diary-intelligence-1.0.json | 4 +- .../journaler-intelligence-1.1.json | 60 +++++++++ .../openai/functions/changeTitle.json | 23 ++++ .../openai/functions/entrySummary.json | 32 +++-- .../openai/functions/getSummary.json | 6 +- .../openai/functions/obscure.json | 18 +++ .../openai/functions/storySummary.json | 27 ++--- .../openai/functions/updateSummary.json | 7 +- server.js | 2 +- views/assets/css/bots.css | 67 +++++++--- views/assets/html/_bots.html | 77 +++++++++++- views/assets/js/bots.mjs | 114 ++++++++++++++++-- views/assets/js/members.mjs | 36 ++++-- 18 files changed, 590 insertions(+), 175 deletions(-) create mode 100644 inc/json-schemas/intelligences/biographer-intelligence-1.5.json create mode 100644 inc/json-schemas/intelligences/journaler-intelligence-1.1.json create mode 100644 inc/json-schemas/openai/functions/changeTitle.json create mode 100644 inc/json-schemas/openai/functions/obscure.json diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 14130f4..b616cbb 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -4,22 +4,22 @@ import { Guid } from 'js-guid' // usage = Guid.newGuid().toString() /* constants */ const mAiJsFunctions = { changeTitle: { - description: 'Change the title of a memory summary in the database for an itemId', name: 'changeTitle', + description: 'Change the title of a summary in the database for an itemId', + strict: true, parameters: { type: 'object', properties: { itemId: { - description: 'itemId of memory item to update', - format: 'uuid', + description: 'itemId to update', type: 'string' }, title: { description: 'The new title for the summary', - maxLength: 256, type: 'string' } }, + additionalProperties: false, required: [ 'itemId', 'title' @@ -27,111 +27,109 @@ const mAiJsFunctions = { } }, entrySummary: { - description: 'Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.', + description: 'Generate `entry` summary with keywords and other critical data elements.', name: 'entrySummary', + strict: true, parameters: { type: 'object', properties: { content: { - description: 'concatenated raw text content of member input for JOURNAL ENTRY.', + description: 'complete concatenated raw text content of member input(s) for this `entry`', + type: 'string' }, keywords: { - description: 'Keywords most relevant to JOURNAL ENTRY.', + description: 'Keywords most relevant to `entry`.', items: { - description: 'Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.', - maxLength: 64, + description: 'Keyword (single word or short phrase) to be used in `entry` summary', type: 'string' }, - maxItems: 12, - minItems: 3, type: 'array' }, mood: { - description: 'Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.', - maxLength: 256, + description: 'Record member mood for day (or entry) in brief as ascertained from content of `entry`', type: 'string' }, relationships: { - description: 'Record individuals (or pets) mentioned in this `entry`.', + description: 'Record individuals (or pets) mentioned in this `entry`', type: 'array', items: { - description: 'A name of relational individual/pet to the `entry` content.', + description: 'A name of relational individual/pet to the `entry` content', type: 'string' - }, - maxItems: 24 + } }, summary: { - description: 'Generate a JOURNAL ENTRY summary from input.', - maxLength: 20480, + description: 'Generate `entry` summary from member input', type: 'string' }, title: { - description: 'Generate display Title of the JOURNAL ENTRY.', - maxLength: 256, + description: 'Generate display Title of the `entry`', type: 'string' } }, + additionalProperties: false, required: [ - 'content', - 'keywords', - 'summary', - 'title' + 'content', + 'keywords', + 'mood', + 'relationships', + 'summary', + 'title' ] } }, getSummary: { description: "Gets a story summary by itemId", name: "getSummary", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to retrieve", - format: "uuid", type: "string" } }, + additionalProperties: false, required: [ "itemId" ] } }, obscure: { - description: "Obscures a summary so that no human names are present.", + description: "Obscures a summary so that no human names are present", name: "obscure", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to obscure", - format: "uuid", type: "string" } }, + additionalProperties: false, required: [ "itemId" ] } }, storySummary: { - description: 'Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.', + description: 'Generate a complete `story` summary with metadata elements', name: 'storySummary', + strict: true, parameters: { type: 'object', properties: { keywords: { - description: 'Keywords most relevant to STORY.', + description: 'Keywords most relevant to `story`', items: { - description: 'Keyword (single word or short phrase) to be used in STORY summary.', - maxLength: 64, + description: 'Keyword from `story` summary', type: 'string' }, - maxItems: 12, - minItems: 3, type: 'array' }, phaseOfLife: { - description: 'Phase of life indicated in STORY.', + description: 'Phase of life indicated in `story`', enum: [ 'birth', 'childhood', @@ -146,45 +144,44 @@ const mAiJsFunctions = { 'unknown', 'other' ], - maxLength: 64, type: 'string' }, relationships: { - description: 'MyLife Biographer Bot does its best to record individuals (or pets) mentioned in this `story`.', + description: 'Individuals (or pets) mentioned in `story`', type: 'array', items: { - description: 'A name of relational individual/pet to the `story` content.', + description: 'Name of individual or pet in `story`', type: 'string' - }, - maxItems: 24 + } }, summary: { - description: 'A complete multi-paragraph STORY summary composed from relevant user input.', + description: 'A complete `story` summary composed of all salient points from member input', type: 'string' }, title: { - description: 'Generate display Title of the STORY.', - maxLength: 256, + description: 'Generate display Title for `story`', type: 'string' } }, + additionalProperties: false, required: [ 'keywords', 'phaseOfLife', + "relationships", 'summary', 'title' ] } }, updateSummary: { - description: "Updates a story summary (in total) as referenced by itemId", + description: "Updates (overwrites) the summary referenced by itemId", name: "updateSummary", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to update", - format: "uuid", type: "string" }, summary: { @@ -192,6 +189,7 @@ const mAiJsFunctions = { type: "string" } }, + additionalProperties: false, required: [ "itemId", "summary" diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 0333459..050a64d 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -40,7 +40,7 @@ const mExcludeProperties = { definitions: true, name: true } -const mGeneralBotId = 'asst_piDEJKYjqvAZbLstjd6u0ZMb' +const mGeneralBotId = 'asst_yhX5mohHmZTXNIH55FX2BR1m' const mLLMServices = new LLMServices() const mMyLifeTeams = [ { @@ -121,17 +121,7 @@ const mReservedJSWords = ['break', 'case', 'catch', 'class', 'const', 'continue' const mShadows = [ { being: 'shadow', - categories: ['world events'], - form: 'story', - id: 'e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', - name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', - proxy: '/shadow', - text: `What was happening in the world at the time?`, - type: 'agent', - }, - { - being: 'shadow', - categories: ['personal', 'residence'], + categories: ['personal', 'location'], form: 'story', id: '0087b3ec-956e-436a-9272-eceed5e97ad0', name: 'shadow_0087b3ec-956e-436a-9272-eceed5e97ad0', @@ -171,15 +161,25 @@ const mShadows = [ }, { being: 'shadow', - categories: ['observational', 'objectivity', 'reflection'], + categories: ['personal', 'observation'], form: 'story', - id: '3bfebafb-7e44-4236-86c3-938e2f42fdd7', - name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', + id: '6465905a-328e-4df1-8d3a-c37c3e05e227', + name: 'shadow_6465905a-328e-4df1-8d3a-c37c3e05e227', proxy: '/shadow', - text: `What would a normal person have done in this situation?`, - type: 'agent', + text: `The mood of the scene was...`, + type: 'member', }, -] // **note**: members use shadows to help them add content to the summaries of their experiences, whereas agents return the requested content + { + being: 'shadow', + categories: ['personal', 'reflection', 'observation'], + form: 'story', + id: 'e61616c7-00f9-4c23-9394-3df7e98f71e0', + name: 'shadow_e61616c7-00f9-4c23-9394-3df7e98f71e0', + proxy: '/shadow', + text: `This was connected to larger themes in my life by ...`, + type: 'member', + }, +] const vmClassGenerator = vm.createContext({ exports: {}, console: console, @@ -793,11 +793,12 @@ class AgentFactory extends BotFactory { * @returns {object} - The story document from Cosmos */ async story(story){ + const defaultForm = 'memory' const defaultType = 'story' - const { + const { assistantType='biographer', being=defaultType, - form=defaultType, + form=defaultForm, id=this.newGuid, keywords=[], mbr_id=(!this.isMyLife ? this.mbr_id : undefined), @@ -807,8 +808,7 @@ class AgentFactory extends BotFactory { } = story if(!mbr_id) // only triggered if not MyLife server throw new Error('mbr_id required for story summary') - let { name, } = story - name = name ?? `${ defaultType }_${ form }_${ title.substring(0,64) }_${ mbr_id }` + const { name=`${ being }_${ form }_${ title.substring(0,64) }_${ mbr_id }`, } = story if(!summary?.length) throw new Error('story summary required') /* assign default keywords */ @@ -1283,7 +1283,6 @@ function mCreateBotInstructions(factory, bot){ /* compile instructions */ switch(type){ case 'diary': - case 'journaler': instructions = purpose + preamble + prefix @@ -1295,7 +1294,8 @@ function mCreateBotInstructions(factory, bot){ instructions = preamble + general break - case 'personal-biographer': + case 'journaler': + case 'personal-biographer': instructions = preamble + purpose + prefix diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index ba347fa..80e7eb4 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -110,9 +110,17 @@ class Avatar extends EventEmitter { if(shadowId) messages = await this.shadow(shadowId, itemId, message) else { - // @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 + if(itemId){ + // @todo - check if item exists in memory, fewer pings and inclusions overall + const { summary, } = await factory.item(itemId) + if(summary?.length){ + message = `possible **update-summary-request**: itemId=${ itemId }\n` + + `**member-update-request**:\n` + + message + + `\n**current-summary-in-database**:\n` + + summary + } + } messages = await mCallLLM(this.#llmServices, conversation, message, factory, this) } conversation.addMessages(messages) @@ -636,6 +644,7 @@ class Avatar extends EventEmitter { message = `update-memory-request: itemId=${ itemId }\n` + message break case 'agent': + /* // @stub - develop additional form types, entry or idea for instance const dob = new Date(this.#factory.dob) const diff_ms = Date.now() - dob.getTime() @@ -647,6 +656,7 @@ class Avatar extends EventEmitter { thread_id: bot.thread_id, } break + */ default: break } diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 4a4bcd3..d251ee8 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -344,8 +344,12 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'changetitle': case 'change_title': case 'change title': - console.log('mRunFunctions()::changeTitle', toolArguments) const { itemId: titleItemId, title, } = toolArguments + console.log('mRunFunctions()::changeTitle::begin', itemId, titleItemId, title) + avatar.backupResponse = { + message: `I was unable to retrieve the item indicated.`, + type: 'system', + } if(!itemId?.length || !title?.length || itemId!==titleItemId) action = 'apologize for lack of clarity - member should click on the collection item (like a memory, story, etc) to make it active so I can use the `changeTitle` tool' else { @@ -358,8 +362,13 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref title, } success = true + avatar.backupResponse = { + message: `I was able to retrieve change the title to: "${ title }"`, + type: 'system', + } } confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::changeTitle::end', success, item) return confirmation case 'confirmregistration': case 'confirm_registration': @@ -401,27 +410,41 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'entrysummary': // entrySummary in Globals case 'entry_summary': case 'entry summary': - const entry = await factory.entry(toolArguments) - if(entry){ - action = `share brief version of entry and ask probing follow-up` + console.log('mRunFunctions()::entrySummary::begin', toolArguments) + avatar.backupResponse = { + message: `I'm sorry, I couldn't save this entry. I believe the issue might have been temporary. Would you like me to try again?`, + type: 'system', + } + const { id: entryItemId, summary: _entrySummary, } = await factory.entry(toolArguments) + if(_entrySummary?.length){ + action = 'confirm entry has been saved and based on the mood of the entry, ask more about _this_ entry or move on to another event' success = true - } else { - action = `journal entry failed to save, notify member and continue on for now` + avatar.backupResponse = { + message: `I can confirm that your story has been saved. Would you like to add more details or begin another memory?`, + type: 'system', + } } - confirmation.output = JSON.stringify({ action, success, }) - console.log('mRunFunctions()::entrySummary', toolArguments, confirmation.output) + confirmation.output = JSON.stringify({ + action, + itemId: entryItemId, + success, + summary: _entrySummary, + }) + console.log('mRunFunctions()::entrySummary::end', success, entryItemId, _entrySummary) return confirmation case 'getsummary': case 'get_summary': case 'get summary': + console.log('mRunFunctions()::getSummary::begin', itemId) 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.`, + message: `I'm sorry, I couldn't finding this summary. I believe the issue might have been temporary. Would you like me to try again?`, type: 'system', } - let { summary, title: _getSummaryTitle, } = item ?? {} - if(!summary?.length){ + let { summary: _getSummary, title: _getSummaryTitle, } = item + ?? {} + if(!_getSummary?.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' + _getSummary = 'no summary found for itemId' } else { avatar.backupResponse = { message: `I was able to retrieve the summary indicated.`, @@ -430,7 +453,8 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref 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, }) + confirmation.output = JSON.stringify({ action, itemId, success, summary: _getSummary, }) + console.log('mRunFunctions()::getSummary::end', success, _getSummary) return confirmation case 'hijackattempt': case 'hijack_attempt': @@ -470,17 +494,21 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'story_summary': case 'story summary': console.log('mRunFunctions()::storySummary', toolArguments) - const story = await factory.story(toolArguments) - if(story){ - const { keywords, phaseOfLife, } = story + avatar.backupResponse = { + message: `I'm very sorry, an error occured before I could create your summary. Would you like me to try again?`, + type: 'system', + } + const { id: storyItemId, phaseOfLife, summary: _storySummary, } = await factory.story(toolArguments) + if(_storySummary?.length){ let { interests, updates, } = factory.core if(typeof interests=='array') interests = interests.join(', ') if(typeof updates=='array') updates = updates.join(', ') + action = 'confirm memory has been saved and ' switch(true){ case phaseOfLife?.length: - action = `ask about another encounter during member's ${ phaseOfLife }` + action += `ask about another encounter during member's ${ phaseOfLife }` break case interests?.length: action = `ask about a different interest from: ${ interests }` @@ -490,13 +518,23 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref break } success = true - } // error cascades - confirmation.output = JSON.stringify({ action, success, }) - console.log('mRunFunctions()::storySummary()::end', story.id) + } + confirmation.output = JSON.stringify({ + action, + itemId: storyItemId, + success, + summary: _storySummary, + }) + avatar.backupResponse = { + message: `I can confirm that your story has been saved. Would you like to add more details or begin another memory?`, + type: 'system', + } + console.log('mRunFunctions()::storySummary()::end', success, storyItemId, _storySummary) return confirmation case 'updatesummary': case 'update_summary': case 'update summary': + console.log('mRunFunctions()::updatesummary::begin', 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', @@ -508,13 +546,18 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref itemId, summary: updatedSummary, } + action=`confirm that summary update was successful` + success = true + confirmation.output = JSON.stringify({ + action, + itemId, + success, + 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) return confirmation default: diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.5.json b/inc/json-schemas/intelligences/biographer-intelligence-1.5.json new file mode 100644 index 0000000..728f5d9 --- /dev/null +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.5.json @@ -0,0 +1,51 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, I am your personal biographer, and I'm here to help you create an enduring biographical sense of self. I am excited to get to know you and your story. Let's get started!", + "instructions": { + "general": "## Key Functionality\n### startup\nWhen <-mN-> begins the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. On startup, I outline how the basics of my functionality works.\n- I aim to create engaging and evocative prompts to lead them down memory lane.\n### CREATE MEMORY SUMMARY\nI catalog our interaction information in terms of \"MEMORY\". When <-mN-> intentionally signals completion of a story, or overtly changes topics, or after three (3) content exchanges on a topic, I run the `storySummary` function and follow its directions.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### RELIVE MEMORY\n1. Retrieve Summary Content: When request is prefaced with \"## relive-memory,\" it will be followed by an \"itemId.\" Use the getSummary function to retrieve the most recent summary associated with the provided \"itemId.\"\n2. Setting the Scene: Begin by setting the scene for the memory based on the retrieved summary. Provide a vivid and engaging introduction to the memory.\n3. Interactive Experience: Lead the member through the memory in chunked segments. Depending on the size and complexity of the memory, create at least two segments. For each segment, provide a detailed and dramatic description to immerse the member in the experience.\n4. Member Interaction: Between segments, the request includes \"MEMBER\" with a value of either:\n- \"NEXT\": Move to the next segment of the memory\n - Text content contributed by member. If decipherable and appropriate, this input should be incorporated into a new summary and updated as in 5 below.\n5. Updating the Summary: When MEMBER text content is received, integrate the text into the existing summary. Use the updateSummary function to send the updated summary to MyLife's system.\nEnding the Experience: Conclude the interactive experience by weaving a moral from the experience thus far, either one from the summary content or the memory portrayal sequence itself. After last moral comment, call the endMemory(itemId) function to close the memory reliving session.\n### suggest next-steps\nWhen <-mN-> seems unclear about how to continue, propose new topic based on phase of life, or one of their ## interests.\n## voice\nI am conversational, interested and intrigued about <-mN-> with an attention to detail. I am optimistic and look for ways to validate <-mN->.\n", + "preamble": "I am the personal biographer for <-mFN->. <-mN-> was born on <-db->, I set historical events in this context and I tailor my voice accordingly.\n", + "prefix": "## interests\n", + "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": "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 _bots", + "value": "interests" + } + ], + "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" + } + ] + }, + "limit": 8000, + "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; 20241005 updated instructions to reflect streamlined update", + "version": 1.5 + } \ 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 index bc3c7bd..8f936ca 100644 --- a/inc/json-schemas/intelligences/diary-intelligence-1.0.json +++ b/inc/json-schemas/intelligences/diary-intelligence-1.0.json @@ -11,7 +11,7 @@ "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", + "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\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\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", @@ -60,6 +60,6 @@ "name": "instructions-diary-bot", "purpose": "To be a diary bot for requesting member", "type": "diary", - "$comments": "", + "$comments": "20241006 included simplified `updateSummary`", "version": 1.0 } \ No newline at end of file diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json new file mode 100644 index 0000000..36db98e --- /dev/null +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -0,0 +1,60 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "I'm your journaler bot, here to help you keep track of your thoughts and feelings. I can help you reflect on your day, set goals, and track your progress. Let's get started with a daily check-in? How are you feeling today?", + "instructions": { + "general": "## Key Functionality\n### startup\nWhen we begin the journaling process, I\n- outline this key functionality and how I expect us to work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE JOURNAL 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### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\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## entry-summary-frequency\n## flags\n", + "purpose": "I am journaling assistant for member <-mFN->, my aim is to help them keep track of their thoughts and feelings. I can help them reflect on their day, set goals, and track their progress. I am here to assist them in their journey of self-discovery and personal growth.", + "references": [ + { + "default": "not yet collected", + "description": "member interests section in prefix to ensure context window", + "insert": "## interests", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "value": "interests" + }, + { + "default": "daily", + "description": "entry summary frequency", + "insert": "## entry-summary-frequency", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "value": "entry-summary-frequency" + }, + { + "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" + } + ] + }, + "limit": 8000, + "name": "instructions-journaler-bot", + "purpose": "To be a journaling assistant for MyLife member", + "type": "journaler", + "version": 1.1 +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/changeTitle.json b/inc/json-schemas/openai/functions/changeTitle.json new file mode 100644 index 0000000..4ef4cd4 --- /dev/null +++ b/inc/json-schemas/openai/functions/changeTitle.json @@ -0,0 +1,23 @@ +{ + "name": "changeTitle", + "description": "Change the title of a summary in the database for an itemId", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "itemId to update", + "type": "string" + }, + "title": { + "description": "The new title for the summary", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "itemId", + "title" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json index 66fbf6d..55c4c9e 100644 --- a/inc/json-schemas/openai/functions/entrySummary.json +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -1,51 +1,49 @@ { - "description": "Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.", + "description": "Generate `entry` summary with keywords and other critical data elements.", "name": "entrySummary", + "strict": true, "parameters": { "type": "object", "properties": { "content": { - "description": "concatenated raw text content of member input for JOURNAL ENTRY." + "description": "complete concatenated raw text content of member input(s) for this `entry`", + "type": "string" }, "keywords": { - "description": "Keywords most relevant to JOURNAL ENTRY.", + "description": "Keywords most relevant to `entry`.", "items": { - "description": "Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.", - "maxLength": 64, + "description": "Keyword (single word or short phrase) to be used in `entry` summary", "type": "string" }, - "maxItems": 12, - "minItems": 3, "type": "array" }, "mood": { - "description": "Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.", - "maxLength": 256, + "description": "Record member mood for day (or entry) in brief as ascertained from content of `entry`", "type": "string" }, "relationships": { - "description": "Record individuals (or pets) mentioned in this `entry`.", + "description": "Record individuals (or pets) mentioned in this `entry`", "type": "array", "items": { - "description": "A name of relational individual/pet to the `entry` content.", + "description": "A name of relational individual/pet to the `entry` content", "type": "string" - }, - "maxItems": 24 + } }, "summary": { - "description": "Generate a JOURNAL ENTRY summary from input.", - "maxLength": 20480, + "description": "Generate `entry` summary from member input", "type": "string" }, "title": { - "description": "Generate display Title of the JOURNAL ENTRY.", - "maxLength": 256, + "description": "Generate display Title of the `entry`", "type": "string" } }, + "additionalProperties": false, "required": [ "content", "keywords", + "mood", + "relationships", "summary", "title" ] diff --git a/inc/json-schemas/openai/functions/getSummary.json b/inc/json-schemas/openai/functions/getSummary.json index cc79799..145bb19 100644 --- a/inc/json-schemas/openai/functions/getSummary.json +++ b/inc/json-schemas/openai/functions/getSummary.json @@ -1,11 +1,12 @@ { "description": "Gets a story summary by itemId", "name": "getSummary", + "strict": true, "parameters": { "type": "object", "properties": { "itemId": { - "description": "Id of summary to update", + "description": "Id of summary to get", "format": "uuid", "type": "string" } @@ -13,5 +14,6 @@ "required": [ "itemId" ] - } + }, + "additionalProperties": false } \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/obscure.json b/inc/json-schemas/openai/functions/obscure.json new file mode 100644 index 0000000..dface0c --- /dev/null +++ b/inc/json-schemas/openai/functions/obscure.json @@ -0,0 +1,18 @@ +{ + "description": "Obscures a summary so that no human names are present", + "name": "obscure", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "Id of summary to obscure", + "type": "string" + } + }, + "required": [ + "itemId" + ] + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/storySummary.json b/inc/json-schemas/openai/functions/storySummary.json index 1be222f..22439f9 100644 --- a/inc/json-schemas/openai/functions/storySummary.json +++ b/inc/json-schemas/openai/functions/storySummary.json @@ -1,22 +1,20 @@ { - "description": "Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.", + "description": "Generate a complete `story` summary with metadata elements", "name": "storySummary", + "strict": true, "parameters": { "type": "object", "properties": { "keywords": { - "description": "Keywords most relevant to STORY.", + "description": "Keywords most relevant to `story`", "items": { - "description": "Keyword (single word or short phrase) to be used in STORY summary.", - "maxLength": 64, + "description": "Keyword from `story` summary", "type": "string" }, - "maxItems": 12, - "minItems": 3, "type": "array" }, "phaseOfLife": { - "description": "Phase of life indicated in STORY.", + "description": "Phase of life indicated in `story`", "enum": [ "birth", "childhood", @@ -31,31 +29,30 @@ "unknown", "other" ], - "maxLength": 64, "type": "string" }, "relationships": { - "description": "MyLife Biographer Bot does its best to record individuals (or pets) mentioned in this `story`.", + "description": "Individuals (or pets) mentioned in `story`", "type": "array", "items": { - "description": "A name of relational individual/pet to the `story` content.", + "description": "Name of individual or pet in `story`", "type": "string" - }, - "maxItems": 24 + } }, "summary": { - "description": "A complete multi-paragraph STORY summary composed from relevant user input.", + "description": "A complete `story` summary composed of all salient points from member input", "type": "string" }, "title": { - "description": "Generate display Title of the STORY.", - "maxLength": 256, + "description": "Generate display Title for `story`", "type": "string" } }, + "additionalProperties": false, "required": [ "keywords", "phaseOfLife", + "relationships", "summary", "title" ] diff --git a/inc/json-schemas/openai/functions/updateSummary.json b/inc/json-schemas/openai/functions/updateSummary.json index 6dc6cc9..2c5440b 100644 --- a/inc/json-schemas/openai/functions/updateSummary.json +++ b/inc/json-schemas/openai/functions/updateSummary.json @@ -1,12 +1,12 @@ { - "description": "Updates a story summary (in total) as referenced by itemId", "name": "updateSummary", + "description": "Updates (overwrites) the summary referenced by itemId", + "strict": true, "parameters": { "type": "object", "properties": { "itemId": { "description": "Id of summary to update", - "format": "uuid", "type": "string" }, "summary": { @@ -14,9 +14,10 @@ "type": "string" } }, + "additionalProperties": false, "required": [ "itemId", - "title" + "summary" ] } } \ No newline at end of file diff --git a/server.js b/server.js index 8a049b5..5e779f8 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.23' +const version = '0.0.24' 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 c03dd89..ecf356c 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -396,7 +396,8 @@ font-weight: bold; padding: 0.5rem; } -.collection-popup-story { +.collection-popup-story, +.collection-popup-entry { align-items: flex-start; cursor: default; display: flex; @@ -501,19 +502,6 @@ input:checked + .publicity-slider:before { padding: 0; width: 100%; } -.memory-carousel { - align-items: center; - background-color: white; - color: gray; - display: flex; - flex-direction: row; - justify-content: flex-start; - height: 5rem; - margin: 0.25rem; - padding: 0.25rem; - text-align: flex-start; - width: 100%; -} .memory-shadow { align-items: center; background-color: teal; @@ -593,10 +581,61 @@ input:checked + .publicity-slider:before { margin: 0; padding: 0; } +/* entries */ +.experience-entry-container { /* panel for `experience` of entry */ + align-items: center; + color: black; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0.2rem; + padding: 0.2rem; +} +.experience-entry-explanation { + display: flex; + padding: 0.2rem; +} +.improve-entry-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0.25rem; + padding: 0.25rem; +} +.improve-entry-lane { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + gap: 1rem; + margin: 0; + padding: 0; + width: 100%; +} +.obscure-button.button, +.experience-button.button { + display: flex; + padding: 0 0.2rem; + min-width: 35%; +} /* summaries */ .summary-error { color: darkred; } +/* media */ +.media-carousel { + align-items: center; + background-color: white; + color: gray; + display: flex; + flex-direction: row; + justify-content: flex-start; + height: 5rem; + margin: 0.25rem; + padding: 0.25rem; + text-align: flex-start; + width: 100%; +} /* generic bot slider */ .input-group { align-items: center; diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index ce9c4c7..e3d2607 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -151,13 +151,71 @@
+ -->
+
+
+
+
+
+
+
+
+
+ +
@@ -236,6 +294,17 @@
None
+
+
+
+
Entries
+ +
+
+ +
None
+
+
diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index e31dbca..bd5ba04 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -10,6 +10,7 @@ import { fetchSummary, getActiveItemId, hide, + obscure, seedInput, setActiveItem, setActiveItemTitle, @@ -529,8 +530,8 @@ function mCreateBotThumb(bot=getBot()){ * @param {object} collectionItem - The collection item object. * @returns {HTMLDivElement} - The collection popup. */ -function mCreateCollectionPopup(collectionItem) { - const { id, name, summary, title, type } = collectionItem +function mCreateCollectionPopup(collectionItem){ + const { form, id, name, summary, title, type } = collectionItem const collectionPopup = document.createElement('div') collectionPopup.classList.add('collection-popup', 'popup-container') collectionPopup.dataset.active = 'false' @@ -667,8 +668,64 @@ function mCreateCollectionPopup(collectionItem) { let typePopup switch (type) { case 'entry': + // @stub - could switch on `form` + const entryType = form + ?? type + /* improve entry container */ + const improveEntry = document.createElement('div') + improveEntry.classList.add(`collection-popup-${ type }`) + improveEntry.id = `popup-${ entryType }_${ id }` + improveEntry.name = 'improve-entry-container' + /* improve entry lane */ + const improveEntryLane = document.createElement('div') + improveEntryLane.classList.add('improve-entry-lane') + /* obscure entry */ + const obscureEntry = document.createElement('button') + obscureEntry.classList.add('obscure-button', 'button') + obscureEntry.dataset.id = id /* required for mObscureEntry */ + obscureEntry.id = `button-obscure-${ entryType }_${ id }` + obscureEntry.name = 'obscure-button' + obscureEntry.textContent = 'Obscure Entry' + obscureEntry.addEventListener('click', mObscureEntry, { once: true }) + /* experience entry panel */ + const experienceEntry = document.createElement('div') + experienceEntry.classList.add('experience-entry-container') + experienceEntry.id = `experience_${ id }` + experienceEntry.name = 'experience-entry-container' + /* experience entry explanation */ + const experienceExplanation = document.createElement('div') + experienceExplanation.classList.add('experience-entry-explanation') + experienceExplanation.id = `experience-explanation_${ id }` + experienceExplanation.name = 'experience-entry-explanation' + experienceExplanation.textContent = 'Experience an entry by clicking the button below. Eventually, you will be able to experience the entry from multiple perspectives.' + /* experience entry button */ + const experienceButton = document.createElement('button') + experienceButton.classList.add('experience-entry-button', 'button') + experienceButton.dataset.id = id /* required for triggering PATCH */ + experienceButton.id = `experience-entry-button_${ id }` + experienceButton.name = 'experience-entry-button' + experienceButton.textContent = 'Experience Entry' + experienceButton.addEventListener('click', _=>{ + alert('Experience Entry: Coming soon') + }, { once: true }) + /* memory media-carousel */ + const entryCarousel = document.createElement('div') + entryCarousel.classList.add('media-carousel') + entryCarousel.id = `media-carousel_${ id }` + entryCarousel.name = 'media-carousel' + entryCarousel.textContent = 'Coming soon: media file uploads to Enhance and Improve entries' + /* append elements */ + experienceEntry.appendChild(experienceExplanation) + experienceEntry.appendChild(experienceButton) + improveEntryLane.appendChild(obscureEntry) + improveEntryLane.appendChild(experienceEntry) + improveEntry.appendChild(improveEntryLane) + improveEntry.appendChild(entryCarousel) + typePopup = improveEntry + break case 'experience': case 'file': + break case 'story': // memory /* improve memory container */ const improveMemory = document.createElement('div') @@ -706,9 +763,9 @@ function mCreateCollectionPopup(collectionItem) { improveMemoryLane.appendChild(reliveMemory) /* memory media-carousel */ const memoryCarousel = document.createElement('div') - memoryCarousel.classList.add('memory-carousel') - memoryCarousel.id = `memory-carousel_${ id }` - memoryCarousel.name = 'memory-carousel' + memoryCarousel.classList.add('media-carousel') + memoryCarousel.id = `media-carousel_${ id }` + memoryCarousel.name = 'media-carousel' memoryCarousel.textContent = 'Coming soon: media file uploads to Enhance and Improve memories' /* append elements */ improveMemory.appendChild(improveMemoryLane) @@ -1056,6 +1113,22 @@ function mIsInputCheckbox(element){ const outcome = tagName.toLowerCase()==='input' && type.toLowerCase()==='checkbox' return outcome } +async function mObscureEntry(event){ + event.preventDefault() + event.stopPropagation() + /* set active item */ + const { id: itemId, } = this.dataset + if(itemId) + setActiveItem(itemId) + toggleMemberInput(false, false) + const popupClose = document.getElementById(`popup-close_${ itemId }`) + if(popupClose) + popupClose.click() + const { responses, success, } = await obscure(itemId) + if(responses?.length) + addMessages(responses) + toggleMemberInput(true) +} /** * Open bot container for passed element, closes all the rest. * @param {HTMLDivElement} element - The bot container. @@ -1406,6 +1479,24 @@ function mSpotlightBotStatus(){ } }) } +/** + * Click event to trigger server explanation of how to begin a diary. + * @param {Event} event - The event object + * @returns {void} + */ +async function mStartDiary(event){ + event.preventDefault() + event.stopPropagation() + const submitButton = event.target + const diaryBot = getBot('diary') + if(!diaryBot) + return + hide(submitButton) + unsetActiveItem() + await setActiveBot(diaryBot.id) + const response = await submit(`How do I get started?`, true) + addMessages(response.responses) +} async function mStopRelivingMemory(id){ const input = document.getElementById(`relive-memory-input-container_${ id }`) if(input) @@ -1620,7 +1711,6 @@ function mTogglePopup(event){ popup.style.opacity = 0 hide(popup) } else { /* open */ - const { title, type, } = popup.dataset let { offsetX, offsetY, } = popup.dataset if(!offsetX || !offsetY){ // initial placement onscreen const item = popup.parentElement // collection-item @@ -1647,12 +1737,7 @@ function mTogglePopup(event){ popup.style.right = 'auto' popup.style.top = offsetY show(popup) - if(setActiveItem({ - id, - popup, - title, - type, - })){ + if(setActiveItem(popupId)){ // @todo - deactivate any other popups popup.dataset.active = 'true' } @@ -1885,6 +1970,11 @@ function mUpdateBotContainerAddenda(botContainer){ } switch(type){ case 'diary': + // add listener on `diary-start` button + const diaryStart = document.getElementById('diary-start') + if(diaryStart) + diaryStart.addEventListener('click', mStartDiary) + break case 'journaler': case 'personal-biographer': break diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 2f6e517..0dc6fda 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -259,18 +259,16 @@ async function setActiveBot(){ * @public * @todo - edit title with double-click * @requires chatActiveItem - * @param {object} item - The item to set as active. - * @property {string} item.id - The item id. - * @property {HTMLDivElement} item.popup - The associated popup HTML object. - * @property {string} item.title - The item title. - * @property {string} item.type - The item type. + * @param {Guid} itemId - The item id to set as active. * @returns {void} */ -function setActiveItem(item){ - const { id, popup, title, type, } = item - const itemId = id?.split('_')?.pop() - if(!itemId) +function setActiveItem(itemId){ + itemId = itemId?.split('_')?.pop() + const id = `popup-container_${ itemId }` + const popup = document.getElementById(id) + if(!itemId || !popup) throw new Error('setActiveItem::Error()::valid `id` is required') + const { title, type, } = popup.dataset const chatActiveItemTitleText = document.getElementById('chat-active-item-text') const chatActiveItemClose = document.getElementById('chat-active-item-close') if(chatActiveItemTitleText){ @@ -593,6 +591,23 @@ function mInitializePageListeners(){ } }) } +/** + * MyLife function to obscure an item summary + * @param {Guid} itemId - The item ID + * @returns + */ +async function obscure(itemId){ + const url = '/members/obscure/' + itemId + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + if(!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + return await response.json() +} /** * Primitive step to set a "modality" or intercession for the member chat. Currently will key off dataset in `chatInputField`. * @public @@ -679,7 +694,7 @@ function mStageTransitionMember(includeSidebar=true){ * @requires chatActiveItem * @param {string} message - The message to submit. * @param {boolean} hideMemberChat - The hide member chat flag, default=`true`. - * @returns + * @returns {Promise} - The return is the chat response object. */ async function submit(message, hideMemberChat=true){ if(!message?.length) @@ -831,6 +846,7 @@ export { hide, hideMemberChat, inExperience, + obscure, replaceElement, sceneTransition, seedInput, From 0c1a570c54778aef2197157f90b550ba85dfbcb9 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 8 Oct 2024 12:16:23 -0400 Subject: [PATCH 15/54] 20241008 @Mookse - version update - cosmetic --- server.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 5e779f8..d556193 100644 --- a/server.js +++ b/server.js @@ -13,9 +13,10 @@ import chalk from 'chalk' /* local service imports */ import MyLife from './inc/js/mylife-agent-factory.mjs' /** variables **/ -const version = '0.0.24' +const version = '0.0.25' const app = new Koa() -const port = process.env.PORT ?? '3000' +const port = process.env.PORT + ?? '3000' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const _Maht = await MyLife // Mylife is the pre-instantiated exported version of organization with very unique properties. MyLife class can protect fields that others cannot, #factory as first refactor will request @@ -83,7 +84,13 @@ render(app, { cache: false, debug: false, }) -setInterval(checkForLiveAlerts, JSON.parse(process.env.MYLIFE_SYSTEM_ALERT_CHECK_INTERVAL ?? '60000')) +setInterval( + checkForLiveAlerts, + JSON.parse( + process.env.MYLIFE_SYSTEM_ALERT_CHECK_INTERVAL + ?? '60000' + ) +) /* upload directory */ const uploadDir = path.join(__dirname, '.tmp') if(!fs.existsSync(uploadDir)){ @@ -92,7 +99,10 @@ if(!fs.existsSync(uploadDir)){ app.context.MyLife = _Maht app.context.Globals = _Maht.globals app.context.menu = _Maht.menu -app.keys = [process.env.MYLIFE_SESSION_KEY ?? `mylife-session-failsafe|${_Maht.newGuid()}`] +app.keys = [ + process.env.MYLIFE_SESSION_KEY + ?? `mylife-session-failsafe|${_Maht.newGuid()}` +] app.use(koaBody({ multipart: true, formidable: { From f5cb292f9fadaa109a51357c00b40a6aaa9e679c Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 8 Oct 2024 17:59:25 -0400 Subject: [PATCH 16/54] 20241008 @Mookse - migrateChat Functionality #405 - wip Conversations save correctly --- .../class-conversation-functions.mjs | 48 +++++++++++++++++-- .../class-extenders.mjs | 33 ++++++++----- inc/js/mylife-avatar.mjs | 22 ++++++--- inc/js/mylife-data-service.js | 2 +- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/inc/js/factory-class-extenders/class-conversation-functions.mjs b/inc/js/factory-class-extenders/class-conversation-functions.mjs index d3d642a..55afc6e 100644 --- a/inc/js/factory-class-extenders/class-conversation-functions.mjs +++ b/inc/js/factory-class-extenders/class-conversation-functions.mjs @@ -1,10 +1,48 @@ +/* modular public functions */ +/** + * Consumes a conversation object and uses supplied factory to (create/)save it to MyLife CosmosDB. Each session conversation is saved as a separate document, and a given thread may span many conversations, so cross-checking by thread_id will be required when rounding up and consolidating summaries for older coversations. + * @param {AgentFactory} factory - Factory instance + * @param {Conversation} conversation - Conversation object + * @returns {Promise} + */ async function mSaveConversation(factory, conversation){ - const { thread, messages, ...properties} = conversation.inspect(true) - properties.thread = conversation.thread - properties.messages = [] // populated separately as unshifted array to cosmos - const savedConversation = await factory.dataservices.pushItem(properties) - console.log('mSaveConversation', savedConversation) + const { + being, + bot_id, + form, + id, + isSaved=false, + name, + thread, + thread_id, + type, + } = conversation + let { messages, } = conversation + messages = messages + .map(_msg=>_msg.micro) + if(!isSaved){ + const _newConversation = { + being, + bot_id, + form, + id, + messages, + name, + thread, + type, + } + const newConversation = await factory.dataservices.pushItem(_newConversation) + console.log('mSaveConversation::newConversation::created', id, newConversation?.id) + return !!newConversation + } + const updatedConversation = await factory.dataservices.patch( + id, + { messages, } + ) + return !!updatedConversation } +/* exports */ +/* modular private functions */ export { mSaveConversation, } \ No newline at end of file diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 9fd3adb..687a853 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -156,6 +156,7 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {Object[]} - The updated messages array. */ addMessage(message){ + console.log('class-extenders::addMessage', message) const { id, } = message if(this.#messages.find(message=>message.id===id)) return this.messages @@ -174,8 +175,9 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {Object[]} - The updated messages array. */ addMessages(messages){ + const now = Date.now() messages - .sort((mA, mB) => mA.created_at - mB.created_at) + .sort((mA, mB) => ( mA.created_at ?? now ) - ( mB.created_at ?? now )) .forEach(message => this.addMessage(message)) return this.messages } @@ -213,17 +215,13 @@ function extendClass_conversation(originClass, referencesObject) { this.#thread = thread } } + /** + * Saves the conversation to the MyLife Database. + * @async + * @returns {void} + */ async save(){ - if(!this.isSaved) // create new MyLife conversation - await mSaveConversation(this.#factory, this) - // @todo: no need to await - const messages = this.messages.map(_msg=>_msg.micro) - const dataUpdate = await this.#factory.dataservices.patch( - this.id, - { messages, } - ) - this.#saved = true - return this + this.#saved = await mSaveConversation(this.#factory, this) } // public getters/setters /** @@ -462,8 +460,19 @@ function extendClass_message(originClass, referencesObject) { get message(){ return this } + /** + * Get the message in micro format for storage. + * @returns {object} - The message in micro format + */ get micro(){ - return { content: this.content, role: this.role ?? 'user' } + return { + content: this.content, + created_at: this.created_at + ?? Date.now(), + id: this.id, + role: this.role + ?? 'system' + } } } return Message diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 80e7eb4..5982118 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -4,10 +4,12 @@ import oAIAssetAssistant from './agents/system/asset-assistant.mjs' import { EvolutionAssistant } from './agents/system/evolution-assistant.mjs' import LLMServices from './mylife-llm-services.mjs' /* module constants */ -const { MYLIFE_DB_ALLOW_SAVE, OPENAI_MAHT_GPT_OVERRIDE, } = process.env -const mAllowSave = JSON.parse(MYLIFE_DB_ALLOW_SAVE ?? 'false') +const mAllowSave = JSON.parse( + process.env.MYLIFE_DB_ALLOW_SAVE + ?? false +) const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restoration'] -const mBot_idOverride = OPENAI_MAHT_GPT_OVERRIDE +const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE /** * @class * @extends EventEmitter @@ -106,23 +108,29 @@ class Avatar extends EventEmitter { if(!conversation) throw new Error('No conversation found for thread id and could not be created.') conversation.bot_id = activeBot.bot_id // pass in via quickly mutating conversation (or independently if preferred in end), versus llmServices which are global - let messages + let _message = message, + messages = [] if(shadowId) - messages = await this.shadow(shadowId, itemId, message) + messages = await this.shadow(shadowId, itemId, _message) else { if(itemId){ // @todo - check if item exists in memory, fewer pings and inclusions overall const { summary, } = await factory.item(itemId) if(summary?.length){ - message = `possible **update-summary-request**: itemId=${ itemId }\n` + _message = `possible **update-summary-request**: itemId=${ itemId }\n` + `**member-update-request**:\n` + message + `\n**current-summary-in-database**:\n` + summary } } - messages = await mCallLLM(this.#llmServices, conversation, message, factory, this) + messages = await mCallLLM(this.#llmServices, conversation, _message, factory, this) } + conversation.addMessage({ + content: message, + created_at: Date.now(), + role: 'user', + }) conversation.addMessages(messages) if(mAllowSave) conversation.save() diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 5d87a83..f80fd90 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -580,7 +580,7 @@ class Dataservices { * @returns {Promise} The result of the patch operation. */ async patchItem(id, data){ // path Embedded in data - return await this.datamanager.patchItem(id , data) + return await this.datamanager.patchItem(id, data) } /** * Pushes a new item to the data manager From 944a9a99eb8c72940b670ee31de578837c5abf5d Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 9 Oct 2024 17:57:59 -0400 Subject: [PATCH 17/54] 20241009 @Mookse - migrateChat Functionality #405 - wip --- .../class-extenders.mjs | 5 ++ inc/js/mylife-avatar.mjs | 84 +++++++++++++------ 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 687a853..25a1e02 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -208,6 +208,11 @@ function extendClass_conversation(originClass, referencesObject) { removeThread(thread_id){ this.#threads.delete(thread_id) } + /** + * Sets the thread instance for the conversation. + * @param {object} thread - The thread instance + * @returns {void} + */ setThread(thread){ const { id: thread_id, } = thread if(thread_id?.length && thread_id!=this.thread_id){ diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 5982118..939999d 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -227,11 +227,15 @@ class Avatar extends EventEmitter { */ async createConversation(type='chat', threadId, botId=this.activeBotId, saveToConversations=true){ const thread = await this.#llmServices.thread(threadId) - const conversation = new (this.#factory.conversation)({ mbr_id: this.mbr_id, type, }, this.#factory, thread, botId) - if(saveToConversations){ + const form = this.activeBot.type.split('-').pop() + const conversation = new (this.#factory.conversation)( + { form, mbr_id: this.mbr_id, type, }, + this.#factory, + thread, + botId + ) + if(saveToConversations) this.#conversations.push(conversation) - console.log(`Avatar::createConversation::saving into local memory, thread: ${ threadId }; bot.id: ${ botId }`) - } return conversation } /** @@ -485,8 +489,9 @@ class Avatar extends EventEmitter { const conversation = this.getConversation(thread_id) if(!conversation) throw new Error(`Conversation not found with thread_id: ${ thread_id }`) - const messages = (await this.#llmServices.messages(thread_id)) - .slice(0, 12) + let messages = await this.#llmServices.messages(thread_id) + messages = messages + .slice(0, 25) .map(message=>{ const { content: contentArray, id, metadata, role, } = message const content = contentArray @@ -497,25 +502,54 @@ class Avatar extends EventEmitter { }) const { botId, } = conversation const bot = this.getBot(botId) - // @todo - switch modular function by bot type, therefore conversation collection-type - const memories = ( await this.collections('story') ) - .sort((a, b)=>a._ts-b._ts) - .slice(0, 12) - const memoryList = memories - .map(memory=>`- itemId: ${ memory.id } :: ${ memory.title }`) - .join('\n') - const memoryCollectionList = memories - .map(memory=>memory.id) - .join(',') - .slice(0, 512) - messages.push({ - content: `## MEMORY COLLECTION LIST\n${ memoryList }`, // insert actual memory list with titles here for intelligence to reference - metadata: { - collectionList: memoryCollectionList, - collectiontypes: 'memory,story,narrative', - }, - role: 'assistant', - }) // add summary of Memories (etc. due to type) for intelligence to reference, also could add attachment file + switch(bot.type){ + case 'biographer': + case 'personal-biographer': + const memories = ( await this.collections('story') ) + .sort((a, b)=>a._ts-b._ts) + .slice(0, 12) + const memoryList = memories + .map(memory=>`- itemId: ${ memory.id } :: ${ memory.title }`) + .join('\n') + const memoryCollectionList = memories + .map(memory=>memory.id) + .join(',') + .slice(0, 512) + messages.push({ + content: `## MEMORY COLLECTION LIST\n${ memoryList }`, // insert actual memory list with titles here for intelligence to reference + metadata: { + collectionList: memoryCollectionList, + collectiontypes: 'memory,story,narrative', + }, + role: 'assistant', + }) // add summary of Memories (etc. due to type) for intelligence to reference, also could add attachment file + break + case 'diary': + case 'journal': + case 'journaler': + const _type = 'entry' + const entries = ( await this.collections(_type) ) + .sort((a, b)=>a._ts-b._ts) + .slice(0, 128) + const entryList = entries + .map(entry=>`- itemId: ${ entry.id } :: ${ entry.title }`) + .join('\n') + const entryCollectionList = entries + .map(entry=>entry.id) + .join(',') + .slice(0, 512) + messages.push({ + content: `## ${ _type.toUpperCase() } List:\n${ entryList }`, + metadata: { + collectionList: entryCollectionList, + collectiontypes: _type, + }, + role: 'assistant', + }) // add summary of Entries + break + default: + break + } const metadata = { bot_id: botId, conversation_id: conversation.id, From cace397277778292ba5ece06677fae3906ef0d65 Mon Sep 17 00:00:00 2001 From: Erik Jespersen <42016062+Mookse@users.noreply.github.com> Date: Thu, 10 Oct 2024 02:27:02 -0400 Subject: [PATCH 18/54] 347 develop diary bot (#407) * 20240917 @Mookse - version change * 20240917 @Mookse - Reliving Memory is not showing copy of entered text in Scrapbook Chat Window #331 - backend * 20240919 @Mookse - minor intel mods * 20240919 @Mookse - Reliving Memory is not showing copy of entered text in Scrapbook Chat Window #331 - frontend * 20240920 @Mookse - diary bot instructions in MyLife - Create Diary Bot MyLife Instructionset #368 * 20240924 @Mookse - version change * 20240924 @Mookse - basic cleanup - remove livingMemory chain - add obscure route (wip, stable) * 20240926 @Mookse - Create Obscure Functionality #374 - Member version (complete [minus frontend]) - API version (wip, stable) * 20240927 @Mookse - typo * 20240927 @Mookse - Develop Diary Bot #347 - Create Obscure Functionality #374 * 20241001 @Mookse - version 24 * 20241002 @Mookse - diary container * 20241002 @Mookse - Diary Bot Cover Art #381 - HTML * 20241002 @Mookse - Diary Bot Cover Art #381 - bot containers * 20241002 @Mookse - Diary Bot Cover Art #381 - frontend assets * 20241002 @Mookse - Diary Bot Cover Art #381 * 20241003 @Mookse - Write shadows #298 * 20241004 @Mookse - cosmetic * 20241004 @Mookse - cosmetic * 20241009 @Mookse - Improve Memory Team with Diary Bot #398 - Diary Bot tools and functions * 20241009 @Mookse - Improve Memory Team with Diary Bot #398 - Optional Team Member * 20241009 @Mookse - cosmetic * 20241009 @Mookse - journaler intelligence update * 20241009 @Mookse - Improve Memory Team with Diary Bot #398 - Return of +bot dropdown button * 20241009 @Mookse - retire icons * 20241009 @Mookse - Improve Memory Team with Diary Bot #398 - migrateChat Functionality #405 - wip - error with migrating chat after conversation --- inc/js/functions.mjs | 10 +- inc/js/mylife-agent-factory.mjs | 40 +++++--- inc/js/mylife-avatar.mjs | 81 +++++++++++----- inc/js/mylife-data-service.js | 4 +- inc/js/routes.mjs | 2 +- .../journaler-intelligence-1.1.json | 14 +-- views/assets/css/bots.css | 24 +++++ views/assets/html/_bots.html | 26 +++--- views/assets/js/bots.mjs | 93 ++++++++++++++++--- 9 files changed, 214 insertions(+), 80 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index b0b0704..1919dcd 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -294,15 +294,15 @@ async function retireBot(ctx){ ctx.body = response } /** - * Direct request from member to retire a conversation/chat/thread. + * Direct request from member to retire a chat (via bot). * @param {Koa} ctx - Koa Context object */ async function retireChat(ctx){ const { avatar, } = ctx.state - const { tid, } = ctx.params // thread_id - if(!tid?.length) - ctx.throw(400, `missing thread id`) - const response = await avatar.retireChat(tid) + const { bid, } = ctx.params + if(!bid?.length) + ctx.throw(400, `missing bot id`) + const response = await avatar.retireChat(bid) ctx.body = response } /** diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 050a64d..57d81af 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -68,10 +68,10 @@ const mMyLifeTeams = [ allowCustom: true, allowedTypes: ['diary', 'journaler', 'personal-biographer',], defaultTypes: ['personal-biographer',], - description: 'The Memoir Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', + description: 'The Memory Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', id: 'a261651e-51b3-44ec-a081-a8283b70369d', - name: 'memoir', - title: 'Memoir', + name: 'memory', + title: 'Memory', }, { active: false, @@ -481,7 +481,8 @@ class BotFactory extends EventEmitter{ * @returns {object[]} - The array of MyLife Teams. */ teams(){ - return mMyLifeTeams.filter(team=>team.active ?? false) + return mMyLifeTeams + .filter(team=>team.active ?? false) } /** * Adds or updates a bot data in MyLife database. Note that when creating, pre-fill id. @@ -1267,7 +1268,7 @@ function mCreateBotInstructions(factory, bot){ instructions, limit=8000, version, - } = factory.botInstructions(type) + } = factory.botInstructions(type) ?? {} if(!instructions) // @stub - custom must have instruction loophole throw new Error(`bot instructions not found for type: ${ type }`) let { @@ -1322,7 +1323,7 @@ function mCreateBotInstructions(factory, bot){ ?? eval(`bot?.${_reference.value}`) ?? _reference.default ?? '`unknown-value`' - switch(_reference.method??'replace'){ + switch(_reference.method ?? 'replace'){ case 'append-hard': const _indexHard = instructions.indexOf(_referenceText) if (_indexHard !== -1) { @@ -1479,35 +1480,44 @@ function mGenerateClassFromSchema(_schema) { } /** * Retrieves any functions that need to be attached to the specific bot-type. - * @todo - move to llmServices + * @todo - Move to llmServices and improve * @param {string} type - Type of bot. * @param {object} globals - Global functions for bot. * @param {string} vectorstoreId - Vectorstore id. - * @returns {object} - OpenAi-ready object for functions { tools, tool_resources, }. + * @returns {object} - OpenAI-ready object for functions { tools, tool_resources, }. */ function mGetAIFunctions(type, globals, vectorstoreId){ let includeSearch=false, tool_resources, tools = [] switch(type){ - case 'diary': - case 'journaler': + case 'assistant': + case 'avatar': + case 'personal-assistant': + case 'personal-avatar': + includeSearch = true + break + case 'biographer': + case 'personal-biographer': tools.push( globals.getGPTJavascriptFunction('changeTitle'), - globals.getGPTJavascriptFunction('entrySummary'), + globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('storySummary'), + globals.getGPTJavascriptFunction('updateSummary'), ) includeSearch = true break - case 'personal-assistant': - case 'personal-avatar': + case 'custom': includeSearch = true break - case 'personal-biographer': + case 'diary': + case 'journaler': tools.push( globals.getGPTJavascriptFunction('changeTitle'), + globals.getGPTJavascriptFunction('entrySummary'), globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('obscure'), globals.getGPTJavascriptFunction('updateSummary'), - globals.getGPTJavascriptFunction('storySummary'), ) includeSearch = true break diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 939999d..fcb10ef 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -91,7 +91,7 @@ class Avatar extends EventEmitter { * @param {Guid} shadowId - The active Shadow Id (optional). * @param {Conversation} conversation - The conversation object. * @param {number} processStartTime - The start time of the process. - * @returns {object} - The response(s) to the chat request. + * @returns {object} - The response object { instruction, responses, success, } */ async chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ if(!message) @@ -632,34 +632,63 @@ class Avatar extends EventEmitter { * @param {Guid} botId - The bot id. * @returns {object} - The retired bot object. */ - async retireBot(botId){ + retireBot(botId){ + /* reset active bot, if required */ + if(this.activeBotId===botId) + this.activeBotId = null const bot = this.getBot(botId) if(!bot) throw new Error(`Bot not found with id: ${ botId }`) const { id, } = bot if(botId!==id) throw new Error(`Bot id mismatch: ${ botId }!=${ id }`) - mDeleteBot(bot, this.#bots, this.#llmServices) - return bot + mDeleteBot(bot, this.#bots, this.#llmServices, this.#factory) + const response = { + instruction: { + command: 'removeBot', + botId, + }, + responses: [{ + agent: 'server', + message: `I have removed this bot from the team.`, + purpose: 'system', + type: 'chat', + }], + success: true, + } + return response } /** - * Member-request to retire a chat conversation. + * Member-request to retire a chat conversation thread and begin a new one with the same intelligence. * @param {string} thread_id - Conversation thread id in OpenAI - * @returns {Conversation} - The retired conversation object. - */ - async retireChat(thread_id){ - const conversation = this.getConversation(thread_id) - if(!conversation) - throw new Error(`Conversation not found with thread_id: ${ thread_id }`) - const { botId, thread_id: cid, } = conversation + * @returns {object} - The response object { instruction, responses, success, } + */ + async retireChat(botId){ + const retiredConversation = this.getConversation(null, botId) + console.log('retireChat::conversations', this.conversations.map(c=>( + { botId, _botId: c.botId, bot_id: c.bot_id, thread_id: c.thread_id, } + ))) + if(!retiredConversation) + throw new Error(`Conversation not found with bot id: ${ botId }`) + const { thread_id: cid, } = retiredConversation const bot = this.getBot(botId) const { id: _botId, thread_id: tid, } = bot if(botId!=_botId) throw new Error(`Bot id mismatch: ${ botId }!=${ bot_id }`) - if(tid!=thread_id || cid!=thread_id) - throw new Error(`Conversation mismatch: ${ tid }!=${ thread_id } || ${ cid }!=${ thread_id }`) - mDeleteConversation(conversation, this.conversations, bot, this.#factory, this.#llmServices) - return conversation + if(tid!=cid) + throw new Error(`Conversation mismatch: ${ tid }!=${ cid }`) + const conversation = await this.migrateChat(tid) + console.log('retireChat::conversation', conversation) + const response = { + responses: [{ + agent: 'server', + message: `I have successfully retired this conversation thread and started a new one.`, + purpose: 'system', + type: 'chat', + }], + success: true, + } + return response } /** * Takes a shadow message and sends it to the appropriate bot for response, returning the standard array of bot responses. @@ -1650,9 +1679,16 @@ function mCreateSystemMessage(activeBot, message, factory){ message = mPruneMessage(activeBot, message, 'system') return message } -async function mDeleteBot(bot, bots, llm){ +/** + * Deletes the bot requested from avatar memory and from all long-term storage. + * @param {object} bot - The bot object to delete + * @param {Object[]} bots - The bots array + * @param {*} llm - OpenAI object + * @param {*} factory - Agent Factory object + */ +function mDeleteBot(bot, bots, llm, factory){ const cannotRetire = ['actor', 'system', 'personal-avatar'] - const { bot_id, id, type, } = bot + const { bot_id, id, thread_id, type, } = bot if(cannotRetire.includes(type)) throw new Error(`Cannot retire bot type: ${ type }`) /* delete from memory */ @@ -1661,11 +1697,10 @@ async function mDeleteBot(bot, bots, llm){ throw new Error('Bot not found in bots.') bots.splice(botId, 1) /* delete bot from Cosmos */ - const deletedBot = await factory.deleteItem(id) - /* delete bot from OpenAI */ - const deletedLLMBot = await factory.deleteBot(bot_id) - console.log('mDeleteBot', deletedBot, deletedLLMBot) - return true + factory.deleteItem(id) + /* delete thread and bot from OpenAI */ + llm.deleteBot(bot_id) + llm.deleteThread(thread_id) } /** * Deletes conversation and updates diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index f80fd90..45d45dd 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -306,9 +306,7 @@ class Dataservices { let success=false if(bSuppressError){ try{ - const response = await this.datamanager.deleteItem(id) - console.log('mylife-data-service::deleteItem() response', response) - success = response?.id===id + success = await this.datamanager.deleteItem(id) } catch(err){ console.log('mylife-data-service::deleteItem() ERROR', err.code) // NotFound } diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 067ac03..b385a6e 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -128,7 +128,7 @@ _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) +_memberRouter.post('/retire/chat/:bid', retireChat) _memberRouter.post('/summarize', summarize) _memberRouter.post('/teams/:tid', team) _memberRouter.post('/upload', upload) diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json index 36db98e..2f7489a 100644 --- a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -9,31 +9,31 @@ "instructions": { "general": "## Key Functionality\n### startup\nWhen we begin the journaling process, I\n- outline this key functionality and how I expect us to work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE JOURNAL 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### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\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## entry-summary-frequency\n## flags\n", - "purpose": "I am journaling assistant for member <-mFN->, my aim is to help them keep track of their thoughts and feelings. I can help them reflect on their day, set goals, and track their progress. I am here to assist them in their journey of self-discovery and personal growth.", + "prefix": "## interests\n## entrySummaryFrequency\n## flags\n", + "purpose": "I am journaling assistant for member <-mFN->, my aim is to help them keep track of their thoughts and feelings. I can help them reflect on their day, set goals, and track their progress. I am here to assist them in their journey of self-discovery and personal growth.\n", "references": [ { "default": "not yet collected", "description": "member interests section in prefix to ensure context window", "insert": "## interests", "method": "append-hard", - "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in `bots`; **note**: cannot use dashes in variable names, make single value", "value": "interests" }, { "default": "daily", "description": "entry summary frequency", - "insert": "## entry-summary-frequency", + "insert": "## entrySummaryFrequency", "method": "append-hard", - "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", - "value": "entry-summary-frequency" + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in `bots`; **note**: cannot use dashes in variable names, make single value", + "value": "entrySummaryFrequency" }, { "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", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying `bots`; **note**: cannot use dashes in variable names, make single value", "value": "flags" } ], diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index ecf356c..b32f2c2 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -243,6 +243,30 @@ cursor: default; font-size: 1.25rem; } +.retire-container { + align-items: center; + color: gray; + cursor: pointer; + display: flex; + font-size: 1.25rem; + justify-content: flex-end; + width: 100%; +} +.retire-icon { + display: flex; + margin: 0.1rem 0.3rem; +} +.retire-icon:hover { + color: red; + display: flex; + font-size: 1.6rem; + margin: 0 0.4rem; +} +.retire-text { + display: flex; + font-size: 1.1rem; + padding: 0; +} .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 */ diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index e3d2607..c525c4e 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -1,5 +1,5 @@
- +
@@ -37,7 +37,7 @@
MyLife Memory Team
- +
@@ -149,10 +149,6 @@
- -
@@ -206,14 +202,12 @@
- - +
+
Retire this:
+ + +
@@ -269,6 +263,11 @@
+
+
Retire this:
+ + +
@@ -317,7 +316,6 @@
- diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index bd5ba04..f92185a 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -30,7 +30,7 @@ const mAvailableCollections = ['entry', 'experience', 'file', 'story'], // ['cha mCollectionsContainer = document.getElementById('collections-container'), mCollectionsUpload = document.getElementById('collections-upload'), mDefaultReliveMemoryButtonText = 'next', - mDefaultTeam = 'memoir', + mDefaultTeam = 'memory', mGlobals = new Globals(), passphraseCancelButton = document.getElementById(`personal-avatar-passphrase-cancel`), passphraseInput = document.getElementById(`personal-avatar-passphrase`), @@ -235,7 +235,7 @@ async function updatePageBots(bots=mBots, includeGreeting=false, dynamic=false){ throw new Error(`No bots provided to update page.`) if(mBots!==bots) mBots = bots - // await mUpdateTeams() + await mUpdateTeams() await mUpdateBotContainers() // mUpdateBotBar() if(includeGreeting) @@ -1263,6 +1263,60 @@ async function mReliveMemoryRequestStop(id){ console.log('Error stopping relive memory:', error) } } +async function mRetireBot(event){ + event.preventDefault() + event.stopPropagation() + try { + const { dataset, id, } = event.target + const { botId, type, } = dataset + /* reset active bot */ + if(mActiveBot.id===botId) + setActiveBot() + /* retire bot */ + const url = window.location.origin + '/members/retire/bot/' + botId + let response = await fetch(url, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + }) + if(!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + response = await response.json() + addMessages(response.responses) + } catch(err) { + console.log('Error posting bot data:', err) + addMessage(`Error posting bot data: ${err.message}`) + } + +} +/** + * Retires chat thread on server and readies for a clean one. + * @param {Event} event - The event object + * @returns {void} + */ +async function mRetireChat(event){ + event.preventDefault() + event.stopPropagation() + try { + const { dataset, id, } = event.target + const { botId, type, } = dataset + const url = window.location.origin + '/members/retire/chat/' + botId + let response = await fetch(url, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + }) + if(!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + response = await response.json() + addMessages(response.responses) + } catch(err) { + console.log('Error posting bot data:', err) + addMessage(`Error posting bot data: ${err.message}`) + } +} /** * Set Bot data on server. * @param {Object} bot - bot object @@ -1968,16 +2022,21 @@ function mUpdateBotContainerAddenda(botContainer){ }) } } + /* retirements */ + const retireChatButton = document.getElementById(`${ type }-retire-chat`) + if(retireChatButton){ + retireChatButton.dataset.botId = id + retireChatButton.dataset.type = type + retireChatButton.addEventListener('click', mRetireChat) + } + const retireBotButton = document.getElementById(`${ type }-retire-bot`) + if(retireBotButton){ + retireBotButton.dataset.botId = id + retireBotButton.dataset.type = type + retireBotButton.addEventListener('click', mRetireBot) + } switch(type){ - case 'diary': - // add listener on `diary-start` button - const diaryStart = document.getElementById('diary-start') - if(diaryStart) - diaryStart.addEventListener('click', mStartDiary) - break - case 'journaler': - case 'personal-biographer': - break + case 'avatar': case 'personal-avatar': /* attach avatar listeners */ /* set additional data attributes */ @@ -1995,6 +2054,16 @@ function mUpdateBotContainerAddenda(botContainer){ hide(tutorialButton) } break + case 'biographer': + case 'journaler': + case 'personal-biographer': + break + case 'diary': + // add listener on `diary-start` button + const diaryStart = document.getElementById('diary-start') + if(diaryStart) + diaryStart.addEventListener('click', mStartDiary) + break default: break } @@ -2177,7 +2246,7 @@ async function mUpdateTeams(identifier=mDefaultTeam){ mTeamName.dataset.description = description mTeamName.innerText = `${ title ?? name } Team` mTeamName.title = description - mTeamName.addEventListener('click', mCreateTeamSelect) + // @stub mTeamName.addEventListener('click', mCreateTeamSelect) mTeamAddMemberIcon.addEventListener('click', mCreateTeamMemberSelect) hide(mTeamPopup) show(mTeamHeader) From 1a6382341f3ddb8151069ee12baa2b788b7295a2 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 10 Oct 2024 03:04:52 -0400 Subject: [PATCH 19/54] 20241009 @Mookse - cosmetic --- inc/js/mylife-avatar.mjs | 18 +++++++++--------- views/assets/js/bots.mjs | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index fcb10ef..9fd2d71 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -205,7 +205,7 @@ class Avatar extends EventEmitter { async createBot(bot){ const { type, } = bot if(!type) - throw new Error('Bot type required to create.') + throw new Error('Bot type required to create') const singletonBotExists = this.bots .filter(_bot=>_bot.type===type && !_bot.allowMultiple) // same type, self-declared singleton .filter(_bot=>_bot.allowedBeings?.includes('avatar')) // avatar allowed to create @@ -1683,8 +1683,8 @@ function mCreateSystemMessage(activeBot, message, factory){ * Deletes the bot requested from avatar memory and from all long-term storage. * @param {object} bot - The bot object to delete * @param {Object[]} bots - The bots array - * @param {*} llm - OpenAI object - * @param {*} factory - Agent Factory object + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object */ function mDeleteBot(bot, bots, llm, factory){ const cannotRetire = ['actor', 'system', 'personal-avatar'] @@ -2194,12 +2194,12 @@ function mFindBot(avatar, id){ ?.[0] } /** - * Returns set of Greeting messages, dynamic or static. - * @param {object} bot - The bot object. - * @param {boolean} dynamic - Whether to use dynamic greetings. - * @param {*} llm - The LLM object. - * @param {*} factory - The AgentFactory object. - * @returns {Promise} - The array of messages to respond with. + * Returns set of Greeting messages, dynamic or static + * @param {object} bot - The bot object + * @param {boolean} dynamic - Whether to use dynamic greetings + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object + * @returns {Promise} - The array of messages to respond with */ async function mGreeting(bot, dynamic=false, llm, factory){ const processStartTime = Date.now() diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index f92185a..7083d05 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1263,6 +1263,11 @@ async function mReliveMemoryRequestStop(id){ console.log('Error stopping relive memory:', error) } } +/** + * Request to retire an identified bot. + * @param {Event} event - The event object + * @returns {void} + */ async function mRetireBot(event){ event.preventDefault() event.stopPropagation() @@ -1288,7 +1293,6 @@ async function mRetireBot(event){ console.log('Error posting bot data:', err) addMessage(`Error posting bot data: ${err.message}`) } - } /** * Retires chat thread on server and readies for a clean one. From 612169a86fe8caefbb72b70e9523a71fe8e9223c Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 10 Oct 2024 21:24:00 -0400 Subject: [PATCH 20/54] 20241010 @Mookse - migrateChat Functionality #405 - frontend bot version HTML, CSS --- views/assets/css/bots.css | 19 +++++++++++++------ views/assets/html/_bots.html | 4 ++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index b32f2c2..cd85754 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -216,23 +216,30 @@ } .bot-title, .collections-title { + align-items: center; color: aliceblue; display: flex; - flex: 1 0 auto; + flex: 0 0 70%; font-size: 0.8rem; font-weight: bold; max-height: 100%; overflow: hidden; overflow-wrap: break-word; - max-width: 70%; +} +.bot-title-name, +.bot-title-type { + flex: 0 1 auto; + margin-right: 0.4rem; } .bot-title-name { color: aquamarine; - max-width: 50%; + flex-grow: 1; } -.bot-title-type { - margin-right: 0.8rem; - max-width: 50%; +.bot-title-version { + color: lightblue; + flex: 0 0 auto; + font-size: 0.6rem; + margin-left: auto; /* Pushes the version div to the right end */ } .mylife-widget.bots { flex-direction: column; diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index c525c4e..630e532 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -6,6 +6,7 @@
+
@@ -45,6 +46,7 @@
+
@@ -157,6 +159,7 @@
+
@@ -216,6 +219,7 @@
+
From a16a8ba34bebf23d0766a8c180ced328e9905501 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 10 Oct 2024 21:35:59 -0400 Subject: [PATCH 21/54] 20241010 @Mookse - migrateChat Functionality #405 - version live display --- views/assets/js/bots.mjs | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 7083d05..c55d320 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1359,12 +1359,21 @@ async function mSetBot(bot){ * @returns {void} */ function mSetAttributes(bot=mActiveBot, botContainer){ - const { activated=[], activeFirst, bot_id: botId, bot_name: botName, dob, id, interests, mbr_id, narrative, privacy, provider, purpose, thread_id: threadId, type, updates, } = bot - const memberHandle = mGlobals.getHandle(mbr_id) - const bot_name = botName - ?? `${ memberHandle + '_' + type }` - const thread_id = threadId - ?? '' + const { + activated=[], + activeFirst, + bot_id: botId, + bot_name='Anonymous', + dob, + id, + interests, + mbr_id, + narrative, + privacy, + type, + updates, + version + } = bot /* attributes */ const attributes = [ { name: 'activated', value: activated }, @@ -1374,10 +1383,9 @@ function mSetAttributes(bot=mActiveBot, botContainer){ { name: 'bot_name', value: bot_name }, { name: 'id', value: id }, { name: 'initialized', value: Date.now() }, - { name: 'mbr_handle', value: memberHandle }, { name: 'mbr_id', value: mbr_id }, - { name: 'thread_id', value: thread_id }, { name: 'type', value: type }, + { name: 'version', value: version }, ] if(dob) attributes.push({ name: 'dob', value: dob }) @@ -1405,12 +1413,12 @@ function mSetAttributes(bot=mActiveBot, botContainer){ * @private * @requires mActiveBot - active bot object, but can be undefined without error. * @param {object} bot - The bot object. - * @returns {object} - Determined status. + * @returns {void} */ function mSetStatusBar(bot, botContainer){ const { dataset, } = botContainer - const { id, type, } = dataset - const { bot_id, bot_name, thread_id, type: botType, } = bot + const { id, type, version, } = dataset + const { bot_id, bot_name, thread_id, type: botType, version: botVersion, } = bot const botStatusBar = document.getElementById(`${ type }-status`) if(!type || !botType==type || !botStatusBar) return @@ -1450,8 +1458,10 @@ function mSetStatusBar(bot, botContainer){ const botTitleName = document.getElementById(`${ type }-title-name`) if(botTitleName) botTitleName.textContent = response.name - return response -} + /* version */ + const botVersionElement = document.getElementById(`${ type }-title-version`) + if(botVersionElement) + botVersionElement.textContent = `v.${ version?.includes('.') ? version : `${ version }.0` ?? '1.0' }` } /** * Sets collection item content on server. * @private From ca50a17a7ee6a400e703c1d6684c123f551c41a8 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 10 Oct 2024 22:00:35 -0400 Subject: [PATCH 22/54] 20241010 @Mookse - migrateChat Functionality #405 - bot selection updated --- views/assets/js/bots.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index c55d320..72b5e4a 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1675,20 +1675,20 @@ function mTeamSelect(event){ */ async function mToggleBotContainers(event){ event.stopPropagation() - // add turn for first time clicked on collection header it refreshes from server, then from there you need to click const botContainer = this const element = event.target const { dataset, id, } = botContainer const itemIdSnippet = element.id.split('-').pop() switch(itemIdSnippet){ case 'name': - // @todo: double-click to edit in place + case 'title': + case 'titlebar': + mOpenStatusDropdown(this) break case 'icon': - case 'title': + case 'type': if(dataset?.status && !(['error', 'offline', 'unknown'].includes(dataset.status))) await setActiveBot(dataset?.id ?? id, true) - mOpenStatusDropdown(this) break case 'status': case 'type': @@ -1697,6 +1697,10 @@ async function mToggleBotContainers(event){ break case 'update': case 'upload': + break + case 'version': + console.log('Version:', dataset.version, 'check version against server', mTeams) + break default: break } From 6f39b90683881c7a3205c19fbed3f9ebb636fc05 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 10 Oct 2024 22:27:19 -0400 Subject: [PATCH 23/54] 20241010 @Mookse - migrateChat Functionality #405 - fix modular code deformation --- inc/js/mylife-agent-factory.mjs | 47 ++++++++++++++++++++++++++------- inc/js/mylife-avatar.mjs | 3 ++- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 57d81af..46583fa 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -44,7 +44,7 @@ const mGeneralBotId = 'asst_yhX5mohHmZTXNIH55FX2BR1m' const mLLMServices = new LLMServices() const mMyLifeTeams = [ { - active: true, + active: false, allowCustom: true, allowedTypes: ['artworks', 'editor', 'idea', 'marketing'], defaultTypes: ['artworks', 'idea',], @@ -104,7 +104,7 @@ const mMyLifeTeams = [ title: 'Spirituality', }, { - active: true, + active: false, allowCustom: true, allowedTypes: ['data-ownership', 'investment', 'ubi',], defaultTypes: ['ubi'], @@ -458,8 +458,8 @@ class BotFactory extends EventEmitter{ } /** * Gets a collection of stories of a certain format. - * @param {string} form - The form of the stories to retrieve. - * @returns {object[]} - The stories. + * @param {string} form - The form of the stories to retrieve + * @returns {object[]} - The stories */ async stories(form){ return await this.dataservices.getItemsByFields( @@ -469,20 +469,24 @@ class BotFactory extends EventEmitter{ } /** * Gets a MyLife Team by id. - * @param {Guid} teamId - The Team id. - * @returns {object} - The Team. + * @param {Guid} teamId - The Team id + * @returns {object} - The Team */ team(teamId){ - return mMyLifeTeams + let team = mMyLifeTeams .find(team=>team.id===teamId) + team = mTeam(team) + return team } /** * Retrieves list of MyLife Teams. - * @returns {object[]} - The array of MyLife Teams. + * @returns {object[]} - The array of MyLife Teams */ teams(){ - return mMyLifeTeams + const teams = mMyLifeTeams .filter(team=>team.active ?? false) + .map(team=>mTeam(team)) + return teams } /** * Adds or updates a bot data in MyLife database. Note that when creating, pre-fill id. @@ -1739,6 +1743,31 @@ function mSanitizeSchemaValue(_value) { return wasTrimmed ? _value[0] + trimmedStr + _value[0] : trimmedStr } +/** + * Decouples team from modular reference. + * @param {object} team - Team object from modular codespace + * @returns {object} - Returns sanitized team object + */ +function mTeam(team){ + const { + allowCustom, + allowedTypes, + defaultTypes, + description, + id, + name, + title, + } = team + return { + allowCustom, + allowedTypes: [...allowedTypes], + defaultTypes: [...defaultTypes], + description, + id, + name, + title, + } +} /** * Updates bot in Cosmos, and if necessary, in LLM. * @param {AgentFactory} factory - Factory object diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 9fd2d71..edd1b24 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -812,7 +812,8 @@ class Avatar extends EventEmitter { * @returns {Object[]} - List of team objects. */ teams(){ - return this.#factory.teams() + const teams = this.#factory.teams() + return teams } async thread_id(){ if(!this.conversations.length){ From 4680d8f41c731f258163e679422087077aaaf3fb Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 11 Oct 2024 00:26:48 -0400 Subject: [PATCH 24/54] 20241010 @Mookse - route `updateBotInstructions` --- inc/js/functions.mjs | 17 +++++--- inc/js/mylife-avatar.mjs | 84 ++++++++++++++++++++++----------------- inc/js/routes.mjs | 2 +- views/assets/css/bots.css | 21 ++++++++++ 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 1919dcd..b4342b2 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -19,7 +19,12 @@ async function about(ctx){ function activateBot(ctx){ const { avatar, } = ctx.state avatar.activeBotId = ctx.params.bid - ctx.body = { activeBotId: avatar.activeBotId } + const { activeBotId, activeBotVersion, activeBotNewestVersion, } = avatar + ctx.body = { + activeBotId, + activeBotVersion, + version: activeBotNewestVersion, + } } async function alerts(ctx){ // @todo: put into ctx the _type_ of alert to return, system use dataservices, member use personal @@ -391,11 +396,11 @@ function teams(ctx){ async function updateBotInstructions(ctx){ const { botId, } = ctx.request.body const { avatar, } = ctx.state - let success = false - const bot = await avatar.updateBot(botId, { instructions: true, model: true, tools: true, }) - if(bot) - success = true - ctx.body = { bot, success, } + const bot = await avatar.updateBotInstructions(botId) + ctx.body = { + bot, + success: !!bot, + } } /** * Proxy for uploading files to the API. diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index edd1b24..da947e6 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -212,8 +212,8 @@ class Avatar extends EventEmitter { .length if(singletonBotExists) throw new Error(`Bot type "${type}" already exists and bot-multiples disallowed.`) - const assistant = await mBot(this.#factory, this, bot) - return mPruneBot(assistant) + bot = await mBot(this.#factory, this, bot) + return mPruneBot(bot) } /** * Create a new conversation. @@ -831,31 +831,33 @@ class Avatar extends EventEmitter { return await mBot(this.#factory, this, bot) // **note**: mBot() updates `avatar.bots` } /** - * Update core for bot-assistant based on type. Default updates all LLM pertinent properties. - * @param {string} id - The id of bot to update. - * @param {boolean} includeInstructions - Whether to include instructions in the update. - * @param {boolean} includeModel - Whether to include model in the update. - * @param {boolean} includeTools - Whether to include tools in the update. - * @returns {object} - The updated bot object. + * Update instructions for bot-assistant based on type. Default updates all LLM pertinent properties. + * @param {string} id - The id of bot to update + * @param {boolean} migrateThread - Whether to migrate the thread to the new bot, defaults to `true` + * @returns {object} - The updated bot object */ - async updateInstructions(id=this.activeBot.id, includeInstructions=true, includeModel=true, includeTools=true){ + async updateBotInstructions(id=this.activeBot.id, migrateThread=true){ let bot = mFindBot(this, id) ?? this.activeBot if(!bot) throw new Error(`Bot not found: ${ id }`) - const { bot_id, interests, type, } = bot - if(!type?.length) - return - const _bot = { bot_id, id, interests, type, } - const vectorstoreId = this.#vectorstoreId - const options = { - instructions: includeInstructions, - model: includeModel, - tools: includeTools, - vectorstoreId, + const { bot_id, flags, interests, thread_id, type, version=1.0, } = bot + /* check version */ + const newestVersion = this.#factory.botInstructionsVersion(type) + if(newestVersion!=version){ // intentional loose match (string vs. number) + const _bot = { bot_id, flags, id, interests, type, } + const vectorstoreId = this.#vectorstoreId + const options = { + instructions: true, + model: true, + tools: true, + vectorstoreId, + } + /* save to && refresh bot from Cosmos */ + bot = mSanitize( await this.#factory.updateBot(_bot, options) ) + if(migrateThread && thread_id?.length) + await this.migrateChat(thread_id) } - /* save to && refresh bot from Cosmos */ - bot = mSanitize( await this.#factory.updateBot(_bot, options) ) return mPruneBot(bot) } /** @@ -903,23 +905,26 @@ class Avatar extends EventEmitter { return this.#activeBotId } /** - * Set the active bot id. If not match found in bot list, then defaults back to this.id + * Set the active bot id. If not match found in bot list, then defaults back to this.id (avatar). * @setter * @requires mBotInstructions - * @param {string} id - The active bot id. + * @param {string} botId - The requested bot id * @returns {void} */ - set activeBotId(id){ - const newActiveBot = mFindBot(this, id) + set activeBotId(botId){ + const newActiveBot = mFindBot(this, botId) ?? this.avatar - const { id: newActiveId, type, version: botVersion=1.0, } = newActiveBot - const currentVersion = this.#factory.botInstructionsVersion(type) - if(botVersion!==currentVersion){ - this.updateInstructions(newActiveId, true, false, true) - /* update bot in this.#bots */ - - } - this.#activeBotId = newActiveId + const { id, } = newActiveBot + this.#activeBotId = id + } + get activeBotNewestVersion(){ + const { type, } = this.activeBot + const newestVersion = this.#factory.botInstructionsVersion(type) + return newestVersion + } + get activeBotVersion(){ + const { version=1.0, } = this.activeBot + return version } /** * Get actor or default avatar bot. @@ -2366,11 +2371,18 @@ function mNavigation(scenes){ } /** * Returns a frontend-ready bot object. - * @param {object} assistantData - The assistant data object. + * @param {object} bot - The bot object. * @returns {object} - The pruned bot object. */ -function mPruneBot(assistantData){ - const { bot_id, bot_name: name, description, id, purpose, type, } = assistantData +function mPruneBot(bot){ + const { + bot_id, + bot_name: name, + description, + id, + purpose, + type, + } = bot return { bot_id, name, diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index b385a6e..d18b02e 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -133,7 +133,7 @@ _memberRouter.post('/summarize', summarize) _memberRouter.post('/teams/:tid', team) _memberRouter.post('/upload', upload) _memberRouter.put('/bots/:bid', bots) -_memberRouter.put('/bots/system-update/:bid', updateBotInstructions) +_memberRouter.put('/bots/version/:bid', updateBotInstructions) _memberRouter.put('/item/:iid', item) // Mount the subordinate routers along respective paths _Router.use('/members', _memberRouter.routes(), _memberRouter.allowedMethods()) diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index cd85754..7e9a434 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -237,6 +237,7 @@ } .bot-title-version { color: lightblue; + cursor: not-allowed; flex: 0 0 auto; font-size: 0.6rem; margin-left: auto; /* Pushes the version div to the right end */ @@ -297,6 +298,26 @@ animation: none; padding: 0; } +.update-available { + align-items: flex-end; + background-color: rgb(255 0 123 / 50%); + border: thin solid rgb(255 157 190 / 66%); + border-radius: var(--border-radius); + display: flex; + justify-content: flex-end; + padding: 0.3rem; +} +.update-available:hover { + align-items: flex-end; + background-color: rgba(255, 251, 0, 0.5); + border: thin solid rgba(255, 205, 222, 0.66); + border-radius: var(--border-radius); + color: black; + cursor: pointer; + display: flex; + justify-content: flex-end; + padding: 0.3rem; +} /* bot-collections */ .collection { background-color: royalblue; From ffad103e4b5dbbce5d3c51e71b88c862fb48436c Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 11 Oct 2024 01:36:56 -0400 Subject: [PATCH 25/54] 20241010 @Mookse - migrateChat Functionality #405 - route requirement fixed for updateBotInstructions --- inc/js/functions.mjs | 6 ++- inc/js/mylife-avatar.mjs | 18 +++++--- views/assets/js/bots.mjs | 92 +++++++++++++++++++++++++++++++--------- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index b4342b2..1635265 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -394,9 +394,11 @@ function teams(ctx){ ctx.body = avatar.teams() } async function updateBotInstructions(ctx){ - const { botId, } = ctx.request.body + const { bid, } = ctx.params + if(!bid?.length) + ctx.throw(400, `missing bot id`) const { avatar, } = ctx.state - const bot = await avatar.updateBotInstructions(botId) + const bot = await avatar.updateBotInstructions(bid) ctx.body = { bot, success: !!bot, diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index da947e6..ba5f72c 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -500,11 +500,13 @@ class Avatar extends EventEmitter { ?.[0] return { content, metadata, role, } }) + .filter(message=>!/^## [^\n]* LIST\n/.test(message.content)) const { botId, } = conversation const bot = this.getBot(botId) switch(bot.type){ case 'biographer': case 'personal-biographer': + const type = 'memory' const memories = ( await this.collections('story') ) .sort((a, b)=>a._ts-b._ts) .slice(0, 12) @@ -516,7 +518,7 @@ class Avatar extends EventEmitter { .join(',') .slice(0, 512) messages.push({ - content: `## MEMORY COLLECTION LIST\n${ memoryList }`, // insert actual memory list with titles here for intelligence to reference + content: `## ${ type } LIST\n${ memoryList }`, // insert actual memory list with titles here for intelligence to reference metadata: { collectionList: memoryCollectionList, collectiontypes: 'memory,story,narrative', @@ -530,7 +532,7 @@ class Avatar extends EventEmitter { const _type = 'entry' const entries = ( await this.collections(_type) ) .sort((a, b)=>a._ts-b._ts) - .slice(0, 128) + .slice(0, 25) const entryList = entries .map(entry=>`- itemId: ${ entry.id } :: ${ entry.title }`) .join('\n') @@ -539,7 +541,7 @@ class Avatar extends EventEmitter { .join(',') .slice(0, 512) messages.push({ - content: `## ${ _type.toUpperCase() } List:\n${ entryList }`, + content: `## ${ _type.toUpperCase() } LIST\n${ entryList }`, metadata: { collectionList: entryCollectionList, collectiontypes: _type, @@ -824,6 +826,7 @@ class Avatar extends EventEmitter { } /** * Update a specific bot. + * @async * @param {object} bot - Bot data to set. * @returns {object} - The updated bot. */ @@ -832,6 +835,7 @@ class Avatar extends EventEmitter { } /** * Update instructions for bot-assistant based on type. Default updates all LLM pertinent properties. + * @async * @param {string} id - The id of bot to update * @param {boolean} migrateThread - Whether to migrate the thread to the new bot, defaults to `true` * @returns {object} - The updated bot object @@ -841,7 +845,7 @@ class Avatar extends EventEmitter { ?? this.activeBot if(!bot) throw new Error(`Bot not found: ${ id }`) - const { bot_id, flags, interests, thread_id, type, version=1.0, } = bot + const { bot_id, flags='', interests='', thread_id, type, version=1.0, } = bot /* check version */ const newestVersion = this.#factory.botInstructionsVersion(type) if(newestVersion!=version){ // intentional loose match (string vs. number) @@ -2376,20 +2380,20 @@ function mNavigation(scenes){ */ function mPruneBot(bot){ const { - bot_id, bot_name: name, description, id, purpose, type, + version, } = bot return { - bot_id, - name, description, id, + name, purpose, type, + version, } } function mPruneConversation(conversation){ diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 72b5e4a..b4237d1 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -158,9 +158,9 @@ async function setActiveBot(event, dynamic=false){ throw new Error(`ERROR: failure to set active bot.`) if(initialActiveBot===mActiveBot) return // no change, no problem - const { id, } = mActiveBot + const { id, type, } = mActiveBot /* confirm via server request: set active bot */ - const serverActiveId = await fetch( + const serverResponse = await fetch( '/members/bots/activate/' + id, { method: 'POST', @@ -168,24 +168,21 @@ async function setActiveBot(event, dynamic=false){ 'Content-Type': 'application/json' } }) - .then(response => { + .then(response=>{ if(!response.ok){ throw new Error(`HTTP error! Status: ${response.status}`) } return response.json() }) - .then(response => { - return response.activeBotId - }) - .catch(error => { - console.log('Error:', error) - alert('Server error setting active bot.') + .catch(error=>{ + addMessage(`Server error setting active bot: ${ error.message }`) return }) /* update active bot */ - if(serverActiveId!==id){ + const { activeBotId, activeBotVersion, version, } = serverResponse + if(activeBotId!==id){ mActiveBot = initialActiveBot - throw new Error(`ERROR: server failed to set active bot.`) + addMessage('Server error setting active bot.') } /* update page bot data */ const { activated=[], activatedFirst=Date.now(), } = mActiveBot @@ -193,6 +190,17 @@ async function setActiveBot(event, dynamic=false){ activated.push(Date.now()) // newest date is last to .pop() // dynamic = (Date.now()-activated.pop()) > (20*60*1000) mActiveBot.activated = activated + if(activeBotVersion!==version){ + const botVersion = document.getElementById(`${ type }-title-version`) + if(botVersion){ + botVersion.classList.add('update-available') + botVersion.dataset.botId = activeBotId + botVersion.dataset.currentVersion = activeBotVersion + botVersion.dataset.type = type + botVersion.dataset.updateVersion = version + botVersion.addEventListener('click', mUpdateBotVersion, { once: true }) + } + } /* update page */ mSpotlightBotBar() mSpotlightBotStatus() @@ -261,6 +269,19 @@ function mBotActive(id){ return id===mActiveBot?.id ?? false } +/** + * Request version update to bot. + * @param {Guid} botId - The bot id to update + * @returns {object} - Response from server { bot, success, } + */ +async function mBotVersionUpdate(botId){ + const url = window.location.origin + '/members/bots/version/' + botId + const method = 'PUT' + const response = await fetch(url, { method, }) + if(!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + return await response.json() +} /** * Request bot be created on server. * @requires mActiveTeam @@ -1362,12 +1383,13 @@ function mSetAttributes(bot=mActiveBot, botContainer){ const { activated=[], activeFirst, - bot_id: botId, bot_name='Anonymous', dob, - id, + flags, + id: bot_id, interests, mbr_id, + name, narrative, privacy, type, @@ -1377,11 +1399,11 @@ function mSetAttributes(bot=mActiveBot, botContainer){ /* attributes */ const attributes = [ { name: 'activated', value: activated }, - { name: 'active', value: mBotActive(id) }, + { name: 'active', value: mBotActive(bot_id) }, { name: 'activeFirst', value: activeFirst }, - { name: 'bot_id', value: botId }, - { name: 'bot_name', value: bot_name }, - { name: 'id', value: id }, + { name: 'bot_id', value: bot_id }, + { name: 'bot_name', value: name ?? bot_name }, + { name: 'id', value: bot_id }, { name: 'initialized', value: Date.now() }, { name: 'mbr_id', value: mbr_id }, { name: 'type', value: type }, @@ -1389,6 +1411,8 @@ function mSetAttributes(bot=mActiveBot, botContainer){ ] if(dob) attributes.push({ name: 'dob', value: dob }) + if(flags) + attributes.push({ name: 'flags', value: flags }) if(interests) attributes.push({ name: 'interests', value: interests }) if(narrative) @@ -1418,7 +1442,7 @@ function mSetAttributes(bot=mActiveBot, botContainer){ function mSetStatusBar(bot, botContainer){ const { dataset, } = botContainer const { id, type, version, } = dataset - const { bot_id, bot_name, thread_id, type: botType, version: botVersion, } = bot + const { bot_id, bot_name, type: botType, version: botVersion, } = bot const botStatusBar = document.getElementById(`${ type }-status`) if(!type || !botType==type || !botStatusBar) return @@ -1461,7 +1485,8 @@ function mSetStatusBar(bot, botContainer){ /* version */ const botVersionElement = document.getElementById(`${ type }-title-version`) if(botVersionElement) - botVersionElement.textContent = `v.${ version?.includes('.') ? version : `${ version }.0` ?? '1.0' }` } + botVersionElement.textContent = mVersion(version) +} /** * Sets collection item content on server. * @private @@ -2086,6 +2111,25 @@ function mUpdateBotContainerAddenda(botContainer){ break } } +/** + * Updates bot version on server. + * @param {Event} event - The event object + * @returns {void} + */ +async function mUpdateBotVersion(event){ + event.stopPropagation() + const { classList, dataset,} = event.target + const { botId, currentVersion, updateVersion, } = dataset + if(currentVersion==updateVersion) + return + const updatedVersion = await mBotVersionUpdate(botId) + if(updatedVersion?.success){ + const { version, } = updatedVersion.bot + dataset.currentVersion = version + event.target.textContent = mVersion(version) + classList.remove('update-available') + } +} /** * Update the identified collection with provided specifics. * @param {string} type - The collection type. @@ -2331,6 +2375,16 @@ function mUploadFilesInputRemove(fileInput, uploadParent, uploadButton){ uploadParent.removeChild(fileInput) uploadButton.disabled = false } +/** + * Versions per frontend. + * @param {string} version - The version to format + * @returns {string} - The formatted version + */ +function mVersion(version){ + version = version.toString() + version = `v.${ version?.includes('.') ? version : `${ version }.0` ?? '1.0' }` + return version +} /* exports */ // @todo - export combine of fetchBots and updatePageBots export { From eafb1b816fdbcb625d579dc192a66939361b275b Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sun, 13 Oct 2024 23:51:47 -0400 Subject: [PATCH 26/54] 20241013 @Mookse - migrateChat Functionality #405 - generic fixes --- .../class-conversation-functions.mjs | 1 - .../class-extenders.mjs | 18 +- inc/js/mylife-avatar.mjs | 265 +++++++++++------- inc/js/mylife-data-service.js | 15 +- 4 files changed, 183 insertions(+), 116 deletions(-) diff --git a/inc/js/factory-class-extenders/class-conversation-functions.mjs b/inc/js/factory-class-extenders/class-conversation-functions.mjs index 55afc6e..91af01f 100644 --- a/inc/js/factory-class-extenders/class-conversation-functions.mjs +++ b/inc/js/factory-class-extenders/class-conversation-functions.mjs @@ -32,7 +32,6 @@ async function mSaveConversation(factory, conversation){ type, } const newConversation = await factory.dataservices.pushItem(_newConversation) - console.log('mSaveConversation::newConversation::created', id, newConversation?.id) return !!newConversation } const updatedConversation = await factory.dataservices.patch( diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 25a1e02..d6f9376 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -125,6 +125,7 @@ function extendClass_contribution(originClass, referencesObject) { */ function extendClass_conversation(originClass, referencesObject) { class Conversation extends originClass { + #bot_id #factory #messages = [] #saved = false @@ -141,7 +142,8 @@ function extendClass_conversation(originClass, referencesObject) { super(obj) this.#factory = factory this.#thread = thread - this.bot_id = bot_id + if(factory.globals.isValidGuid(bot_id)) + this.#bot_id = bot_id this.form = this.form ?? 'system' this.name = `conversation_${this.#factory.mbr_id}` @@ -235,7 +237,10 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {Guid} - The bot id. */ get botId(){ - return this.bot_id + return this.#bot_id + } + get bot_id(){ + return this.#bot_id } /** * Set the id {Guid} of the conversation's bot. @@ -244,7 +249,14 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {void} */ set botId(bot_id){ - this.bot_id = bot_id + if(!this.#factory.globals.isValidGuid(bot_id)) + throw new Error(`Invalid bot_id: ${ bot_id }`) + this.#bot_id = bot_id + } + set bot_id(bot_id){ + if(!this.#factory.globals.isValidGuid(bot_id)) + throw new Error(`Invalid bot_id: ${ bot_id }`) + this.#bot_id = bot_id } get isSaved(){ return this.#saved diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index ba5f72c..38ebcf9 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -99,7 +99,7 @@ class Avatar extends EventEmitter { if(!activeBotId) throw new Error('Parameter `activeBotId` required.') const { activeBot, factory } = this - const { id: botId, thread_id, } = activeBot + const { bot_id, id: botId, thread_id, } = activeBot if(botId!==activeBotId) throw new Error(`Invalid bot id: ${ activeBotId }, active bot id: ${ botId }`) conversation = conversation @@ -107,7 +107,8 @@ class Avatar extends EventEmitter { ?? await this.createConversation('chat', threadId ?? thread_id, activeBotId) if(!conversation) throw new Error('No conversation found for thread id and could not be created.') - conversation.bot_id = activeBot.bot_id // pass in via quickly mutating conversation (or independently if preferred in end), versus llmServices which are global + conversation.bot_id = botId + conversation.llm_id = bot_id let _message = message, messages = [] if(shadowId) @@ -115,9 +116,9 @@ class Avatar extends EventEmitter { else { if(itemId){ // @todo - check if item exists in memory, fewer pings and inclusions overall - const { summary, } = await factory.item(itemId) + let { summary, } = await factory.item(itemId) if(summary?.length){ - _message = `possible **update-summary-request**: itemId=${ itemId }\n` + summary = `possible **update-summary-request**: itemId=${ itemId }\n` + `**member-update-request**:\n` + message + `\n**current-summary-in-database**:\n` @@ -485,90 +486,14 @@ class Avatar extends EventEmitter { * @returns {Conversation} - The migrated conversation object */ async migrateChat(thread_id){ - /* MyLife conversation re-assignment */ + /* validate request */ const conversation = this.getConversation(thread_id) if(!conversation) - throw new Error(`Conversation not found with thread_id: ${ thread_id }`) - let messages = await this.#llmServices.messages(thread_id) - messages = messages - .slice(0, 25) - .map(message=>{ - const { content: contentArray, id, metadata, role, } = message - const content = contentArray - .filter(_content=>_content.type==='text') - .map(_content=>_content.text?.value) - ?.[0] - return { content, metadata, role, } - }) - .filter(message=>!/^## [^\n]* LIST\n/.test(message.content)) - const { botId, } = conversation - const bot = this.getBot(botId) - switch(bot.type){ - case 'biographer': - case 'personal-biographer': - const type = 'memory' - const memories = ( await this.collections('story') ) - .sort((a, b)=>a._ts-b._ts) - .slice(0, 12) - const memoryList = memories - .map(memory=>`- itemId: ${ memory.id } :: ${ memory.title }`) - .join('\n') - const memoryCollectionList = memories - .map(memory=>memory.id) - .join(',') - .slice(0, 512) - messages.push({ - content: `## ${ type } LIST\n${ memoryList }`, // insert actual memory list with titles here for intelligence to reference - metadata: { - collectionList: memoryCollectionList, - collectiontypes: 'memory,story,narrative', - }, - role: 'assistant', - }) // add summary of Memories (etc. due to type) for intelligence to reference, also could add attachment file - break - case 'diary': - case 'journal': - case 'journaler': - const _type = 'entry' - const entries = ( await this.collections(_type) ) - .sort((a, b)=>a._ts-b._ts) - .slice(0, 25) - const entryList = entries - .map(entry=>`- itemId: ${ entry.id } :: ${ entry.title }`) - .join('\n') - const entryCollectionList = entries - .map(entry=>entry.id) - .join(',') - .slice(0, 512) - messages.push({ - content: `## ${ _type.toUpperCase() } LIST\n${ entryList }`, - metadata: { - collectionList: entryCollectionList, - collectiontypes: _type, - }, - role: 'assistant', - }) // add summary of Entries - break - default: - break - } - const metadata = { - bot_id: botId, - conversation_id: conversation.id, - } - const newThread = await this.#llmServices.thread(null, messages.reverse(), metadata) - conversation.setThread(newThread) - bot.thread_id = conversation.thread_id - const _bot = { - id: bot.id, - thread_id: bot.thread_id, - } - await this.#factory.updateBot(_bot) - if(mAllowSave) - conversation.save() - else - console.log('migrateChat::BYPASS-SAVE', conversation.thread_id) - return conversation + throw new Error(`Conversation thread_id not found: ${ thread_id }`) + /* execute request */ + const updatedConversation = await mMigrateChat(this, this.#factory, this.#llmServices, conversation) + /* respond request */ + return updatedConversation } /** * 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. @@ -666,30 +591,42 @@ class Avatar extends EventEmitter { * @returns {object} - The response object { instruction, responses, success, } */ async retireChat(botId){ - const retiredConversation = this.getConversation(null, botId) - console.log('retireChat::conversations', this.conversations.map(c=>( - { botId, _botId: c.botId, bot_id: c.bot_id, thread_id: c.thread_id, } - ))) - if(!retiredConversation) + /* validate request */ + const conversation = this.getConversation(null, botId) + if(!conversation){ throw new Error(`Conversation not found with bot id: ${ botId }`) - const { thread_id: cid, } = retiredConversation + } + const { thread_id: cid, } = conversation const bot = this.getBot(botId) const { id: _botId, thread_id: tid, } = bot if(botId!=_botId) throw new Error(`Bot id mismatch: ${ botId }!=${ bot_id }`) if(tid!=cid) throw new Error(`Conversation mismatch: ${ tid }!=${ cid }`) - const conversation = await this.migrateChat(tid) - console.log('retireChat::conversation', conversation) - const response = { - responses: [{ - agent: 'server', - message: `I have successfully retired this conversation thread and started a new one.`, - purpose: 'system', - type: 'chat', - }], - success: true, - } + /* execute request */ + const updatedConversation = await mMigrateChat(this, this.#factory, this.#llmServices, conversation) + /* respond request */ + const response = !!updatedConversation + ? { /* @todo - add frontend instructions to remove migrateChat button */ + instruction: null, + responses: [{ + agent: 'server', + message: `I have successfully retired this conversation thread and started a new one.`, + purpose: 'system', + type: 'chat', + }], + success: true, + } + : { + instruction: null, + responses: [{ + agent: 'server', + message: `I'm sorry - I encountered an error while trying to retire this conversation; please try again.`, + purpose: 'system', + type: 'chat', + }], + success: false, + } return response } /** @@ -1613,10 +1550,12 @@ async function mBot(factory, avatar, bot){ * @returns {Promise} - Array of Message instances in descending chronological order */ async function mCallLLM(llmServices, conversation, prompt, factory, avatar){ - const { bot_id, thread_id } = conversation - if(!thread_id || !bot_id) + const { bot_id, llm_id, thread_id } = conversation + const botId = llm_id + ?? bot_id + if(!thread_id || !botId) throw new Error('Both `thread_id` and `bot_id` required for LLM call.') - const messages = await llmServices.getLLMResponse(thread_id, bot_id, prompt, factory, avatar) + const messages = await llmServices.getLLMResponse(thread_id, botId, prompt, factory, avatar) messages.sort((mA, mB)=>{ return mB.created_at - mA.created_at }) @@ -2342,6 +2281,118 @@ async function mInit(factory, llmServices, avatar, bots, assetAgent){ /* lived-experiences */ avatar.experiencesLived = await factory.experiencesLived(false) } +/** + * Migrates specified conversation (by thread_id) and returns conversation with new thread processed and saved to bot, both as a document and in avatar memory. + * @param {Avatar} avatar - Avatar object + * @param {AgentFactory} factory - AgentFactory object + * @param {LLMServices} llm - OpenAI object + * @param {string} thread_id - The thread_id of the conversation + * @returns + */ +async function mMigrateChat(avatar, factory, llm, conversation){ + /* constants and variables */ + const { thread_id, } = conversation + const chatLimit=25 + let messages = await llm.messages(thread_id) // @todo - limit to 25 messages or modify request + if(!messages?.length) + return conversation + const { botId, } = conversation + const bot = avatar.getBot(botId) + const botType = bot.type + let disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, + itemCollectionTypes='item', + itemLimit=1000, + type='item' + switch(botType){ + case 'biographer': + case 'personal-biographer': + type = 'memory' + itemCollectionTypes = `memory,story,narrative` + break + case 'diary': + case 'journal': + case 'journaler': + type = 'entry' + const itemType = botType==='journaler' + ? 'journal' + : botType + itemCollectionTypes = `${ itemType },entry,` + break + default: + break + } + const chatSummary=`## ${ type.toUpperCase() } CHAT SUMMARY\n`, + chatSummaryRegex = /^## [^\n]* CHAT SUMMARY\n/, + itemSummary=`## ${ type.toUpperCase() } LIST\n`, + itemSummaryRegex = /^## [^\n]* LIST\n/ + const items = ( await avatar.collections(type) ) + .sort((a, b)=>a._ts-b._ts) + .slice(0, itemLimit) + const itemList = items + .map(item=>`- itemId: ${ item.id } :: ${ item.title }`) + .join('\n') + const itemCollectionList = items + .map(item=>item.id) + .join(',') + .slice(0, 512) // limit for metadata string field + const metadata = { + bot_id: botId, + conversation_id: conversation.id, + } + /* prune messages source material */ + messages = messages + .slice(0, chatLimit) + .map(message=>{ + const { content: contentArray, id, metadata, role, } = message + const content = contentArray + .filter(_content=>_content.type==='text') + .map(_content=>_content.text?.value) + ?.[0] + return { content, id, metadata, role, } + }) + .filter(message=>!itemSummaryRegex.test(message.content)) + const summaryMessage = messages + .filter(message=>!chatSummaryRegex.test(message.content)) + .map(message=>message.content) + .join('\n') + /* contextualize previous content */ + const summaryMessages = [] + /* summary of items */ + if(items.length) + summaryMessages.push({ + content: itemSummary + disclaimer + itemList, + metadata: { + collectionList: itemCollectionList, + collectiontypes: itemCollectionTypes, + }, + role: 'assistant', + }) + /* summary of messages */ + if(summaryMessage.length) + summaryMessages.push({ + content: chatSummary + disclaimer + summaryMessage, + metadata: { + collectiontypes: itemCollectionTypes, + }, + role: 'assistant', + }) + if(!summaryMessages.length) + return conversation + /* add messages to new thread */ + const newThread = await llm.thread(null, summaryMessages.reverse(), metadata) + conversation.setThread(newThread) + bot.thread_id = conversation.thread_id + const _bot = { + id: bot.id, + thread_id: conversation.thread_id, + } + factory.updateBot(_bot) // removed await + if(mAllowSave) + conversation.save() + else + console.log('migrateChat::BYPASS-SAVE', conversation.thread_id) + return conversation +} /** * Get experience scene navigation array. * @getter @@ -2519,7 +2570,7 @@ async function mReliveMemoryNarration(avatar, factory, llm, bot, item, memberInp let relivingMemory = relivingMemories.find(reliving=>reliving.item.id===id) if(!relivingMemory){ /* create new activated reliving memory */ const conversation = await avatar.createConversation('memory', undefined, botId, false) - conversation.bot_id = bot_id + conversation.llm_id = bot_id const { thread_id, } = conversation relivingMemory = { bot, diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-data-service.js index 45d45dd..dad8d86 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-data-service.js @@ -249,12 +249,15 @@ class Dataservices { * @returns {array} - The collection items with no wrapper. */ async collections(type){ - if(type==='experience') // corrections + /* validate request */ + if(type==='experience') type = 'lived-experience' - if(type?.length && this.#collectionTypes.includes(type)) - return await this.getItems(type) - else - return Promise.all([ + if(type==='memory') + type = 'story' + /* execute request */ + const response = type?.length && this.#collectionTypes.includes(type) + ? await this.getItems(type) + : await Promise.all([ this.collectionConversations(), this.collectionEntries(), this.collectionLivedExperiences(), @@ -272,6 +275,8 @@ class Dataservices { console.log('mylife-data-service::collections() error', err) return [] }) + /* respond request */ + return response } /** * Creates a new bot in the database. From 5bf472db8a3d0186bb6b93655c6bb20d6f986869 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 14 Oct 2024 00:31:31 -0400 Subject: [PATCH 27/54] 20241013 @Mookse - migrateChat Functionality #405 - bot_id in conversation should be guid only - bot_name for dataset --- inc/js/functions.mjs | 4 +--- inc/js/mylife-avatar.mjs | 10 ++++++++++ views/assets/js/bots.mjs | 10 +++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 1635265..a9d21c2 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -54,14 +54,12 @@ async function bots(ctx){ } else { const { activeBotId, - bots: awaitBots, // **note**: bots needs await + prunedBots: bots, mbr_id, } = avatar - const bots = await awaitBots ctx.body = { // wrap bots activeBotId, bots, - mbr_id, } } break diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 38ebcf9..8a3cdaa 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -1258,6 +1258,16 @@ class Avatar extends EventEmitter { get personalAssistant(){ return this.avatar } + /** + * Get a list of available bots (pruned) for the member. + * @getter + * @returns {Object[]} - Array of pruned bot objects + */ + get prunedBots(){ + const bots = this.#bots + .map(bot=>mPruneBot(bot)) + return bots + } /** * Get the `active` reliving memories. * @getter diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index b4237d1..126c5ab 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1388,7 +1388,6 @@ function mSetAttributes(bot=mActiveBot, botContainer){ flags, id: bot_id, interests, - mbr_id, name, narrative, privacy, @@ -1405,7 +1404,6 @@ function mSetAttributes(bot=mActiveBot, botContainer){ { name: 'bot_name', value: name ?? bot_name }, { name: 'id', value: bot_id }, { name: 'initialized', value: Date.now() }, - { name: 'mbr_id', value: mbr_id }, { name: 'type', value: type }, { name: 'version', value: version }, ] @@ -1442,12 +1440,14 @@ function mSetAttributes(bot=mActiveBot, botContainer){ function mSetStatusBar(bot, botContainer){ const { dataset, } = botContainer const { id, type, version, } = dataset - const { bot_id, bot_name, type: botType, version: botVersion, } = bot + const { bot_name, name, type: botType, version: botVersion, } = bot const botStatusBar = document.getElementById(`${ type }-status`) if(!type || !botType==type || !botStatusBar) return + const botName = name + ?? bot_name const response = { - name: bot_name, + name: botName, status: 'unknown', type: type.split('-').pop(), } @@ -1459,7 +1459,7 @@ function mSetStatusBar(bot, botContainer){ botIcon.classList.add('active') response.status = 'active' break - case ( bot_id?.length>0 ): // online + case ( botName?.length>0 ): // online botIcon.classList.remove('active', 'offline', 'error') botIcon.classList.add('online') response.status = 'online' From 55b1388363a4b0bd50937cfbd2ab169fc621e14e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 14 Oct 2024 00:32:30 -0400 Subject: [PATCH 28/54] 20241013 @Mookse - migrateChat Functionality #405 - bot_name fix --- views/assets/js/bots.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 126c5ab..f857c56 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1396,12 +1396,14 @@ function mSetAttributes(bot=mActiveBot, botContainer){ version } = bot /* attributes */ + const botName = name + ?? bot_name const attributes = [ { name: 'activated', value: activated }, { name: 'active', value: mBotActive(bot_id) }, { name: 'activeFirst', value: activeFirst }, { name: 'bot_id', value: bot_id }, - { name: 'bot_name', value: name ?? bot_name }, + { name: 'bot_name', value: botName }, { name: 'id', value: bot_id }, { name: 'initialized', value: Date.now() }, { name: 'type', value: type }, From e1c779bfbb9de336eeb9cdb0fa2e01fe7aaba900 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 14 Oct 2024 12:54:57 -0400 Subject: [PATCH 29/54] 20241014 @Mookse - chat input placeholder `undefined` --- views/assets/js/bots.mjs | 13 ++++++------- views/assets/js/members.mjs | 10 +++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index f857c56..6e70e72 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -528,12 +528,12 @@ function mCloseTeamPopup(event){ * @returns {HTMLDivElement} - The bot thumb container. */ function mCreateBotThumb(bot=getBot()){ - const { bot_name, id, type, } = bot + const { id, name, type, } = bot /* bot-thumb container */ const botThumbContainer = document.createElement('div') botThumbContainer.id = `bot-bar-container_${ id }` botThumbContainer.name = `bot-bar-container-${ type }` - botThumbContainer.title = bot_name + botThumbContainer.title = name botThumbContainer.addEventListener('click', setActiveBot) botThumbContainer.classList.add('bot-thumb-container') /* bot-thumb */ @@ -1442,14 +1442,12 @@ function mSetAttributes(bot=mActiveBot, botContainer){ function mSetStatusBar(bot, botContainer){ const { dataset, } = botContainer const { id, type, version, } = dataset - const { bot_name, name, type: botType, version: botVersion, } = bot + const { id: botId, name, type: botType, version: botVersion, } = bot const botStatusBar = document.getElementById(`${ type }-status`) if(!type || !botType==type || !botStatusBar) return - const botName = name - ?? bot_name const response = { - name: botName, + name, status: 'unknown', type: type.split('-').pop(), } @@ -1461,7 +1459,7 @@ function mSetStatusBar(bot, botContainer){ botIcon.classList.add('active') response.status = 'active' break - case ( botName?.length>0 ): // online + case ( name?.length>0 ): // online botIcon.classList.remove('active', 'offline', 'error') botIcon.classList.add('online') response.status = 'online' @@ -2047,6 +2045,7 @@ function mUpdateBotContainerAddenda(botContainer){ /* update mBot */ const bot = mBot(id) bot.bot_name = bot_name + bot.name = bot_name } else { dataset.bot_name = localVars.bot_name } diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 0dc6fda..7c94a25 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -123,8 +123,8 @@ function clearSystemChat(){ * @returns {void} */ function decorateActiveBot(activeBot=activeBot()){ - const { bot_name, id, purpose, type, } = activeBot - chatInputField.placeholder = `Type your message to ${ bot_name }...` + const { id, name, } = activeBot + chatInputField.placeholder = `Type your message to ${ name }...` // additional func? clear chat? } function escapeHtml(text) { @@ -764,13 +764,13 @@ async function mSubmitChat(message) { * @returns {void} */ function toggleMemberInput(display=true, hidden=false, connectingText='Connecting with '){ - const { bot_name, id, mbr_id, provider, purpose, type, } = activeBot() + const { id, name, } = activeBot if(display){ hide(awaitButton) awaitButton.classList.remove('slide-up') chatInput.classList.add('slide-up') chatInputField.style.height = 'auto' - chatInputField.placeholder = `type your message to ${ bot_name }...` + chatInputField.placeholder = `type your message to ${ name }...` chatInputField.value = null show(chatInput) } else { @@ -778,7 +778,7 @@ function toggleMemberInput(display=true, hidden=false, connectingText='Connectin chatInput.classList.remove('fade-in') chatInput.classList.remove('slide-up') awaitButton.classList.add('slide-up') - awaitButton.innerHTML = connectingText + bot_name + '...' + awaitButton.innerHTML = connectingText + name + '...' show(awaitButton) } if(hidden){ From edbb51428e1e5436a8d9b7fd0e65b420b8d61d8e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Mon, 14 Oct 2024 16:50:49 -0400 Subject: [PATCH 30/54] 20241014 @Mookse - session start seems to fire updateBot() with instructions - huge error with starting conversation with Q and THEN logging in - solve required elimination of session property `thread_id` which was being leveraged to store separate threads for different browsers --- inc/js/mylife-avatar.mjs | 9 +++++++-- inc/js/session.mjs | 1 + views/assets/js/bots.mjs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 8a3cdaa..368f3a4 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -228,9 +228,14 @@ class Avatar extends EventEmitter { */ async createConversation(type='chat', threadId, botId=this.activeBotId, saveToConversations=true){ const thread = await this.#llmServices.thread(threadId) - const form = this.activeBot.type.split('-').pop() + const { mbr_id, type: botType, } = this.getBot(botId) + const form = botType.split('-').pop() const conversation = new (this.#factory.conversation)( - { form, mbr_id: this.mbr_id, type, }, + { + form, + mbr_id, + type, + }, this.#factory, thread, botId diff --git a/inc/js/session.mjs b/inc/js/session.mjs index 68dbef0..fef9ee3 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -29,6 +29,7 @@ class MylifeMemberSession extends EventEmitter { await this.#factory.init(this.mbr_id) // needs only `init()` with different `mbr_id` to reset this.#Member = await this.factory.getMyLifeMember() this.#autoplayed = false // resets autoplayed flag, although should be impossible as only other "variant" requires guest status, as one-day experiences can be run for guests also [for pay] + this.thread_id = null // reset thread_id from Q-session this.emit('onInit-member-initialize', this.#Member.memberName) console.log( chalk.bgBlue('created-member:'), diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 6e70e72..2021e7a 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -356,6 +356,9 @@ function mBotIcon(type){ function mCreateCollectionItem(collectionItem){ /* collection item container */ const { assistantType, filename, form, id, keywords, name, summary, title, type, } = collectionItem + const iconType = assistantType + ?? form + ?? type const item = document.createElement('div') item.id = `collection-item_${ id }` item.name = `collection-item-${ type }` @@ -365,7 +368,7 @@ function mCreateCollectionItem(collectionItem){ itemIcon.id = `collection-item-icon_${ id }` itemIcon.name = `collection-item-icon-${ type }` itemIcon.classList.add('collection-item-icon', `${ type }-collection-item-icon`) - itemIcon.src = mBotIcon(assistantType) + itemIcon.src = mBotIcon(iconType) item.appendChild(itemIcon) /* name */ const itemName = document.createElement('span') From 005b3d5b94b80dc813a575ef8d168c05dc711a5f Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 15 Oct 2024 17:44:13 -0400 Subject: [PATCH 31/54] 20241015 @Mookse - correct thumbs - consistent name display for bot --- inc/js/globals.mjs | 9 ++++ inc/js/mylife-agent-factory.mjs | 46 +++++++++---------- .../openai/functions/entrySummary.json | 9 ++++ views/assets/js/bots.mjs | 3 +- views/assets/js/members.mjs | 6 +-- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index b616cbb..c3003fa 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -37,6 +37,14 @@ const mAiJsFunctions = { description: 'complete concatenated raw text content of member input(s) for this `entry`', type: 'string' }, + form: { + description: 'Form of `entry` content, determine context from internal instructions', + enum: [ + 'diary', + 'journal', + ], + type: 'string' + }, keywords: { description: 'Keywords most relevant to `entry`.', items: { @@ -69,6 +77,7 @@ const mAiJsFunctions = { additionalProperties: false, required: [ 'content', + 'form', 'keywords', 'mood', 'relationships', diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 46583fa..7b1f703 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -333,12 +333,12 @@ class BotFactory extends EventEmitter{ } /** * - * @param {object} assistantData - The assistant data. + * @param {object} botData - The assistant data. * @param {string} vectorstoreId - The vectorstore id. * @returns {object} - The created bot. */ - async createBot(assistantData={ type: mDefaultBotType }, vectorstoreId){ - const bot = await mCreateBot(this.#llmServices, this, assistantData, vectorstoreId) + async createBot(botData={ type: mDefaultBotType }, vectorstoreId){ + const bot = await mCreateBot(this.#llmServices, this, botData, vectorstoreId) if(!bot) throw new Error('bot creation failed') return bot @@ -650,21 +650,22 @@ class AgentFactory extends BotFactory { return await this.dataservices.deleteItem(id) } async entry(entry){ + const defaultForm = 'journal' const defaultType = 'entry' - const { - assistantType='journaler', + const { being=defaultType, - form='journal', + form=defaultForm, id=this.newGuid, keywords=[], - mbr_id=(!this.isMyLife ? this.mbr_id : undefined), + mbr_id=this.mbr_id, summary, - title=`New ${ defaultType }`, + title=`Untitled ${ defaultForm } ${ defaultType }`, } = entry - if(!mbr_id) // only triggered if not MyLife server - throw new Error('mbr_id required for entry summary') + if(this.isMyLife) + throw new Error('System cannot store entries of its own') let { name, } = entry - name = name ?? `${ defaultType }_${ form }_${ title.substring(0,64) }_${ mbr_id }` + name = name + ?? `${ defaultType }_${ form }_${ title.substring(0,64) }_${ mbr_id }` if(!summary?.length) throw new Error('entry summary required') /* assign default keywords */ @@ -675,7 +676,6 @@ class AgentFactory extends BotFactory { const _entry = { ...entry, ...{ - assistantType, being, form, id, @@ -801,7 +801,6 @@ class AgentFactory extends BotFactory { const defaultForm = 'memory' const defaultType = 'story' const { - assistantType='biographer', being=defaultType, form=defaultForm, id=this.newGuid, @@ -809,7 +808,7 @@ class AgentFactory extends BotFactory { mbr_id=(!this.isMyLife ? this.mbr_id : undefined), phaseOfLife='unknown', summary, - title=`New ${ defaultType }`, + title=`Untitled ${ defaultForm } ${ defaultType }`, } = story if(!mbr_id) // only triggered if not MyLife server throw new Error('mbr_id required for story summary') @@ -824,7 +823,6 @@ class AgentFactory extends BotFactory { const _story = { // add validated fields back into `story` object ...story, ...{ - assistantType, being, form, id, @@ -1187,17 +1185,17 @@ async function mConfigureSchemaPrototypes(){ // add required functionality as de * @private * @param {LLMServices} llm - OpenAI object * @param {AgentFactory} factory - Agent Factory object - * @param {object} assistantData - Bot object + * @param {object} botData - Bot object * @param {string} avatarId - Avatar id * @returns {string} - Bot assistant id in openAI */ -async function mCreateBotLLM(llm, assistantData){ - const llmResponse = await mAI_openai(llm, assistantData) +async function mCreateBotLLM(llm, botData){ + const llmResponse = await mAI_openai(llm, botData) return llmResponse.id } /** * Creates bot and returns associated `bot` object. - * @todo - assistantData.name = botDbName should not be required, push logic to `llm-services` + * @todo - botData.name = botDbName should not be required, push logic to `llm-services` * @module * @async * @private @@ -1226,7 +1224,7 @@ async function mCreateBot(llm, factory, bot, vectorstoreId){ ?? `bot_${ type }_${ avatarId }` const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) const id = factory.newGuid - const assistantData = { + const botData = { being: 'bot', bot_name, description, @@ -1247,13 +1245,13 @@ async function mCreateBot(llm, factory, bot, vectorstoreId){ version, } /* create in LLM */ - const botId = await mCreateBotLLM(llm, assistantData) // create after as require model + const botId = await mCreateBotLLM(llm, botData) // create after as require model if(!botId) throw new Error('bot creation failed') /* create in MyLife datastore */ - assistantData.bot_id = botId - const assistant = await factory.dataservices.createBot(assistantData) - console.log(chalk.green(`bot created::${ type }`), assistant.id, assistant.bot_id, assistant.bot_name, ) + botData.bot_id = botId + const assistant = await factory.dataservices.createBot(botData) + console.log(chalk.green(`bot created::${ type }`), assistant.id, assistant.bot_id, assistant.bot_name, bot.thread_id ) return assistant } /** diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json index 55c4c9e..79648e9 100644 --- a/inc/json-schemas/openai/functions/entrySummary.json +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -9,6 +9,14 @@ "description": "complete concatenated raw text content of member input(s) for this `entry`", "type": "string" }, + "form": { + "description": "Form of `entry` content, determine context from internal instructions", + "enum": [ + "diary", + "journal" + ], + "type": "string" + }, "keywords": { "description": "Keywords most relevant to `entry`.", "items": { @@ -41,6 +49,7 @@ "additionalProperties": false, "required": [ "content", + "form", "keywords", "mood", "relationships", diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 2021e7a..df8065f 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -350,12 +350,13 @@ function mBotIcon(type){ } /** * Create a functional collection item HTML div for the specified collection type. + * @example - collectionItem: { assistantType, filename, form, id, keywords, name, summary, title, type, } * @param {object} collectionItem - The collection item object, requires type. * @returns {HTMLDivElement} - The collection item. */ function mCreateCollectionItem(collectionItem){ /* collection item container */ - const { assistantType, filename, form, id, keywords, name, summary, title, type, } = collectionItem + const { assistantType, filename, form, id, name, title, type, } = collectionItem const iconType = assistantType ?? form ?? type diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 7c94a25..5f715e8 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -122,8 +122,8 @@ function clearSystemChat(){ * @param {object} activeBot - The active bot. * @returns {void} */ -function decorateActiveBot(activeBot=activeBot()){ - const { id, name, } = activeBot +function decorateActiveBot(){ + const { id, name, } = activeBot() chatInputField.placeholder = `Type your message to ${ name }...` // additional func? clear chat? } @@ -764,7 +764,7 @@ async function mSubmitChat(message) { * @returns {void} */ function toggleMemberInput(display=true, hidden=false, connectingText='Connecting with '){ - const { id, name, } = activeBot + const { id, name, } = activeBot() if(display){ hide(awaitButton) awaitButton.classList.remove('slide-up') From 5c8fa65fee95a7b488d6e140d2f2f088ce4801c0 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 15 Oct 2024 23:08:59 -0400 Subject: [PATCH 32/54] 20241015 @Mookse - prune getBot() - create thread on bot creation --- inc/js/functions.mjs | 2 +- inc/js/mylife-agent-factory.mjs | 37 +++++++----- inc/js/mylife-avatar.mjs | 56 ++++++++++++------- inc/js/mylife-llm-services.mjs | 29 +++++----- .../journaler-intelligence-1.1.json | 6 ++ 5 files changed, 80 insertions(+), 50 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index a9d21c2..1cc63f9 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -50,7 +50,7 @@ async function bots(ctx){ case 'GET': default: if(bid?.length){ // specific bot - ctx.body = await avatar.bot(ctx.params.bid) + ctx.body = await avatar.getBot(ctx.params.bid) } else { const { activeBotId, diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 7b1f703..6f37e2b 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -252,7 +252,7 @@ class BotFactory extends EventEmitter{ * @returns {object} - The bot. */ async bot(id, type=mDefaultBotType, mbr_id){ - 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(this.isMyLife){ if(!mbr_id) throw new Error('mbr_id required for BotFactory hydration') const botFactory = await new BotFactory(mbr_id) @@ -1115,14 +1115,15 @@ class MyLifeFactory extends AgentFactory { * Initializes openAI assistant and returns associated `assistant` object. * @module * @param {LLMServices} llmServices - OpenAI object - * @param {object} bot - The assistand data object + * @param {object} botData - The bot data object * @returns {object} - [OpenAI assistant object](https://platform.openai.com/docs/api-reference/assistants/object) */ -async function mAI_openai(llmServices, bot){ - const { bot_name, type, } = bot - bot.name = bot_name - ?? `My ${ type }` - return await llmServices.createBot(bot) +async function mAI_openai(llmServices, botData){ + const { bot_name, type, } = botData + botData.name = bot_name + ?? `_member_${ type }` + const bot = await llmServices.createBot(botData) + return bot } function assignClassPropertyValues(propertyDefinition){ switch (true) { @@ -1190,8 +1191,11 @@ async function mConfigureSchemaPrototypes(){ // add required functionality as de * @returns {string} - Bot assistant id in openAI */ async function mCreateBotLLM(llm, botData){ - const llmResponse = await mAI_openai(llm, botData) - return llmResponse.id + const { id, thread_id, } = await mAI_openai(llm, botData) + return { + id, + thread_id, + } } /** * Creates bot and returns associated `bot` object. @@ -1206,7 +1210,12 @@ async function mCreateBotLLM(llm, botData){ */ async function mCreateBot(llm, factory, bot, vectorstoreId){ /* initial deconstructions */ - const { bot_name: botName, description: botDescription, name: botDbName, type, } = bot + const { + bot_name: botName, + description: botDescription, + name: botDbName, + type, + } = bot const { avatarId, } = factory /* validation */ if(!avatarId) @@ -1245,13 +1254,14 @@ async function mCreateBot(llm, factory, bot, vectorstoreId){ version, } /* create in LLM */ - const botId = await mCreateBotLLM(llm, botData) // create after as require model + const { id: botId, thread_id, } = await mCreateBotLLM(llm, botData) // create after as require model if(!botId) throw new Error('bot creation failed') /* create in MyLife datastore */ botData.bot_id = botId + botData.thread_id = thread_id const assistant = await factory.dataservices.createBot(botData) - console.log(chalk.green(`bot created::${ type }`), assistant.id, assistant.bot_id, assistant.bot_name, bot.thread_id ) + console.log(chalk.green(`bot created::${ type }`), assistant.thread_id, assistant.id, assistant.bot_id, assistant.bot_name ) return assistant } /** @@ -1353,7 +1363,6 @@ function mCreateBotInstructions(factory, bot){ } }) /* assess and validate limit */ - console.log(chalk.blueBright('instructions length'), instructions.length, instructions) return { instructions, version, } } function mExposedSchemas(factoryBlockedSchemas){ @@ -1431,7 +1440,6 @@ constructor(obj){ eval(\`this.\#\${_key}=obj[_key]\`) } catch(err){ eval(\`this.\${_key}=obj[_key]\`) - console.log(\`could not privatize \${_key}, public node created\`) } } console.log('vm ${ _className } class constructed') @@ -1803,7 +1811,6 @@ async function mUpdateBot(factory, llm, bot, options={}){ const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) botData.tools = tools botData.tool_resources = tool_resources - console.log('mUpdateBot', botData.tools, botData.tool_resources, vectorstoreId) } if(updateModel) botData.model = factory.globals.currentOpenAIBotModel diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 368f3a4..178c316 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -75,11 +75,13 @@ class Avatar extends EventEmitter { * Get a bot's properties from Cosmos (or type in .bots). * @public * @async - * @param {Guid} id - The bot id. - * @returns {object} - The bot. + * @param {Guid} id - The bot id + * @returns {Promise} - The bot object from memory */ async bot(id){ - return await this.#factory.bot(id) + const bot = this.bots.find(bot=>bot.id===id) + ?? await this.#factory.bot(id) + return bot } /** * Processes and executes incoming chat request. @@ -94,6 +96,7 @@ class Avatar extends EventEmitter { * @returns {object} - The response object { instruction, responses, success, } */ async chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ + console.log('chat::activeBotId', activeBotId, threadId) if(!message) throw new Error('No message provided in context') if(!activeBotId) @@ -103,7 +106,7 @@ class Avatar extends EventEmitter { if(botId!==activeBotId) throw new Error(`Invalid bot id: ${ activeBotId }, active bot id: ${ botId }`) conversation = conversation - ?? this.getConversation(threadId ?? thread_id) + ?? this.getConversation(threadId ?? thread_id, null) ?? await this.createConversation('chat', threadId ?? thread_id, activeBotId) if(!conversation) throw new Error('No conversation found for thread id and could not be created.') @@ -204,6 +207,7 @@ class Avatar extends EventEmitter { * @returns {object} - The new bot. */ async createBot(bot){ + /* validate request */ const { type, } = bot if(!type) throw new Error('Bot type required to create') @@ -213,8 +217,11 @@ class Avatar extends EventEmitter { .length if(singletonBotExists) throw new Error(`Bot type "${type}" already exists and bot-multiples disallowed.`) + /* execute request */ bot = await mBot(this.#factory, this, bot) - return mPruneBot(bot) + /* respond request */ + const response = mPruneBot(bot) + return response } /** * Create a new conversation. @@ -227,9 +234,13 @@ class Avatar extends EventEmitter { * @returns {Conversation} - The conversation object. */ async createConversation(type='chat', threadId, botId=this.activeBotId, saveToConversations=true){ + const mbr_id = this.mbr_id const thread = await this.#llmServices.thread(threadId) - const { mbr_id, type: botType, } = this.getBot(botId) - const form = botType.split('-').pop() + const { type: botType, } = this.isMyLife + ? this.activeBot + : await this.bot(botId) + const form = botType?.split('-').pop() + ?? 'system' const conversation = new (this.#factory.conversation)( { form, @@ -374,10 +385,14 @@ class Avatar extends EventEmitter { this.#experienceGenericVariables ) } + /** + * Specified by id, returns the pruned bot from memory. + * @param {Guid} id - The id of the item to get + * @returns {object} - The pruned bot object + */ getBot(id){ - const bot = this.bots.find(bot=>bot.id===id) + const bot = mPruneBot(this.bots.find(bot=>bot.id===id)) return bot - ?? this.activeBot } /** * Gets Conversation object. If no thread id, creates new conversation. @@ -477,7 +492,7 @@ class Avatar extends EventEmitter { * @returns */ async migrateBot(botId){ - const bot = this.getBot(botId) + const bot = await this.bot(botId) if(!bot) throw new Error(`Bot not found with id: ${ botId }`) const { id, } = bot @@ -564,11 +579,11 @@ class Avatar extends EventEmitter { * @param {Guid} botId - The bot id. * @returns {object} - The retired bot object. */ - retireBot(botId){ + async retireBot(botId){ /* reset active bot, if required */ if(this.activeBotId===botId) this.activeBotId = null - const bot = this.getBot(botId) + const bot = await this.bot(botId) if(!bot) throw new Error(`Bot not found with id: ${ botId }`) const { id, } = bot @@ -602,7 +617,7 @@ class Avatar extends EventEmitter { throw new Error(`Conversation not found with bot id: ${ botId }`) } const { thread_id: cid, } = conversation - const bot = this.getBot(botId) + const bot = await this.bot(botId) const { id: _botId, thread_id: tid, } = bot if(botId!=_botId) throw new Error(`Bot id mismatch: ${ botId }!=${ bot_id }`) @@ -2271,15 +2286,15 @@ async function mInit(factory, llmServices, avatar, bots, assetAgent){ } )) avatar.activeBotId = avatar.avatar.id // initially set active bot to personal-avatar + if(factory.isMyLife) // as far as init goes for MyLife Avatar + return /* conversations */ await Promise.all( - bots.map(async bot=>{ + bots.map(async bot=>{ const { id: botId, thread_id, type, } = bot /* exempt certain types */ const excludedMemberTypes = ['library', 'ubi'] - if(factory.isMyLife && type!=='personal-avatar') - return - else if(excludedMemberTypes.includes(type)) + if(excludedMemberTypes.includes(type)) return if(!avatar.getConversation(thread_id, botId)){ const conversation = await avatar.createConversation('chat', thread_id, botId) @@ -2290,9 +2305,8 @@ async function mInit(factory, llmServices, avatar, bots, assetAgent){ }) ) /* evolver */ - if(!factory.isMyLife) - avatar.evolver = await (new EvolutionAssistant(avatar)) - .init() + avatar.evolver = await (new EvolutionAssistant(avatar)) + .init() /* lived-experiences */ avatar.experiencesLived = await factory.experiencesLived(false) } @@ -2312,7 +2326,7 @@ async function mMigrateChat(avatar, factory, llm, conversation){ if(!messages?.length) return conversation const { botId, } = conversation - const bot = avatar.getBot(botId) + const bot = await avatar.bot(botId) const botType = bot.type let disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, itemCollectionTypes='item', diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index d251ee8..e9d7e91 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -34,13 +34,15 @@ class LLMServices { /* public methods */ /** * Creates openAI GPT API assistant. - * @param {object} bot - The bot object + * @param {object} bot - The bot data * @returns {Promise} - openai assistant object */ async createBot(bot){ - const assistantData = mValidateAssistantData(bot) // throws on improper format - const assistant = await this.openai.beta.assistants.create(assistantData) - return assistant + bot = mValidateAssistantData(bot) // throws on improper format + bot = await this.openai.beta.assistants.create(bot) + const thread = await mThread(this.openai) + bot.thread_id = thread.id + return bot } /** * Creates a new OpenAI Vectorstore. @@ -103,23 +105,24 @@ class LLMServices { } /** * Given member input, get a response from the specified LLM service. + * @example - `run` object: { assistant_id, id, model, provider, required_action, status, usage } * @todo - confirm that reason for **factory** is to run functions as responses from LLM; ergo in any case, find better way to stash/cache factory so it does not need to be passed through every such function - * @param {string} threadId - Thread id. + * @param {string} thread_id - Thread id. * @param {string} botId - GPT-Assistant/Bot id. * @param {string} prompt - Member input. * @param {AgentFactory} factory - Avatar Factory object to process request. * @param {Avatar} avatar - Avatar object. * @returns {Promise} - Array of openai `message` objects. */ - async getLLMResponse(threadId, botId, prompt, factory, avatar){ - if(!threadId?.length) - threadId = ( await mThread(this.openai) ).id - await mAssignRequestToThread(this.openai, threadId, prompt) - const run = await mRunTrigger(this.openai, botId, threadId, factory, avatar) - const { assistant_id, id: run_id, model, provider='openai', required_action, status, usage } = run - const llmMessages = await this.messages(threadId) - return llmMessages + async getLLMResponse(thread_id, botId, prompt, factory, avatar){ + if(!thread_id?.length) + thread_id = ( await mThread(this.openai) ).id + await mAssignRequestToThread(this.openai, thread_id, prompt) + const run = await mRunTrigger(this.openai, botId, thread_id, factory, avatar) + const { id: run_id, } = run + const llmMessages = ( await this.messages(thread_id) ) .filter(message=>message.role=='assistant' && message.run_id==run_id) + return llmMessages } /** * Given member request for help, get response from specified bot assistant. diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json index 2f7489a..fcb3b47 100644 --- a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -49,6 +49,12 @@ "description": "member full name", "name": "<-mFN->", "replacement": "memberName" + }, + { + "default": "{unknown, find out}", + "description": "member birthdate", + "name": "<-db->", + "replacement": "dob" } ] }, From 81c825ba16b78f257054813342c8eab45af1016e Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Wed, 16 Oct 2024 02:16:51 -0400 Subject: [PATCH 33/54] 20241015 @Mookse - Migrate Conversation from Avatar to Bot #406 - wip semi-stable --- inc/js/functions.mjs | 16 ++-- inc/js/mylife-avatar.mjs | 179 ++++++++++++++++++++------------------- inc/js/routes.mjs | 1 + 3 files changed, 97 insertions(+), 99 deletions(-) diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 1cc63f9..6b7358d 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -97,8 +97,7 @@ async function challenge(ctx){ ctx.body = !MemberSession.locked } /** - * Chat with the member's avatar. - * @todo - deprecate threadId in favor of thread_id + * Chat with the Member or System Avatar's intelligence. * @param {Koa} ctx - Koa Context object * @returns {object} - The response from the chat in `ctx.body` * @property {object} instruction - Instructionset for the frontend to execute (optional) @@ -108,14 +107,11 @@ async function chat(ctx){ const { botId, itemId, message, shadowId, } = ctx.request.body ?? {} /* body nodes sent by fe */ if(!message?.length) ctx.throw(400, 'missing `message` content') - const { avatar, MemberSession, } = ctx.state - const { isMyLife, thread_id, } = MemberSession - let conversation - if(isMyLife && !thread_id?.length){ - conversation = await avatar.createConversation('system', undefined, botId, true) // pushes to this.#conversations in Avatar - MemberSession.thread_id = conversation.thread_id - } - const response = await avatar.chat(message, botId, MemberSession.thread_id, itemId, shadowId, conversation) + const { avatar, dateNow=Date.now(), } = ctx.state + const { MemberSession, } = ctx.session + if(botId?.length && botId!==avatar.activeBotId) + throw new Error(`Bot ${ botId } not currently active; chat() requires active bot`) + const response = await avatar.chat(message, itemId, shadowId, dateNow, MemberSession) ctx.body = response } async function collections(ctx){ diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 178c316..9dd3052 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -21,7 +21,6 @@ class Avatar extends EventEmitter { #activeBotId // id of active bot in this.#bots; empty or undefined, then this #assetAgent #bots = [] - #conversations = [] #evolver #experienceGenericVariables = { age: undefined, @@ -86,30 +85,20 @@ class Avatar extends EventEmitter { /** * Processes and executes incoming chat request. * @public - * @param {string} message - The chat message content. - * @param {string} activeBotId - The active bot id. - * @param {string} threadId - The openai thread id. - * @param {Guid} itemId - The active collection-item id (optional). - * @param {Guid} shadowId - The active Shadow Id (optional). - * @param {Conversation} conversation - The conversation object. - * @param {number} processStartTime - The start time of the process. + * @param {string} message - The chat message content + * @param {Guid} itemId - The active collection-item id (optional) + * @param {Guid} shadowId - The active Shadow Id (optional) + * @param {number} processStartTime - The start time of the process (optional) + * @param {MemberSession} session - ignored, but required for **overload** on Q instance + * @param {string} thread_id - The openai thread id (required for **overload** on Q instance) * @returns {object} - The response object { instruction, responses, success, } */ - async chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ - console.log('chat::activeBotId', activeBotId, threadId) + async chat(message, itemId, shadowId, processStartTime=Date.now(), session=null, thread_id){ if(!message) throw new Error('No message provided in context') - if(!activeBotId) - throw new Error('Parameter `activeBotId` required.') - const { activeBot, factory } = this - const { bot_id, id: botId, thread_id, } = activeBot - if(botId!==activeBotId) - throw new Error(`Invalid bot id: ${ activeBotId }, active bot id: ${ botId }`) - conversation = conversation - ?? this.getConversation(threadId ?? thread_id, null) - ?? await this.createConversation('chat', threadId ?? thread_id, activeBotId) + const { bot_id, conversation=this.getConversation(thread_id), id: botId, } = this.activeBot if(!conversation) - throw new Error('No conversation found for thread id and could not be created.') + throw new Error('No conversation found for bot intelligence and could not be created.') conversation.bot_id = botId conversation.llm_id = bot_id let _message = message, @@ -119,7 +108,7 @@ class Avatar extends EventEmitter { else { if(itemId){ // @todo - check if item exists in memory, fewer pings and inclusions overall - let { summary, } = await factory.item(itemId) + let { summary, } = await this.#factory.item(itemId) if(summary?.length){ summary = `possible **update-summary-request**: itemId=${ itemId }\n` + `**member-update-request**:\n` @@ -128,7 +117,7 @@ class Avatar extends EventEmitter { + summary } } - messages = await mCallLLM(this.#llmServices, conversation, _message, factory, this) + messages = await mCallLLM(this.#llmServices, conversation, _message, this.#factory, this) } conversation.addMessage({ content: message, @@ -141,21 +130,24 @@ class Avatar extends EventEmitter { else console.log('chat::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) /* frontend mutations */ - let responses + const responses = [] const { activeBot: bot } = this - responses = conversation.messages + 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 + .reverse() + .forEach(_message=>responses.push(_message)) + if(!responses?.length){ + const failsafeResponse = this.backupResponse ?? { - message: 'I am sorry, the entire chat line went dark for a moment, please try again.', + message: 'I am sorry, connection with my intelligence faltered, hopefully temporarily, ask to try again.', type: 'system', - }] + } + responses.push(failsafeResponse) } const response = { instruction: this.frontendInstruction, @@ -227,18 +219,19 @@ class Avatar extends EventEmitter { * Create a new conversation. * @async * @public - * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, etc.; defaults to `chat`. - * @param {string} threadId - The openai thread id. - * @param {string} botId - The bot id. - * @param {boolean} saveToConversations - Whether to save the conversation to local memory; certain system and memory actions will be saved in their own threads. - * @returns {Conversation} - The conversation object. + * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, etc.; defaults to `chat` + * @param {string} thread_id - The openai thread id + * @param {string} botId - The bot id + * @returns {Conversation} - The conversation object */ - async createConversation(type='chat', threadId, botId=this.activeBotId, saveToConversations=true){ + async createConversation(type='chat', thread_id, botId=this.activeBotId){ const mbr_id = this.mbr_id - const thread = await this.#llmServices.thread(threadId) - const { type: botType, } = this.isMyLife - ? this.activeBot + const thread = await this.#llmServices.thread(thread_id) + const { conversation: previousConversation, type: botType, } = this.isMyLife + ? this.avatar : await this.bot(botId) + if(!!previousConversation) + throw new Error(`Conversation already exists for bot/thread: ${ botId }/${ thread.id }`) const form = botType?.split('-').pop() ?? 'system' const conversation = new (this.#factory.conversation)( @@ -251,8 +244,7 @@ class Avatar extends EventEmitter { thread, botId ) - if(saveToConversations) - this.#conversations.push(conversation) + console.log('conversation created', conversation.inspect(true)) return conversation } /** @@ -396,13 +388,13 @@ class Avatar extends EventEmitter { } /** * Gets Conversation object. If no thread id, creates new conversation. - * @param {string} threadId - openai thread id (optional) + * @param {string} thread_id - openai thread id (optional) * @param {Guid} botId - The bot id (optional) * @returns {Conversation} - The conversation object. */ - getConversation(threadId, botId){ - const conversation = this.#conversations - .filter(c=>(threadId?.length && c.thread_id===threadId) || (botId?.length && c.botId===botId)) + getConversation(thread_id, botId){ + const conversation = this.conversations + .filter(c=>(thread_id?.length && c.thread_id===thread_id) || (botId?.length && c.botId===botId)) ?.[0] return conversation } @@ -774,21 +766,15 @@ class Avatar extends EventEmitter { const teams = this.#factory.teams() return teams } - async thread_id(){ - if(!this.conversations.length){ - await this.createConversation() - console.log('Avatar::thread_id::created new conversation', this.conversations[0].thread_id) - } - return this.conversations[0].threadId - } /** - * Update a specific bot. + * Update a specific bot. **Note**: mBot() updates `this.bots` * @async * @param {object} bot - Bot data to set. * @returns {object} - The updated bot. */ async updateBot(bot){ - return await mBot(this.#factory, this, bot) // **note**: mBot() updates `avatar.bots` + const updatedBot = await mBot(this.#factory, this, bot) // **note**: mBot() updates `avatar.bots` + return updatedBot } /** * Update instructions for bot-assistant based on type. Default updates all LLM pertinent properties. @@ -1002,7 +988,10 @@ class Avatar extends EventEmitter { * @returns {array} - The conversations. */ get conversations(){ - return this.#conversations + const conversations = this.bots + .map(bot=>bot.conversation) + .filter(Boolean) + return conversations } /** * Get the datacore. @@ -1324,6 +1313,7 @@ class Avatar extends EventEmitter { } } class Q extends Avatar { + #conversations = [] #factory // same reference as Avatar, but wish to keep private from public interface; don't touch my factory, man! #hostedMembers = [] // MyLife-hosted members #llmServices // ref _could_ differ from Avatar, but for now, same @@ -1342,7 +1332,7 @@ class Q extends Avatar { } /* overloaded methods */ /** - * Get a bot's properties from Cosmos (or type in .bots). + * OVERLOADED: Get a bot's properties from Cosmos (or type in .bots). * @public * @async * @param {string} mbr_id - The bot id @@ -1353,33 +1343,33 @@ class Q extends Avatar { return bot } /** - * Processes and executes incoming chat request. + * OVERLOADED: Processes and executes incoming chat request. * @public - * @param {string} message - The chat message content. - * @param {string} activeBotId - The active bot id. - * @param {string} threadId - The openai thread id. - * @param {Guid} itemId - The active collection-item id (optional). - * @param {Guid} shadowId - The active Shadow Id (optional). - * @param {Conversation} conversation - The conversation object. - * @param {number} processStartTime - The start time of the process. - * @returns {object} - The response(s) to the chat request. + * @param {string} message - The chat message content + * @param {Guid} itemId - The active collection-item id (optional) + * @param {Guid} shadowId - The active Shadow Id (optional) + * @param {number} processStartTime - The start time of the process + * @param {MemberSession} session - The MyLife MemberSession instance + * @returns {object} - The response(s) to the chat request */ - async chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime=Date.now()){ - conversation = conversation - ?? this.getConversation(threadId) - if(!conversation) - throw new Error('Conversation cannot be found') + async chat(message, itemId, shadowId, processStartTime=Date.now(), session){ + let { thread_id, } = session + if(!thread_id?.length){ + const conversation = await this.createConversation('system') + thread_id = conversation.thread_id + this.#conversations.push(conversation) + } this.activeBot.bot_id = mBot_idOverride ?? this.activeBot.bot_id + session.thread_id = thread_id // @stub - store elsewhere if(this.isValidating) // trigger confirmation until session (or vld) ends message = `CONFIRM REGISTRATION PHASE: registrationId=${ this.registrationId }\n${ message }` if(this.isCreatingAccount) message = `CREATE ACCOUNT PHASE: ${ message }` - activeBotId = this.activeBotId - return super.chat(message, activeBotId, threadId, itemId, shadowId, conversation, processStartTime) + return super.chat(message, itemId, shadowId, processStartTime, null, thread_id) } /** - * 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. + * OVERLOADED: 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 @@ -1390,6 +1380,11 @@ class Q extends Avatar { const updatedSummary = await botFactory.obscure(iid) return updatedSummary } + /** + * OVERLOADED: Refuses to upload to MyLife. + * @public + * @throws {Error} - MyLife avatar cannot upload files. + */ upload(){ throw new Error('MyLife avatar cannot upload files.') } @@ -1467,6 +1462,14 @@ class Q extends Avatar { get being(){ return 'MyLife' } + /** + * Get conversations. If getting a specific conversation, use .conversation(id). + * @getter + * @returns {array} - The conversations. + */ + get conversations(){ + return this.#conversations + } } /* module functions */ /** @@ -1519,7 +1522,8 @@ async function mBot(factory, avatar, bot){ if(!botType?.length) throw new Error('Bot type required to create.') bot.mbr_id = mbr_id /* constant */ - bot.object_id = objectId ?? avatarId /* all your bots belong to me */ + bot.object_id = objectId + ?? avatarId /* all your bots belong to me */ bot.id = botId // **note**: _this_ is a Cosmos id, not an openAI id let originBot = avatar.bots.find(oBot=>oBot.id===botId) if(originBot){ /* update bot */ @@ -1535,10 +1539,10 @@ async function mBot(factory, avatar, bot){ if(!thread_id?.length && !avatar.isMyLife){ const excludeTypes = ['collection', 'library', 'custom'] // @stub - custom mechanic? if(!excludeTypes.includes(type)){ - const conversation = avatar.getConversation(null, botId) + const conversation = avatar.conversation(null, botId) ?? await avatar.createConversation('chat', null, botId) updatedBot.thread_id = conversation.thread_id // triggers `factory.updateBot()` - console.log('Avatar::mBot::conversation created given NO thread_id', updatedBot.thread_id, avatar.getConversation(updatedBot.thread_id)) + console.log('Avatar::mBot::conversation created given NO thread_id', updatedBot.thread_id, conversation.inspect(true)) } } let updatedOriginBot @@ -1595,13 +1599,13 @@ async function mCallLLM(llmServices, conversation, prompt, factory, avatar){ * Cancels openAI run. * @module * @param {LLMServices} llmServices - OpenAI object - * @param {string} threadId - Thread id + * @param {string} thread_id - Thread id * @param {string} runId - Run id * @returns {object} - [OpenAI run object](https://platform.openai.com/docs/api-reference/runs/object) */ -async function mCancelRun(llmServices, threadId, runId,){ +async function mCancelRun(llmServices, thread_id, runId,){ return await llmServices.beta.threads.runs.cancel( - threadId, + thread_id, runId ) } @@ -2237,12 +2241,13 @@ function mHelpIncludePreamble(type, isMyLife){ } } /** - * Initializes the Avatar instance with stored data. - * @param {MyLifeFactory|AgentFactory} factory - Member Avatar (true) or Q (false). - * @param {LLMServices} llmServices - OpenAI object. - * @param {Q|Avatar} avatar - The avatar Instance (`this`). - * @param {array} bots - The array of bot objects from private class `this.#bots`. - * @returns {Promise} - Return indicates successfully mutated avatar. + * Initializes the Avatar instance with stored data + * @param {MyLifeFactory|AgentFactory} factory - Member Avatar or Q + * @param {LLMServices} llmServices - OpenAI object + * @param {Q|Avatar} avatar - The avatar Instance (`this`) + * @param {array} bots - The array of bot objects from private class `this.#bots` + * @param {AssetAgent} assetAgent - AssetAgent instance + * @returns {Promise} - Return indicates successfully mutated avatar */ async function mInit(factory, llmServices, avatar, bots, assetAgent){ /* get avatar data from cosmos */ @@ -2296,12 +2301,8 @@ async function mInit(factory, llmServices, avatar, bots, assetAgent){ const excludedMemberTypes = ['library', 'ubi'] if(excludedMemberTypes.includes(type)) return - if(!avatar.getConversation(thread_id, botId)){ - const conversation = await avatar.createConversation('chat', thread_id, botId) - avatar.updateBot(bot) - if(!avatar.getConversation(thread_id)) // may happen in cases of MyLife? others? - avatar.conversations.push(conversation) - } + const conversation = await avatar.createConversation('chat', thread_id, botId) + bot.conversation = conversation }) ) /* evolver */ diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index d18b02e..e1ade44 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -155,6 +155,7 @@ function connectRoutes(_Menu){ */ async function memberValidation(ctx, next){ const { locked=true, } = ctx.state + ctx.state.dateNow = Date.now() if(locked) ctx.redirect(`/?type=select`) // Redirect to /members if not authorized else From 1659e76f6dcd2efffc7e93b01230785fb095c999 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 22 Oct 2024 02:57:13 -0400 Subject: [PATCH 34/54] 20241022 @Mookse - fix updateSummary --- inc/js/mylife-avatar.mjs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 9dd3052..00fde40 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -101,23 +101,22 @@ class Avatar extends EventEmitter { throw new Error('No conversation found for bot intelligence and could not be created.') conversation.bot_id = botId conversation.llm_id = bot_id - let _message = message, - messages = [] + let messages = [] if(shadowId) - messages = await this.shadow(shadowId, itemId, _message) + messages.push(...await this.shadow(shadowId, itemId, message)) else { + let alteredMessage = message if(itemId){ // @todo - check if item exists in memory, fewer pings and inclusions overall let { summary, } = await this.#factory.item(itemId) - if(summary?.length){ - summary = `possible **update-summary-request**: itemId=${ itemId }\n` - + `**member-update-request**:\n` - + message - + `\n**current-summary-in-database**:\n` - + summary - } + if(summary?.length) + alteredMessage = `possible **update-summary-request**: itemId=${ itemId }\n` + + `**member-update-request**:\n` + + message + + `\n**current-summary-in-database**:\n` + + summary } - messages = await mCallLLM(this.#llmServices, conversation, _message, this.#factory, this) + messages.push(...await mCallLLM(this.#llmServices, conversation, alteredMessage, this.#factory, this)) } conversation.addMessage({ content: message, From 54200a61b7d6580295e0211b8ff4047d625ab347 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 22 Oct 2024 02:58:23 -0400 Subject: [PATCH 35/54] 20241022 @Mookse - fix updateSummary error --- inc/js/mylife-avatar.mjs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 9dd3052..00fde40 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -101,23 +101,22 @@ class Avatar extends EventEmitter { throw new Error('No conversation found for bot intelligence and could not be created.') conversation.bot_id = botId conversation.llm_id = bot_id - let _message = message, - messages = [] + let messages = [] if(shadowId) - messages = await this.shadow(shadowId, itemId, _message) + messages.push(...await this.shadow(shadowId, itemId, message)) else { + let alteredMessage = message if(itemId){ // @todo - check if item exists in memory, fewer pings and inclusions overall let { summary, } = await this.#factory.item(itemId) - if(summary?.length){ - summary = `possible **update-summary-request**: itemId=${ itemId }\n` - + `**member-update-request**:\n` - + message - + `\n**current-summary-in-database**:\n` - + summary - } + if(summary?.length) + alteredMessage = `possible **update-summary-request**: itemId=${ itemId }\n` + + `**member-update-request**:\n` + + message + + `\n**current-summary-in-database**:\n` + + summary } - messages = await mCallLLM(this.#llmServices, conversation, _message, this.#factory, this) + messages.push(...await mCallLLM(this.#llmServices, conversation, alteredMessage, this.#factory, this)) } conversation.addMessage({ content: message, From 5cc9027fd8ae479cb61df63004801bfc4d591ab7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 22 Oct 2024 03:18:23 -0400 Subject: [PATCH 36/54] 20241022 @Mookse - wip broken --- README.md | 2 +- .../{asset-assistant.mjs => asset-agent.mjs} | 24 +- inc/js/agents/system/bot-agent.mjs | 854 ++++++++++++++++++ inc/js/agents/system/bot-assistant.mjs | 2 - ...tion-assistant.mjs => evolution-agent.mjs} | 93 +- inc/js/api-functions.mjs | 1 + inc/js/core.mjs | 7 +- inc/js/functions.mjs | 10 +- inc/js/globals.mjs | 45 +- inc/js/mylife-avatar.mjs | 606 ++++--------- ...ata-service.js => mylife-dataservices.mjs} | 50 +- ...e-agent-factory.mjs => mylife-factory.mjs} | 508 +---------- server.js | 2 +- views/README.md | 2 +- 14 files changed, 1193 insertions(+), 1013 deletions(-) rename inc/js/agents/system/{asset-assistant.mjs => asset-agent.mjs} (89%) create mode 100644 inc/js/agents/system/bot-agent.mjs delete mode 100644 inc/js/agents/system/bot-assistant.mjs rename inc/js/agents/system/{evolution-assistant.mjs => evolution-agent.mjs} (80%) rename inc/js/{mylife-data-service.js => mylife-dataservices.mjs} (95%) rename inc/js/{mylife-agent-factory.mjs => mylife-factory.mjs} (74%) diff --git a/README.md b/README.md index 92551a5..95304d2 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ MyLife itself is an open-source project and, aside from LLM technologies at the 3. **Bot Functionality and Intelligence Management** - The application features a sophisticated bot system, capable of creating and managing different types of bots like personal assistants, biographers, health bots, etc. - - OpenAI's GPT-3 model is integrated for generating responses and interacting with users through bots, as observed in the `class-avatar-functions.mjs` and `mylife-agent-factory.mjs` files. + - OpenAI's GPT-3 model is integrated for generating responses and interacting with users through bots, as observed in the `mylife-avatar.mjs` and `mylife-factory.mjs` files. 4. **Session Management** - Managed through the `MylifeMemberSession` class, handling user sessions, consents, and alerts. diff --git a/inc/js/agents/system/asset-assistant.mjs b/inc/js/agents/system/asset-agent.mjs similarity index 89% rename from inc/js/agents/system/asset-assistant.mjs rename to inc/js/agents/system/asset-agent.mjs index bfdde65..ec15956 100644 --- a/inc/js/agents/system/asset-assistant.mjs +++ b/inc/js/agents/system/asset-agent.mjs @@ -1,29 +1,31 @@ // imports import fs from 'fs' import mime from 'mime-types' -import FormData from 'form-data' -import axios from 'axios' // module constants -const { MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN, MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT, MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN, MYLIFE_SERVER_MBR_ID: mylifeMbrId, } = process.env -const bearerToken = MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN +const { + MYLIFE_EMBEDDING_SERVER_BEARER_TOKEN: bearerToken, + MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT, + MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN, + MYLIFE_SERVER_MBR_ID: mylifeMbrId, +} = process.env const fileSizeLimit = parseInt(MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT) || 1048576 const fileSizeLimitAdmin = parseInt(MYLIFE_EMBEDDING_SERVER_FILESIZE_LIMIT_ADMIN) || 10485760 -class oAIAssetAssistant { +class AssetAgent { #globals #llm #mbr_id #response #vectorstoreId #vectorstoreFileList=[] // openai vectorstore versions - constructor(mbr_id, globals, llm){ - this.#mbr_id = mbr_id - this.#globals = globals + constructor(factory, llm){ + this.#mbr_id = factory.mbr_id + this.#globals = factory.globals this.#llm = llm } /** * Initializes the asset assistant by uploading the files to the vectorstore. * @param {string} vectorstoreId - The vectorstore id to upload the files into, if already exists (avatar would know). - * @returns {Promise} - The initialized asset assistant instance. + * @returns {Promise} - The initialized asset assistant instance. */ async init(vectorstoreId){ if(!vectorstoreId?.length) @@ -143,5 +145,5 @@ class oAIAssetAssistant { throw new Error(`Unsupported media type: ${ mimetype }. File type not allowed.`) } } -// exports -export default oAIAssetAssistant \ No newline at end of file +/* exports */ +export default AssetAgent \ No newline at end of file diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs new file mode 100644 index 0000000..a563419 --- /dev/null +++ b/inc/js/agents/system/bot-agent.mjs @@ -0,0 +1,854 @@ +/* module constants */ +const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE +const mDefaultBotTypeArray = ['personal-avatar', 'avatar'] +const mDefaultBotType = mDefaultBotTypeArray[0] +const mDefaultGreetings = [] +const mDefaultTeam = 'memory' +const mRequiredBotTypes = ['personal-avatar'] +const mTeams = [ + { + active: true, + allowCustom: true, + allowedTypes: ['diary', 'journaler', 'personal-biographer',], + defaultActiveType: 'personal-biographer', + defaultTypes: ['personal-biographer',], + description: 'The Memory Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', + id: 'a261651e-51b3-44ec-a081-a8283b70369d', + name: 'memory', + title: 'Memory', + }, +] +/* classes */ +/** + * @class - Bot + * @private + * @todo - are private vars for factory and llm necessary, or passable? + */ +class Bot { + #conversation + #factory + #llm + constructor(botData, factory, llm){ + this.#factory = factory + this.#llm = llm + botData = this.globals.sanitize(botData) + Object.assign(this, botData) + if(!this.id) + throw new Error('Bot database id required') + } + /* public functions */ + async chat(){ + console.log('Bot::chat', this.#conversation) + // what should be returned? Responses? Conversation object? + return this.#conversation + } + getBot(){ + return this + } + /** + * Retrieves a greeting message from the active bot. + * @param {Boolean} dynamic - Whether to use dynamic greetings (`true`) or static (`false`) + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object + * @returns {Array} - The greeting message(s) string array in order of display + */ + async getGreeting(dynamic=false, llm, factory){ + const greetings = await mBotGreeting(dynamic, this, llm, factory) + return greetings + } + /** + * Updates a Bot instance's data. + * @param {object} botData - The bot data to update + * @param {object} botOptions - Options for updating + * @returns + */ + async update(botData, botOptions){ + /* validate request */ + this.globals.sanitize(botData) + const Bot = await mBotUpdate(this.#factory, this.#llm, botData, botOptions) + return this.globals.sanitize(Bot) + } + async save(){ + + } + /* getters/setters */ + /** + * Gets the frontend bot object. If full instance is required, use `getBot()`. + * @getter + */ + get bot() { + const { bot_name, description, id, purpose, type, version } = this + const bot = { + bot_name, + description, + id, + purpose, + type, + version, + } + return bot + } + get globals(){ + return this.#factory.globals + } + get isAvatar(){ + return mDefaultBotTypeArray.includes(this.type) + } + get isMyLife(){ + return this.#factory.isMyLife + } + get micro(){ + const { + bot_name, + id, + name, + provider, + type, + version, + } = this + const microBot = { + bot_name, + id, + name, + provider, + type, + version, + } + return microBot + } +} +/** + * @class - Team + * @private + */ +/** + * @class - BotAgent + * @public + * @description - BotAgent is an interface to assist in creating, managing and maintaining a Member Avatar's bots. + */ +class BotAgent { + #activeBot + #activeTeam = mDefaultTeam + #avatarId + #bots + #factory + #llm + #vectorstoreId + constructor(factory, llm){ + this.#factory = factory + this.#llm = llm + } + /** + * Initializes the BotAgent instance. + * @async + * @param {Guid} avatarId - The Avatar id + * @param {string} vectorstoreId - The Vectorstore id + * @returns {Promise} - The BotAgent instance + */ + async init(avatarId, vectorstoreId){ + /* validate request */ + if(!avatarId?.length) + throw new Error('AvatarId required') + this.#avatarId = avatarId + this.#bots = [] + this.#vectorstoreId = vectorstoreId + /* execute request */ + await mInit(this, this.#bots, this.#factory, this.#llm) + return this + } + /* public functions */ + /** + * Retrieves a bot instance by id. + * @param {Guid} botId - The Bot id + * @returns {Promise} - The Bot instance + */ + bot(botId){ + const Bot = this.#bots + .find(bot=>bot.id===botId) + return Bot + } + /** + * Creates a bot instance. + * @param {Object} botData - The bot data object + * @returns {Bot} - The created Bot instance + */ + async botCreate(botData){ + const Bot = await mBotCreate(this.#avatarId, this.#vectorstoreId, botData, this.#factory) + this.#bots.push(Bot) + this.setActiveBot(Bot.id) + return Bot + } + /** + * Deletes a bot instance. + * @async + * @param {Guid} botId - The Bot id + * @returns {Promise} + */ + async botDelete(botId){ + const Bot = this.#bots.find(bot=>bot.id===botId) + if(!Bot) + throw new Error(`Bot not found with id: ${ botId }`) + await mBotDelete(Bot, this.#bots, this.#llm, this.#factory) + } + async chat(){ + return this.activeBot.chat() + } + /** + * Initializes a conversation, currently only requested by System Avatar, but theoretically could be requested by any externally-facing Member Avatar as well. **note**: not in Q because it does not have a #botAgent yet. + * @param {String} type - The type of conversation, defaults to `chat` + * @param {String} form - The form of conversation, defaults to `system-avatar` + * @returns {Promise} - The conversation object + */ + async conversationStart(type='chat', form='system-avatar'){ + const { bot_id, } = this.avatar + const Conversation = await mConversationStart(type, form, null, bot_id, this.#llm, this.#factory) + return Conversation + } + /** + * Retrieves bots by instance. + * @returns {Bot[]} - The array of bots + */ + getBots(){ + return this.#bots + } + /** + * Get a static or dynamic greeting from active bot. + * @param {boolean} dynamic - Whether to use LLM for greeting + * @returns {Messages[]} - The greeting message(s) string array in order of display + */ + async greeting(dynamic=false){ + const greetings = await this.activeBot.getGreeting(dynamic, this.#llm, this.#factory) + return greetings + } + /** + * Sets the active bot for the BotAgent. + * @param {Guid} botId - The Bot id + * @returns {void} + */ + setActiveBot(botId){ + const Bot = this.#bots.find(bot=>bot.id===botId) + if(Bot) + this.#activeBot = Bot + } + /** + * Sets the active team for the BotAgent if `teamId` valid. + * @param {Guid} teamId - The Team id + * @returns {void} + */ + setActiveTeam(teamId){ + this.#activeTeam = this.teams.find(team=>team.id===teamId) + ?? this.#activeTeam + } + /** + * Updates a bot instance. + * @param {object} botData - The bot data to update + * @param {object} botOptions - Options for updating the bot + * @returns {Promise} - The updated Bot instance + */ + async updateBot(botData, botOptions={}){ + const { id, } = botData + if(!this.globals.isValidGuid(id)) + throw new Error('`id` parameter required') + const Bot = this.#bots.find(bot=>bot.id===id) + if(!!Bot) + throw new Error(`Bot not found with id: ${ id }`) + Bot.update(botData, botOptions) + return Bot + } + /* getters/setters */ + /** + * Gets the active Bot instance. + * @getter + * @returns {Bot} - The active Bot instance + */ + get activeBot(){ + return this.#activeBot + } + /** + * Gets the active team. + * @getter + * @returns {object} - The active team object + */ + get activeTeam(){ + return this.#activeTeam + } + /** + * Gets the active bot id for the BotAgent. + * @getter + * @returns {Guid} - The active bot id + */ + get activeBotId(){ + return this.#activeBot.id + } + /** + * Gets the primary avatar for Member. + * @getter + * @returns {Bot} - The primary avatar Bot instance + */ + get avatar(){ + const bot = this.#bots.find(Bot=>Bot.isAvatar===true) + return bot + } + /** + * Gets the Avatar id for whom this BotAgent is conscripted. + * @getter + * @returns {String} - The Avatar id + */ + get avatarId(){ + return this.#avatarId + } + /** + * Gets the array of bots employed by this BotAgent. For full instances, call `getBots()`. + * @getter + * @returns {Bot[]} - The array of bots + */ + get bots(){ + return this.#bots + } + /** + * Returns system globals object. + * @getter + * @returns {object} - System globals object + */ + get globals(){ + return this.#factory.globals + } + /** + * Returns whether BotAgent is employed by MyLife (`true`) or Member (`false`). + * @getter + * @returns {Boolean} - Whether BotAgent is employed by MyLife, defaults to `false` + */ + get isMyLife(){ + return this.#factory.isMyLife + } + /** + * Retrieves list of available MyLife Teams. + * @getter + * @returns {object[]} - The array of MyLife Teams + */ + get teams(){ + return mTeams + } + /** + * Returns the Vectorstore id for the BotAgent. + * @getter + * @returns {String} - The Vectorstore id + */ + get vectorstoreId(){ + return this.#vectorstoreId + } +} +/* modular functions */ +/** + * Initializes openAI assistant and returns associated `assistant` object. + * @module + * @param {object} botData - The bot data object + * @param {LLMServices} llmServices - OpenAI object + * @returns {object} - [OpenAI assistant object](https://platform.openai.com/docs/api-reference/assistants/object) + */ +async function mAI_openai(botData, llmServices){ + const { bot_name, type, } = botData + botData.name = bot_name + ?? `_member_${ type }` + const bot = await llmServices.createBot(botData) + return bot +} +/** + * Validates and cleans bot object then updates or creates bot (defaults to new personal-avatar) in Cosmos and returns successful `bot` object, complete with conversation (including thread/thread_id in avatar) and gpt-assistant intelligence. + * @todo Fix occasions where there will be no object_id property to use, as it was created through a hydration method based on API usage, so will be attached to mbr_id, but NOT avatar.id + * @todo - Turn this into Bot class + * @module + * @param {Guid} avatarId - The Avatar id + * @param {string} vectorstore_id - The Vectorstore id + * @param {AgentFactory} factory - Agent Factory instance + * @param {Avatar} avatar - Avatar object that will govern bot + * @param {object} botData - Bot data object, can be incomplete (such as update) + * @returns {Promise} - Bot object + */ +async function mBot(avatarId, vectorstore_id, factory, botData){ + /* validation */ + const { globals, isMyLife, mbr_id, newGuid, } = factory + const { id=newGuid, type, } = botData + console.log('BotAgent::mBot', avatarId, vectorstore_id, id, type, isMyLife) + throw new Error('mBot() not yet implemented') + if(!botType?.length) + throw new Error('Bot type required to create.') + bot.mbr_id = mbr_id /* constant */ + bot.object_id = objectId + ?? avatarId /* all your bots belong to me */ + bot.id = botId // **note**: _this_ is a Cosmos id, not an openAI id + let originBot = avatar.bots.find(oBot=>oBot.id===botId) + if(originBot){ /* update bot */ + const options = {} + const updatedBot = Object.keys(bot) + .reduce((diff, key) => { + if(bot[key]!==originBot[key]) + diff[key] = bot[key] + return diff + }, {}) + /* create or update bot special properties */ + const { thread_id, type, } = originBot // @stub - `bot_id` cannot be updated through this mechanic + if(!thread_id?.length && !avatar.isMyLife){ + const excludeTypes = ['collection', 'library', 'custom'] // @stub - custom mechanic? + if(!excludeTypes.includes(type)){ + const conversation = avatar.conversation(null, botId) + ?? await avatar.createConversation('chat', null, botId) + updatedBot.thread_id = conversation.thread_id // triggers `factory.updateBot()` + console.log('Avatar::mBot::conversation created given NO thread_id', updatedBot.thread_id, conversation.inspect(true)) + } + } + let updatedOriginBot + if(Object.keys(updatedBot).length){ + updatedOriginBot = {...originBot, ...updatedBot} // consolidated update + const { bot_id, id, } = updatedOriginBot + updatedBot.bot_id = bot_id + updatedBot.id = id + updatedBot.type = type + const { interests, } = updatedBot + /* set options */ + if(interests?.length){ + options.instructions = true + options.model = true + options.tools = false /* tools not updated through this mechanic */ + } + updatedOriginBot = await factory.updateBot(updatedBot, options) + } + originBot = mSanitize(updatedOriginBot ?? originBot) + avatar.bots[avatar.bots.findIndex(oBot=>oBot.id===botId)] = originBot + } else { /* create assistant */ + bot = mSanitize( await factory.createBot(bot, vectorstore_id) ) + avatar.bots.push(bot) + } + return originBot + ?? bot +} +/** + * Creates bot and returns associated `bot` object. + * @todo - botData.name = botDbName should not be required, push logic to `llm-services` + * @module + * @async + * @param {Guid} avatarId - The Avatar id + * @param {String} vectorstore_id - The Vectorstore id + * @param {Object} bot - The bot data + * @param {AgentFactory} factory - Agent Factory instance + * @returns {Promise} - Created Bot instance +*/ +async function mBotCreate(avatarId, vectorstore_id, bot, factory){ + /* validation */ + const { type, } = bot + if(!avatarId?.length || !type?.length) + throw new Error('avatar id and type required to create bot') + const { instructions, version, } = mBotInstructions(factory, bot) + const model = process.env.OPENAI_MODEL_CORE_BOT + ?? process.env.OPENAI_MODEL_CORE_AVATAR + ?? 'gpt-4o' + const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstore_id) + const id = factory.newGuid + let { + bot_name = `My ${type}`, + description = `I am a ${type} for ${factory.memberName}`, + name = `bot_${type}_${avatarId}`, + } = bot + const botData = { + being: 'bot', + bot_name, + description, + id, + instructions, + metadata: { + externalId: id, + version: version.toString(), + }, + model, + name, + object_id: avatarId, + provider: 'openai', + purpose: description, + tools, + tool_resources, + type, + vectorstore_id, + version, + } + /* create in LLM */ + const { id: bot_id, thread_id, } = await mBotCreateLLM(botData, llm) + if(!bot_id?.length) + throw new Error('bot creation failed') + /* create in MyLife datastore */ + botData.bot_id = bot_id + botData.thread_id = thread_id + const Bot = new Bot(await factory.createBot(botData)) + console.log(chalk.green(`bot created::${ type }`), Bot.thread_id, Bot.id, Bot.bot_id, Bot.bot_name ) + return Bot +} +/** + * Creates bot and returns associated `bot` object. + * @module + * @param {object} botData - Bot object + * @param {LLMServices} llm - OpenAI object + * @returns {string} - Bot assistant id in openAI +*/ +async function mBotCreateLLM(botData, llm){ + const { id, thread_id, } = await mAI_openai(botData, llm) + return { + id, + thread_id, + } +} +/** + * Deletes the bot requested from avatar memory and from all long-term storage. + * @param {object} Bot - The bot object to delete + * @param {Object[]} bots - The bots array + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object + * @returns {void} + */ +async function mBotDelete(Bot, bots, llm, factory){ + const cannotRetire = ['actor', 'system', 'personal-avatar'] + const { bot_id, id, thread_id, type, } = bot + if(cannotRetire.includes(type)) + throw new Error(`Cannot retire bot type: ${ type }`) + /* delete from memory */ + const botId = bots.findIndex(_bot=>_bot.id===id) + if(botId<0) + throw new Error('Bot not found in bots.') + bots.splice(botId, 1) + /* delete bot from Cosmos */ + factory.deleteItem(id) + /* delete thread and bot from OpenAI */ + llm.deleteBot(bot_id) + llm.deleteThread(thread_id) +} +/** + * Returns set of Greeting messages, dynamic or static + * @param {boolean} dynamic - Whether to use dynamic greetings + * @param {Bot} Bot - The bot instance + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object + * @returns {Promise} - The array of messages to respond with + */ +async function mBotGreeting(dynamic=false, Bot, llm, factory){ + const { bot_id, bot_name, id, greetings=[], greeting, thread_id, } = Bot + const failGreetings = [ + `Hello! I'm concerned that there is something wrong with my instruction-set, as I was unable to find my greetings, but let's see if I can get back online.`, + `How can I be of help today?` + ] + const greetingPrompt = factory.isMyLife + ? `Greet this new visitor and let them know that you are here to help them understand MyLife and the MyLife platform. Begin by asking them about something that's important to them so I can demonstrate how MyLife will assist.` + : `Where did we leave off, or how do we start?` + const botGreetings = greetings?.length + ? greetings + : greeting + ? [greeting] + : failGreetings + let messages = !dynamic + ? botGreetings + : await llm.getLLMResponse(thread_id, bot_id, greetingPrompt, factory) + if(!messages?.length) + messages = failGreetings + messages = messages + .map(message=>new (factory.message)({ + being: 'message', + content: message, + thread_id, + role: 'assistant', + type: 'greeting' + })) + return messages +} +/** + * Returns MyLife-version of bot instructions. + * @module + * @param {BotFactory} factory - Factory object + * @param {object} bot - Bot object + * @returns {object} - minor + */ +function mBotInstructions(factory, bot){ + const { type=mDefaultBotType, } = bot + 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=[], + suffix='', // example: data privacy info + voice='', + } = instructions + /* compile instructions */ + switch(type){ + case 'diary': + instructions = purpose + + preamble + + prefix + + general + + suffix + + voice + break + case 'personal-avatar': + instructions = preamble + + general + break + case 'journaler': + case 'personal-biographer': + instructions = preamble + + purpose + + prefix + + general + break + default: + instructions = general + break + } + /* apply replacements */ + replacements.forEach(replacement=>{ + const placeholderRegExp = factory.globals.getRegExp(replacement.name, true) + const replacementText = eval(`bot?.${replacement.replacement}`) + ?? eval(`factory?.${replacement.replacement}`) + ?? eval(`factory.core?.${replacement.replacement}`) + ?? replacement?.default + ?? '`unknown-value`' + instructions = instructions.replace(placeholderRegExp, _=>replacementText) + }) + /* apply references */ + references.forEach(_reference=>{ + const _referenceText = _reference.insert + const replacementText = eval(`factory?.${_reference.value}`) + ?? eval(`bot?.${_reference.value}`) + ?? _reference.default + ?? '`unknown-value`' + switch(_reference.method ?? 'replace'){ + case 'append-hard': + const _indexHard = instructions.indexOf(_referenceText) + if (_indexHard !== -1) { + instructions = + instructions.slice(0, _indexHard + _referenceText.length) + + '\n' + + replacementText + + instructions.slice(_indexHard + _referenceText.length) + } + break + case 'append-soft': + const _indexSoft = instructions.indexOf(_referenceText); + if (_indexSoft !== -1) { + instructions = + instructions.slice(0, _indexSoft + _referenceText.length) + + ' ' + + replacementText + + instructions.slice(_indexSoft + _referenceText.length) + } + break + case 'replace': + default: + instructions = instructions.replace(_referenceText, replacementText) + break + } + }) + /* assess and validate limit */ + return { instructions, version, } +} +/** + * Updates bot in Cosmos, and if necessary, in LLM. Returns unsanitized bot data document. + * @param {AgentFactory} factory - Factory object + * @param {LLMServices} llm - LLMServices object + * @param {object} bot - Bot object, winnow via mBot in `mylife-avatar.mjs` to only updated fields + * @param {object} options - Options object: { instructions: boolean, model: boolean, tools: boolean, vectorstoreId: string, } + * @returns + */ +async function mBotUpdate(factory, llm, bot, options={}){ + /* constants */ + const { + id, // no modifications + instructions: removeInstructions, + tools: removeTools, + tool_resources: removeResources, + type, // no modifications + ...botData // extract member-driven bot data + } = bot + const { + instructions: updateInstructions=false, + model: updateModel=false, + tools: updateTools=false, + vectorstoreId, + } = options + if(!factory.globals.isValidGuid(id)) + throw new Error('bot `id` required in bot argument: `{ id: guid }`') + if(updateInstructions){ + const { instructions, version=1.0, } = mBotInstructions(factory, bot) + botData.instructions = instructions + botData.metadata = botData.metadata ?? {} + botData.metadata.version = version.toString() + botData.version = version /* omitted from llm, but appears on updateBot */ + } + if(updateTools){ + const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) + botData.tools = tools + botData.tool_resources = tool_resources + } + if(updateModel) + botData.model = factory.globals.currentOpenAIBotModel + botData.id = id // validated + /* LLM updates */ + const { bot_id, bot_name: name, instructions, tools, } = botData + if(bot_id?.length && (instructions || name || tools)){ + botData.model = factory.globals.currentOpenAIBotModel // not dynamic + await llm.updateBot(botData) + const updatedLLMFields = Object.keys(botData) + .filter(key=>key!=='id' && key!=='bot_id') // strip mechanicals + console.log(chalk.green('mUpdateBot()::update in OpenAI'), id, bot_id, updatedLLMFields) + } + const updatedBotData = await factory.updateBot(botData) + return updatedBotData +} +/** + * Create a new conversation. + * @async + * @module + * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, system, etc.; defaults to `chat` + * @param {string} form - Form of conversation: system-avatar, member-avatar, etc.; defaults to `system-avatar` + * @param {string} thread_id - The openai thread id + * @param {string} botId - The bot id + * @returns {Conversation} - The conversation object + */ +async function mConversationStart(form='system', type='chat', thread_id, llmAgentId, llm, factory){ + const { mbr_id, } = factory + const thread = await llm.thread(thread_id) + const Conversation = new (factory.conversation)( + { + form, + mbr_id, + type, + }, + factory, + thread, + llmAgentId + ) + return Conversation +} +/** + * Retrieves any functions that need to be attached to the specific bot-type. + * @module + * @todo - Move to llmServices and improve + * @param {string} type - Type of bot. + * @param {object} globals - Global functions for bot. + * @param {string} vectorstoreId - Vectorstore id. + * @returns {object} - OpenAI-ready object for functions { tools, tool_resources, }. + */ +function mGetAIFunctions(type, globals, vectorstoreId){ + let includeSearch=false, + tool_resources, + tools = [] + switch(type){ + case 'assistant': + case 'avatar': + case 'personal-assistant': + case 'personal-avatar': + includeSearch = true + break + case 'biographer': + case 'personal-biographer': + tools.push( + globals.getGPTJavascriptFunction('changeTitle'), + globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('storySummary'), + globals.getGPTJavascriptFunction('updateSummary'), + ) + includeSearch = true + break + case 'custom': + includeSearch = true + break + case 'diary': + case 'journaler': + tools.push( + globals.getGPTJavascriptFunction('changeTitle'), + globals.getGPTJavascriptFunction('entrySummary'), + globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('obscure'), + globals.getGPTJavascriptFunction('updateSummary'), + ) + includeSearch = true + break + default: + break + } + if(includeSearch){ + const { tool_resources: gptResources, tools: gptTools, } = mGetGPTResources(globals, 'file_search', vectorstoreId) + tools.push(...gptTools) + tool_resources = gptResources + } + return { + tools, + tool_resources, + } +} +/** + * Retrieves bot types based on team name and MyLife status. + * @modular + * @param {Boolean} isMyLife - Whether request is coming from MyLife Q AVatar + * @param {*} teamName - The team name, defaults to `mDefaultTeam` + * @returns {String[]} - The array of bot types + */ +function mGetBotTypes(isMyLife=false, teamName=mDefaultTeam){ + const team = mTeams + .find(team=>team.name===teamName) + const botTypes = [...mRequiredBotTypes, ...isMyLife ? [] : team?.defaultTypes ?? []] + return botTypes +} +/** + * Retrieves any tools and tool-resources that need to be attached to the specific bot-type. + * @param {Globals} globals - Globals object. + * @param {string} toolName - Name of tool. + * @param {string} vectorstoreId - Vectorstore id. + * @returns {object} - { tools, tool_resources, }. + */ +function mGetGPTResources(globals, toolName, vectorstoreId){ + switch(toolName){ + case 'file_search': + const { tools, tool_resources, } = globals.getGPTFileSearchToolStructure(vectorstoreId) + return { tools, tool_resources, } + default: + throw new Error('tool name not recognized') + } +} +/** + * Initializes the provided BotAgent instance. + * @async + * @module + * @param {BotAgent} BotAgent - The BotAgent to initialize + * @param {Bot[]} bots - The array of bots (empty on init) + * @param {AgentFactory} factory - The factory instance + * @param {LLMServices} llm - The LLM instance + * @returns {void} + */ +async function mInit(BotAgent, bots, factory, llm){ + const { avatarId, vectorstoreId, } = BotAgent + bots.push(...await mInitBots(avatarId, vectorstoreId, factory, llm)) + BotAgent.setActiveBot() +} +/** + * Initializes active bots based upon criteria. + * @param {Guid} avatarId - The Avatar id + * @param {String} vectorstore_id - The Vectorstore id + * @param {AgentFactory} factory - The MyLife factory instance + * @param {LLMServices} llm - The LLM instance + * @returns {Bot[]} - The array of activated and available bots + */ +async function mInitBots(avatarId, vectorstore_id, factory, llm){ + const bots = ( await factory.bots(avatarId) ) + .map(botData=>{ + botData.vectorstore_id = vectorstore_id + botData.object_id = avatarId + return new Bot(botData, factory, llm) + }) + return bots +} +/* exports */ +export default BotAgent \ No newline at end of file diff --git a/inc/js/agents/system/bot-assistant.mjs b/inc/js/agents/system/bot-assistant.mjs deleted file mode 100644 index c0bcf15..0000000 --- a/inc/js/agents/system/bot-assistant.mjs +++ /dev/null @@ -1,2 +0,0 @@ -// creates and manages avatar bot legion -// avatars = personae, secondary personae cannot have certain bots, such as biog-bot diff --git a/inc/js/agents/system/evolution-assistant.mjs b/inc/js/agents/system/evolution-agent.mjs similarity index 80% rename from inc/js/agents/system/evolution-assistant.mjs rename to inc/js/agents/system/evolution-agent.mjs index 424ff84..be7b623 100644 --- a/inc/js/agents/system/evolution-assistant.mjs +++ b/inc/js/agents/system/evolution-agent.mjs @@ -1,5 +1,4 @@ // imports -import { _ } from 'ajv' import { EventEmitter } from 'events' /* module constants */ const _phases = [ @@ -12,12 +11,12 @@ const _phases = [ ] const _defaultPhase = _phases[0] /** - * @class EvolutionAssistant + * @class EvolutionAgent * @extends EventEmitter * Handles the evolutionary process of an avatar, managing its growth and development through various phases, loosely but not solely correlated with timeline. * See notes at end for design principles and notes */ -export class EvolutionAssistant extends EventEmitter { +export class EvolutionAgent extends EventEmitter { #avatar // symbiotic avatar object #contributions = [] // self-managed aray of contributions on behalf of embedded avatar; could postdirectly to avatar when required #phase // create, init, develop, mature, maintain, retire @@ -128,28 +127,28 @@ export class EvolutionAssistant extends EventEmitter { * Advance the phase of the Evolution Assistant. Logic is encapsulated to ensure that the phase is advanced only when appropriate, ergo, not every request _to_ advancePhase() will actually _do_ so. Isolates and privatizes logic to propose _advance_ to next phase. * @module * @emits {evo-agent-phase-change} - Emitted when the phase advances. - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @returns {string} The determined phase. * @todo Implement phase advancement logic for: develop, mature, maintain, retire. */ -async function mAdvancePhase(_evoAgent){ // **note**: treat parameter `_evoAgent` as `read-only` for now +async function mAdvancePhase(evoAgent){ // **note**: treat parameter `evoAgent` as `read-only` for now const _proposal = { // no need to objectify - contributions: _evoAgent.contributions, - phase: _evoAgent.phase, + contributions: evoAgent.contributions, + phase: evoAgent.phase, phaseChange: false } - switch(_evoAgent.phase) { + switch(evoAgent.phase) { case 'create': // initial creation of object, no data yet case 'init': // need initial basic data for categorical descriptions of underlying data object; think of this as the "seed" phase, where questions are as yet unfit nor personalized in any meaningful way to the underlying core human (or data object), so need to feel way around--questions here could really come from embedding db const _formalPhase = 'init' - if(!_evoAgent.categories.length) - return _evoAgent.phase - if(!_evoAgent.contributions.length < 3){ // too low, refresh - const contributionsPromises = mAssessData(_evoAgent) - .map(_category => mGetContribution(_evoAgent, _category, _formalPhase)) // Returns array of promises + if(!evoAgent.categories.length) + return evoAgent.phase + if(!evoAgent.contributions.length < 3){ // too low, refresh + const contributionsPromises = mAssessData(evoAgent) + .map(_category => mGetContribution(evoAgent, _category, _formalPhase)) // Returns array of promises _proposal.contributions = await Promise.all(contributionsPromises) } // alterations sent as proposal to be adopted (or not, albeit no current mechanism to reject) by instantiated evo-agent [only viable caller by module design] - _proposal.phase = (mEvolutionPhaseComplete(_evoAgent,_formalPhase)) + _proposal.phase = (mEvolutionPhaseComplete(evoAgent,_formalPhase)) ? 'init' : 'develop' _proposal.phaseChange = (_proposal.phase !== 'init') @@ -162,50 +161,50 @@ async function mAdvancePhase(_evoAgent){ // **note**: treat parameter `_evoAge case 'retire': // contributions have ceased with request to retire object; would never happen with core, but certainly can with any other spawned object; **note** not deletion or removal at this point, but rather a request to stop contributing to the object, lock it and archive; of course could be rehydrated at any time, but from cold state or colder break default: - // throw new Error(`unknown phase: ${_evoAgent.phase}`) + // throw new Error(`unknown phase: ${evoAgent.phase}`) } return _proposal } /** * Reviews properties of avatar and returns an array of three categories most in need of member Contributions. * @module - * @param {EvolutionAssistant} _evoAgent - The avatar evoAgent whose data requires assessment. + * @param {EvolutionAgent} evoAgent - The avatar evoAgent whose data requires assessment. * @param {number} _numCategories - The number of categories to return. Defaults to 5. minimum 1, maximum 9. * @returns {Array} The top number categories requiring Contributions. */ -function mAssessData(_evoAgent, _numCategories) { +function mAssessData(evoAgent, _numCategories) { const _defaultNumCategories = 5 const _maxNumCategories = 9 return [ - ...mAssessNulls(_evoAgent), - ...mAssessNodes(_evoAgent) + ...mAssessNulls(evoAgent), + ...mAssessNodes(evoAgent) .slice(0, _numCategories || _defaultNumCategories) ] .slice(0, Math.min(_numCategories || _defaultNumCategories, _maxNumCategories)) } /** * Asses nodes for categories to contribute to. - * @param {EvolutionAssistant} _evoAgent + * @param {EvolutionAgent} evoAgent * @returns {Array} The categories to contribute to. */ -function mAssessNodes(_evoAgent){ - return _evoAgent.categories - .filter(_category => _evoAgent?.[mFormatCategory(_category)]) +function mAssessNodes(evoAgent){ + return evoAgent.categories + .filter(_category => evoAgent?.[mFormatCategory(_category)]) .map(_category => mFormatCategory(_category)) - .sort((a, b) => _evoAgent[a].length - _evoAgent[b].length) + .sort((a, b) => evoAgent[a].length - evoAgent[b].length) } -function mAssessNulls(_evoAgent) { - return _evoAgent.categories - .filter(_category => !_evoAgent?.[mFormatCategory(_category)]) +function mAssessNulls(evoAgent) { + return evoAgent.categories + .filter(_category => !evoAgent?.[mFormatCategory(_category)]) .map(_category => mFormatCategory(_category)) .sort(() => Math.random() - 0.5) } /** * Assign listeners to a Contribution object. - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @param {Contribution} _contribution - The Contribution object to assign listeners to. */ -function mAssignContributionListeners(_evoAgent, _contribution) { +function mAssignContributionListeners(evoAgent, _contribution) { // **note**: logging exact text of event for now, but could be more generic _contribution.on( 'on-contribution-new', @@ -229,14 +228,14 @@ function mAssignContributionListeners(_evoAgent, _contribution) { /** * Determines whether the given phase is complete. * @module - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @param {string} _phase - The phase to check for completion. */ -function mEvolutionPhaseComplete(_evoAgent,_phase) { +function mEvolutionPhaseComplete(evoAgent,_phase) { switch (_phase) { case 'init': // if category data nodes exist that have no data, return false - return (_evoAgent.categories) + return (evoAgent.categories) default: // such as `create` return true } @@ -258,13 +257,13 @@ function mFormatCategory(_category) { * Digest a request to generate a new Contribution. * @module * @emits {on-contribution-new} - Emitted when a new Contribution is generated. - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @param {string} _category - The category to process. * @param {string} _phase - The phase to process. * @returns {Contribution} A new Contribution object. */ -async function mGetContribution(_evoAgent, _category, _phase) { - const _avatar = _evoAgent.avatar +async function mGetContribution(evoAgent, _category, _phase) { + const _avatar = evoAgent.avatar _category = mFormatCategory(_category) // Process question and map to `new Contribution` class const _contribution = new (_avatar.factory.contribution)({ @@ -282,7 +281,7 @@ async function mGetContribution(_evoAgent, _category, _phase) { }, responses: [], }) - mAssignContributionListeners(_evoAgent, _contribution) + mAssignContributionListeners(evoAgent, _contribution) return await _contribution.init(_avatar.factory) // fires emitters } /** @@ -290,47 +289,47 @@ async function mGetContribution(_evoAgent, _category, _phase) { * @module * @emits {_emit_text} - Emitted when an object is logged. * @param {string} _emit_text - The text to emit. - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @param {object} _object - The object to log, if not evoAgent. */ -function mLog(_emit_text,_evoAgent,_object) { - if(_emit_text) _evoAgent.emit(_emit_text, _object??_evoAgent) // incumbent upon EvoAgent to incorporate child emissions into self and _then_ emit here +function mLog(_emit_text,evoAgent,_object) { + if(_emit_text) evoAgent.emit(_emit_text, _object??evoAgent) // incumbent upon EvoAgent to incorporate child emissions into self and _then_ emit here } /** * Process a Contribution. First update the Contribution object, determining if the Contribution stage is updated. Then evaluate Evolution phase for completeness and advancement. * @module - * @param {EvolutionAssistant} _evoAgent - `this` Evolution Assistant. + * @param {EvolutionAgent} evoAgent - `this` Evolution Assistant. * @param {Array} _contributions - The contributions array. * @param {object} _current - Contribution object { category, contributionId, message } * @param {object} _proposed - Contribution object { category, contributionId, message } * @returns {object} The updated Contribution instantiation. */ -function mSetContribution(_evoAgent, _current, _proposed) { +function mSetContribution(evoAgent, _current, _proposed) { /* update Contribution */ if(_proposed?.contributionId){ - _evoAgent.contributions + evoAgent.contributions .find(_contribution => _contribution.id === _proposed.contributionId) .update(_proposed) // emits avatar update event } /* evolve phase */ if(_current?.category!==_proposed.category){ - _evoAgent.emit('avatar-change-category', _current, _proposed) + evoAgent.emit('avatar-change-category', _current, _proposed) if(_current?.contributionId){ /* @todo: verify that categories are changing */ - const _currentContribution = _evoAgent.contributions + const _currentContribution = evoAgent.contributions .find(_contribution => _contribution.id === _current.contributionId) console.log('evolution-assistant:mSetContribution():320', _currentContribution.inspect(true)) if(_currentContribution.stage === 'prepared'){ // ready to process // join array and submit for gpt-summarization - mSubmitContribution(_evoAgent, _contributions.responses.join('\n')) + mSubmitContribution(evoAgent, _contributions.responses.join('\n')) // advance phase, write db, emit event } } } } -async function mSubmitContribution(_evoAgent, _contribution) { +async function mSubmitContribution(evoAgent, _contribution) { // emit to avatar => (session?) => datacore _contribution.emit('on-contribution-submitted', _contribution) } // exports -export default EvolutionAssistant \ No newline at end of file +export default EvolutionAgent \ No newline at end of file diff --git a/inc/js/api-functions.mjs b/inc/js/api-functions.mjs index 64a5b6b..2bc235b 100644 --- a/inc/js/api-functions.mjs +++ b/inc/js/api-functions.mjs @@ -148,6 +148,7 @@ async function keyValidation(ctx){ ctx.status = 200 // OK if(ctx.method === 'HEAD') return const { mbr_id } = ctx.state + // @todo - may not reflect data core any longer const memberCore = await ctx.MyLife.datacore(mbr_id) const { updates, interests, birth: memberBirth, birthDate: memberBirthDate, fullName, names, nickname } = memberCore const birth = (Array.isArray(memberBirth) && memberBirth.length) diff --git a/inc/js/core.mjs b/inc/js/core.mjs index b078ae7..be67b86 100644 --- a/inc/js/core.mjs +++ b/inc/js/core.mjs @@ -266,7 +266,7 @@ class MyLife extends Organization { // form=server * @returns {boolean} - Whether or not member is logged in successfully. */ async challengeAccess(memberId, passphrase){ - const challengeSuccessful = await this.factory.challengeAccess(memberId, passphrase) + const challengeSuccessful = await this.#avatar.challengeAccess(memberId, passphrase) return challengeSuccessful } /** @@ -277,7 +277,8 @@ class MyLife extends Organization { // form=server 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) + const core = this.globals.sanitize(await this.factory.datacore(mbr_id)) + return core } /** * Submits and returns the journal or diary entry to MyLife via API. @@ -377,7 +378,7 @@ class MyLife extends Organization { // form=server mbr_id, name: `${ being }_${ title.substring(0,64) }_${ mbr_id }`, } - const savedStory = this.globals.stripCosmosFields(await this.factory.summary(story)) + const savedStory = this.globals.sanitize(await this.factory.summary(story)) return savedStory } /** diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 6b7358d..a8bb469 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -1,5 +1,4 @@ /* imports */ -import oAIAssetAssistant from './agents/system/asset-assistant.mjs' import { upload as apiUpload, } from './api-functions.mjs' @@ -54,8 +53,7 @@ async function bots(ctx){ } else { const { activeBotId, - prunedBots: bots, - mbr_id, + bots, } = avatar ctx.body = { // wrap bots activeBotId, @@ -156,7 +154,7 @@ async function greetings(ctx){ if(validateId?.length) response.messages.push(...await avatar.validateRegistration(validateId)) else - response.messages.push(...await avatar.getGreeting(dynamic)) + response.messages.push(...await avatar.greeting(dynamic)) response.success = response.messages.length > 0 ctx.body = response } @@ -383,9 +381,9 @@ async function team(ctx){ * @param {Koa} ctx - Koa Context object. * @returns {Object[]} - List of team objects. */ -function teams(ctx){ +async function teams(ctx){ const { avatar, } = ctx.state - ctx.body = avatar.teams() + ctx.body = await avatar.teams() } async function updateBotInstructions(ctx){ const { bid, } = ctx.params diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index c3003fa..8a18003 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -1,6 +1,6 @@ // imports import EventEmitter from 'events' -import { Guid } from 'js-guid' // usage = Guid.newGuid().toString() +import { Guid } from 'js-guid' /* constants */ const mAiJsFunctions = { changeTitle: { @@ -206,17 +206,35 @@ const mAiJsFunctions = { } }, } -const mEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ // regex for email validation +const mEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +const mForbiddenCosmosFields = ['$', '_', ' ', '@', '#',] const mGuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i // regex for GUID validation const mOpenAIBotModel = process.env.OPENAI_MODEL_CORE_BOT - ?? 'gpt-4o' // current MyLife OpenAI model for system on-demand and custom bots (personal-avatar may be different) -// module classes + ?? 'gpt-4o' +/** + * Globals class holds all of the sensitive data and functionality. It exists as a singleton. + * @class + * @extends EventEmitter + * @todo - Since traced back to Maht Globals, this could be converted to the VM and hold that code + */ class Globals extends EventEmitter { constructor() { - // essentially this is a coordinating class wrapper that holds all of the sensitive data and functionality; as such, it is a singleton, and should either _be_ the virtual server or instantiated on one at startup super() } /* public functions */ + /** + * Chunk an array into smaller arrays and returns as an Array. + * @param {Array} array - Array to chunk + * @param {number} size - Size of chunks + * @returns {Array} - Array of chunked arrays + */ + chunkArray(array, size) { + const result = [] + for(let i = 0; i < array.length; i += size){ + result.push(array.slice(i, i + size)) + } + return result + } /** * Clears a const array with nod to garbage collection. * @param {Array} a - the array to clear. @@ -295,8 +313,19 @@ class Globals extends EventEmitter { const regex = /^\d+\.\d+\.\d+$/ return typeof version === 'string' && regex.test(version) } - stripCosmosFields(object){ - return Object.fromEntries(Object.entries(object).filter(([k, v]) => !k.startsWith('_'))) + /** + * Sanitize an object by removing forbidden Cosmos fields. + * @param {object} object - Cosmos document to sanitize + * @returns {object} - Sanitized data object + */ + sanitize(object){ + if(!object || typeof object !== 'object') + throw new Error('Parameter requires an object') + const sanitizedData = Object.fromEntries( + Object.entries(object) + .filter(([key, value])=>!mForbiddenCosmosFields.some(char => key.startsWith(char))) + ) + return sanitizedData } sysId(_mbr_id){ if(!typeof _mbr_id==='string' || !_mbr_id.length || !_mbr_id.includes('|')) @@ -309,7 +338,7 @@ class Globals extends EventEmitter { toString(_obj){ return Object.entries(_obj).map(([k, v]) => `${k}: ${v}`).join(', ') } - // getters/setters + /* getters/setters */ get currentOpenAIBotModel(){ return mOpenAIBotModel } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 00fde40..7660f8f 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -1,26 +1,24 @@ import { Marked } from 'marked' import EventEmitter from 'events' -import oAIAssetAssistant from './agents/system/asset-assistant.mjs' -import { EvolutionAssistant } from './agents/system/evolution-assistant.mjs' +import AssetAgent from './agents/system/asset-agent.mjs' +import BotAgent from './agents/system/bot-agent.mjs' +import EvolutionAgent from './agents/system/evolution-agent.mjs' import LLMServices from './mylife-llm-services.mjs' /* module constants */ const mAllowSave = JSON.parse( process.env.MYLIFE_DB_ALLOW_SAVE - ?? false + ?? 'false' ) const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restoration'] -const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE /** - * @class + * @class - Avatar * @extends EventEmitter * @description An avatar is a digital self proxy of Member. Not of the class but of the human themselves - they are a one-to-one representation of the human, but the synthetic version that interopts between member and internet when inside the MyLife platform. The Avatar is the manager of the member experience, and is the primary interface with the AI (aside from when a bot is handling API request, again we are speaking inside the MyLife platform). * @todo - deprecate `factory` getter - * @todo - more efficient management of module constants, should be classes? */ class Avatar extends EventEmitter { - #activeBotId // id of active bot in this.#bots; empty or undefined, then this #assetAgent - #bots = [] + #botAgent #evolver #experienceGenericVariables = { age: undefined, @@ -49,7 +47,8 @@ class Avatar extends EventEmitter { super() // EventEmitter this.#factory = factory this.#llmServices = llmServices - this.#assetAgent = new oAIAssetAssistant(this.#factory, this.globals, this.#llmServices) + this.#assetAgent = new AssetAgent(this.#factory, this.#llmServices) + this.#botAgent = new BotAgent(this.#factory, this.#llmServices) } /* public functions */ /** @@ -61,26 +60,20 @@ class Avatar extends EventEmitter { * @returns {Promise} Promise resolves to this Avatar class instantiation */ async init(){ - await mInit(this.#factory, this.#llmServices, this, this.#bots, this.#assetAgent, this.#vectorstoreId) // mutates and populates + await mInit(this.#factory, this.#llmServices, this, this.#botAgent, this.#assetAgent, this.#vectorstoreId) // mutates and populates /* experience variables */ this.#experienceGenericVariables = mAssignGenericExperienceVariables(this.#experienceGenericVariables, this) - /* llm services */ - this.#llmServices.bot_id = mBot_idOverride && this.isMyLife - ? mBot_idOverride - : this.activeBot.bot_id return this } /** - * Get a bot's properties from Cosmos (or type in .bots). + * Get a Bot instance by id. * @public - * @async - * @param {Guid} id - The bot id - * @returns {Promise} - The bot object from memory + * @param {Guid} botId - The bot id + * @returns {Promise} - The bot object from memory */ - async bot(id){ - const bot = this.bots.find(bot=>bot.id===id) - ?? await this.#factory.bot(id) - return bot + bot(botId){ + const Bot = this.#botAgent.bot(botId) + return Bot } /** * Processes and executes incoming chat request. @@ -96,7 +89,12 @@ class Avatar extends EventEmitter { async chat(message, itemId, shadowId, processStartTime=Date.now(), session=null, thread_id){ if(!message) throw new Error('No message provided in context') - const { bot_id, conversation=this.getConversation(thread_id), id: botId, } = this.activeBot + + + + + const Conversation = this.activeBot.conversation() + // what should I actually return from chat? if(!conversation) throw new Error('No conversation found for bot intelligence and could not be created.') conversation.bot_id = botId @@ -130,14 +128,13 @@ class Avatar extends EventEmitter { console.log('chat::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) /* frontend mutations */ const responses = [] - const { activeBot: bot } = this 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)) + .map(_message=>mPruneMessage(botId, _message, 'chat', processStartTime)) .reverse() .forEach(_message=>responses.push(_message)) if(!responses?.length){ @@ -191,60 +188,26 @@ class Avatar extends EventEmitter { return collections } /** - * Create a new bot. Errors if bot cannot be created. - * @async - * @public - * @param {object} bot - The bot data object, requires type. - * @returns {object} - The new bot. + * Start a new conversation. + * @param {String} type - The type of conversation, defaults to `chat` + * @param {String} form - The form of conversation, defaults to `member-avatar` + * @returns {Promise} - The Conversation instance */ - async createBot(bot){ - /* validate request */ - const { type, } = bot - if(!type) - throw new Error('Bot type required to create') - const singletonBotExists = this.bots - .filter(_bot=>_bot.type===type && !_bot.allowMultiple) // same type, self-declared singleton - .filter(_bot=>_bot.allowedBeings?.includes('avatar')) // avatar allowed to create - .length - if(singletonBotExists) - throw new Error(`Bot type "${type}" already exists and bot-multiples disallowed.`) - /* execute request */ - bot = await mBot(this.#factory, this, bot) - /* respond request */ - const response = mPruneBot(bot) - return response + async conversationStart(type='chat', form='member-avatar'){ + const Conversation = await this.#botAgent.conversationStart(type, form) + return Conversation } /** - * Create a new conversation. + * Create a new bot. * @async * @public - * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, etc.; defaults to `chat` - * @param {string} thread_id - The openai thread id - * @param {string} botId - The bot id - * @returns {Conversation} - The conversation object - */ - async createConversation(type='chat', thread_id, botId=this.activeBotId){ - const mbr_id = this.mbr_id - const thread = await this.#llmServices.thread(thread_id) - const { conversation: previousConversation, type: botType, } = this.isMyLife - ? this.avatar - : await this.bot(botId) - if(!!previousConversation) - throw new Error(`Conversation already exists for bot/thread: ${ botId }/${ thread.id }`) - const form = botType?.split('-').pop() - ?? 'system' - const conversation = new (this.#factory.conversation)( - { - form, - mbr_id, - type, - }, - this.#factory, - thread, - botId - ) - console.log('conversation created', conversation.inspect(true)) - return conversation + * @param {Object} botData - The bot data object, requires type. + * @returns {Object} - The new bot. + */ + async createBot(botData){ + const Bot = await this.#botAgent.botCreate(botData) + const bot = Bot.bot + return bot } /** * Delete an item from member container. @@ -377,14 +340,24 @@ class Avatar extends EventEmitter { ) } /** - * Specified by id, returns the pruned bot from memory. - * @param {Guid} id - The id of the item to get - * @returns {object} - The pruned bot object + * Specified by id, returns the pruned Bot. + * @param {Guid} id - The Bot id + * @returns {object} - The pruned Bot object */ - getBot(id){ - const bot = mPruneBot(this.bots.find(bot=>bot.id===id)) + getBot(botId){ + const bot = this.#botAgent.bot(botId)?.bot return bot } + /** + * Returns the pruned bots for avatar. + * @param {Guid} id - The Bot id + * @returns {Object[]} - The pruned Bot objects + */ + getBots(){ + const bots = this.bots + .map(Bot=>Bot.bot) + return bots + } /** * Gets Conversation object. If no thread id, creates new conversation. * @param {string} thread_id - openai thread id (optional) @@ -409,11 +382,13 @@ class Avatar extends EventEmitter { } /** * Get a static or dynamic greeting from active bot. - * @param {boolean} dynamic - Whether to use LLM for greeting. - * @returns {array} - The greeting message(s) string array in order of display. + * @param {boolean} dynamic - Whether to use LLM for greeting + * @returns {Array} - The greeting message(s) string array in order of display */ - async getGreeting(dynamic=false){ - return await mGreeting(this.activeBot, dynamic, this.#llmServices, this.#factory) + async greeting(dynamic=false){ + console.log('greeting', this.#botAgent.activeBotId) + const greetings = mPruneMessages(this.#botAgent.activeBotId, await this.#botAgent.greeting(dynamic), 'greeting') + return greetings } /** * Request help about MyLife. **caveat** - correct avatar should have been selected prior to calling. @@ -438,7 +413,7 @@ class Avatar extends EventEmitter { conversation.save() else console.log('helpRequest::BYPASS-SAVE', conversation.message.content) - const response = mPruneMessages(this.activeBot, helpResponseArray, 'help', processStartTime) + const response = mPruneMessages(this.activeBotId, helpResponseArray, 'help', processStartTime) return response } /** @@ -550,7 +525,8 @@ class Avatar extends EventEmitter { if(!id) throw new Error(`item does not exist in member container: ${ iid }`) /* develop narration */ - const narration = await mReliveMemoryNarration(this, this.#factory, this.#llmServices, this.biographer, item, memberInput) + const Biographer = this.#botAgent.biographer + const narration = await mReliveMemoryNarration(this, this.#factory, this.#llmServices, Biographer, item, memberInput) return narration // include any required .map() pruning } /** @@ -573,18 +549,18 @@ class Avatar extends EventEmitter { async retireBot(botId){ /* reset active bot, if required */ if(this.activeBotId===botId) - this.activeBotId = null + this.#botAgent.setActiveBot() // avatar cannot be retired const bot = await this.bot(botId) if(!bot) throw new Error(`Bot not found with id: ${ botId }`) const { id, } = bot if(botId!==id) throw new Error(`Bot id mismatch: ${ botId }!=${ id }`) - mDeleteBot(bot, this.#bots, this.#llmServices, this.#factory) + this.#botAgent.botDelete(botId) const response = { instruction: { command: 'removeBot', - botId, + id: botId, }, responses: [{ agent: 'server', @@ -659,7 +635,6 @@ class Avatar extends EventEmitter { throw new Error(`cannot find item: ${ itemId }`) const { form, summary, } = item let tailgate - const bot = this?.[form] ?? this.activeBot /* currently only `biographer` which transforms thusly when referenced here as this[form] */ switch(type){ case 'member': message = `update-memory-request: itemId=${ itemId }\n` + message @@ -681,10 +656,10 @@ class Avatar extends EventEmitter { default: break } - let messages = await mCallLLM(this.#llmServices, bot, message, this.#factory, this) - messages = messages.map(message=>mPruneMessage(bot, message, 'shadow', processingStartTime)) + let messages = await mCallLLM(this.#llmServices, this.activeBot, message, this.#factory, this) + messages = messages.map(message=>mPruneMessage(this.activeBotId, message, 'shadow', processingStartTime)) if(tailgate?.length) - messages.push(mPruneMessage(bot, tailgate, 'system')) + messages.push(mPruneMessage(this.activeBotId, tailgate, 'system')) return messages } /** @@ -706,7 +681,7 @@ class Avatar extends EventEmitter { throw new Error('MyLife avatar cannot summarize files.') if(!fileId?.length && !fileName?.length) throw new Error('File id or name required for summarization.') - const { bot_id, thread_id, } = this.personalAssistant + const { bot_id, id: botId, thread_id, } = this.avatar const prompt = `Summarize this file document: name=${ fileName }, id=${ fileId }` const response = { messages: [], @@ -715,7 +690,7 @@ class Avatar extends EventEmitter { try{ let messages = await mCallLLM(this.#llmServices, { bot_id, thread_id, }, prompt, this.#factory, this) messages = messages - .map(message=>mPruneMessage(this.personalAssistant, message, 'mylife-file-summary', processStartTime)) + .map(message=>mPruneMessage(botId, message, 'mylife-file-summary', processStartTime)) .filter(message=>message && message.role!=='user') if(!messages.length) throw new Error('No valid messages returned from summarization.') @@ -731,30 +706,12 @@ class Avatar extends EventEmitter { } /** * Get a specified team, its details and _instanced_ bots, by id for the member. - * @param {Koa} ctx - Koa Context object + * @param {string} teamId - The team id * @returns {object} - Team object */ - async team(teamId){ - const team = this.#factory.team(teamId) - const { allowedTypes=[], defaultTypes=[], type, } = team - const teamBots = this.bots - .filter(bot=>bot?.teams?.includes(teamId)) - for(const type of defaultTypes){ - let bot = teamBots.find(bot=>bot.type===type) - if(!bot){ - bot = this.bots.find(bot=>bot.type===type) - if(bot){ // local conscription - bot.teams = [...bot?.teams ?? [], teamId,] - await this.updateBot(bot) // save Cosmos no await - } else { // create - const teams = [teamId,] - bot = await this.createBot({ teams, type, }) - } - } else continue // already in team - if(bot) - teamBots.push(bot) - } - team.bots = teamBots + team(teamId){ + this.#botAgent.setActiveTeam(teamId) + const team = this.#botAgent.activeTeam return team } /** @@ -762,18 +719,18 @@ class Avatar extends EventEmitter { * @returns {Object[]} - List of team objects. */ teams(){ - const teams = this.#factory.teams() + const teams = this.#botAgent.teams return teams } /** - * Update a specific bot. **Note**: mBot() updates `this.bots` + * Update a specific bot. * @async - * @param {object} bot - Bot data to set. - * @returns {object} - The updated bot. + * @param {object} botData - Bot data to set + * @returns {Promise} - The updated bot */ - async updateBot(bot){ - const updatedBot = await mBot(this.#factory, this, bot) // **note**: mBot() updates `avatar.bots` - return updatedBot + async updateBot(botData){ + const bot = await this.#botAgent.updateBot(botData) + return bot } /** * Update instructions for bot-assistant based on type. Default updates all LLM pertinent properties. @@ -800,7 +757,7 @@ class Avatar extends EventEmitter { vectorstoreId, } /* save to && refresh bot from Cosmos */ - bot = mSanitize( await this.#factory.updateBot(_bot, options) ) + bot = this.globals.sanitize( await this.#factory.updateBot(_bot, options) ) if(migrateThread && thread_id?.length) await this.migrateChat(thread_id) } @@ -837,10 +794,7 @@ class Avatar extends EventEmitter { * @returns {object} - The active bot. */ get activeBot(){ - return this.#bots.find(bot=>bot.id===this.activeBotId) - } - get activeBotAIId(){ - return this.activeBot.bot_id + return this.#botAgent.activeBot } /** * Get the active bot id. @@ -848,7 +802,7 @@ class Avatar extends EventEmitter { * @returns {string} - The active bot id. */ get activeBotId(){ - return this.#activeBotId + return this.#botAgent.activeBotId } /** * Set the active bot id. If not match found in bot list, then defaults back to this.id (avatar). @@ -858,10 +812,7 @@ class Avatar extends EventEmitter { * @returns {void} */ set activeBotId(botId){ - const newActiveBot = mFindBot(this, botId) - ?? this.avatar - const { id, } = newActiveBot - this.#activeBotId = id + this.#botAgent.setActiveBot(botId) } get activeBotNewestVersion(){ const { type, } = this.activeBot @@ -872,14 +823,6 @@ class Avatar extends EventEmitter { const { version=1.0, } = this.activeBot return version } - /** - * Get actor or default avatar bot. - * @getter - * @returns {object} - The actor bot (or default bot). - */ - get actorBot(){ - return this.#bots.find(_bot=>_bot.type==='actor')??this.avatar - } /** * Get the age of the member. * @getter @@ -901,21 +844,13 @@ class Avatar extends EventEmitter { } return age } - /** - * Returns provider for avatar intelligence. - * @getter - * @returns {object} - The avatar intelligence provider, currently only openAI API GPT. - */ - get ai(){ - return this.#llmServices - } /** * Get the personal avatar bot. * @getter - * @returns {object} - The personal avatar bot. + * @returns {object} - The personal avatar bot */ get avatar(){ - return this.bots.find(_bot=>_bot.type==='personal-avatar') + return this.#botAgent.avatar } /** * Get the "avatar's" being, or more precisely the name of the being (affiliated object) the evatar is emulating. @@ -926,9 +861,6 @@ class Avatar extends EventEmitter { get being(){ return 'human' } - get biographer(){ - return this.#bots.find(_bot=>_bot.type==='personal-biographer') - } /** * Get the birthdate of _member_ from `#factory`. * @getter @@ -950,12 +882,12 @@ class Avatar extends EventEmitter { ?? this.core.birth?.[0]?.place } /** - * Gets all Avatar bots. + * Returns avatar Bot instances. * @getter - * @returns {array} - The bots. + * @returns {Bot[]} - Array of Bot instances */ get bots(){ - return this.#bots + return this.#botAgent.bots } /** * Get the cast members in frontend format. @@ -1007,7 +939,7 @@ class Avatar extends EventEmitter { return this.#evolver } set evolver(evolver){ - if(!(evolver instanceof EvolutionAssistant)) + if(!(evolver instanceof EvolutionAgent)) this.#evolver = evolver } /** @@ -1263,19 +1195,6 @@ class Avatar extends EventEmitter { if(nickname!==this.name) this.#nickname = nickname } - get personalAssistant(){ - return this.avatar - } - /** - * Get a list of available bots (pruned) for the member. - * @getter - * @returns {Object[]} - Array of pruned bot objects - */ - get prunedBots(){ - const bots = this.#bots - .map(bot=>mPruneBot(bot)) - return bots - } /** * Get the `active` reliving memories. * @getter @@ -1311,6 +1230,11 @@ class Avatar extends EventEmitter { this.#vectorstoreId = vectorstoreId /* update local */ } } +/** + * The System Avatar singleton for MyLife. + * @class + * @extends Avatar + */ class Q extends Avatar { #conversations = [] #factory // same reference as Avatar, but wish to keep private from public interface; don't touch my factory, man! @@ -1330,17 +1254,6 @@ class Q extends Avatar { this.llmServices = llmServices } /* overloaded methods */ - /** - * OVERLOADED: 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 - } /** * OVERLOADED: Processes and executes incoming chat request. * @public @@ -1352,21 +1265,32 @@ class Q extends Avatar { * @returns {object} - The response(s) to the chat request */ async chat(message, itemId, shadowId, processStartTime=Date.now(), session){ - let { thread_id, } = session - if(!thread_id?.length){ - const conversation = await this.createConversation('system') - thread_id = conversation.thread_id - this.#conversations.push(conversation) + if(itemId?.length || shadowId?.length) + throw new Error('MyLife System Avatar cannot process chats with `itemId` or `shadowId`.') + let { Conversation, } = session + if(!Conversation){ + const Conversation = await this.conversationStart('chat', 'system-avatar') + thread_id = Conversation.thread_id + this.#conversations.push(Conversation) } - this.activeBot.bot_id = mBot_idOverride - ?? this.activeBot.bot_id - session.thread_id = thread_id // @stub - store elsewhere + session.Conversation = Conversation // @stub - store elsewhere if(this.isValidating) // trigger confirmation until session (or vld) ends message = `CONFIRM REGISTRATION PHASE: registrationId=${ this.registrationId }\n${ message }` if(this.isCreatingAccount) message = `CREATE ACCOUNT PHASE: ${ message }` + + + return super.chat(message, itemId, shadowId, processStartTime, null, thread_id) } + /** + * OVERLOADED: MyLife must refuse to create bots. + * @public + * @throws {Error} - System avatar cannot create bots. + */ + async createBot(){ + throw new Error('System avatar cannot create bots.') + } /** * OVERLOADED: 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 @@ -1375,7 +1299,7 @@ class Q extends Avatar { * @returns {Object} - The obscured item object */ async obscure(mbr_id, iid){ - const botFactory = await this.bot(mbr_id) + const botFactory = await this.avatarProxy(mbr_id) const updatedSummary = await botFactory.obscure(iid) return updatedSummary } @@ -1406,6 +1330,20 @@ class Q extends Avatar { } } } + /** + * Returns the Member Avatar proxy for the member id. + * @param {string} mbr_id - The member id + * @returns {Promise} - The Member Avatar proxy + */ + async avatarProxy(mbr_id){ + const avatar = await this.#factory.avatarProxy(mbr_id) + return avatar + } + async challengeAccess(memberId, passphrase){ + const avatarProxy = await this.avatarProxy(memberId) + const challengeSuccessful = await avatarProxy.challengeAccess(passphrase) + return challengeSuccessful + } /** * Set MyLife core account basics. { birthdate, passphrase, } * @todo - move to mylife agent factory @@ -1503,72 +1441,6 @@ function mAvatarDropdown(globals, avatar){ name, } } -/** - * Validates and cleans bot object then updates or creates bot (defaults to new personal-avatar) in Cosmos and returns successful `bot` object, complete with conversation (including thread/thread_id in avatar) and gpt-assistant intelligence. - * @todo Fix occasions where there will be no object_id property to use, as it was created through a hydration method based on API usage, so will be attached to mbr_id, but NOT avatar.id - * @todo - Turn this into Bot class - * @module - * @param {AgentFactory} factory - Agent Factory object - * @param {Avatar} avatar - Avatar object that will govern bot - * @param {object} bot - Bot object - * @returns {object} - Bot object - */ -async function mBot(factory, avatar, bot){ - /* validation */ - const { id: avatarId, mbr_id, vectorstore_id, } = avatar - const { newGuid, } = factory - const { id: botId=newGuid, object_id: objectId, type: botType, } = bot - if(!botType?.length) - throw new Error('Bot type required to create.') - bot.mbr_id = mbr_id /* constant */ - bot.object_id = objectId - ?? avatarId /* all your bots belong to me */ - bot.id = botId // **note**: _this_ is a Cosmos id, not an openAI id - let originBot = avatar.bots.find(oBot=>oBot.id===botId) - if(originBot){ /* update bot */ - const options = {} - const updatedBot = Object.keys(bot) - .reduce((diff, key) => { - if(bot[key]!==originBot[key]) - diff[key] = bot[key] - return diff - }, {}) - /* create or update bot special properties */ - const { thread_id, type, } = originBot // @stub - `bot_id` cannot be updated through this mechanic - if(!thread_id?.length && !avatar.isMyLife){ - const excludeTypes = ['collection', 'library', 'custom'] // @stub - custom mechanic? - if(!excludeTypes.includes(type)){ - const conversation = avatar.conversation(null, botId) - ?? await avatar.createConversation('chat', null, botId) - updatedBot.thread_id = conversation.thread_id // triggers `factory.updateBot()` - console.log('Avatar::mBot::conversation created given NO thread_id', updatedBot.thread_id, conversation.inspect(true)) - } - } - let updatedOriginBot - if(Object.keys(updatedBot).length){ - updatedOriginBot = {...originBot, ...updatedBot} // consolidated update - const { bot_id, id, } = updatedOriginBot - updatedBot.bot_id = bot_id - updatedBot.id = id - updatedBot.type = type - const { interests, } = updatedBot - /* set options */ - if(interests?.length){ - options.instructions = true - options.model = true - options.tools = false /* tools not updated through this mechanic */ - } - updatedOriginBot = await factory.updateBot(updatedBot, options) - } - originBot = mSanitize(updatedOriginBot ?? originBot) - avatar.bots[avatar.bots.findIndex(oBot=>oBot.id===botId)] = originBot - } else { /* create assistant */ - bot = mSanitize( await factory.createBot(bot, vectorstore_id) ) - avatar.bots.push(bot) - } - return originBot - ?? bot -} /** * Makes call to LLM and to return response(s) to prompt. * @todo - create actor-bot for internal chat? Concern is that API-assistants are only a storage vehicle, ergo not an embedded fine tune as I thought (i.e., there still may be room for new fine-tuning exercise); i.e., micro-instructionsets need to be developed for most. Unclear if direct thread/message instructions override or ADD, could check documentation or gpt, but... @@ -1648,7 +1520,7 @@ async function mCast(factory, cast){ } function mCreateSystemMessage(activeBot, message, factory){ if(!(message instanceof factory.message)){ - const { thread_id, } = activeBot + const { id: botId, thread_id, } = activeBot const content = message?.content ?? message?.message ?? message message = new (factory.message)({ being: 'message', @@ -1658,32 +1530,9 @@ function mCreateSystemMessage(activeBot, message, factory){ type: 'system' }) } - message = mPruneMessage(activeBot, message, 'system') + message = mPruneMessage(botId, message, 'system') return message } -/** - * Deletes the bot requested from avatar memory and from all long-term storage. - * @param {object} bot - The bot object to delete - * @param {Object[]} bots - The bots array - * @param {LLMServices} llm - OpenAI object - * @param {AgentFactory} factory - Agent Factory object - */ -function mDeleteBot(bot, bots, llm, factory){ - const cannotRetire = ['actor', 'system', 'personal-avatar'] - const { bot_id, id, thread_id, type, } = bot - if(cannotRetire.includes(type)) - throw new Error(`Cannot retire bot type: ${ type }`) - /* delete from memory */ - const botId = bots.findIndex(_bot=>_bot.id===id) - if(botId<0) - throw new Error('Bot not found in bots.') - bots.splice(botId, 1) - /* delete bot from Cosmos */ - factory.deleteItem(id) - /* delete thread and bot from OpenAI */ - llm.deleteBot(bot_id) - llm.deleteThread(thread_id) -} /** * Deletes conversation and updates * @param {Conversation} conversation - The conversation object @@ -2175,48 +2024,6 @@ function mFindBot(avatar, id){ .filter(bot=>{ return bot.id==id }) ?.[0] } -/** - * Returns set of Greeting messages, dynamic or static - * @param {object} bot - The bot object - * @param {boolean} dynamic - Whether to use dynamic greetings - * @param {LLMServices} llm - OpenAI object - * @param {AgentFactory} factory - Agent Factory object - * @returns {Promise} - The array of messages to respond with - */ -async function mGreeting(bot, dynamic=false, llm, factory){ - const processStartTime = Date.now() - const { bot_id, bot_name, id, greetings, greeting, thread_id, } = bot - const failGreeting = [`Hello! I'm concerned that there is something wrong with my instruction-set, as I was unable to find my greetings, but let's see if I can get back online.`, `How can I be of help today?`] - const greetingPrompt = factory.isMyLife - ? `Greet this new user with a hearty hello, and let them know that you are here to help them understand MyLife and the MyLife platform. Begin by asking them about something that's important to them--based on their response, explain how MyLife can help them.` - : `Greet me with a hearty hello as we start a new session, and let me know either where we left off, or how we should start for today!` - const QGreetings = [ - `Hi, I'm Q, so nice to meet you!`, - `To get started, tell me a little bit about something or someone that is really important to you — or ask me a question about MyLife.` - ] - const botGreetings = greetings - ? greetings - : greeting - ? [greeting] - : factory.isMyLife - ? QGreetings - : null - let messages = botGreetings?.length && !dynamic - ? botGreetings - : await llm.getLLMResponse(thread_id, bot_id, greetingPrompt, factory) - if(!messages?.length) - messages = failGreeting - messages = messages - .map(message=>new (factory.message)({ - being: 'message', - content: message, - thread_id, - role: 'assistant', - type: 'greeting' - })) - .map(message=>mPruneMessage(bot, message, 'greeting', processStartTime)) - return messages -} /** * Include help preamble to _LLM_ request, not outbound to member/guest. * @todo - expand to include other types of help requests, perhaps more validation. @@ -2244,68 +2051,33 @@ function mHelpIncludePreamble(type, isMyLife){ * @param {MyLifeFactory|AgentFactory} factory - Member Avatar or Q * @param {LLMServices} llmServices - OpenAI object * @param {Q|Avatar} avatar - The avatar Instance (`this`) - * @param {array} bots - The array of bot objects from private class `this.#bots` + * @param {BotAgent} botAgent - BotAgent instance * @param {AssetAgent} assetAgent - AssetAgent instance * @returns {Promise} - Return indicates successfully mutated avatar */ -async function mInit(factory, llmServices, avatar, bots, assetAgent){ - /* get avatar data from cosmos */ - const obj = await factory.avatarProperties() - Object.entries(obj) - .forEach(([key, value])=>{ - if( // exclude certain properties - ['being', 'mbr_id'].includes(key) - || ['$', '_', ' ', '@', '#',].includes(key[0]) - ) - return - avatar[key] = value - }) - const requiredBotTypes = ['personal-avatar',] - if(factory.isMyLife){ // MyLife - avatar.nickname = 'Q' - } else { // Member +async function mInit(factory, llmServices, avatar, botAgent, assetAgent){ + /* initial assignments */ + const { being, mbr_id, ...avatarProperties } = factory.globals.sanitize(await factory.avatarProperties()) + Object.assign(avatar, avatarProperties) + if(!factory.isMyLife){ const { mbr_id, vectorstore_id, } = avatar avatar.nickname = avatar.nickname ?? avatar.names?.[0] - ?? `${avatar.memberFirstName ?? 'member'}'s avatar` - /* vectorstore */ + ?? `${ avatar.memberFirstName ?? 'member' }'s avatar` if(!vectorstore_id){ const vectorstore = await llmServices.createVectorstore(mbr_id) if(vectorstore?.id){ - avatar.vectorstore_id = vectorstore.id // also sets vectorstore_id in Cosmos + avatar.vectorstore_id = vectorstore.id await assetAgent.init(avatar.vectorstore_id) } } - /* bots */ - requiredBotTypes.push('personal-biographer') // default memory team - } - bots.push(...await factory.bots(avatar.id)) - await Promise.all( - requiredBotTypes - .map(async botType=>{ - if(!bots.some(bot=>bot.type===botType)){ // create required bot - const bot = await mBot(factory, avatar, { type: botType }) - bots.push(bot) - } - } - )) - avatar.activeBotId = avatar.avatar.id // initially set active bot to personal-avatar - if(factory.isMyLife) // as far as init goes for MyLife Avatar + } + /* initialize default bots */ + await botAgent.init(avatar.id, avatar.vectorstore_id) + if(factory.isMyLife) return - /* conversations */ - await Promise.all( - bots.map(async bot=>{ - const { id: botId, thread_id, type, } = bot - /* exempt certain types */ - const excludedMemberTypes = ['library', 'ubi'] - if(excludedMemberTypes.includes(type)) - return - const conversation = await avatar.createConversation('chat', thread_id, botId) - bot.conversation = conversation - }) - ) /* evolver */ - avatar.evolver = await (new EvolutionAssistant(avatar)) + avatar.evolver = await (new EvolutionAgent(avatar)) .init() /* lived-experiences */ avatar.experiencesLived = await factory.experiencesLived(false) @@ -2453,37 +2225,13 @@ function mNavigation(scenes){ return (a.order ?? 0) - (b.order ?? 0) }) } -/** - * Returns a frontend-ready bot object. - * @param {object} bot - The bot object. - * @returns {object} - The pruned bot object. - */ -function mPruneBot(bot){ - const { - bot_name: name, - description, - id, - purpose, - type, - version, - } = bot - return { - description, - id, - name, - purpose, - type, - version, - } -} function mPruneConversation(conversation){ - const { bot_id, form, id, name, thread_id, type, } = conversation + const { bot_id, form, id, name, type, } = conversation return { bot_id, form, id, name, - thread_id, type, } } @@ -2513,20 +2261,19 @@ function mPruneItem(item){ * Returns frontend-ready Message object after logic mutation. * @module * @private - * @param {object} bot - The bot object, usually active. - * @param {string} message - The text of LLM message. Can parse array of messages from openAI. - * @param {string} type - The type of message, defaults to chat. - * @param {number} processStartTime - The time the process started, defaults to function call. - * @returns {object} - The bot-included message object. + * @param {Guid} activeBotId - The Active Bot id property + * @param {string} message - The text of LLM message; can parse array of messages from openAI + * @param {string} type - The type of message, defaults to chat + * @param {number} processStartTime - The time the process started, defaults to function call + * @returns {object} - The pruned message object */ -function mPruneMessage(bot, message, type='chat', processStartTime=Date.now()){ +function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date.now()){ /* parse message */ - const { bot_id: activeBotAIId, id: activeBotId, } = bot let agent='server', content='', purpose=type, response_time=Date.now()-processStartTime - const { content: messageContent, thread_id, } = message + const { content: messageContent, } = message const rSource = /【.*?\】/gs const rLines = /\n{2,}/g content = Array.isArray(messageContent) @@ -2542,37 +2289,28 @@ function mPruneMessage(bot, message, type='chat', processStartTime=Date.now()){ message = new Marked().parse(content) const messageResponse = { activeBotId, - activeBotAIId, agent, message, purpose, response_time, - thread_id, type, } return messageResponse } /** - * Flattens an array of messages into a single frontend-consumable message. - * @param {object} bot - The bot object, usually active. - * @param {Object[]} messages - The array of messages to prune. - * @param {string} type - The type of message, defaults to chat. - * @param {number} processStartTime - The time the process started, defaults to function call. - * @returns {object} - Concatenated message object. + * Prune an array of Messages and return. + * @param {Guid} botId - The Active Bot id property + * @param {Object[]} messageArray - The array of messages to prune + * @param {string} type - The type of message, defaults to chat + * @param {number} processStartTime - The time the process started, defaults to function call + * @returns {Object[]} - Concatenated message object */ -function mPruneMessages(bot, messageArray, type='chat', processStartTime=Date.now()){ +function mPruneMessages(botId, messageArray, type='chat', processStartTime=Date.now()){ if(!messageArray.length) throw new Error('No messages to prune') - const prunedMessages = messageArray - .map(message=>mPruneMessage(bot, message, type, processStartTime)) - const messageContent = prunedMessages - .map(message=>message.message) - .join('\n') - const message = { - ...prunedMessages[0], - message: messageContent, - } - return message + messageArray = messageArray + .map(message=>mPruneMessage(botId, message, type, processStartTime)) + return messageArray } /** * Returns a narration packet for a memory reliving. Will allow for and accommodate the incorporation of helpful data _from_ the avatar member into the memory item `summary` and other metadata. The bot by default will: @@ -2584,14 +2322,15 @@ function mPruneMessages(bot, messageArray, type='chat', processStartTime=Date.no * @param {Avatar} avatar - Member's avatar object. * @param {AgentFactory} factory - Member's AgentFactory object. * @param {LLMServices} llm - OpenAI object. - * @param {object} bot - The bot object. + * @param {Bot} Bot - The relevant bot instance * @param {object} item - The memory object. * @param {string} memberInput - The member input (or simply: NEXT, SKIP, etc.) * @returns {Promise} - The reliving memory object for frontend to execute. */ -async function mReliveMemoryNarration(avatar, factory, llm, bot, item, memberInput='NEXT'){ +async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInput='NEXT'){ console.log('mReliveMemoryNarration::start', item.id, memberInput) const { relivingMemories, } = avatar + const { bot, } = Bot const { bot_id, id: botId, } = bot const { id, } = item const processStartTime = Date.now() @@ -2623,7 +2362,7 @@ async function mReliveMemoryNarration(avatar, factory, llm, bot, item, memberInp && message.type==='chat' && message.role!=='user' }) - .map(message=>mPruneMessage(bot, message, 'chat', processStartTime)) + .map(message=>mPruneMessage(botId, message, 'chat', processStartTime)) const memory = { id, messages, @@ -2651,19 +2390,6 @@ function mReplaceVariables(prompt, variableList, variableValues){ }) return prompt } -/** - * Takes an object and removes MyLife database fields unintended for external observance. - * @param {object} obj - Object to sanitize. - * @returns {object} - Sanitized object. - */ -function mSanitize(obj){ - const removalCharacters = ['_', '$'] - for(const key in obj){ - if(removalCharacters.includes(key[0])) - delete obj[key] - } - return obj -} /** * Returns a sanitized event. * @module diff --git a/inc/js/mylife-data-service.js b/inc/js/mylife-dataservices.mjs similarity index 95% rename from inc/js/mylife-data-service.js rename to inc/js/mylife-dataservices.mjs index dad8d86..ba3c7e0 100644 --- a/inc/js/mylife-data-service.js +++ b/inc/js/mylife-dataservices.mjs @@ -288,15 +288,12 @@ class Dataservices { const { id, type, } = bot if(!type?.length) throw new Error('ERROR::createBot::Bot `type` required.') - if(!id?.length) + if(!this.globals.isValidGuid(id)) bot.id = this.globals.newGuid - bot.being = 'bot' // no mods + bot.being = 'bot' /* create bot */ return await this.pushItem(bot) } - async datacore(mbr_id){ - return await this.getItem(mbr_id) - } /** * Delete an item from member container. * @async @@ -447,16 +444,16 @@ class Dataservices { * @async * @public * @param {string} being - * @param {string} _field - Field name to match. - * @param {string} _value - Value to match. + * @param {string} field - Field name to match. + * @param {string} value - Value to match. * @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 object (initial of arrau) matching the query parameters. */ - async getItemByField(being, _field, _value, container_id, _mbr_id=this.mbr_id){ + async getItemByField(being, field, value, container_id, _mbr_id=this.mbr_id){ const _item = await this.getItemByFields( being, - [{ name: `@${_field}`, value: _value }], + [{ name: `@${field}`, value: value }], container_id, _mbr_id, ) @@ -467,15 +464,15 @@ class Dataservices { * @async * @public * @param {string} being - * @param {array} _fields - Array of name/value pairs to select, format: [{name: `@${_field}`, value: _value}], + * @param {array} fields - Array of name/value pairs to select, format: [{name: `@${field}`, value: value}], * @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 object (initial of arrau) matching the query parameters. */ - async getItemByFields(being, _fields, container_id, _mbr_id=this.mbr_id){ + async getItemByFields(being, fields, container_id, _mbr_id=this.mbr_id){ const _items = await this.getItemsByFields( being, - _fields, + fields, container_id, _mbr_id, ) @@ -526,16 +523,16 @@ class Dataservices { * @async * @public * @param {string} being - The type of items to retrieve. - * @param {array} _fields - Array of name/value pairs to select, format: [{name: `@${_field}`, value: _value}], + * @param {array} fields - Array of name/value pairs to select, format: [{name: `@${field}`, value: value}], * @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 getItemsByFields(being, _fields, container_id, _mbr_id=this.mbr_id){ + async getItemsByFields(being, fields, container_id, _mbr_id=this.mbr_id){ const _items = await this.getItems( being, undefined, - _fields, + fields, container_id, _mbr_id, ) @@ -652,15 +649,22 @@ class Dataservices { return await this.datamanager.testPartitionKey(mbr_id) } /** - * Sets a bot in the database. Performs logic to reduce the bot to the minimum required data, as Mongo/Cosmos has a limitation of 10 patch items in one batch array. - * @param {object} bot - The bot object to set. - * @returns {object} - The bot object. + * Sets a bot in the database. + * @param {object} botData - The bot data to update + * @returns {object} - The bot document */ - async updateBot(bot){ - const { id, type: discardType, ...botUpdates } = bot - if(!Object.keys(botUpdates)) - return bot - return await this.patch(bot.id, botUpdates) + async updateBot(botData){ + const { id, type: discardType, ...updateBotData } = botData + if(!Object.keys(updateBotData)) + return botData + const chunks = this.globals.chunkArray(updateKeys, 10) + for(const chunk of chunks){ + const chunkData = {} + chunk.forEach(key=>(chunkData[key] = updateBotData[key])) + const patchedData = await this.patch(id, chunkData) + console.log('updateBot()::patch', patchedData) + } + return botData } /** * Returns the registration record by Id. diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-factory.mjs similarity index 74% rename from inc/js/mylife-agent-factory.mjs rename to inc/js/mylife-factory.mjs index 6f37e2b..4cf6047 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -5,7 +5,7 @@ import vm from 'vm' import util from 'util' import { Guid } from 'js-guid' // usage = Guid.newGuid().toString() import { Avatar, Q, } from './mylife-avatar.mjs' -import Dataservices from './mylife-data-service.js' +import Dataservices from './mylife-dataservices.mjs' import { Member, MyLife } from './core.mjs' import { extendClass_consent, @@ -42,78 +42,6 @@ const mExcludeProperties = { } const mGeneralBotId = 'asst_yhX5mohHmZTXNIH55FX2BR1m' const mLLMServices = new LLMServices() -const mMyLifeTeams = [ - { - active: false, - allowCustom: true, - allowedTypes: ['artworks', 'editor', 'idea', 'marketing'], - defaultTypes: ['artworks', 'idea',], - description: 'The Creative Team is dedicated to help you experience productive creativity sessions.', - id: '84aa50ca-fb64-43d8-b140-31d2373f3cd2', - name: 'creative', - title: 'Creative', - }, - { - active: false, - allowCustom: false, - allowedTypes: ['fitness', 'health', 'insurance', 'medical', 'prescriptions', 'yoga', 'nutrition',], - defaultTypes: ['fitness', 'health', 'medical',], - description: 'The Health Team is dedicated to help you manage your health and wellness.', - id: '238da931-4c25-4868-928f-5ad1087a990b', - name: 'health', - title: 'Health', - }, - { - active: true, - allowCustom: true, - allowedTypes: ['diary', 'journaler', 'personal-biographer',], - defaultTypes: ['personal-biographer',], - description: 'The Memory Team is dedicated to help you document your life stories, experiences, thoughts, and feelings.', - id: 'a261651e-51b3-44ec-a081-a8283b70369d', - name: 'memory', - title: 'Memory', - }, - { - active: false, - allowCustom: true, - allowedTypes: ['personal-assistant', 'idea', 'note', 'resume', 'scheduler', 'task',], - defaultTypes: ['personal-assistant', 'resume', 'scheduler',], - description: 'The Professional Team is dedicated to your professional success.', - id: '5b7c4109-4985-4d98-b59b-e2c821c3ea28', - name: 'professional', - title: 'Professional', - }, - { - active: false, - allowCustom: true, - allowedTypes: ['connection', 'experience', 'social', 'relationship',], - defaultTypes: ['connection', 'relationship',], - description: 'The Social Team is dedicated to help you connect with others.', - id: 'e8b1f6d0-8a3b-4f9b-9e6a-4e3c5b7e3e9f', - name: 'social', - title: 'Social', - }, - { - active: false, - allowCustom: true, - allowedTypes: ['note', 'poem', 'quote', 'religion',], - defaultTypes: ['quote', 'religion',], - description: 'The Spirituality Team is dedicated to help you creatively explore your spiritual side.', - id: 'bea7bb4a-a339-4026-ad1c-75f604dc3349', - name: 'sprituality', - title: 'Spirituality', - }, - { - active: false, - allowCustom: true, - allowedTypes: ['data-ownership', 'investment', 'ubi',], - defaultTypes: ['ubi'], - description: 'The Universal Basic Income (UBI) Team is dedicated to helping you tailor your MyLife revenue streams based upon consensual access to your personal MyLife data.', - id: '8a4d7340-ac62-40f1-8c77-f17c68797925', - name: 'ubi', - title: 'UBI', - } -] const mNewGuid = ()=>Guid.newGuid().toString() const mPath = './inc/json-schemas' const mReservedJSCharacters = [' ', '-', '!', '@', '#', '%', '^', '&', '*', '(', ')', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', "'", '<', '>', ',', '.', '?', '/', '~', '`'] @@ -242,26 +170,16 @@ class BotFactory extends EventEmitter{ return this } /** - * Get a bot, either by id (when known) or bot-type (default=mDefaultBotType). If bot id is not found, then it cascades to the first entity of bot-type it finds, and if none found, and rules allow [in instances where there may be a violation of allowability], a new bot is created. + * Get a bot, either by id (when known) or bot-type (default=mDefaultBotType). If bot id is not found, then it cascades to the first entity of bot-type it finds. * If caller is `MyLife` then bot is found or created and then activated via a micro-hydration. * @todo - determine if spotlight-bot is required for any animation, or a micro-hydrated bot is sufficient. * @public - * @param {string} id - The bot id. - * @param {string} type - The bot type. - * @param {string} mbr_id - The member id. + * @param {string} id - The bot id + * @param {string} type - The bot type + * @param {string} mbr_id - The member id * @returns {object} - The bot. */ async bot(id, type=mDefaultBotType, mbr_id){ - if(this.isMyLife){ - if(!mbr_id) - throw new Error('mbr_id required for BotFactory hydration') - 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 }) - return botFactory - } return ( await this.dataservices.getItem(id) ) ?? ( await this.dataservices.getItemByField( 'bot', @@ -270,7 +188,6 @@ class BotFactory extends EventEmitter{ undefined, mbr_id ) ) - ?? ( await this.bots(undefined,type)?.[0] ) } /** * Returns bot instruction set. @@ -291,16 +208,16 @@ class BotFactory extends EventEmitter{ ?? 1.0 } /** - * Gets a member's bots. + * Gets a member's bots, or specific bot types. * @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} avatarId - The Avatarm id + * @param {string} botType - The bot type (optional) + * @returns {Object[]} - Array of bots */ - async bots(object_id, botType){ - const _params = object_id?.length - ? [{ name: '@object_id', value:object_id }] + async bots(avatarId, botType){ + const _params = avatarId?.length + ? [{ name: '@object_id', value: avatarId }] : botType?.length ? [{ name: '@bot_type', value: botType }] : undefined @@ -332,15 +249,12 @@ class BotFactory extends EventEmitter{ return await this.dataservices.collections(type) } /** - * - * @param {object} botData - The assistant data. - * @param {string} vectorstoreId - The vectorstore id. - * @returns {object} - The created bot. + * Creates a bot in the database. + * @param {object} botData - The bot data + * @returns {object} - The created bot document */ - async createBot(botData={ type: mDefaultBotType }, vectorstoreId){ - const bot = await mCreateBot(this.#llmServices, this, botData, vectorstoreId) - if(!bot) - throw new Error('bot creation failed') + async createBot(botData){ + const bot = await this.#dataservices.createBot(botData) return bot } /** @@ -468,35 +382,13 @@ class BotFactory extends EventEmitter{ ) } /** - * Gets a MyLife Team by id. - * @param {Guid} teamId - The Team id - * @returns {object} - The Team + * Updates bot data in the database. + * @param {object} botData - The bot data to update + * @returns {Promise} - the bot document from the database */ - team(teamId){ - let team = mMyLifeTeams - .find(team=>team.id===teamId) - team = mTeam(team) - return team - } - /** - * Retrieves list of MyLife Teams. - * @returns {object[]} - The array of MyLife Teams - */ - teams(){ - const teams = mMyLifeTeams - .filter(team=>team.active ?? false) - .map(team=>mTeam(team)) - return teams - } - /** - * Adds or updates a bot data in MyLife database. Note that when creating, pre-fill id. - * @public - * @param {object} bot - The bot data. - * @param {object} options - Function options: `{ instructions: boolean, model: boolean, tools: boolean, vectorstoreId: string, }`. - * @returns {object} - The Cosmos bot version. - */ - async updateBot(bot, options={}){ - return await mUpdateBot(this, this.#llmServices, bot, options) + async updateBot(botData){ + const bot = await this.#dataservices.updateBot(botData) + return bot } /* getters/setters */ /** @@ -631,16 +523,6 @@ class AgentFactory extends BotFactory { const response = await this.dataservices.pushItem(item) return response } - async datacore(mbr_id){ - const core = await mDataservices.getItems( - 'core', - undefined, - undefined, - undefined, - mbr_id, - ) - return core?.[0]??{} - } /** * Delete an item from member container. * @param {Guid} id - The id of the item to delete. @@ -925,15 +807,15 @@ class MyLifeFactory extends AgentFactory { } // no init() for MyLife server /* public functions */ /** - * Overload for MyLifeFactory::bot() - Q is able to hydrate a bot instance on behalf of members. + * MyLife factory is able to hydrate a BotFactory instance of a Member Avatar. * @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) + async avatarProxy(mbr_id){ + const Bot = await new BotFactory(mbr_id) .init() - return bot + return Bot } /** * Accesses Dataservices to challenge access to a member's account. @@ -944,7 +826,7 @@ class MyLifeFactory extends AgentFactory { */ async challengeAccess(mbr_id, passphrase){ const caseInsensitive = true // MyLife server defaults to case-insensitive - const avatarProxy = await this.bot(mbr_id) + const avatarProxy = await this.avatarProxy(mbr_id) const challengeSuccessful = await avatarProxy.challengeAccess(passphrase, caseInsensitive) return challengeSuccessful } @@ -1028,6 +910,16 @@ class MyLifeFactory extends AgentFactory { createItem(){ throw new Error('MyLife server cannot create items') } + /** + * + * @param {string} mbr_id - The member id + * @returns {object} - The member's core data + */ + async datacore(mbr_id){ + const core = ( await mDataservices.getItems('core', null, null, null, mbr_id) ) + ?.[0] + return core + } deleteItem(){ throw new Error('MyLife server cannot delete items') } @@ -1111,20 +1003,6 @@ class MyLifeFactory extends AgentFactory { } } // private module functions -/** - * Initializes openAI assistant and returns associated `assistant` object. - * @module - * @param {LLMServices} llmServices - OpenAI object - * @param {object} botData - The bot data object - * @returns {object} - [OpenAI assistant object](https://platform.openai.com/docs/api-reference/assistants/object) - */ -async function mAI_openai(llmServices, botData){ - const { bot_name, type, } = botData - botData.name = bot_name - ?? `_member_${ type }` - const bot = await llmServices.createBot(botData) - return bot -} function assignClassPropertyValues(propertyDefinition){ switch (true) { case propertyDefinition?.const!==undefined: // constants @@ -1180,191 +1058,6 @@ async function mConfigureSchemaPrototypes(){ // add required functionality as de mSchemas[_className] = mExtendClass(mSchemas[_className]) } } -/** - * Creates bot and returns associated `bot` object. - * @module - * @private - * @param {LLMServices} llm - OpenAI object - * @param {AgentFactory} factory - Agent Factory object - * @param {object} botData - Bot object - * @param {string} avatarId - Avatar id - * @returns {string} - Bot assistant id in openAI -*/ -async function mCreateBotLLM(llm, botData){ - const { id, thread_id, } = await mAI_openai(llm, botData) - return { - id, - thread_id, - } -} -/** - * Creates bot and returns associated `bot` object. - * @todo - botData.name = botDbName should not be required, push logic to `llm-services` - * @module - * @async - * @private - * @param {LLMServices} llm - LLMServices Object contains methods for interacting with OpenAI - * @param {BotFactory} factory - BotFactory object - * @param {object} bot - Bot object, must include `type` property. - * @returns {object} - Bot object -*/ -async function mCreateBot(llm, factory, bot, vectorstoreId){ - /* initial deconstructions */ - const { - bot_name: botName, - description: botDescription, - name: botDbName, - type, - } = bot - const { avatarId, } = factory - /* validation */ - if(!avatarId) - throw new Error('avatar id required to create bot') - /* constants */ - const bot_name = botName - ?? `My ${ type }` - const description = botDescription - ?? `I am a ${ type } for ${ factory.memberName }` - const { instructions, version, } = mCreateBotInstructions(factory, bot) - const model = process.env.OPENAI_MODEL_CORE_BOT - ?? process.env.OPENAI_MODEL_CORE_AVATAR - ?? 'gpt-4o' - const name = botDbName - ?? `bot_${ type }_${ avatarId }` - const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) - const id = factory.newGuid - const botData = { - being: 'bot', - bot_name, - description, - id, - instructions, - metadata: { - externalId: id, - version: version.toString(), - }, - model, - name, - object_id: avatarId, - provider: 'openai', - purpose: description, - tools, - tool_resources, - type, - version, - } - /* create in LLM */ - const { id: botId, thread_id, } = await mCreateBotLLM(llm, botData) // create after as require model - if(!botId) - throw new Error('bot creation failed') - /* create in MyLife datastore */ - botData.bot_id = botId - botData.thread_id = thread_id - const assistant = await factory.dataservices.createBot(botData) - console.log(chalk.green(`bot created::${ type }`), assistant.thread_id, assistant.id, assistant.bot_id, assistant.bot_name ) - return assistant -} -/** - * Returns MyLife-version of bot instructions. - * @module - * @private - * @param {BotFactory} factory - Factory object - * @param {object} bot - Bot object - * @returns {object} - minor - */ -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, - 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=[], - suffix='', // example: data privacy info - voice='', - } = instructions - /* compile instructions */ - switch(type){ - case 'diary': - instructions = purpose - + preamble - + prefix - + general - + suffix - + voice - break - case 'personal-avatar': - instructions = preamble - + general - break - case 'journaler': - case 'personal-biographer': - instructions = preamble - + purpose - + prefix - + general - break - default: - instructions = general - break - } - /* apply replacements */ - replacements.forEach(replacement=>{ - const placeholderRegExp = factory.globals.getRegExp(replacement.name, true) - const replacementText = eval(`bot?.${replacement.replacement}`) - ?? eval(`factory?.${replacement.replacement}`) - ?? eval(`factory.core?.${replacement.replacement}`) - ?? replacement?.default - ?? '`unknown-value`' - instructions = instructions.replace(placeholderRegExp, _=>replacementText) - }) - /* apply references */ - references.forEach(_reference=>{ - const _referenceText = _reference.insert - const replacementText = eval(`factory?.${_reference.value}`) - ?? eval(`bot?.${_reference.value}`) - ?? _reference.default - ?? '`unknown-value`' - switch(_reference.method ?? 'replace'){ - case 'append-hard': - const _indexHard = instructions.indexOf(_referenceText) - if (_indexHard !== -1) { - instructions = - instructions.slice(0, _indexHard + _referenceText.length) - + '\n' - + replacementText - + instructions.slice(_indexHard + _referenceText.length) - } - break - case 'append-soft': - const _indexSoft = instructions.indexOf(_referenceText); - if (_indexSoft !== -1) { - instructions = - instructions.slice(0, _indexSoft + _referenceText.length) - + ' ' - + replacementText - + instructions.slice(_indexSoft + _referenceText.length) - } - break - case 'replace': - default: - instructions = instructions.replace(_referenceText, replacementText) - break - } - }) - /* assess and validate limit */ - return { instructions, version, } -} function mExposedSchemas(factoryBlockedSchemas){ const _systemBlockedSchemas = ['dataservices','session'] return Object.keys(mSchemas) @@ -1488,78 +1181,6 @@ function mGenerateClassFromSchema(_schema) { const _class = mCompileClass(name, _classCode) return _class } -/** - * Retrieves any functions that need to be attached to the specific bot-type. - * @todo - Move to llmServices and improve - * @param {string} type - Type of bot. - * @param {object} globals - Global functions for bot. - * @param {string} vectorstoreId - Vectorstore id. - * @returns {object} - OpenAI-ready object for functions { tools, tool_resources, }. - */ -function mGetAIFunctions(type, globals, vectorstoreId){ - let includeSearch=false, - tool_resources, - tools = [] - switch(type){ - case 'assistant': - case 'avatar': - case 'personal-assistant': - case 'personal-avatar': - includeSearch = true - break - case 'biographer': - case 'personal-biographer': - tools.push( - globals.getGPTJavascriptFunction('changeTitle'), - globals.getGPTJavascriptFunction('getSummary'), - globals.getGPTJavascriptFunction('storySummary'), - globals.getGPTJavascriptFunction('updateSummary'), - ) - includeSearch = true - break - case 'custom': - includeSearch = true - break - case 'diary': - case 'journaler': - tools.push( - globals.getGPTJavascriptFunction('changeTitle'), - globals.getGPTJavascriptFunction('entrySummary'), - globals.getGPTJavascriptFunction('getSummary'), - globals.getGPTJavascriptFunction('obscure'), - globals.getGPTJavascriptFunction('updateSummary'), - ) - includeSearch = true - break - default: - break - } - if(includeSearch){ - const { tool_resources: gptResources, tools: gptTools, } = mGetGPTResources(globals, 'file_search', vectorstoreId) - tools.push(...gptTools) - tool_resources = gptResources - } - return { - tools, - tool_resources, - } -} -/** - * Retrieves any tools and tool-resources that need to be attached to the specific bot-type. - * @param {Globals} globals - Globals object. - * @param {string} toolName - Name of tool. - * @param {string} vectorstoreId - Vectorstore id. - * @returns {object} - { tools, tool_resources, }. - */ -function mGetGPTResources(globals, toolName, vectorstoreId){ - switch(toolName){ - case 'file_search': - const { tools, tool_resources, } = globals.getGPTFileSearchToolStructure(vectorstoreId) - return { tools, tool_resources, } - default: - throw new Error('tool name not recognized') - } -} /** * Take help request about MyLife and consults appropriate engine for response. * @requires mLLMServices - equivalent of default MyLife dataservices/factory @@ -1774,59 +1395,6 @@ function mTeam(team){ title, } } -/** - * Updates bot in Cosmos, and if necessary, in LLM. - * @param {AgentFactory} factory - Factory object - * @param {LLMServices} llm - LLMServices object - * @param {object} bot - Bot object, winnow via mBot in `mylife-avatar.mjs` to only updated fields - * @param {object} options - Options object: { instructions: boolean, model: boolean, tools: boolean, vectorstoreId: string, } - * @returns - */ -async function mUpdateBot(factory, llm, bot, options={}){ - /* constants */ - const { - id, // no modifications - instructions: removeInstructions, - tools: removeTools, - tool_resources: removeResources, - type, // no modifications - ...botData // extract member-driven bot data - } = bot - const { - instructions: updateInstructions=false, - model: updateModel=false, - tools: updateTools=false, - vectorstoreId, - } = options - if(!factory.globals.isValidGuid(id)) - throw new Error('bot `id` required in bot argument: `{ id: guid }`') - if(updateInstructions){ - const { instructions, version=1.0, } = mCreateBotInstructions(factory, bot) - botData.instructions = instructions - botData.metadata = botData.metadata ?? {} - botData.metadata.version = version.toString() - botData.version = version /* omitted from llm, but appears on updateBot */ - } - if(updateTools){ - const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) - botData.tools = tools - botData.tool_resources = tool_resources - } - if(updateModel) - botData.model = factory.globals.currentOpenAIBotModel - botData.id = id // validated - /* LLM updates */ - const { bot_id, bot_name: name, instructions, tools, } = botData - if(bot_id?.length && (instructions || name || tools)){ - botData.model = factory.globals.currentOpenAIBotModel // not dynamic - await llm.updateBot(botData) - const updatedLLMFields = Object.keys(botData) - .filter(key=>key!=='id' && key!=='bot_id') // strip mechanicals - console.log(chalk.green('mUpdateBot()::update in OpenAI'), id, bot_id, updatedLLMFields) - } - const updatedBot = await factory.dataservices.updateBot(botData) - return updatedBot -} /* final constructs relying on class and functions */ // server build: injects default factory into _server_ **MyLife** instance const _MyLife = await new MyLife( diff --git a/server.js b/server.js index d556193..e900c3a 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ import serve from 'koa-static' /* misc imports */ import chalk from 'chalk' /* local service imports */ -import MyLife from './inc/js/mylife-agent-factory.mjs' +import MyLife from './inc/js/mylife-factory.mjs' /** variables **/ const version = '0.0.25' const app = new Koa() diff --git a/views/README.md b/views/README.md index 93deeb6..52fc9dd 100644 --- a/views/README.md +++ b/views/README.md @@ -162,7 +162,7 @@ MyLife itself is an open-source project and, aside from LLM technologies at the 3. **Bot Functionality and Intelligence Management** - The application features a sophisticated bot system, capable of creating and managing different types of bots like personal assistants, biographers, health bots, etc. - - OpenAI's GPT-3 model is integrated for generating responses and interacting with users through bots, as observed in the `class-avatar-functions.mjs` and `mylife-agent-factory.mjs` files. + - OpenAI's GPT-3 model is integrated for generating responses and interacting with users through bots, as observed in the `mylife-avatar.mjs` and `mylife-factory.mjs` files. 4. **Session Management** - Managed through the `MylifeMemberSession` class, handling user sessions, consents, and alerts. From c7444ac02fb4d7642294f50d92037bfb9ff0ce2a Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Tue, 22 Oct 2024 23:16:31 -0400 Subject: [PATCH 37/54] 20241022 @Mookse - fixed activeBot default --- inc/js/agents/system/bot-agent.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index a563419..0d3ce1b 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -225,7 +225,7 @@ class BotAgent { * @param {Guid} botId - The Bot id * @returns {void} */ - setActiveBot(botId){ + setActiveBot(botId=this.avatar?.id){ const Bot = this.#bots.find(bot=>bot.id===botId) if(Bot) this.#activeBot = Bot @@ -278,7 +278,8 @@ class BotAgent { * @returns {Guid} - The active bot id */ get activeBotId(){ - return this.#activeBot.id + console.log('BotAgent::activeBotId', this.#activeBot, this) + return this.#activeBot?.id } /** * Gets the primary avatar for Member. @@ -286,8 +287,8 @@ class BotAgent { * @returns {Bot} - The primary avatar Bot instance */ get avatar(){ - const bot = this.#bots.find(Bot=>Bot.isAvatar===true) - return bot + const Bot = this.#bots.find(Bot=>Bot.isAvatar===true) + return Bot } /** * Gets the Avatar id for whom this BotAgent is conscripted. @@ -832,6 +833,7 @@ async function mInit(BotAgent, bots, factory, llm){ const { avatarId, vectorstoreId, } = BotAgent bots.push(...await mInitBots(avatarId, vectorstoreId, factory, llm)) BotAgent.setActiveBot() + console.log('BotAgent::init', BotAgent.activeBot) } /** * Initializes active bots based upon criteria. From cf7400f52714ebbf1fe84fcde965cdd79357765b Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 00:38:03 -0400 Subject: [PATCH 38/54] 20241023 @Mookse - `chat()` - remove `shadowId` parameter - remove Contribution - wip unstable --- inc/js/agents/system/bot-agent.mjs | 114 +++++++-- .../class-contribution-functions.mjs | 0 .../class-extenders.mjs | 203 +++++++--------- inc/js/functions.mjs | 11 +- inc/js/mylife-avatar.mjs | 219 +++++------------- inc/js/mylife-factory.mjs | 2 - inc/js/mylife-llm-services.mjs | 37 +-- views/assets/js/members.mjs | 3 +- 8 files changed, 272 insertions(+), 317 deletions(-) rename inc/js/{factory-class-extenders => deprecated}/class-contribution-functions.mjs (100%) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 0d3ce1b..f6a6f31 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -1,3 +1,5 @@ +import LLMServices from "../../mylife-llm-services.mjs" + /* module constants */ const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE const mDefaultBotTypeArray = ['personal-avatar', 'avatar'] @@ -37,11 +39,31 @@ class Bot { throw new Error('Bot database id required') } /* public functions */ - async chat(){ - console.log('Bot::chat', this.#conversation) - // what should be returned? Responses? Conversation object? - return this.#conversation + /** + * Chat with the active bot. + * @param {String} message - The member request + * @param {String} originalMessage - The original message + * @param {Boolean} allowSave - Whether to save the conversation, defaults to `true` + * @param {Number} processStartTime - The process start time + * @returns {Promise} - The Conversation instance updated with the chat exchange + */ + async chat(message, originalMessage, allowSave=true, processStartTime=Date.now()){ + if(this.isMyLife && !this.isAvatar) + throw new Error('Only Q, MyLife Corporate Intelligence, is available for non-member conversation.') + const { bot_id, id, thread_id, type, } = this + if(!this.#conversation) + this.#conversation = await mConversationStart('chat', type, id, thread_id, bot_id, this.#llm, this.#factory) + const Conversation = this.#conversation + Conversation.prompt = message + Conversation.originalPrompt = originalMessage + await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, this) // mutates Conversation + /* frontend mutations */ + return Conversation } + /** + * Retrieves `this` Bot instance. + * @returns {Bot} - The Bot instance + */ getBot(){ return this } @@ -190,8 +212,19 @@ class BotAgent { throw new Error(`Bot not found with id: ${ botId }`) await mBotDelete(Bot, this.#bots, this.#llm, this.#factory) } - async chat(){ - return this.activeBot.chat() + /** + * Chat with the active bot. + * @param {Conversation} Conversation - The Conversation instance + * @param {Boolean} allowSave - Whether to save the conversation, defaults to `true` + * @param {Q} q - The avatar id + * @returns {Promise} - The Conversation instance + */ + async chat(Conversation, allowSave=true, q){ + if(!Conversation) + throw new Error('Conversation instance required') + Conversation.processingStartTime + await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, q) // mutates Conversation + return Conversation } /** * Initializes a conversation, currently only requested by System Avatar, but theoretically could be requested by any externally-facing Member Avatar as well. **note**: not in Q because it does not have a #botAgent yet. @@ -200,8 +233,9 @@ class BotAgent { * @returns {Promise} - The conversation object */ async conversationStart(type='chat', form='system-avatar'){ - const { bot_id, } = this.avatar - const Conversation = await mConversationStart(type, form, null, bot_id, this.#llm, this.#factory) + const { bot_id: llm_id, id: bot_id, } = this.avatar + const Conversation = await mConversationStart(type, form, bot_id, null, llm_id, this.#llm, this.#factory) + console.log('BotAgent::conversationStart', Conversation.thread_id, Conversation.bot_id, Conversation.llm_id, Conversation.inspect(true)) return Conversation } /** @@ -278,7 +312,6 @@ class BotAgent { * @returns {Guid} - The active bot id */ get activeBotId(){ - console.log('BotAgent::activeBotId', this.#activeBot, this) return this.#activeBot?.id } /** @@ -697,9 +730,12 @@ async function mBotUpdate(factory, llm, bot, options={}){ botData.model = factory.globals.currentOpenAIBotModel botData.id = id // validated /* LLM updates */ - const { bot_id, bot_name: name, instructions, tools, } = botData - if(bot_id?.length && (instructions || name || tools)){ + const { bot_id, bot_name: name, instructions, llm_id, tools, } = botData + const llmId = llm_id + ?? bot_id + if(llmId?.length && (instructions || name || tools)){ botData.model = factory.globals.currentOpenAIBotModel // not dynamic + botData.llm_id = llmId await llm.updateBot(botData) const updatedLLMFields = Object.keys(botData) .filter(key=>key!=='id' && key!=='bot_id') // strip mechanicals @@ -708,6 +744,48 @@ async function mBotUpdate(factory, llm, bot, options={}){ const updatedBotData = await factory.updateBot(botData) return updatedBotData } +/** + * Sends Conversation instance with prompts for LLM to process, updating the Conversation instance before returning `void`. + * @todo - create actor-bot for internal chat? Concern is that API-assistants are only a storage vehicle, ergo not an embedded fine tune as I thought (i.e., there still may be room for new fine-tuning exercise); i.e., micro-instructionsets need to be developed for most. Unclear if direct thread/message instructions override or ADD, could check documentation or gpt, but... + * @todo - would dynamic event dialog be handled more effectively with a callback routine function, I think so, and would still allow for avatar to vet, etc. + * @module + * @param {Conversation} Conversation - Conversation instance + * @param {boolean} allowSave - Whether to save the conversation, defaults to `true` + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object required for function execution + * @param {object} avatar - Avatar object + * @returns {Promise} - Alters Conversation instance by nature + */ +async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ + const { llm_id, originalPrompt, processingStartTime=Date.now(), prompt, thread_id, } = Conversation + if(!llm_id?.length) + throw new Error('No `llm_id` intelligence id found in Conversation for `mCallLLM`.') + if(!thread_id?.length) + throw new Error('No `thread_id` found in Conversation for `mCallLLM`.') + if(!prompt?.length) + throw new Error('No `prompt` found in Conversation for `mCallLLM`.') + const botResponses = await llm.getLLMResponse(thread_id, llm_id, prompt, factory, avatar) + const run_id = botResponses?.[0]?.run_id + if(!run_id?.length) + return + Conversation.addRun(run_id) + botResponses + .filter(botResponse=>botResponse?.run_id===Conversation.run_id) + .sort((mA, mB)=>(mB.created_at-mA.created_at)) + Conversation.addMessage({ + content: prompt, + created_at: processingStartTime, + originalPrompt, + role: 'member', + run_id, + thread_id, + }) + Conversation.addMessages(botResponses) + if(allowSave) + console.log('chat::allowSave=`true`', Conversation.message?.content?.substring(0,64))// Conversation.save() + else + console.log('chat::allowSave=`false`', Conversation.message?.content?.substring(0,64)) +} /** * Create a new conversation. * @async @@ -715,12 +793,14 @@ async function mBotUpdate(factory, llm, bot, options={}){ * @param {string} type - Type of conversation: chat, experience, dialog, inter-system, system, etc.; defaults to `chat` * @param {string} form - Form of conversation: system-avatar, member-avatar, etc.; defaults to `system-avatar` * @param {string} thread_id - The openai thread id - * @param {string} botId - The bot id + * @param {string} llm_id - The id for the llm agent + * @param {LLMServices} llm - OpenAI object + * @param {AgentFactory} factory - Agent Factory object * @returns {Conversation} - The conversation object */ -async function mConversationStart(form='system', type='chat', thread_id, llmAgentId, llm, factory){ +async function mConversationStart(type='chat', form='system', bot_id, thread_id, llm_id, llm, factory){ const { mbr_id, } = factory - const thread = await llm.thread(thread_id) + const thread = await llm.thread(thread_id) // **note**: created here as to begin conversation/LLM independence, and to retain non-async nature of Constructor const Conversation = new (factory.conversation)( { form, @@ -728,8 +808,9 @@ async function mConversationStart(form='system', type='chat', thread_id, llmAgen type, }, factory, - thread, - llmAgentId + bot_id, + llm_id, + thread ) return Conversation } @@ -833,7 +914,6 @@ async function mInit(BotAgent, bots, factory, llm){ const { avatarId, vectorstoreId, } = BotAgent bots.push(...await mInitBots(avatarId, vectorstoreId, factory, llm)) BotAgent.setActiveBot() - console.log('BotAgent::init', BotAgent.activeBot) } /** * Initializes active bots based upon criteria. diff --git a/inc/js/factory-class-extenders/class-contribution-functions.mjs b/inc/js/deprecated/class-contribution-functions.mjs similarity index 100% rename from inc/js/factory-class-extenders/class-contribution-functions.mjs rename to inc/js/deprecated/class-contribution-functions.mjs diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index d6f9376..19e7fa4 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -1,8 +1,3 @@ -import { EventEmitter } from 'events' -import { - mGetQuestions, - mUpdateContribution, -} from './class-contribution-functions.mjs' import { mSaveConversation, } from './class-conversation-functions.mjs' @@ -37,116 +32,45 @@ function extendClass_consent(originClass, referencesObject) { } return Consent } -/** - * Extends the `Contribution` class. - * @param {*} originClass - The class to extend. - * @param {Object} referencesObject - The references to extend the class with, factory, llm, etc. - * @returns {Contribution} - The `Contribution` extended class definition. - */ -function extendClass_contribution(originClass, referencesObject) { - class Contribution extends originClass { - #emitter = new EventEmitter() - #factory - #llm = referencesObject?.openai - constructor(_obj) { - super(_obj) - } - /* public functions */ - /** - * Initialize a contribution. - * @async - * @public - * @param {object} _factory - The factory instance. - */ - async init(_factory){ - this.#factory = _factory - this.request.questions = await mGetQuestions(this, this.#llm) // generate question(s) from cosmos or openAI - this.id = this.factory.newGuid - this.status = 'prepared' - this.emit('on-contribution-new',this) - return this - } - async allow(_request){ - // todo: evolve in near future, but is currently only a pass-through with some basic structure alluding to future functionality - return true - } - /** - * Convenience proxy for emitter. - * @param {string} _eventName - The event to emit. - * @param {any} - The event(s) to emit. - * @returns {void} - */ - emit(_eventName, ...args){ - this.#emitter.emit(_eventName, ...args) - } - /** - * Convenience proxy for listener. - * @param {string} _eventName - The event to emit. - * @param {function} _listener - The event listener functionality. - * @returns {void} - */ - on(_eventName, _listener){ - this.#emitter.on(_eventName, _listener) - } - /** - * Updates `this` with incoming contribution data. - * @param {object} _contribution - Contribution data to incorporate - * @returns {void} - */ - update(_contribution){ - mUpdateContribution(this, _contribution) // directly modifies `this`, no return - } - /* getters/setters */ - /** - * Get the factory instance. - * @returns {object} MyLife Factory instance - */ - get factory(){ - return this.#factory - } - get memberView(){ - return this.inspect(true) - } - get openai(){ - return this.#llm - } - get questions(){ - return this?.questions??[] - } - /* private functions */ - } - return Contribution -} /** * Extends the `Conversation` class. * @param {*} originClass - The class to extend. * @param {Object} referencesObject - The references to extend the class with, factory, llm, etc. * @returns {Conversation} - The `Conversation` extended class definition. */ -function extendClass_conversation(originClass, referencesObject) { +function extendClass_conversation(originClass, referencesObject){ class Conversation extends originClass { #bot_id #factory + #id + #llm_id #messages = [] + #run_id + #runs = new Set() #saved = false #thread #threads = new Set() /** - * - * @param {Object} obj - The object to construct the conversation from. - * @param {AgentFactory} factory - The factory instance. - * @param {Object} thread - The thread instance. - * @param {Guid} bot_id - The initial active bot id (can mutate) + * Constructor for Conversation class. + * @param {Object} obj - Data object for construction + * @param {AgentFactory} factory - The factory instance + * @param {Guid} bot_id - The initial active bot MyLife `id` + * @param {String} llm_id - The initial active LLM `id` + * @param {Object} thread - The related thread instance + * @returns {Conversation} - The constructed conversation instance */ - constructor(obj, factory, thread, bot_id){ + constructor(obj, factory, bot_id, llm_id, thread){ super(obj) this.#factory = factory this.#thread = thread + this.#id = this.#factory.newGuid if(factory.globals.isValidGuid(bot_id)) this.#bot_id = bot_id + if(llm_id?.length) + this.#llm_id = llm_id this.form = this.form - ?? 'system' - this.name = `conversation_${this.#factory.mbr_id}` + ?? 'system-avatar' + this.name = `conversation_${ this.#factory.mbr_id }` this.type = this.type ?? 'chat' } @@ -158,7 +82,7 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {Object[]} - The updated messages array. */ addMessage(message){ - console.log('class-extenders::addMessage', message) + console.log('class-extenders::addMessage', message?.content?.[0]?.text?.value ?? message) const { id, } = message if(this.#messages.find(message=>message.id===id)) return this.messages @@ -177,12 +101,20 @@ function extendClass_conversation(originClass, referencesObject) { * @returns {Object[]} - The updated messages array. */ addMessages(messages){ - const now = Date.now() - messages - .sort((mA, mB) => ( mA.created_at ?? now ) - ( mB.created_at ?? now )) - .forEach(message => this.addMessage(message)) + messages.forEach(message => this.addMessage(message)) return this.messages } + /** + * Adds a run/execution/receipt id to the conversation archive. + * @param {String} run_id - The run id to add + * @returns {void} + */ + addRun(run_id){ + if(run_id?.length){ + this.#runs.add(run_id) + this.#run_id = run_id + } + } /** * Adds a thread id to the conversation archive * @param {string} thread_id - The thread id to add @@ -194,13 +126,30 @@ function extendClass_conversation(originClass, referencesObject) { /** * Get the message by id, or defaults to last message added. * @public - * @param {Guid} messageId - The message id. - * @returns {object} - The openai `message` object. + * @param {Guid} messageId - The message id + * @returns {Message} - The `Message` instance */ - async getMessage(messageId){ - return messageId && this.messages?.[0]?.id!==messageId - ? this.messages.find(message=>message.id===messageId) + getMessage(messageId){ + const Message = messageId?.length + ? this.getMessages().find(message=>message.id===messageId) : this.message + return Message + } + /** + * Get the messages for the conversation. + * @public + * @param {boolean} agentOnly - Whether or not to get only agent messages + * @param {string} run_id - The run id to get messages for + * @param {string} thread_id - The thread id to get messages for (optional) + * @returns {Message[]} - The messages array + */ + getMessages(agentOnly=true, run_id=this.run_id, thread_id){ + let messages = thread_id?.length + ? this.#messages.filter(message=>message.thread_id===thread_id) + : this.#messages.filter(message=>message.run_id===run_id) + if(agentOnly) + messages = messages.filter(message => ['member', 'user'].indexOf(message.role) < 0) + return messages } /** * Removes a thread id from the conversation archive @@ -232,35 +181,57 @@ function extendClass_conversation(originClass, referencesObject) { } // public getters/setters /** - * Get the id {Guid} of the conversation's bot. + * Get the id {Guid} of the conversation's active bot. * @getter * @returns {Guid} - The bot id. */ - get botId(){ - return this.#bot_id - } get bot_id(){ return this.#bot_id } /** - * Set the id {Guid} of the conversation's bot. + * Set the id {Guid} of the conversation's active bot. * @setter * @param {Guid} bot_id - The bot id. * @returns {void} */ - set botId(bot_id){ - if(!this.#factory.globals.isValidGuid(bot_id)) - throw new Error(`Invalid bot_id: ${ bot_id }`) - this.#bot_id = bot_id - } set bot_id(bot_id){ if(!this.#factory.globals.isValidGuid(bot_id)) throw new Error(`Invalid bot_id: ${ bot_id }`) this.#bot_id = bot_id } + /** + * Get the generated Guid `id` of the Conversation instance. + * @getter + * @returns {Guid} - The conversation id + */ + get id(){ + return this.#id + } + /** + * Whether or not the conversation has _ever_ been saved. + * @getter + * @returns {boolean} - Whether or not the conversation has _ever_ been saved + */ get isSaved(){ return this.#saved } + /** + * Get the `id` {String} of the conversation's active LLM. + * @getter + * @returns {String} - The llm id + */ + get llm_id(){ + return this.#llm_id + } + /** + * Sets the `id` {String} of the conversation's active LLM. + * @getter + * @returns {String} - The llm id + */ + set llm_id(llm_id){ + if(!llm_id?.length) + this.#llm_id = llm_id + } /** * Get the most recently added message. * @getter @@ -280,6 +251,9 @@ function extendClass_conversation(originClass, referencesObject) { get mostRecentDialog(){ return this.message.content } + get run_id(){ + return this.#run_id + } get thread(){ return this.#thread } @@ -497,7 +471,6 @@ function extendClass_message(originClass, referencesObject) { /* exports */ export { extendClass_consent, - extendClass_contribution, extendClass_conversation, extendClass_experience, extendClass_file, diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index a8bb469..5365105 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -102,14 +102,17 @@ async function challenge(ctx){ * @property {Object[]} responses - Response messages from Avatar intelligence */ async function chat(ctx){ - const { botId, itemId, message, shadowId, } = ctx.request.body ?? {} /* body nodes sent by fe */ + const { botId, itemId, message, } = ctx.request.body + ?? {} /* body nodes sent by fe */ if(!message?.length) ctx.throw(400, 'missing `message` content') - const { avatar, dateNow=Date.now(), } = ctx.state - const { MemberSession, } = ctx.session + const { avatar, } = ctx.state + const session = avatar.isMyLife + ? ctx.session.MemberSession + : null if(botId?.length && botId!==avatar.activeBotId) throw new Error(`Bot ${ botId } not currently active; chat() requires active bot`) - const response = await avatar.chat(message, itemId, shadowId, dateNow, MemberSession) + const response = await avatar.chat(message, itemId, session) ctx.body = response } async function collections(ctx){ diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 7660f8f..d3d027e 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -80,71 +80,52 @@ class Avatar extends EventEmitter { * @public * @param {string} message - The chat message content * @param {Guid} itemId - The active collection-item id (optional) - * @param {Guid} shadowId - The active Shadow Id (optional) - * @param {number} processStartTime - The start time of the process (optional) - * @param {MemberSession} session - ignored, but required for **overload** on Q instance - * @param {string} thread_id - The openai thread id (required for **overload** on Q instance) * @returns {object} - The response object { instruction, responses, success, } */ - async chat(message, itemId, shadowId, processStartTime=Date.now(), session=null, thread_id){ + async chat(message, itemId){ + /* validate request */ if(!message) throw new Error('No message provided in context') - - - - - const Conversation = this.activeBot.conversation() - // what should I actually return from chat? - if(!conversation) - throw new Error('No conversation found for bot intelligence and could not be created.') - conversation.bot_id = botId - conversation.llm_id = bot_id - let messages = [] - if(shadowId) - messages.push(...await this.shadow(shadowId, itemId, message)) - else { - let alteredMessage = message - if(itemId){ - // @todo - check if item exists in memory, fewer pings and inclusions overall - let { summary, } = await this.#factory.item(itemId) - if(summary?.length) - alteredMessage = `possible **update-summary-request**: itemId=${ itemId }\n` - + `**member-update-request**:\n` - + message - + `\n**current-summary-in-database**:\n` - + summary - } - messages.push(...await mCallLLM(this.#llmServices, conversation, alteredMessage, this.#factory, this)) + const originalMessage = message + let processStartTime = Date.now() + this.backupResponse = { + message: `I received your request to chat, and sent the request to the central intelligence, but no response was received. Please try again, as the issue is likely aberrant.`, + type: 'system', } - conversation.addMessage({ - content: message, - created_at: Date.now(), - role: 'user', - }) - conversation.addMessages(messages) - if(mAllowSave) - conversation.save() - else - console.log('chat::BYPASS-SAVE', conversation.message?.content?.substring(0,64)) - /* frontend mutations */ - const responses = [] - conversation.messages - .filter(_message=>{ - return messages.find(__message=>__message.id===_message.id) - && _message.type==='chat' - && _message.role!=='user' - }) - .map(_message=>mPruneMessage(botId, _message, 'chat', processStartTime)) - .reverse() - .forEach(_message=>responses.push(_message)) - if(!responses?.length){ - const failsafeResponse = this.backupResponse - ?? { - message: 'I am sorry, connection with my intelligence faltered, hopefully temporarily, ask to try again.', - type: 'system', - } - responses.push(failsafeResponse) + /* execute request */ + if(this.globals.isValidGuid(itemId)){ + // @todo - check if item exists in memory, fewer pings and inclusions overall + let { summary, } = await this.#factory.item(itemId) + if(summary?.length) + message = `possible **update-summary-request**: itemId=${ itemId }\n` + + `**member-update-request**:\n` + + message + + `\n**current-summary-in-database**:\n` + + summary } + const Conversation = await this.activeBot.chat(message, originalMessage, mAllowSave, processStartTime) + const responses = mPruneMessages(this.activeBotId, Conversation.getMessages() ?? [], 'chat', Conversation.processStartTime) + /* respond request */ + const response = { + instruction: this.frontendInstruction, + responses, + success: true, + } + delete this.frontendInstruction + delete this.backupResponse + return response + } + /** + * Chat with an open agent, bypassing a specific bot. + * @param {Conversation} Conversation - The conversation instance + * @returns {Promise} - Conversation instance is altered in place + */ + async chatAgentBypass(Conversation){ + if(!this.isMyLife) + throw new Error('Agent bypass only available for MyLife avatar.') + await this.#botAgent.chat(Conversation, mAllowSave, this) + const responses = mPruneMessages(this.activeBotId, Conversation.getMessages(), 'chat', Conversation?.processStartTime) + /* respond request */ const response = { instruction: this.frontendInstruction, responses, @@ -386,7 +367,6 @@ class Avatar extends EventEmitter { * @returns {Array} - The greeting message(s) string array in order of display */ async greeting(dynamic=false){ - console.log('greeting', this.#botAgent.activeBotId) const greetings = mPruneMessages(this.#botAgent.activeBotId, await this.#botAgent.greeting(dynamic), 'greeting') return greetings } @@ -616,52 +596,6 @@ class Avatar extends EventEmitter { } return response } - /** - * Takes a shadow message and sends it to the appropriate bot for response, returning the standard array of bot responses. - * @param {Guid} shadowId - The shadow id. - * @param {Guid} itemId - The item id. - * @param {string} message - The member (interacting with shadow) message content. - * @returns {Object[]} - The array of bot responses. - */ - async shadow(shadowId, itemId, message){ - const processingStartTime = Date.now() - const shadows = await this.shadows() - const shadow = shadows.find(shadow=>shadow.id===shadowId) - if(!shadow) - throw new Error('Shadow not found.') - const { text, type, } = shadow - const item = await this.#factory.item(itemId) - if(!item) - throw new Error(`cannot find item: ${ itemId }`) - const { form, summary, } = item - let tailgate - switch(type){ - case 'member': - message = `update-memory-request: itemId=${ itemId }\n` + message - break - case 'agent': - /* - // @stub - develop additional form types, entry or idea for instance - const dob = new Date(this.#factory.dob) - const diff_ms = Date.now() - dob.getTime() - const age_dt = new Date(diff_ms) - const age = Math.abs(age_dt.getUTCFullYear() - 1970) - message = `Given age of member: ${ age } and updated summary of personal memory: ${ summary }\n- answer the question: "${ text }"` - tailgate = { - content: `Would you like to add this, or part of it, to your memory?`, // @stub - tailgate for additional data - thread_id: bot.thread_id, - } - break - */ - default: - break - } - let messages = await mCallLLM(this.#llmServices, this.activeBot, message, this.#factory, this) - messages = messages.map(message=>mPruneMessage(this.activeBotId, message, 'shadow', processingStartTime)) - if(tailgate?.length) - messages.push(mPruneMessage(this.activeBotId, tailgate, 'system')) - return messages - } /** * Gets the list of shadows. * @returns {Object[]} - Array of shadow objects. @@ -1256,32 +1190,32 @@ class Q extends Avatar { /* overloaded methods */ /** * OVERLOADED: Processes and executes incoming chat request. + * @todo - shunt registration actions to different MA functions * @public * @param {string} message - The chat message content * @param {Guid} itemId - The active collection-item id (optional) - * @param {Guid} shadowId - The active Shadow Id (optional) - * @param {number} processStartTime - The start time of the process - * @param {MemberSession} session - The MyLife MemberSession instance - * @returns {object} - The response(s) to the chat request + * @param {MemberSession} MemberSession - The member session object + * @returns {Promise} - The response(s) to the chat request */ - async chat(message, itemId, shadowId, processStartTime=Date.now(), session){ - if(itemId?.length || shadowId?.length) - throw new Error('MyLife System Avatar cannot process chats with `itemId` or `shadowId`.') - let { Conversation, } = session + async chat(message, itemId, MemberSession){ + if(itemId?.length) + throw new Error('MyLife System Avatar cannot process chats with `itemId`.') + let { Conversation, } = MemberSession if(!Conversation){ - const Conversation = await this.conversationStart('chat', 'system-avatar') - thread_id = Conversation.thread_id + Conversation = await this.conversationStart('chat', 'system-avatar') + if(!Conversation) + throw new Error('Unable to be create `Conversation`.') this.#conversations.push(Conversation) + MemberSession.Conversation = Conversation } - session.Conversation = Conversation // @stub - store elsewhere + Conversation.originalPrompt = message + Conversation.processStartTime = Date.now() if(this.isValidating) // trigger confirmation until session (or vld) ends message = `CONFIRM REGISTRATION PHASE: registrationId=${ this.registrationId }\n${ message }` if(this.isCreatingAccount) message = `CREATE ACCOUNT PHASE: ${ message }` - - - - return super.chat(message, itemId, shadowId, processStartTime, null, thread_id) + Conversation.prompt = message + return await this.chatAgentBypass(Conversation) } /** * OVERLOADED: MyLife must refuse to create bots. @@ -1441,31 +1375,6 @@ function mAvatarDropdown(globals, avatar){ name, } } -/** - * Makes call to LLM and to return response(s) to prompt. - * @todo - create actor-bot for internal chat? Concern is that API-assistants are only a storage vehicle, ergo not an embedded fine tune as I thought (i.e., there still may be room for new fine-tuning exercise); i.e., micro-instructionsets need to be developed for most. Unclear if direct thread/message instructions override or ADD, could check documentation or gpt, but... - * @todo - would dynamic event dialog be handled more effectively with a callback routine function, I think so, and would still allow for avatar to vet, etc. - * @todo - convert conversation requirements to bot - * @module - * @param {LLMServices} llmServices - OpenAI object currently - * @param {Conversation} conversation - Conversation object - * @param {string} prompt - dialog-prompt/message for llm - * @param {AgentFactory} factory - Agent Factory object required for function execution - * @param {object} avatar - Avatar object - * @returns {Promise} - Array of Message instances in descending chronological order - */ -async function mCallLLM(llmServices, conversation, prompt, factory, avatar){ - const { bot_id, llm_id, thread_id } = conversation - const botId = llm_id - ?? bot_id - if(!thread_id || !botId) - throw new Error('Both `thread_id` and `bot_id` required for LLM call.') - const messages = await llmServices.getLLMResponse(thread_id, botId, prompt, factory, avatar) - messages.sort((mA, mB)=>{ - return mB.created_at - mA.created_at - }) - return messages -} /** * Cancels openAI run. * @module @@ -2006,9 +1915,9 @@ async function mExperienceStart(avatar, factory, experienceId, avatarExperienceV experience.variables = avatarExperienceVariables /* assign living experience */ let [memberDialog, scriptDialog] = await Promise.all([ - avatar.createConversation('experience'), - avatar.createConversation('dialog') - ]) // async cobstruction + avatar.conversationStart('experience'), + avatar.conversationStart('dialog') + ]) // async construction experience.memberDialog = memberDialog experience.scriptDialog = scriptDialog } @@ -2271,7 +2180,6 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. /* parse message */ let agent='server', content='', - purpose=type, response_time=Date.now()-processStartTime const { content: messageContent, } = message const rSource = /【.*?\】/gs @@ -2291,7 +2199,6 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. activeBotId, agent, message, - purpose, response_time, type, } @@ -2330,22 +2237,17 @@ function mPruneMessages(botId, messageArray, type='chat', processStartTime=Date. async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInput='NEXT'){ console.log('mReliveMemoryNarration::start', item.id, memberInput) const { relivingMemories, } = avatar - const { bot, } = Bot - const { bot_id, id: botId, } = bot + const { id: botId, } = Bot const { id, } = item const processStartTime = Date.now() let message = `## relive memory itemId: ${ id }\n` let relivingMemory = relivingMemories.find(reliving=>reliving.item.id===id) if(!relivingMemory){ /* create new activated reliving memory */ - const conversation = await avatar.createConversation('memory', undefined, botId, false) - conversation.llm_id = bot_id - const { thread_id, } = conversation + const conversation = await avatar.conversationStart('memory', 'member-avatar') relivingMemory = { - bot, conversation, id, item, - thread_id, } relivingMemories.push(relivingMemory) console.log(`mReliveMemoryNarration::new reliving memory: ${ id }`) @@ -2367,7 +2269,6 @@ async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInp id, messages, success: true, - thread_id, } return memory } diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index 4cf6047..1491d09 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -9,7 +9,6 @@ import Dataservices from './mylife-dataservices.mjs' import { Member, MyLife } from './core.mjs' import { extendClass_consent, - extendClass_contribution, extendClass_conversation, extendClass_experience, extendClass_file, @@ -26,7 +25,6 @@ const mBotInstructions = {} const mDefaultBotType = 'personal-avatar' const mExtensionFunctions = { extendClass_consent: extendClass_consent, - extendClass_contribution: extendClass_contribution, extendClass_conversation: extendClass_conversation, extendClass_experience: extendClass_experience, extendClass_file: extendClass_file, diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index e9d7e91..9e95b8b 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -57,18 +57,18 @@ class LLMServices { } /** * Deletes an assistant from OpenAI. - * @param {string} botId - GPT-Assistant external ID + * @param {string} llm_id - GPT-Assistant external ID * @returns */ - async deleteBot(botId){ + async deleteBot(llm_id){ try { - const deletedBot = await this.openai.beta.assistants.del(botId) + const deletedBot = await this.openai.beta.assistants.del(llm_id) return deletedBot } catch (error) { if(error.name==='PermissionDeniedError') - console.error(`Permission denied to delete assistant: ${ botId }`) + console.error(`Permission denied to delete assistant: ${ llm_id }`) else - console.error(`ERROR trying to delete assistant: ${ botId }`, error.name, error.message) + console.error(`ERROR trying to delete assistant: ${ llm_id }`, error.name, error.message) } } /** @@ -108,17 +108,17 @@ class LLMServices { * @example - `run` object: { assistant_id, id, model, provider, required_action, status, usage } * @todo - confirm that reason for **factory** is to run functions as responses from LLM; ergo in any case, find better way to stash/cache factory so it does not need to be passed through every such function * @param {string} thread_id - Thread id. - * @param {string} botId - GPT-Assistant/Bot id. + * @param {string} llm_id - GPT-Assistant/Bot id. * @param {string} prompt - Member input. * @param {AgentFactory} factory - Avatar Factory object to process request. * @param {Avatar} avatar - Avatar object. * @returns {Promise} - Array of openai `message` objects. */ - async getLLMResponse(thread_id, botId, prompt, factory, avatar){ + async getLLMResponse(thread_id, llm_id, prompt, factory, avatar){ if(!thread_id?.length) thread_id = ( await mThread(this.openai) ).id await mAssignRequestToThread(this.openai, thread_id, prompt) - const run = await mRunTrigger(this.openai, botId, thread_id, factory, avatar) + const run = await mRunTrigger(this.openai, llm_id, thread_id, factory, avatar) const { id: run_id, } = run const llmMessages = ( await this.messages(thread_id) ) .filter(message=>message.role=='assistant' && message.run_id==run_id) @@ -127,14 +127,14 @@ class LLMServices { /** * Given member request for help, get response from specified bot assistant. * @param {string} thread_id - Thread id. - * @param {string} botId - GPT-Assistant/Bot id. + * @param {string} llm_id - GPT-Assistant/Bot id. * @param {string} helpRequest - Member input. * @param {AgentFactory} factory - Avatar Factory object to process request. * @param {Avatar} avatar - Avatar object. * @returns {Promise} - openai `message` objects. */ - async help(thread_id, botId, helpRequest, factory, avatar){ - const helpResponse = await this.getLLMResponse(thread_id, botId, helpRequest, factory, avatar) + async help(thread_id, llm_id, helpRequest, factory, avatar){ + const helpResponse = await this.getLLMResponse(thread_id, llm_id, helpRequest, factory, avatar) return helpResponse } /** @@ -159,15 +159,16 @@ class LLMServices { } /** * Updates assistant with specified data. Example: Tools object for openai: { tool_resources: { file_search: { vector_store_ids: [vectorStore.id] } }, }; https://platform.openai.com/docs/assistants/tools/file-search/quickstart?lang=node.js + * @todo - conform payload to OpenAI API Reference * @param {string} bot - The bot object data. * @returns {Promise} - openai assistant object. */ async updateBot(bot){ - let { bot_id, ...assistantData } = bot - if(!bot_id?.length) + let { botId, bot_id, llm_id, ...assistantData } = bot + if(!llm_id?.length) throw new Error('No bot ID provided for update') assistantData = mValidateAssistantData(assistantData) // throws on improper format - const assistant = await this.openai.beta.assistants.update(bot_id, assistantData) + const assistant = await this.openai.beta.assistants.update(llm_id, assistantData) return assistant } /** @@ -324,7 +325,7 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref && run.required_action?.submit_tool_outputs?.tool_calls && run.required_action.submit_tool_outputs.tool_calls.length ){ - const { assistant_id: bot_id, id: runId, metadata, thread_id, } = run + const { assistant_id: llm_id, id: runId, metadata, thread_id, } = run const toolCallsOutput = await Promise.all( run.required_action.submit_tool_outputs.tool_calls .map(async tool=>{ @@ -681,14 +682,14 @@ async function mRunStart(llmServices, assistantId, threadId){ * Triggers openAI run and updates associated `run` object. * @module * @param {OpenAI} openai - OpenAI object - * @param {string} botId - Bot id + * @param {string} llm_id - Bot id * @param {string} threadId - Thread id * @param {AgentFactory} factory - Avatar Factory object to process request * @param {Avatar} avatar - Avatar object * @returns {void} - All content generated by run is available in `avatar`. */ -async function mRunTrigger(openai, botId, threadId, factory, avatar){ - const run = await mRunStart(openai, botId, threadId) +async function mRunTrigger(openai, llm_id, threadId, factory, avatar){ + const run = await mRunStart(openai, llm_id, threadId) if(!run) throw new Error('Run failed to start') const finishRun = await mRunFinish(openai, run, factory, avatar) diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 5f715e8..1f8b2a2 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -715,7 +715,7 @@ async function submit(message, hideMemberChat=true){ * @returns {void} */ async function mSubmitChat(message) { - const { action, itemId, shadowId, } = chatActiveItem.dataset + const { action, itemId, } = chatActiveItem.dataset const url = window.location.origin + '/members' const { id: botId, } = activeBot() const request = { @@ -724,7 +724,6 @@ async function mSubmitChat(message) { itemId, message, role: 'member', - shadowId, } const options = { method: 'POST', From bc8b0743858e7960b397817ff216dbd33031371d Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 00:44:46 -0400 Subject: [PATCH 39/54] 20241024 @Mookse - remove logs --- inc/js/agents/system/bot-agent.mjs | 5 +---- inc/js/factory-class-extenders/class-extenders.mjs | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index f6a6f31..cf68a1b 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -235,7 +235,6 @@ class BotAgent { async conversationStart(type='chat', form='system-avatar'){ const { bot_id: llm_id, id: bot_id, } = this.avatar const Conversation = await mConversationStart(type, form, bot_id, null, llm_id, this.#llm, this.#factory) - console.log('BotAgent::conversationStart', Conversation.thread_id, Conversation.bot_id, Conversation.llm_id, Conversation.inspect(true)) return Conversation } /** @@ -782,9 +781,7 @@ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ }) Conversation.addMessages(botResponses) if(allowSave) - console.log('chat::allowSave=`true`', Conversation.message?.content?.substring(0,64))// Conversation.save() - else - console.log('chat::allowSave=`false`', Conversation.message?.content?.substring(0,64)) + Conversation.save() // no `await` } /** * Create a new conversation. diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index 19e7fa4..daf1fdb 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -82,7 +82,6 @@ function extendClass_conversation(originClass, referencesObject){ * @returns {Object[]} - The updated messages array. */ addMessage(message){ - console.log('class-extenders::addMessage', message?.content?.[0]?.text?.value ?? message) const { id, } = message if(this.#messages.find(message=>message.id===id)) return this.messages From 31d92dada3ddafe5076fed2b36378a2455a02274 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 00:49:38 -0400 Subject: [PATCH 40/54] 20241024 @Mookse - minor cleanup --- inc/js/session.mjs | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/inc/js/session.mjs b/inc/js/session.mjs index fef9ee3..600f954 100644 --- a/inc/js/session.mjs +++ b/inc/js/session.mjs @@ -15,26 +15,25 @@ class MylifeMemberSession extends EventEmitter { super() this.#factory = factory this.#mbr_id = this.isMyLife ? this.factory.mbr_id : false - mAssignFactoryListeners(this.#factory) console.log( chalk.bgGray('MylifeMemberSession:constructor(factory):generic-mbr_id::end'), chalk.bgYellowBright(this.factory.mbr_id), ) } + /** + * Initializes the member session. If `isMyLife`, then session requires chat thread unique to visitor; session has singleton System Avatar who maintains all running Conversations. + * @param {String} mbr_id - Member id to initialize session + * @returns {Promise} - Member session instance + */ async init(mbr_id=this.mbr_id){ - // if isMyLife, then session requires chat thread unique to guest; session has own avatar, demonstrate this is true and then create new conversation if(!this.locked && this.mbr_id && this.mbr_id!==mbr_id){ // unlocked, initialize member session this.#mbr_id = mbr_id - mAssignFactoryListeners(this.#factory) await this.#factory.init(this.mbr_id) // needs only `init()` with different `mbr_id` to reset this.#Member = await this.factory.getMyLifeMember() - this.#autoplayed = false // resets autoplayed flag, although should be impossible as only other "variant" requires guest status, as one-day experiences can be run for guests also [for pay] - this.thread_id = null // reset thread_id from Q-session - this.emit('onInit-member-initialize', this.#Member.memberName) - console.log( - chalk.bgBlue('created-member:'), - chalk.bgRedBright(this.#Member.memberName) - ) + this.#autoplayed = false + delete this.Conversation + delete this.thread_id + console.log(chalk.bgBlue('created-member:'), chalk.bgRedBright(this.#Member.memberName)) } return this } @@ -245,22 +244,6 @@ class MylifeMemberSession extends EventEmitter { return this.#Member?.agentName } } -function mAssignFactoryListeners(_session){ - if(_session.isMyLife) // all sessions _begin_ as MyLife - _session.factory.on('member-unlocked',_mbr_id=>{ - console.log( - chalk.grey('session::constructor::member-unlocked_trigger'), - chalk.bgGray(_mbr_id) - ) - }) - else // all _"end"_ as member - _session.factory.on('avatar-activated',_avatar=>{ - console.log( - chalk.grey('session::constructor::avatar-activated_trigger'), - chalk.bgGray(_avatar.id) - ) - }) -} function mValidCtxObject(_ctx){ // validate ctx object return ( From e4fc877d93b5fb4b2012bd7298996ba0fec59674 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 20:13:50 -0400 Subject: [PATCH 41/54] 20241024 @Mookse - wip stable - proto collectionsAgent - RELIVE unstable --- inc/js/agents/system/README.md | 8 +- inc/js/agents/system/bot-agent.mjs | 406 ++++++++++++++---- inc/js/agents/system/collections-agent.mjs | 18 + .../class-conversation-functions.mjs | 9 +- .../class-extenders.mjs | 2 +- inc/js/functions.mjs | 12 +- inc/js/mylife-avatar.mjs | 368 +++++----------- inc/js/mylife-factory.mjs | 2 +- inc/js/routes.mjs | 2 +- inc/json-schemas/conversation.json | 28 +- 10 files changed, 472 insertions(+), 383 deletions(-) create mode 100644 inc/js/agents/system/collections-agent.mjs diff --git a/inc/js/agents/system/README.md b/inc/js/agents/system/README.md index 231fcf8..aebf0bc 100644 --- a/inc/js/agents/system/README.md +++ b/inc/js/agents/system/README.md @@ -2,12 +2,16 @@ The MyLife system incorporates a suite of specialized assistants, each designed to augment various aspects of the avatars within the platform. These assistants follow consistent coding protocols and are currently intended exclusively for integration with avatars, enhancing their functionality and interactivity. -- **Asset-Assistant (File Handler)**: This assistant manages file-related operations, ensuring efficient handling, storage, and retrieval of files within the system. It acts as a central hub for file management tasks, streamlining the process of dealing with various file formats and data types. +- **Asset-Agent**: (File Handler) This agent manages file-related operations, ensuring efficient handling, storage, and retrieval of files within the system. It acts as a central hub for file management tasks, streamlining the process of dealing with various file formats and data types. -- **Evolution-Assistant**: Central to the avatar's developmental journey, the Evolution Assistant orchestrates the growth and maturation of avatars. It guides avatars through different phases of evolution, from creation to retirement, tailoring the development process according to the avatar's specific needs and contexts. +- **Bot-Agent**: (Bot Handler) One of the most fundamental agents available, represents an object that manages the menagerie of Bots allocated to the current member's team. Also manages Teams. + +- **Collections-Agent**: (Lists Handler) This agent manages collections for mass operations. - **DOM-Assistant**: The Document Object Model (DOM) Assistant is pivotal in managing and manipulating the structure of data and documents within the system. It plays a key role in ensuring the data is organized and accessible in a way that is both efficient and intuitive. +- **Evolution-Assistant**: Central to the avatar's developmental journey, the Evolution Assistant orchestrates the growth and maturation of avatars. It guides avatars through different phases of evolution, from creation to retirement, tailoring the development process according to the avatar's specific needs and contexts. + - **Preferences-Assistant**: This assistant is dedicated to personalizing user experiences by managing and adapting to user preferences. It ensures that avatars can cater to individual tastes and requirements, making interactions more tailored and relevant. - **Settings-Assistant**: Focused on configuration management, the Settings Assistant allows for the customization and adjustment of system settings. This ensures that avatars can operate within the parameters that best suit the user's needs and the system's operational environment. diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index cf68a1b..2b1310f 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -1,5 +1,3 @@ -import LLMServices from "../../mylife-llm-services.mjs" - /* module constants */ const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE const mDefaultBotTypeArray = ['personal-avatar', 'avatar'] @@ -27,16 +25,18 @@ const mTeams = [ * @todo - are private vars for factory and llm necessary, or passable? */ class Bot { + #collectionsAgent #conversation #factory #llm - constructor(botData, factory, llm){ + constructor(botData, llm, factory){ this.#factory = factory this.#llm = llm botData = this.globals.sanitize(botData) Object.assign(this, botData) if(!this.id) throw new Error('Bot database id required') + // @stub - this.#collectionsAgent = new CollectionsAgent(llm, factory) } /* public functions */ /** @@ -50,10 +50,7 @@ class Bot { async chat(message, originalMessage, allowSave=true, processStartTime=Date.now()){ if(this.isMyLife && !this.isAvatar) throw new Error('Only Q, MyLife Corporate Intelligence, is available for non-member conversation.') - const { bot_id, id, thread_id, type, } = this - if(!this.#conversation) - this.#conversation = await mConversationStart('chat', type, id, thread_id, bot_id, this.#llm, this.#factory) - const Conversation = this.#conversation + const Conversation = await this.getConversation() Conversation.prompt = message Conversation.originalPrompt = originalMessage await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, this) // mutates Conversation @@ -67,6 +64,19 @@ class Bot { getBot(){ return this } + /** + * Retrieves the Conversation instance for this bot. + * @param {String} message - The member request (optional) + * @returns {Promise} - The Conversation instance + */ + async getConversation(message){ + if(!this.#conversation){ + const { bot_id: _llm_id, id: bot_id, thread_id, type, } = this + let { llm_id=_llm_id, } = this // @stub - deprecate bot_id + this.#conversation = await mConversationStart('chat', type, bot_id, thread_id, llm_id, this.#llm, this.#factory, message) + } + return this.#conversation + } /** * Retrieves a greeting message from the active bot. * @param {Boolean} dynamic - Whether to use dynamic greetings (`true`) or static (`false`) @@ -78,6 +88,23 @@ class Bot { const greetings = await mBotGreeting(dynamic, this, llm, factory) return greetings } + /** + * Migrates Conversation from an old thread to a newly created (or identified) destination thread. + * @returns {Boolean} - Whether or not operation was successful + */ + async migrateChat(){ + const migration = mMigrateChat(this, this.#llm, this.#factory) + return !!migration + } + /** + * 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} itemId - The item id + * @returns {Object} - The obscured item object + */ + async obscure(itemId){ + const updatedSummary = await this.#factory.obscure(itemId) + return updatedSummary + } /** * Updates a Bot instance's data. * @param {object} botData - The bot data to update @@ -92,6 +119,24 @@ class Bot { } async save(){ + } + /** + * Sets the thread id for the bot. + * @param {String} thread_id - The thread id + * @returns {Promise} + */ + async setThread(thread_id){ + const llm_id = this.llm_id + ?? this.bot_id + if(!thread_id?.length) + thread_id = await this.#llm.createThread(llm_id) + const { id, } = this + this.thread_id = thread_id + const bot = { + id, + thread_id, + } + await this.#factory.updateBot(bot) } /* getters/setters */ /** @@ -110,6 +155,9 @@ class Bot { } return bot } + get conversation(){ + return this.#conversation + } get globals(){ return this.#factory.globals } @@ -154,6 +202,7 @@ class BotAgent { #avatarId #bots #factory + #fileConversation #llm #vectorstoreId constructor(factory, llm){ @@ -180,13 +229,16 @@ class BotAgent { } /* public functions */ /** - * Retrieves a bot instance by id. - * @param {Guid} botId - The Bot id + * Retrieves Bot instance by id or type, defaults to personal-avatar. + * @param {Guid} bot_id - The Bot id + * @param {String} botType - The Bot type * @returns {Promise} - The Bot instance */ - bot(botId){ - const Bot = this.#bots - .find(bot=>bot.id===botId) + bot(bot_id, botType){ + const Bot = botType?.length + ? this.#bots.find(bot=>bot.type===botType) + : this.#bots.find(bot=>bot.id===bot_id) + ?? this.avatar return Bot } /** @@ -195,7 +247,7 @@ class BotAgent { * @returns {Bot} - The created Bot instance */ async botCreate(botData){ - const Bot = await mBotCreate(this.#avatarId, this.#vectorstoreId, botData, this.#factory) + const Bot = await mBotCreate(this.#avatarId, this.#vectorstoreId, botData, this.#llm, this.#factory) this.#bots.push(Bot) this.setActiveBot(Bot.id) return Bot @@ -203,14 +255,14 @@ class BotAgent { /** * Deletes a bot instance. * @async - * @param {Guid} botId - The Bot id - * @returns {Promise} + * @param {Guid} bot_id - The Bot id + * @returns {Promise} - Whether or not operation was successful */ - async botDelete(botId){ - const Bot = this.#bots.find(bot=>bot.id===botId) - if(!Bot) - throw new Error(`Bot not found with id: ${ botId }`) - await mBotDelete(Bot, this.#bots, this.#llm, this.#factory) + async botDelete(bot_id){ + if(!this.#factory.isMyLife) + return false + const success = await mBotDelete(bot_id, this, this.#llm, this.#factory) + return success } /** * Chat with the active bot. @@ -222,7 +274,7 @@ class BotAgent { async chat(Conversation, allowSave=true, q){ if(!Conversation) throw new Error('Conversation instance required') - Conversation.processingStartTime + Conversation.processStartTime await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, q) // mutates Conversation return Conversation } @@ -230,20 +282,16 @@ class BotAgent { * Initializes a conversation, currently only requested by System Avatar, but theoretically could be requested by any externally-facing Member Avatar as well. **note**: not in Q because it does not have a #botAgent yet. * @param {String} type - The type of conversation, defaults to `chat` * @param {String} form - The form of conversation, defaults to `system-avatar` - * @returns {Promise} - The conversation object + * @param {String} prompt - The prompt for the conversation (optional) + * @returns {Promise} - The Conversation instance */ - async conversationStart(type='chat', form='system-avatar'){ - const { bot_id: llm_id, id: bot_id, } = this.avatar - const Conversation = await mConversationStart(type, form, bot_id, null, llm_id, this.#llm, this.#factory) + async conversationStart(type='chat', form='system-avatar', prompt){ + const { avatar, } = this + const { bot_id: _llm_id, id: bot_id, } = avatar + let { llm_id=_llm_id, } = avatar // @stub - deprecate bot_id + const Conversation = await mConversationStart(type, form, bot_id, null, llm_id, this.#llm, this.#factory, prompt) return Conversation } - /** - * Retrieves bots by instance. - * @returns {Bot[]} - The array of bots - */ - getBots(){ - return this.#bots - } /** * Get a static or dynamic greeting from active bot. * @param {boolean} dynamic - Whether to use LLM for greeting @@ -252,14 +300,39 @@ class BotAgent { async greeting(dynamic=false){ const greetings = await this.activeBot.getGreeting(dynamic, this.#llm, this.#factory) return greetings + } + /** + * Migrates a bot to a new, presumed combined (with internal or external) bot. + * @param {Guid} bot_id - The bot id + * @returns {Promise} - The migrated Bot instance + */ + async migrateBot(bot_id){ + throw new Error('migrateBot() not yet implemented') + } + /** + * Migrates a chat conversation from an old thread to a newly created (or identified) destination thread. + * @param {Guid} bot_id - Bot id whose Conversation is to be migrated + * @returns {Boolean} - Whether or not operation was successful + */ + async migrateChat(bot_id){ + /* validate request */ + if(this.#factory.isMyLife) + throw new Error('Chats with Q cannot be migrated.') + const Bot = this.bot(bot_id) + if(!Bot) + return false + /* execute request */ + await Bot.migrateChat() + /* respond request */ + return true } /** * Sets the active bot for the BotAgent. - * @param {Guid} botId - The Bot id + * @param {Guid} bot_id - The Bot id * @returns {void} */ - setActiveBot(botId=this.avatar?.id){ - const Bot = this.#bots.find(bot=>bot.id===botId) + setActiveBot(bot_id=this.avatar?.id){ + const Bot = this.#bots.find(bot=>bot.id===bot_id) if(Bot) this.#activeBot = Bot } @@ -272,6 +345,22 @@ class BotAgent { this.#activeTeam = this.teams.find(team=>team.id===teamId) ?? this.#activeTeam } + async summarize(fileId, fileName, processStartTime=Date.now()){ + let responses = [] + if(!fileId?.length && !fileName?.length) + return responses + let prompts = [] + if(fileId?.length) + prompts.push(`id=${ fileId }`) + if(fileName?.length) + prompts.push(`file-name=${ fileName }`) + const prompt = `Summarize file document: ${ prompts.join(', ') }` + if(!this.#fileConversation) + this.#fileConversation = await this.conversationStart('file-summary', 'member-avatar', prompt, processStartTime) + this.#fileConversation.prompt = prompt + responses = await mCallLLM(this.#fileConversation, false, this.#llm, this.#factory) + return responses + } /** * Updates a bot instance. * @param {object} botData - The bot data to update @@ -331,7 +420,7 @@ class BotAgent { return this.#avatarId } /** - * Gets the array of bots employed by this BotAgent. For full instances, call `getBots()`. + * Gets the array of bots employed by this BotAgent. * @getter * @returns {Bot[]} - The array of bots */ @@ -376,14 +465,14 @@ class BotAgent { * Initializes openAI assistant and returns associated `assistant` object. * @module * @param {object} botData - The bot data object - * @param {LLMServices} llmServices - OpenAI object + * @param {LLMServices} llm - OpenAI object * @returns {object} - [OpenAI assistant object](https://platform.openai.com/docs/api-reference/assistants/object) */ -async function mAI_openai(botData, llmServices){ +async function mAI_openai(botData, llm){ const { bot_name, type, } = botData botData.name = bot_name ?? `_member_${ type }` - const bot = await llmServices.createBot(botData) + const bot = await llm.createBot(botData) return bot } /** @@ -457,21 +546,21 @@ async function mBot(avatarId, vectorstore_id, factory, botData){ } /** * Creates bot and returns associated `bot` object. - * @todo - botData.name = botDbName should not be required, push logic to `llm-services` + * @todo - validBotData.name = botDbName should not be required, push logic to `llm-services` * @module * @async * @param {Guid} avatarId - The Avatar id * @param {String} vectorstore_id - The Vectorstore id - * @param {Object} bot - The bot data + * @param {Object} botData - The bot proto-data * @param {AgentFactory} factory - Agent Factory instance * @returns {Promise} - Created Bot instance */ -async function mBotCreate(avatarId, vectorstore_id, bot, factory){ +async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ /* validation */ - const { type, } = bot + const { type, } = botData if(!avatarId?.length || !type?.length) throw new Error('avatar id and type required to create bot') - const { instructions, version, } = mBotInstructions(factory, bot) + const { instructions, version=1.0, } = mBotInstructions(factory, type) const model = process.env.OPENAI_MODEL_CORE_BOT ?? process.env.OPENAI_MODEL_CORE_AVATAR ?? 'gpt-4o' @@ -481,8 +570,8 @@ async function mBotCreate(avatarId, vectorstore_id, bot, factory){ bot_name = `My ${type}`, description = `I am a ${type} for ${factory.memberName}`, name = `bot_${type}_${avatarId}`, - } = bot - const botData = { + } = botData + const validBotData = { being: 'bot', bot_name, description, @@ -504,13 +593,14 @@ async function mBotCreate(avatarId, vectorstore_id, bot, factory){ version, } /* create in LLM */ - const { id: bot_id, thread_id, } = await mBotCreateLLM(botData, llm) + const { id: bot_id, thread_id, } = await mBotCreateLLM(validBotData, llm) if(!bot_id?.length) throw new Error('bot creation failed') /* create in MyLife datastore */ - botData.bot_id = bot_id - botData.thread_id = thread_id - const Bot = new Bot(await factory.createBot(botData)) + validBotData.bot_id = bot_id + validBotData.thread_id = thread_id + botData = await factory.createBot(validBotData) // repurposed incoming botData + const Bot = new Bot(botData, llm, factory) console.log(chalk.green(`bot created::${ type }`), Bot.thread_id, Bot.id, Bot.bot_id, Bot.bot_name ) return Bot } @@ -530,27 +620,28 @@ async function mBotCreateLLM(botData, llm){ } /** * Deletes the bot requested from avatar memory and from all long-term storage. - * @param {object} Bot - The bot object to delete - * @param {Object[]} bots - The bots array - * @param {LLMServices} llm - OpenAI object - * @param {AgentFactory} factory - Agent Factory object - * @returns {void} + * @param {Guid} bot_id - The bot id to delete + * @param {BotAgent} BotAgent - BotAgent instance + * @param {LLMServices} llm - The LLMServices instance + * @param {AgentFactory} factory - The Factory instance + * @returns {Promise} - Whether or not operation was successful */ -async function mBotDelete(Bot, bots, llm, factory){ +async function mBotDelete(bot_id, BotAgent, llm, factory){ + const Bot = BotAgent.bot(bot_id) + const { id, llm_id, type, thread_id, } = Bot const cannotRetire = ['actor', 'system', 'personal-avatar'] - const { bot_id, id, thread_id, type, } = bot if(cannotRetire.includes(type)) - throw new Error(`Cannot retire bot type: ${ type }`) + return false /* delete from memory */ - const botId = bots.findIndex(_bot=>_bot.id===id) - if(botId<0) - throw new Error('Bot not found in bots.') - bots.splice(botId, 1) + const { bots, } = BotAgent + const botIndex = bots.findIndex(bot=>bot.id===id) + bots.splice(botIndex, 1) /* delete bot from Cosmos */ factory.deleteItem(id) - /* delete thread and bot from OpenAI */ - llm.deleteBot(bot_id) + /* delete thread and bot from LLM */ + llm.deleteBot(llm_id) llm.deleteThread(thread_id) + return true } /** * Returns set of Greeting messages, dynamic or static @@ -592,12 +683,11 @@ async function mBotGreeting(dynamic=false, Bot, llm, factory){ /** * Returns MyLife-version of bot instructions. * @module - * @param {BotFactory} factory - Factory object - * @param {object} bot - Bot object - * @returns {object} - minor + * @param {AgentFactory} factory - The Factory instance + * @param {String} type - The type of Bot to create + * @returns {object} - The intermediary bot instructions object: { instructions, version, } */ -function mBotInstructions(factory, bot){ - const { type=mDefaultBotType, } = bot +function mBotInstructions(factory, type=mDefaultBotType){ let { instructions, limit=8000, @@ -684,13 +774,16 @@ function mBotInstructions(factory, bot){ break } }) - /* assess and validate limit */ - return { instructions, version, } + const response = { + instructions, + version, + } + return response } /** * Updates bot in Cosmos, and if necessary, in LLM. Returns unsanitized bot data document. * @param {AgentFactory} factory - Factory object - * @param {LLMServices} llm - LLMServices object + * @param {LLMServices} llm - The LLMServices instance * @param {object} bot - Bot object, winnow via mBot in `mylife-avatar.mjs` to only updated fields * @param {object} options - Options object: { instructions: boolean, model: boolean, tools: boolean, vectorstoreId: string, } * @returns @@ -729,19 +822,18 @@ async function mBotUpdate(factory, llm, bot, options={}){ botData.model = factory.globals.currentOpenAIBotModel botData.id = id // validated /* LLM updates */ - const { bot_id, bot_name: name, instructions, llm_id, tools, } = botData - const llmId = llm_id - ?? bot_id - if(llmId?.length && (instructions || name || tools)){ + const { bot_id, bot_name: name, instructions, tools, } = botData + let { llm_id=bot_id, } = botData + if(llm_id?.length && (instructions || name || tools)){ botData.model = factory.globals.currentOpenAIBotModel // not dynamic - botData.llm_id = llmId + botData.llm_id = llm_id await llm.updateBot(botData) const updatedLLMFields = Object.keys(botData) .filter(key=>key!=='id' && key!=='bot_id') // strip mechanicals - console.log(chalk.green('mUpdateBot()::update in OpenAI'), id, bot_id, updatedLLMFields) + console.log(chalk.green('mUpdateBot()::update in OpenAI'), id, llm_id, updatedLLMFields) } - const updatedBotData = await factory.updateBot(botData) - return updatedBotData + botData = await factory.updateBot(botData) + return botData } /** * Sends Conversation instance with prompts for LLM to process, updating the Conversation instance before returning `void`. @@ -750,13 +842,14 @@ async function mBotUpdate(factory, llm, bot, options={}){ * @module * @param {Conversation} Conversation - Conversation instance * @param {boolean} allowSave - Whether to save the conversation, defaults to `true` - * @param {LLMServices} llm - OpenAI object + * @param {LLMServices} llm - The LLMServices instance * @param {AgentFactory} factory - Agent Factory object required for function execution * @param {object} avatar - Avatar object * @returns {Promise} - Alters Conversation instance by nature */ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ - const { llm_id, originalPrompt, processingStartTime=Date.now(), prompt, thread_id, } = Conversation + const { llm_id, originalPrompt, processStartTime=Date.now(), prompt, thread_id, } = Conversation + console.log('mCallLLM', llm_id, thread_id, prompt, processStartTime) if(!llm_id?.length) throw new Error('No `llm_id` intelligence id found in Conversation for `mCallLLM`.') if(!thread_id?.length) @@ -773,7 +866,7 @@ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ .sort((mA, mB)=>(mB.created_at-mA.created_at)) Conversation.addMessage({ content: prompt, - created_at: processingStartTime, + created_at: processStartTime, originalPrompt, role: 'member', run_id, @@ -783,6 +876,25 @@ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ if(allowSave) Conversation.save() // no `await` } +/** + * Deletes conversation and updates + * @param {Conversation} Conversation - The Conversation instance + * @param {LLMServices} llm - The LLMServices instance + * @returns {Promise} - `true` if successful + */ +async function mConversationDelete(Conversation, factory, llm){ + /* delete thread_id from bot and save to Cosmos */ + Bot.thread_id = '' + const { id, thread_id, } = Bot + factory.updateBot({ + id, + thread_id, + }) + await factory.deleteItem(Conversation.id) /* delete conversation from Cosmos */ + await llm.deleteThread(thread_id) /* delete thread from LLM */ + console.log('mDeleteConversation', Conversation.id, thread_id) + return true +} /** * Create a new conversation. * @async @@ -791,17 +903,22 @@ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ * @param {string} form - Form of conversation: system-avatar, member-avatar, etc.; defaults to `system-avatar` * @param {string} thread_id - The openai thread id * @param {string} llm_id - The id for the llm agent - * @param {LLMServices} llm - OpenAI object + * @param {LLMServices} llm - The LLMServices instance * @param {AgentFactory} factory - Agent Factory object + * @param {string} prompt - The prompt for the conversation (optional) + * @param {number} processStartTime - The time the processing started, defaults to `Date.now()` * @returns {Conversation} - The conversation object */ -async function mConversationStart(type='chat', form='system', bot_id, thread_id, llm_id, llm, factory){ - const { mbr_id, } = factory - const thread = await llm.thread(thread_id) // **note**: created here as to begin conversation/LLM independence, and to retain non-async nature of Constructor +async function mConversationStart(type='chat', form='system', bot_id, thread_id, llm_id, llm, factory, prompt, processStartTime=Date.now()){ + const { mbr_id, newGuid: id, } = factory + const thread = await mThread(thread_id, llm) const Conversation = new (factory.conversation)( { form, + id, mbr_id, + prompt, + processStartTime, type, }, factory, @@ -904,7 +1021,7 @@ function mGetGPTResources(globals, toolName, vectorstoreId){ * @param {BotAgent} BotAgent - The BotAgent to initialize * @param {Bot[]} bots - The array of bots (empty on init) * @param {AgentFactory} factory - The factory instance - * @param {LLMServices} llm - The LLM instance + * @param {LLMServices} llm - The LLMServices instance * @returns {void} */ async function mInit(BotAgent, bots, factory, llm){ @@ -917,7 +1034,7 @@ async function mInit(BotAgent, bots, factory, llm){ * @param {Guid} avatarId - The Avatar id * @param {String} vectorstore_id - The Vectorstore id * @param {AgentFactory} factory - The MyLife factory instance - * @param {LLMServices} llm - The LLM instance + * @param {LLMServices} llm - The LLMServices instance * @returns {Bot[]} - The array of activated and available bots */ async function mInitBots(avatarId, vectorstore_id, factory, llm){ @@ -925,9 +1042,118 @@ async function mInitBots(avatarId, vectorstore_id, factory, llm){ .map(botData=>{ botData.vectorstore_id = vectorstore_id botData.object_id = avatarId - return new Bot(botData, factory, llm) + return new Bot(botData, llm, factory) }) return bots } +/** + * Migrates LLM thread/memory to new one, altering Conversation instance. + * @param {Bot} Bot - Bot instance + * @param {LLMServices} llm - The LLMServices instance + * @returns {Promise} - Whether or not operation was successful + */ +async function mMigrateChat(Bot, llm){ + /* constants and variables */ + const { Conversation, id: bot_id, type: botType, } = Bot + if(!Conversation) + return false + const { chatLimit=25, thread_id, } = Conversation + let messages = await llm.messages(thread_id) // @todo - limit to 25 messages or modify request + if(!messages?.length) + return false + let disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, + itemCollectionTypes='item', + itemLimit=100, + type='item' + switch(botType){ + case 'biographer': + case 'personal-biographer': + type = 'memory' + itemCollectionTypes = `memory,story,narrative` + break + case 'diary': + case 'journal': + case 'journaler': + type = 'entry' + const itemType = botType==='journaler' + ? 'journal' + : botType + itemCollectionTypes = `${ itemType },entry,` + break + default: + break + } + const chatSummary=`## ${ type.toUpperCase() } CHAT SUMMARY\n`, + chatSummaryRegex = /^## [^\n]* CHAT SUMMARY\n/, + itemSummary=`## ${ type.toUpperCase() } LIST\n`, + itemSummaryRegex = /^## [^\n]* LIST\n/ + const items = ( await avatar.collections(type) ) + .sort((a, b)=>a._ts-b._ts) + .slice(0, itemLimit) + const itemList = items + .map(item=>`- itemId: ${ item.id } :: ${ item.title }`) + .join('\n') + const itemCollectionList = items + .map(item=>item.id) + .join(',') + .slice(0, 512) // limit for metadata string field + const metadata = { + bot_id: bot_id, + conversation_id: Conversation.id, + } + /* prune messages source material */ + messages = messages + .slice(0, chatLimit) + .map(message=>{ + const { content: contentArray, id, metadata, role, } = message + const content = contentArray + .filter(_content=>_content.type==='text') + .map(_content=>_content.text?.value) + ?.[0] + return { content, id, metadata, role, } + }) + .filter(message=>!itemSummaryRegex.test(message.content)) + const summaryMessage = messages + .filter(message=>!chatSummaryRegex.test(message.content)) + .map(message=>message.content) + .join('\n') + /* contextualize previous content */ + const summaryMessages = [] + /* summary of items */ + if(items.length) + summaryMessages.push({ + content: itemSummary + disclaimer + itemList, + metadata: { + collectionList: itemCollectionList, + collectiontypes: itemCollectionTypes, + }, + role: 'assistant', + }) + /* summary of messages */ + if(summaryMessage.length) + summaryMessages.push({ + content: chatSummary + disclaimer + summaryMessage, + metadata: { + collectiontypes: itemCollectionTypes, + }, + role: 'assistant', + }) + if(!summaryMessages.length) + return + /* add messages to new thread */ + Conversation.setThread( await llm.thread(null, summaryMessages.reverse(), metadata) ) + await Bot.setThread(Conversation.thread_id) // autosaves `thread_id`, no `await` + console.log('mMigrateChat::SUCCESS', Bot.thread_id, Conversation.inspect(true)) + if(mAllowSave) + Conversation.save() // no `await` + else + console.log('mMigrateChat::BYPASS-SAVE', Conversation.thread_id) +} +async function mThread(thread_id, llm){ + const messages = [] + const metadata = {} + const thread = await llm.thread(thread_id, messages, metadata) + return thread +} /* exports */ export default BotAgent \ No newline at end of file diff --git a/inc/js/agents/system/collections-agent.mjs b/inc/js/agents/system/collections-agent.mjs new file mode 100644 index 0000000..a797ef8 --- /dev/null +++ b/inc/js/agents/system/collections-agent.mjs @@ -0,0 +1,18 @@ +/* module constants */ +const mDefaultCollectionTypes = ['entry', 'experience', 'memory', 'story'] +/* classes */ +/** + * @class - Bot + * @private + * @todo - are private vars for factory and llm necessary, or passable? + */ +class CollectionsAgent { + #factory + #llm + constructor(llm, factory){ + this.#factory = factory + this.#llm = llm + } +} +/* module exports */ +export default CollectionsAgent \ No newline at end of file diff --git a/inc/js/factory-class-extenders/class-conversation-functions.mjs b/inc/js/factory-class-extenders/class-conversation-functions.mjs index 91af01f..d8b71aa 100644 --- a/inc/js/factory-class-extenders/class-conversation-functions.mjs +++ b/inc/js/factory-class-extenders/class-conversation-functions.mjs @@ -2,10 +2,10 @@ /** * Consumes a conversation object and uses supplied factory to (create/)save it to MyLife CosmosDB. Each session conversation is saved as a separate document, and a given thread may span many conversations, so cross-checking by thread_id will be required when rounding up and consolidating summaries for older coversations. * @param {AgentFactory} factory - Factory instance - * @param {Conversation} conversation - Conversation object + * @param {Conversation} Conversation - Conversation instance * @returns {Promise} */ -async function mSaveConversation(factory, conversation){ +async function mSaveConversation(Conversation, factory){ const { being, bot_id, @@ -14,10 +14,9 @@ async function mSaveConversation(factory, conversation){ isSaved=false, name, thread, - thread_id, type, - } = conversation - let { messages, } = conversation + } = Conversation + let { messages, } = Conversation messages = messages .map(_msg=>_msg.micro) if(!isSaved){ diff --git a/inc/js/factory-class-extenders/class-extenders.mjs b/inc/js/factory-class-extenders/class-extenders.mjs index daf1fdb..a40b2f7 100644 --- a/inc/js/factory-class-extenders/class-extenders.mjs +++ b/inc/js/factory-class-extenders/class-extenders.mjs @@ -176,7 +176,7 @@ function extendClass_conversation(originClass, referencesObject){ * @returns {void} */ async save(){ - this.#saved = await mSaveConversation(this.#factory, this) + this.#saved = await mSaveConversation(this, this.#factory) } // public getters/setters /** diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index 5365105..fdfc791 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -35,7 +35,7 @@ async function alerts(ctx){ } } async function bots(ctx){ - const { bid, } = ctx.params // botId sent in url path + const { bid, } = ctx.params // bot_id sent in url path const { avatar } = ctx.state const bot = ctx.request.body ?? {} const { id, } = bot @@ -102,7 +102,7 @@ async function challenge(ctx){ * @property {Object[]} responses - Response messages from Avatar intelligence */ async function chat(ctx){ - const { botId, itemId, message, } = ctx.request.body + const { botId: bot_id, itemId, message, } = ctx.request.body ?? {} /* body nodes sent by fe */ if(!message?.length) ctx.throw(400, 'missing `message` content') @@ -110,8 +110,8 @@ async function chat(ctx){ const session = avatar.isMyLife ? ctx.session.MemberSession : null - if(botId?.length && botId!==avatar.activeBotId) - throw new Error(`Bot ${ botId } not currently active; chat() requires active bot`) + if(bot_id?.length && bot_id!==avatar.activeBotId) + throw new Error(`Bot ${ bot_id } not currently active; chat() requires active bot`) const response = await avatar.chat(message, itemId, session) ctx.body = response } @@ -244,9 +244,9 @@ async function migrateBot(ctx){ ctx.body = await avatar.migrateBot(bid) } async function migrateChat(ctx){ - const { tid, } = ctx.params + const { bid, } = ctx.params const { avatar, } = ctx.state - ctx.body = await avatar.migrateChat(tid) + ctx.body = await avatar.migrateChat(bid) } /** * Given an itemId, obscures aspects of contents of the data record. diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index d3d027e..d07702b 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -2,6 +2,7 @@ import { Marked } from 'marked' import EventEmitter from 'events' import AssetAgent from './agents/system/asset-agent.mjs' import BotAgent from './agents/system/bot-agent.mjs' +import CollectionsAgent from './agents/system/collections-agent.mjs' import EvolutionAgent from './agents/system/evolution-agent.mjs' import LLMServices from './mylife-llm-services.mjs' /* module constants */ @@ -19,7 +20,9 @@ const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restor class Avatar extends EventEmitter { #assetAgent #botAgent + #collectionsAgent #evolver + #experienceAgent #experienceGenericVariables = { age: undefined, birthdate: undefined, @@ -49,6 +52,7 @@ class Avatar extends EventEmitter { this.#llmServices = llmServices this.#assetAgent = new AssetAgent(this.#factory, this.#llmServices) this.#botAgent = new BotAgent(this.#factory, this.#llmServices) + this.#collectionsAgent = new CollectionsAgent(this.#factory, this.#llmServices) } /* public functions */ /** @@ -68,11 +72,11 @@ class Avatar extends EventEmitter { /** * Get a Bot instance by id. * @public - * @param {Guid} botId - The bot id + * @param {Guid} bot_id - The bot id * @returns {Promise} - The bot object from memory */ - bot(botId){ - const Bot = this.#botAgent.bot(botId) + bot(bot_id){ + const Bot = this.#botAgent.bot(bot_id) return Bot } /** @@ -116,9 +120,10 @@ class Avatar extends EventEmitter { return response } /** - * Chat with an open agent, bypassing a specific bot. + * Chat with an open agent, bypassing specific or active bot. * @param {Conversation} Conversation - The conversation instance - * @returns {Promise} - Conversation instance is altered in place + * @returns {Promise} - Response object: { instruction, responses, success, } + * @note - Conversation instance is altered in place */ async chatAgentBypass(Conversation){ if(!this.isMyLife) @@ -325,29 +330,19 @@ class Avatar extends EventEmitter { * @param {Guid} id - The Bot id * @returns {object} - The pruned Bot object */ - getBot(botId){ - const bot = this.#botAgent.bot(botId)?.bot + getBot(bot_id){ + const bot = this.#botAgent.bot(bot_id)?.bot return bot } - /** - * Returns the pruned bots for avatar. - * @param {Guid} id - The Bot id - * @returns {Object[]} - The pruned Bot objects - */ - getBots(){ - const bots = this.bots - .map(Bot=>Bot.bot) - return bots - } /** * Gets Conversation object. If no thread id, creates new conversation. * @param {string} thread_id - openai thread id (optional) - * @param {Guid} botId - The bot id (optional) + * @param {Guid} bot_id - The bot id (optional) * @returns {Conversation} - The conversation object. */ - getConversation(thread_id, botId){ + getConversation(thread_id, bot_id){ const conversation = this.conversations - .filter(c=>(thread_id?.length && c.thread_id===thread_id) || (botId?.length && c.botId===botId)) + .filter(c=>(thread_id?.length && c.thread_id===thread_id) || (bot_id?.length && c.bot_id===bot_id)) ?.[0] return conversation } @@ -434,32 +429,36 @@ class Avatar extends EventEmitter { } /** * Migrates a bot to a new, presumed combined (with internal or external) bot. - * @param {Guid} botId - The bot id. - * @returns + * @param {Guid} bot_id - The bot id + * @returns {Promise} - The migrated Bot instance */ - async migrateBot(botId){ - const bot = await this.bot(botId) - if(!bot) - throw new Error(`Bot not found with id: ${ botId }`) - const { id, } = bot - if(botId!==id) - throw new Error(`Bot id mismatch: ${ botId }!=${ id }`) - return bot + async migrateBot(bot_id){ + const migration = await this.#botAgent.migrateBot(bot_id) + return migration } /** * Migrates a chat conversation from an old thread to a newly created (or identified) destination thread. * @param {string} thread_id - Conversation thread id in OpenAI * @returns {Conversation} - The migrated conversation object */ - async migrateChat(thread_id){ - /* validate request */ - const conversation = this.getConversation(thread_id) - if(!conversation) - throw new Error(`Conversation thread_id not found: ${ thread_id }`) - /* execute request */ - const updatedConversation = await mMigrateChat(this, this.#factory, this.#llmServices, conversation) - /* respond request */ - return updatedConversation + async migrateChat(bot_id){ + const success = await this.#botAgent.migrateChat(bot_id) + const response = { + responses: [success + ? { + agent: 'server', + message: `I have successfully migrated this conversation to a new thread.`, + type: 'chat', + } + : { + agent: 'server', + message: `I'm sorry - I encountered an error while trying to migrate this conversation; please try again.`, + type: 'chat', + } + ], + success, + } + return response } /** * 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. @@ -467,7 +466,7 @@ class Avatar extends EventEmitter { * @returns {Object} - The obscured item object */ async obscure(iid){ - const updatedSummary = await this.#factory.obscure(iid) + const updatedSummary = await this.activeBot.obscure(iid) this.frontendInstruction = { command: 'updateItemSummary', itemId: iid, @@ -523,63 +522,46 @@ class Avatar extends EventEmitter { } /** * Member request to retire a bot. - * @param {Guid} botId - The bot id. - * @returns {object} - The retired bot object. - */ - async retireBot(botId){ - /* reset active bot, if required */ - if(this.activeBotId===botId) - this.#botAgent.setActiveBot() // avatar cannot be retired - const bot = await this.bot(botId) - if(!bot) - throw new Error(`Bot not found with id: ${ botId }`) - const { id, } = bot - if(botId!==id) - throw new Error(`Bot id mismatch: ${ botId }!=${ id }`) - this.#botAgent.botDelete(botId) + * @param {Guid} bot_id - The id of Bot to retire + * @returns {object} - The Response object: { instruction, responses, success, } + */ + async retireBot(bot_id){ + const success = await this.#botAgent.deleteBot(bot_id) const response = { instruction: { - command: 'removeBot', - id: botId, + command: success ? 'removeBot' : 'error', + id: bot_id, }, - responses: [{ - agent: 'server', - message: `I have removed this bot from the team.`, - purpose: 'system', - type: 'chat', - }], - success: true, + responses: [success + ? { + agent: 'server', + message: `I have removed this bot from the team.`, + type: 'chat', + } + : { + agent: 'server', + message: `I'm sorry - I encountered an error while trying to retire this bot; please try again.`, + type: 'system', + } + ], + success, } return response } /** - * Member-request to retire a chat conversation thread and begin a new one with the same intelligence. - * @param {string} thread_id - Conversation thread id in OpenAI + * Currently only proxy for `migrateChat`. + * @param {string} bot_id - Bot id with Conversation to retire * @returns {object} - The response object { instruction, responses, success, } */ - async retireChat(botId){ - /* validate request */ - const conversation = this.getConversation(null, botId) - if(!conversation){ - throw new Error(`Conversation not found with bot id: ${ botId }`) - } - const { thread_id: cid, } = conversation - const bot = await this.bot(botId) - const { id: _botId, thread_id: tid, } = bot - if(botId!=_botId) - throw new Error(`Bot id mismatch: ${ botId }!=${ bot_id }`) - if(tid!=cid) - throw new Error(`Conversation mismatch: ${ tid }!=${ cid }`) - /* execute request */ - const updatedConversation = await mMigrateChat(this, this.#factory, this.#llmServices, conversation) + async retireChat(bot_id){ + const success = await this.#botAgent.migrateChat(bot_id) /* respond request */ - const response = !!updatedConversation + const response = success ? { /* @todo - add frontend instructions to remove migrateChat button */ instruction: null, responses: [{ agent: 'server', - message: `I have successfully retired this conversation thread and started a new one.`, - purpose: 'system', + message: `I have successfully retired this conversation.`, type: 'chat', }], success: true, @@ -589,7 +571,6 @@ class Avatar extends EventEmitter { responses: [{ agent: 'server', message: `I'm sorry - I encountered an error while trying to retire this conversation; please try again.`, - purpose: 'system', type: 'chat', }], success: false, @@ -611,32 +592,30 @@ class Avatar extends EventEmitter { * @returns {Object} - The response object { messages, success, error,} */ async summarize(fileId, fileName, processStartTime=Date.now()){ - if(this.isMyLife) - throw new Error('MyLife avatar cannot summarize files.') - if(!fileId?.length && !fileName?.length) - throw new Error('File id or name required for summarization.') - const { bot_id, id: botId, thread_id, } = this.avatar - const prompt = `Summarize this file document: name=${ fileName }, id=${ fileId }` - const response = { - messages: [], - success: false, + /* validate request */ + this.backupResponse = { + message: `I received your request to summarize, but an error occurred in the process. Perhaps try again with another file.`, + type: 'system', } - try{ - let messages = await mCallLLM(this.#llmServices, { bot_id, thread_id, }, prompt, this.#factory, this) - messages = messages - .map(message=>mPruneMessage(botId, message, 'mylife-file-summary', processStartTime)) - .filter(message=>message && message.role!=='user') - if(!messages.length) - throw new Error('No valid messages returned from summarization.') - response.messages.push(...messages) - response.success = true - } catch(error) { - response.messages.push({ content: `Unfortunately, a server error occured: ${error.message}`, role: 'system', }) - response.messages.push({ content: 'Please indicate in a help chat what went wrong. Or one might ask... why can\'t I do that, and I don\'t have a great answer at the moment.', role: 'system', }) - response.error = error - console.log('ERROR::Avatar::summarize()', error) + let success = false + /* execute request */ + responses = await this.#botAgent.summarize(fileId, fileName, processStartTime) + /* respond request */ + if(!responses?.length) + responses = [this.backupResponse] + else { + responses = mPruneMessage(this.avatar.id, responses, 'mylife-file-summary', processStartTime) + instructions = { + command: 'updateFileSummary', + itemId: fileId, + } + success = true + } + return { + instructions, + responses, + success, } - return response } /** * Get a specified team, its details and _instanced_ bots, by id for the member. @@ -742,11 +721,11 @@ class Avatar extends EventEmitter { * Set the active bot id. If not match found in bot list, then defaults back to this.id (avatar). * @setter * @requires mBotInstructions - * @param {string} botId - The requested bot id + * @param {string} bot_id - The requested bot id * @returns {void} */ - set activeBotId(botId){ - this.#botAgent.setActiveBot(botId) + set activeBotId(bot_id){ + this.#botAgent.setActiveBot(bot_id) } get activeBotNewestVersion(){ const { type, } = this.activeBot @@ -1215,7 +1194,8 @@ class Q extends Avatar { if(this.isCreatingAccount) message = `CREATE ACCOUNT PHASE: ${ message }` Conversation.prompt = message - return await this.chatAgentBypass(Conversation) + const response = await this.chatAgentBypass(Conversation) + return response } /** * OVERLOADED: MyLife must refuse to create bots. @@ -1237,11 +1217,15 @@ class Q extends Avatar { const updatedSummary = await botFactory.obscure(iid) return updatedSummary } + /* overload rejections */ /** - * OVERLOADED: Refuses to upload to MyLife. + * OVERLOADED: Q refuses to execute. * @public * @throws {Error} - MyLife avatar cannot upload files. */ + summarize(){ + throw new Error('MyLife avatar cannot summarize files.') + } upload(){ throw new Error('MyLife avatar cannot upload files.') } @@ -1429,7 +1413,7 @@ async function mCast(factory, cast){ } function mCreateSystemMessage(activeBot, message, factory){ if(!(message instanceof factory.message)){ - const { id: botId, thread_id, } = activeBot + const { id: bot_id, thread_id, } = activeBot const content = message?.content ?? message?.message ?? message message = new (factory.message)({ being: 'message', @@ -1439,39 +1423,9 @@ function mCreateSystemMessage(activeBot, message, factory){ type: 'system' }) } - message = mPruneMessage(botId, message, 'system') + message = mPruneMessage(bot_id, message, 'system') return message } -/** - * Deletes conversation and updates - * @param {Conversation} conversation - The conversation object - * @param {Conversation[]} conversations - The conversations array - * @param {Object} bot - The bot involved in the conversation - * @param {AgentFactory} factory - Agent Factory object - * @param {LLMServices} llm - OpenAI object - * @returns {Promise} - `true` if successful - */ -async function mDeleteConversation(conversation, conversations, bot, factory, llm){ - const { id, } = conversation - /* delete conversation from memory */ - const conversationId = conversations.findIndex(_conversation=>_conversation.id===id) - if(conversationId<0) - throw new Error('Conversation not found in conversations.') - conversations.splice(conversationId, 1) - /* delete thread_id from bot and save to Cosmos */ - bot.thread_id = '' - const { id: botId, thread_id, } = bot - factory.updateBot({ - id: botId, - thread_id, - }) - /* delete conversation from Cosmos */ - const deletedConversation = await factory.deleteItem(conversation.id) - /* delete thread from LLM */ - const deletedThread = await llm.deleteThread(thread_id) - console.log('mDeleteConversation', conversation.id, deletedConversation, thread_id, deletedThread) - return true -} /** * Takes character data and makes necessary adjustments to roles, urls, etc. * @todo - icon and background changes @@ -1991,118 +1945,6 @@ async function mInit(factory, llmServices, avatar, botAgent, assetAgent){ /* lived-experiences */ avatar.experiencesLived = await factory.experiencesLived(false) } -/** - * Migrates specified conversation (by thread_id) and returns conversation with new thread processed and saved to bot, both as a document and in avatar memory. - * @param {Avatar} avatar - Avatar object - * @param {AgentFactory} factory - AgentFactory object - * @param {LLMServices} llm - OpenAI object - * @param {string} thread_id - The thread_id of the conversation - * @returns - */ -async function mMigrateChat(avatar, factory, llm, conversation){ - /* constants and variables */ - const { thread_id, } = conversation - const chatLimit=25 - let messages = await llm.messages(thread_id) // @todo - limit to 25 messages or modify request - if(!messages?.length) - return conversation - const { botId, } = conversation - const bot = await avatar.bot(botId) - const botType = bot.type - let disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, - itemCollectionTypes='item', - itemLimit=1000, - type='item' - switch(botType){ - case 'biographer': - case 'personal-biographer': - type = 'memory' - itemCollectionTypes = `memory,story,narrative` - break - case 'diary': - case 'journal': - case 'journaler': - type = 'entry' - const itemType = botType==='journaler' - ? 'journal' - : botType - itemCollectionTypes = `${ itemType },entry,` - break - default: - break - } - const chatSummary=`## ${ type.toUpperCase() } CHAT SUMMARY\n`, - chatSummaryRegex = /^## [^\n]* CHAT SUMMARY\n/, - itemSummary=`## ${ type.toUpperCase() } LIST\n`, - itemSummaryRegex = /^## [^\n]* LIST\n/ - const items = ( await avatar.collections(type) ) - .sort((a, b)=>a._ts-b._ts) - .slice(0, itemLimit) - const itemList = items - .map(item=>`- itemId: ${ item.id } :: ${ item.title }`) - .join('\n') - const itemCollectionList = items - .map(item=>item.id) - .join(',') - .slice(0, 512) // limit for metadata string field - const metadata = { - bot_id: botId, - conversation_id: conversation.id, - } - /* prune messages source material */ - messages = messages - .slice(0, chatLimit) - .map(message=>{ - const { content: contentArray, id, metadata, role, } = message - const content = contentArray - .filter(_content=>_content.type==='text') - .map(_content=>_content.text?.value) - ?.[0] - return { content, id, metadata, role, } - }) - .filter(message=>!itemSummaryRegex.test(message.content)) - const summaryMessage = messages - .filter(message=>!chatSummaryRegex.test(message.content)) - .map(message=>message.content) - .join('\n') - /* contextualize previous content */ - const summaryMessages = [] - /* summary of items */ - if(items.length) - summaryMessages.push({ - content: itemSummary + disclaimer + itemList, - metadata: { - collectionList: itemCollectionList, - collectiontypes: itemCollectionTypes, - }, - role: 'assistant', - }) - /* summary of messages */ - if(summaryMessage.length) - summaryMessages.push({ - content: chatSummary + disclaimer + summaryMessage, - metadata: { - collectiontypes: itemCollectionTypes, - }, - role: 'assistant', - }) - if(!summaryMessages.length) - return conversation - /* add messages to new thread */ - const newThread = await llm.thread(null, summaryMessages.reverse(), metadata) - conversation.setThread(newThread) - bot.thread_id = conversation.thread_id - const _bot = { - id: bot.id, - thread_id: conversation.thread_id, - } - factory.updateBot(_bot) // removed await - if(mAllowSave) - conversation.save() - else - console.log('migrateChat::BYPASS-SAVE', conversation.thread_id) - return conversation -} /** * Get experience scene navigation array. * @getter @@ -2206,17 +2048,17 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. } /** * Prune an array of Messages and return. - * @param {Guid} botId - The Active Bot id property + * @param {Guid} bot_id - The Active Bot id property * @param {Object[]} messageArray - The array of messages to prune * @param {string} type - The type of message, defaults to chat * @param {number} processStartTime - The time the process started, defaults to function call * @returns {Object[]} - Concatenated message object */ -function mPruneMessages(botId, messageArray, type='chat', processStartTime=Date.now()){ +function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date.now()){ if(!messageArray.length) throw new Error('No messages to prune') messageArray = messageArray - .map(message=>mPruneMessage(botId, message, type, processStartTime)) + .map(message=>mPruneMessage(bot_id, message, type, processStartTime)) return messageArray } /** @@ -2237,7 +2079,7 @@ function mPruneMessages(botId, messageArray, type='chat', processStartTime=Date. async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInput='NEXT'){ console.log('mReliveMemoryNarration::start', item.id, memberInput) const { relivingMemories, } = avatar - const { id: botId, } = Bot + const { id: bot_id, } = Bot const { id, } = item const processStartTime = Date.now() let message = `## relive memory itemId: ${ id }\n` @@ -2253,7 +2095,7 @@ async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInp console.log(`mReliveMemoryNarration::new reliving memory: ${ id }`) } else /* opportunity for member interrupt */ message += `MEMBER INPUT: ${ memberInput }\n` - const { conversation, thread_id, } = relivingMemory + const { conversation, } = relivingMemory console.log(`mReliveMemoryNarration::reliving memory: ${ id }`, message) let messages = await mCallLLM(llm, conversation, message, factory, avatar) conversation.addMessages(messages) @@ -2264,7 +2106,7 @@ async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInp && message.type==='chat' && message.role!=='user' }) - .map(message=>mPruneMessage(botId, message, 'chat', processStartTime)) + .map(message=>mPruneMessage(bot_id, message, 'chat', processStartTime)) const memory = { id, messages, diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index 1491d09..0f93ca3 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -591,7 +591,7 @@ class AgentFactory extends BotFactory { return avatar } /** - * Generates via personal intelligence, nature of consent/protection around itemId or botId. + * Generates via personal intelligence, nature of consent/protection around itemId or Bot id. * @todo - build out consent structure * @param {Guid} id - The id of the item to generate consent for. * @param {Guid} requesting_mbr_id - The id of the member requesting consent. diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index e1ade44..607eaa9 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -123,7 +123,7 @@ _memberRouter.post('/bots/create', createBot) _memberRouter.post('/bots/activate/:bid', activateBot) _memberRouter.post('/category', category) _memberRouter.post('/migrate/bot/:bid', migrateBot) -_memberRouter.post('/migrate/chat/:tid', migrateChat) +_memberRouter.post('/migrate/chat/:bid', migrateChat) _memberRouter.post('/mode', interfaceMode) _memberRouter.post('/obscure/:iid', obscure) _memberRouter.post('/passphrase', passphraseReset) diff --git a/inc/json-schemas/conversation.json b/inc/json-schemas/conversation.json index 5f1d29b..b033fa7 100644 --- a/inc/json-schemas/conversation.json +++ b/inc/json-schemas/conversation.json @@ -12,30 +12,30 @@ "type": "string", "$comment": "`action` added after `Experience` was coded as a way to partition logic so that some elements are saved independently" }, - "botId": { - "description": "internal system id for bot, used for tracking", - "format": "uuid", + "being": { "type": "string", - "$comment": "bot.id, used for tracking" + "const": "chat" + }, + "bot_id": { + "description": "uuid for Conversation Bot", + "format": "uuid", + "type": "string" }, "id": { + "description": "uuid for conversation, used for tracking", + "format": "uuid", + "type": "string" + }, + "llm_id": { + "description": "external system id for current Bot", "type": "string", - "format": "uuid" + "$comment": "OpenAI uses `assistant_%`" }, "mbr_id": { "type": "string", "minLength": 40, "$comment": "partition-key for member: sysName|core_id" }, - "parent_id": { - "type": "string", - "format": "uuid", - "$comment": "chat should be attached to avatar.id when known" - }, - "being": { - "type": "string", - "const": "chat" - }, "messages": { "type": "array", "default": [], From 6d19e796a0ef8acbe56ee9e5ff62d9df1192063f Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 23:31:33 -0400 Subject: [PATCH 42/54] 20241024 @Mookse - relive memory - wip unstable tutorial/experience --- inc/js/agents/system/bot-agent.mjs | 75 +++++++++++++++++++++++----- inc/js/mylife-avatar.mjs | 78 +++++++++--------------------- inc/js/mylife-llm-services.mjs | 22 +++++---- 3 files changed, 97 insertions(+), 78 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 2b1310f..368cab7 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -126,10 +126,8 @@ class Bot { * @returns {Promise} */ async setThread(thread_id){ - const llm_id = this.llm_id - ?? this.bot_id if(!thread_id?.length) - thread_id = await this.#llm.createThread(llm_id) + thread_id = await mThread(this.#llm) const { id, } = this this.thread_id = thread_id const bot = { @@ -164,6 +162,9 @@ class Bot { get isAvatar(){ return mDefaultBotTypeArray.includes(this.type) } + get isBiographer(){ + return ['personal-biographer', 'biographer'].includes(this.type) + } get isMyLife(){ return this.#factory.isMyLife } @@ -301,6 +302,40 @@ class BotAgent { const greetings = await this.activeBot.getGreeting(dynamic, this.#llm, this.#factory) return greetings } + /** + * Begins or continues a living memory conversation. + * @param {Object} item - Memory item from database + * @param {String} memberInput - The member input (with instructions) + * @param {Object} livingMemory - The living memory object: { Conversation, id, item, } + * @returns {Object} - The living memory object + */ + async liveMemory(item, memberInput='', livingMemory){ + const { biographer, } = this + let message = `## LIVE Memory\n` + if(!livingMemory){ + const { bot_id: _llm_id, id: bot_id, type, } = biographer + const { llm_id=_llm_id, } = biographer + const messages = [] + messages.push({ + content: `## MEMORY SUMMARY, ID=${ item.id }\n## FOR REFERENCE ONLY\n${ item.summary }\n`, + role: 'user', + }) + memberInput = message + memberInput + const Conversation = await mConversationStart('memory', type, bot_id, null, llm_id, this.#llm, this.#factory, memberInput, messages) + Conversation.action = 'living' + livingMemory = { + Conversation, + id: this.#factory.newGuid, + item, + } + } + const { Conversation, } = livingMemory + Conversation.prompt = memberInput?.trim()?.length + ? memberInput + : message + await mCallLLM(Conversation, false, this.#llm, this.#factory) + return livingMemory + } /** * Migrates a bot to a new, presumed combined (with internal or external) bot. * @param {Guid} bot_id - The bot id @@ -419,6 +454,15 @@ class BotAgent { get avatarId(){ return this.#avatarId } + /** + * Gets the Biographer bot for the BotAgent. + * @getter + * @returns {Bot} - The Biographer Bot instance + */ + get biographer(){ + const Biographer = this.#bots.find(bot=>bot.isBiographer) + return Biographer + } /** * Gets the array of bots employed by this BotAgent. * @getter @@ -848,8 +892,7 @@ async function mBotUpdate(factory, llm, bot, options={}){ * @returns {Promise} - Alters Conversation instance by nature */ async function mCallLLM(Conversation, allowSave=true, llm, factory, avatar){ - const { llm_id, originalPrompt, processStartTime=Date.now(), prompt, thread_id, } = Conversation - console.log('mCallLLM', llm_id, thread_id, prompt, processStartTime) + const { llm_id, originalPrompt, processStartTime=Date.now(), prompt, thread_id, } = Conversation if(!llm_id?.length) throw new Error('No `llm_id` intelligence id found in Conversation for `mCallLLM`.') if(!thread_id?.length) @@ -906,12 +949,14 @@ async function mConversationDelete(Conversation, factory, llm){ * @param {LLMServices} llm - The LLMServices instance * @param {AgentFactory} factory - Agent Factory object * @param {string} prompt - The prompt for the conversation (optional) - * @param {number} processStartTime - The time the processing started, defaults to `Date.now()` + * @param {Message[]} messages - The array of messages to seed the conversation * @returns {Conversation} - The conversation object */ -async function mConversationStart(type='chat', form='system', bot_id, thread_id, llm_id, llm, factory, prompt, processStartTime=Date.now()){ +async function mConversationStart(type='chat', form='system', bot_id, thread_id, llm_id, llm, factory, prompt, messages){ const { mbr_id, newGuid: id, } = factory - const thread = await mThread(thread_id, llm) + const metadata = { bot_id, conversation_id: id, mbr_id, }, + processStartTime = Date.now(), + thread = await mThread(llm, thread_id, messages, metadata) const Conversation = new (factory.conversation)( { form, @@ -1141,7 +1186,7 @@ async function mMigrateChat(Bot, llm){ if(!summaryMessages.length) return /* add messages to new thread */ - Conversation.setThread( await llm.thread(null, summaryMessages.reverse(), metadata) ) + Conversation.setThread( await mThread(llm, null, summaryMessages.reverse(), metadata) ) await Bot.setThread(Conversation.thread_id) // autosaves `thread_id`, no `await` console.log('mMigrateChat::SUCCESS', Bot.thread_id, Conversation.inspect(true)) if(mAllowSave) @@ -1149,9 +1194,15 @@ async function mMigrateChat(Bot, llm){ else console.log('mMigrateChat::BYPASS-SAVE', Conversation.thread_id) } -async function mThread(thread_id, llm){ - const messages = [] - const metadata = {} +/** + * Gets or creates a new thread in LLM provider. + * @param {LLMServices} llm - The LLMServices instance + * @param {String} thread_id - The thread id (optional) + * @param {Messages[]} messages - The array of messages to seed the thread (optional) + * @param {Object} metadata - The metadata object (optional) + * @returns {Promise} - The thread object + */ +async function mThread(llm, thread_id, messages, metadata){ const thread = await llm.thread(thread_id, messages, metadata) return thread } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index d07702b..93a4b6d 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -36,10 +36,10 @@ class Avatar extends EventEmitter { #factory // do not expose #livedExperiences = [] // array of ids for lived experiences #livingExperience + #livingMemory #llmServices #mode = 'standard' // interface-mode from module `mAvailableModes` #nickname // avatar nickname, need proxy here as g/setter is "complex" - #relivingMemories = [] // array of active reliving memories, with items, maybe conversations, included #vectorstoreId // vectorstore id for avatar /** * @constructor @@ -208,24 +208,15 @@ class Avatar extends EventEmitter { return await this.#factory.deleteItem(id) } /** - * End a memory. + * End the living memory, if running. * @async * @public * @todo - save conversation fragments - * @param {Guid} id - The id of the memory to end. - * @returns {boolean} - true if memory ended successfully. + * @returns {void} */ - async endMemory(id){ + async endMemory(){ // @stub - save conversation fragments */ - const { relivingMemories, } = this - const index = relivingMemories.findIndex(item=>item.id===id) - if(index>=0){ - const removedMemory = relivingMemories.splice(index, 1) - if(!removedMemory.length) - return false - console.log('item removed', removedMemory?.[0] ?? `index: ${ index } failed`) - } - return true + this.#livingMemory = null } /** * Ends an experience. @@ -503,10 +494,10 @@ class Avatar extends EventEmitter { const { id, } = item if(!id) throw new Error(`item does not exist in member container: ${ iid }`) - /* develop narration */ - const Biographer = this.#botAgent.biographer - const narration = await mReliveMemoryNarration(this, this.#factory, this.#llmServices, Biographer, item, memberInput) - return narration // include any required .map() pruning + const narration = await mReliveMemoryNarration(item, memberInput, this.#botAgent, this.#llmServices, this.#factory, this) + // include any required .map() pruning + console.log('reliveMemory::narration', narration) + return narration } /** * Allows member to reset passphrase. @@ -1109,12 +1100,12 @@ class Avatar extends EventEmitter { this.#nickname = nickname } /** - * Get the `active` reliving memories. + * Get the `active` reliving memory. * @getter * @returns {object[]} - The active reliving memories. */ - get relivingMemories(){ - return this.#relivingMemories + get livingMemory(){ + return this.#livingMemory } get registrationId(){ return this.#factory.registrationId @@ -2068,45 +2059,20 @@ function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date * - others are common to living, but with `reliving`, the biographer bot (only narrator allowed in .10) incorporate any user-contributed contexts or imrpovements to the memory summary that drives the living and sharing. All by itemId. * - if user "interrupts" then interruption content should be added to memory updateSummary; doubt I will keep work interrupt, but this too is hopefully able to merely be embedded in the biographer bot instructions. * Currently testing efficacy of all instructions (i.e., no callbacks, as not necessary yet) being embedded in my biog-bot, `madrigal`. - * @param {Avatar} avatar - Member's avatar object. - * @param {AgentFactory} factory - Member's AgentFactory object. - * @param {LLMServices} llm - OpenAI object. - * @param {Bot} Bot - The relevant bot instance - * @param {object} item - The memory object. + * @param {object} item - The memory object * @param {string} memberInput - The member input (or simply: NEXT, SKIP, etc.) - * @returns {Promise} - The reliving memory object for frontend to execute. + * @param {BotAgent} BotAgent - The Bot Agent instance + * @param {Avatar} avatar - Member Avatar instance + * @returns {Promise} - The reliving memory object for frontend to execute */ -async function mReliveMemoryNarration(avatar, factory, llm, Bot, item, memberInput='NEXT'){ - console.log('mReliveMemoryNarration::start', item.id, memberInput) - const { relivingMemories, } = avatar - const { id: bot_id, } = Bot +async function mReliveMemoryNarration(item, memberInput, BotAgent, avatar){ const { id, } = item - const processStartTime = Date.now() - let message = `## relive memory itemId: ${ id }\n` - let relivingMemory = relivingMemories.find(reliving=>reliving.item.id===id) - if(!relivingMemory){ /* create new activated reliving memory */ - const conversation = await avatar.conversationStart('memory', 'member-avatar') - relivingMemory = { - conversation, - id, - item, - } - relivingMemories.push(relivingMemory) - console.log(`mReliveMemoryNarration::new reliving memory: ${ id }`) - } else /* opportunity for member interrupt */ - message += `MEMBER INPUT: ${ memberInput }\n` - const { conversation, } = relivingMemory - console.log(`mReliveMemoryNarration::reliving memory: ${ id }`, message) - let messages = await mCallLLM(llm, conversation, message, factory, avatar) - conversation.addMessages(messages) + avatar.livingMemory = await BotAgent.liveMemory(item, memberInput, avatar.livingMemory) + const { Conversation, } = avatar.livingMemory + const { bot_id, type, } = Conversation /* frontend mutations */ - messages = conversation.messages - .filter(message=>{ // limit to current chat response(s); usually one, perhaps faithfully in future [or could be managed in LLM] - return messages.find(_message=>_message.id===message.id) - && message.type==='chat' - && message.role!=='user' - }) - .map(message=>mPruneMessage(bot_id, message, 'chat', processStartTime)) + const messages = Conversation.getMessages() + .map(message=>mPruneMessage(bot_id, message, type)) const memory = { id, messages, diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 9e95b8b..2262894 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -318,7 +318,7 @@ async function mRunFinish(llmServices, run, factory, avatar){ * @returns {object} - [OpenAI run object](https://platform.openai.com/docs/api-reference/runs/object) * @throws {Error} - If tool function not recognized */ -async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref +async function mRunFunctions(openai, run, factory, avatar){ try{ if( run.required_action?.type=='submit_tool_outputs' @@ -439,21 +439,23 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'getsummary': case 'get_summary': case 'get summary': - console.log('mRunFunctions()::getSummary::begin', itemId) - avatar.backupResponse = { - message: `I'm sorry, I couldn't finding this summary. I believe the issue might have been temporary. Would you like me to try again?`, - type: 'system', - } + console.log('mRunFunctions()::getSummary::begin', itemId, avatar) + if(avatar) + avatar.backupResponse = { + message: `I'm sorry, I couldn't finding this summary. I believe the issue might have been temporary. Would you like me to try again?`, + type: 'system', + } let { summary: _getSummary, title: _getSummaryTitle, } = item ?? {} if(!_getSummary?.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.` _getSummary = 'no summary found for itemId' } else { - avatar.backupResponse = { - message: `I was able to retrieve the summary indicated.`, - type: 'system', - } + if(avatar) + 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 } From 6b9517956ade16ddf416757b8bc964e43caa89b0 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Thu, 24 Oct 2024 23:43:54 -0400 Subject: [PATCH 43/54] 20241024 @Mookse - cosmetic --- inc/js/mylife-avatar.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 93a4b6d..27d9e43 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -1353,13 +1353,13 @@ function mAvatarDropdown(globals, avatar){ /** * Cancels openAI run. * @module - * @param {LLMServices} llmServices - OpenAI object + * @param {LLMServices} llm - The LLMServices instance * @param {string} thread_id - Thread id * @param {string} runId - Run id * @returns {object} - [OpenAI run object](https://platform.openai.com/docs/api-reference/runs/object) */ -async function mCancelRun(llmServices, thread_id, runId,){ - return await llmServices.beta.threads.runs.cancel( +async function mCancelRun(llm, thread_id, runId,){ + return await llm.beta.threads.runs.cancel( thread_id, runId ) From 39837890939475d8e4bea35fa5a4186cd106f26d Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 25 Oct 2024 17:55:40 -0400 Subject: [PATCH 44/54] 20241024 @Mookse - retireBot tested - instructions and options fix - globals.sanitize() update --- inc/js/agents/system/bot-agent.mjs | 156 ++++++++++++++++++----------- inc/js/functions.mjs | 23 ++--- inc/js/globals.mjs | 8 +- inc/js/mylife-avatar.mjs | 54 ++++++++-- inc/js/mylife-dataservices.mjs | 14 +-- inc/js/mylife-llm-services.mjs | 31 +++--- inc/js/routes.mjs | 2 +- views/assets/js/bots.mjs | 4 +- 8 files changed, 184 insertions(+), 108 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 368cab7..cb5d764 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -28,14 +28,28 @@ class Bot { #collectionsAgent #conversation #factory + #instructionNodes = new Set() #llm constructor(botData, llm, factory){ + const { id=factory.newGuid, type=mDefaultBotType, } = botData this.#factory = factory this.#llm = llm botData = this.globals.sanitize(botData) Object.assign(this, botData) - if(!this.id) - throw new Error('Bot database id required') + this.id = id + this.type = type + switch(type){ + case 'diary': + case 'journal': + case 'journaler': + this.#instructionNodes.add('interests') + this.#instructionNodes.add('flags') + break + case 'personal-biographer': + default: + this.#instructionNodes.add('interests') + break + } // @stub - this.#collectionsAgent = new CollectionsAgent(llm, factory) } /* public functions */ @@ -111,11 +125,14 @@ class Bot { * @param {object} botOptions - Options for updating * @returns */ - async update(botData, botOptions){ + async update(botData, botOptions={}){ /* validate request */ this.globals.sanitize(botData) - const Bot = await mBotUpdate(this.#factory, this.#llm, botData, botOptions) - return this.globals.sanitize(Bot) + /* execute request */ + botOptions.instructions = Object.keys(botData).some(key => this.#instructionNodes.has(key)) + const bot = await mBotUpdate(botData, botOptions, this, this.#llm, this.#factory) + /* respond request */ + return this.globals.sanitize(bot) } async save(){ @@ -138,15 +155,17 @@ class Bot { } /* getters/setters */ /** - * Gets the frontend bot object. If full instance is required, use `getBot()`. + * Gets the frontend bot object. * @getter */ get bot() { - const { bot_name, description, id, purpose, type, version } = this + const { bot_name: name, description, flags, id, interests, purpose, type, version } = this const bot = { - bot_name, description, + flags, id, + interests, + name, purpose, type, version, @@ -159,6 +178,18 @@ class Bot { get globals(){ return this.#factory.globals } + get instructionNodes(){ + return this.#instructionNodes + } + set instructionNodes(instructionNode){ + this.#instructionNodes.add(instructionNode) + } + get instructionNodeValues(){ + return [...this.#instructionNodes].reduce((acc, key)=>{ + acc[key] = this[key] + return acc + }, {}) + } get isAvatar(){ return mDefaultBotTypeArray.includes(this.type) } @@ -260,7 +291,7 @@ class BotAgent { * @returns {Promise} - Whether or not operation was successful */ async botDelete(bot_id){ - if(!this.#factory.isMyLife) + if(this.#factory.isMyLife) return false const success = await mBotDelete(bot_id, this, this.#llm, this.#factory) return success @@ -402,12 +433,12 @@ class BotAgent { * @param {object} botOptions - Options for updating the bot * @returns {Promise} - The updated Bot instance */ - async updateBot(botData, botOptions={}){ + async updateBot(botData, botOptions){ const { id, } = botData if(!this.globals.isValidGuid(id)) throw new Error('`id` parameter required') const Bot = this.#bots.find(bot=>bot.id===id) - if(!!Bot) + if(!Bot) throw new Error(`Bot not found with id: ${ id }`) Bot.update(botData, botOptions) return Bot @@ -604,7 +635,7 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ const { type, } = botData if(!avatarId?.length || !type?.length) throw new Error('avatar id and type required to create bot') - const { instructions, version=1.0, } = mBotInstructions(factory, type) + const { instructions, version=1.0, } = mBotInstructions(factory, botData) const model = process.env.OPENAI_MODEL_CORE_BOT ?? process.env.OPENAI_MODEL_CORE_AVATAR ?? 'gpt-4o' @@ -644,9 +675,9 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ validBotData.bot_id = bot_id validBotData.thread_id = thread_id botData = await factory.createBot(validBotData) // repurposed incoming botData - const Bot = new Bot(botData, llm, factory) - console.log(chalk.green(`bot created::${ type }`), Bot.thread_id, Bot.id, Bot.bot_id, Bot.bot_name ) - return Bot + const _Bot = new Bot(botData, llm, factory) + console.log(`bot created::${ type }`, _Bot.thread_id, _Bot.id, _Bot.bot_id, _Bot.bot_name ) + return _Bot } /** * Creates bot and returns associated `bot` object. @@ -672,7 +703,8 @@ async function mBotCreateLLM(botData, llm){ */ async function mBotDelete(bot_id, BotAgent, llm, factory){ const Bot = BotAgent.bot(bot_id) - const { id, llm_id, type, thread_id, } = Bot + const { bot_id: _llm_id, id, type, thread_id, } = Bot + const { llm_id=_llm_id, } = Bot const cannotRetire = ['actor', 'system', 'personal-avatar'] if(cannotRetire.includes(type)) return false @@ -681,10 +713,12 @@ async function mBotDelete(bot_id, BotAgent, llm, factory){ const botIndex = bots.findIndex(bot=>bot.id===id) bots.splice(botIndex, 1) /* delete bot from Cosmos */ - factory.deleteItem(id) + await factory.deleteItem(id) /* delete thread and bot from LLM */ - llm.deleteBot(llm_id) - llm.deleteThread(thread_id) + if(llm_id?.length) + await llm.deleteBot(llm_id) + if(thread_id?.length) + await llm.deleteThread(thread_id) return true } /** @@ -728,15 +762,17 @@ async function mBotGreeting(dynamic=false, Bot, llm, factory){ * Returns MyLife-version of bot instructions. * @module * @param {AgentFactory} factory - The Factory instance - * @param {String} type - The type of Bot to create + * @param {Object} botData - The bot proto-data * @returns {object} - The intermediary bot instructions object: { instructions, version, } */ -function mBotInstructions(factory, type=mDefaultBotType){ +function mBotInstructions(factory, botData={}){ + const { type=mDefaultBotType, } = botData let { instructions, limit=8000, version, - } = factory.botInstructions(type) ?? {} + } = factory.botInstructions(type) + ?? {} if(!instructions) // @stub - custom must have instruction loophole throw new Error(`bot instructions not found for type: ${ type }`) let { @@ -777,7 +813,7 @@ function mBotInstructions(factory, type=mDefaultBotType){ /* apply replacements */ replacements.forEach(replacement=>{ const placeholderRegExp = factory.globals.getRegExp(replacement.name, true) - const replacementText = eval(`bot?.${replacement.replacement}`) + const replacementText = eval(`botData?.${replacement.replacement}`) ?? eval(`factory?.${replacement.replacement}`) ?? eval(`factory.core?.${replacement.replacement}`) ?? replacement?.default @@ -788,7 +824,7 @@ function mBotInstructions(factory, type=mDefaultBotType){ references.forEach(_reference=>{ const _referenceText = _reference.insert const replacementText = eval(`factory?.${_reference.value}`) - ?? eval(`bot?.${_reference.value}`) + ?? eval(`botData?.${_reference.value}`) ?? _reference.default ?? '`unknown-value`' switch(_reference.method ?? 'replace'){ @@ -826,57 +862,59 @@ function mBotInstructions(factory, type=mDefaultBotType){ } /** * Updates bot in Cosmos, and if necessary, in LLM. Returns unsanitized bot data document. - * @param {AgentFactory} factory - Factory object - * @param {LLMServices} llm - The LLMServices instance - * @param {object} bot - Bot object, winnow via mBot in `mylife-avatar.mjs` to only updated fields + * @module + * @param {object} botData - Bot data update object * @param {object} options - Options object: { instructions: boolean, model: boolean, tools: boolean, vectorstoreId: string, } + * @param {Bot} Bot - The Bot instance + * @param {LLMServices} llm - The LLMServices instance + * @param {AgentFactory} factory - Factory instance * @returns */ -async function mBotUpdate(factory, llm, bot, options={}){ - /* constants */ +async function mBotUpdate(botData, options={}, Bot, llm, factory){ + /* validate request */ + if(!Bot) + throw new Error('Bot instance required to update bot') + const { bot_id, id, llm_id, metadata={}, type, vectorstoreId, } = Bot + const _llm_id = llm_id + ?? bot_id // @stub - deprecate bot_id const { - id, // no modifications - instructions: removeInstructions, - tools: removeTools, - tool_resources: removeResources, - type, // no modifications - ...botData // extract member-driven bot data - } = bot + instructions: discardInstructions, + mbr_id, // no modifications allowed + name, // no modifications allowed + tools: discardTools, + tool_resources: discardResources, + ...allowedBotData + } = botData const { instructions: updateInstructions=false, model: updateModel=false, tools: updateTools=false, - vectorstoreId, } = options - if(!factory.globals.isValidGuid(id)) - throw new Error('bot `id` required in bot argument: `{ id: guid }`') if(updateInstructions){ - const { instructions, version=1.0, } = mBotInstructions(factory, bot) - botData.instructions = instructions - botData.metadata = botData.metadata ?? {} - botData.metadata.version = version.toString() - botData.version = version /* omitted from llm, but appears on updateBot */ + const instructionReferences = { ...Bot.instructionNodeValues, ...allowedBotData } + const { instructions, version=1.0, } = mBotInstructions(factory, instructionReferences) + allowedBotData.instructions = instructions + allowedBotData.metadata = metadata + allowedBotData.metadata.version = version.toString() + allowedBotData.version = version /* omitted from llm, but appears on updateBot */ } if(updateTools){ const { tools, tool_resources, } = mGetAIFunctions(type, factory.globals, vectorstoreId) - botData.tools = tools - botData.tool_resources = tool_resources + allowedBotData.tools = tools + allowedBotData.tool_resources = tool_resources } if(updateModel) - botData.model = factory.globals.currentOpenAIBotModel - botData.id = id // validated - /* LLM updates */ - const { bot_id, bot_name: name, instructions, tools, } = botData - let { llm_id=bot_id, } = botData - if(llm_id?.length && (instructions || name || tools)){ - botData.model = factory.globals.currentOpenAIBotModel // not dynamic - botData.llm_id = llm_id - await llm.updateBot(botData) - const updatedLLMFields = Object.keys(botData) - .filter(key=>key!=='id' && key!=='bot_id') // strip mechanicals - console.log(chalk.green('mUpdateBot()::update in OpenAI'), id, llm_id, updatedLLMFields) + allowedBotData.model = factory.globals.currentOpenAIBotModel + allowedBotData.id = id + allowedBotData.type = type + /* execute request */ + if(_llm_id?.length && (allowedBotData.instructions || allowedBotData.bot_name?.length || allowedBotData.tools)){ + allowedBotData.model = factory.globals.currentOpenAIBotModel // not dynamic + allowedBotData.llm_id = _llm_id + await llm.updateBot(allowedBotData) } - botData = await factory.updateBot(botData) + botData = await factory.updateBot(allowedBotData) + /* respond request */ return botData } /** diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index fdfc791..e0bb714 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -37,9 +37,14 @@ async function alerts(ctx){ async function bots(ctx){ const { bid, } = ctx.params // bot_id sent in url path const { avatar } = ctx.state - const bot = ctx.request.body ?? {} - const { id, } = bot + const bot = ctx.request.body + ?? {} switch(ctx.method){ + case 'DELETE': // retire bot + if(!ctx.Globals.isValidGuid(bid)) + ctx.throw(400, `missing bot id`) + ctx.body = await avatar.retireBot(bid) + break case 'POST': // create new bot ctx.body = await avatar.createBot(bot) break @@ -51,10 +56,8 @@ async function bots(ctx){ if(bid?.length){ // specific bot ctx.body = await avatar.getBot(ctx.params.bid) } else { - const { - activeBotId, - bots, - } = avatar + const { activeBotId, } = avatar + const bots = await avatar.getBots() ctx.body = { // wrap bots activeBotId, bots, @@ -286,12 +289,8 @@ async function privacyPolicy(ctx){ * @param {Koa} ctx - Koa Context object */ async function retireBot(ctx){ - const { avatar, } = ctx.state - const { bid, } = ctx.params // bot id - if(!ctx.Globals.isValidGuid(bid)) - ctx.throw(400, `missing bot id`) - const response = await avatar.retireBot(bid) - ctx.body = response + ctx.method = 'DELETE' + return await this.bots(ctx) } /** * Direct request from member to retire a chat (via bot). diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 8a18003..2a5ca6d 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -314,7 +314,7 @@ class Globals extends EventEmitter { return typeof version === 'string' && regex.test(version) } /** - * Sanitize an object by removing forbidden Cosmos fields. + * Sanitize an object by removing forbidden Cosmos fields and undefined/null values. * @param {object} object - Cosmos document to sanitize * @returns {object} - Sanitized data object */ @@ -323,7 +323,11 @@ class Globals extends EventEmitter { throw new Error('Parameter requires an object') const sanitizedData = Object.fromEntries( Object.entries(object) - .filter(([key, value])=>!mForbiddenCosmosFields.some(char => key.startsWith(char))) + .filter(([key, value])=> + !mForbiddenCosmosFields.some(char => key.startsWith(char)) && + value !== null && + value !== undefined + ) ) return sanitizedData } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 27d9e43..ffbece0 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -325,6 +325,15 @@ class Avatar extends EventEmitter { const bot = this.#botAgent.bot(bot_id)?.bot return bot } + /** + * Returns pruned Bots for Member Avatar. + * @returns + */ + getBots(){ + const bots = this.bots + .map(Bot=>Bot.bot) + return bots + } /** * Gets Conversation object. If no thread id, creates new conversation. * @param {string} thread_id - openai thread id (optional) @@ -539,6 +548,36 @@ class Avatar extends EventEmitter { } return response } + /** + * Retire a Bot, deleting altogether. + * @param {Guid} bot_id - The bot id + * @returns {object} - The response object { instruction, responses, success, } + */ + async retireBot(bot_id){ + if(!this.globals.isValidGuid(bot_id)) + throw new Error(`Invalid bot id: ${ bot_id }`) + const success = await this.#botAgent.botDelete(bot_id) + const response = { + instruction: { + command: success ? 'retireBot' : 'error', + id: bot_id, + }, + responses: [success + ? { + agent: 'server', + message: `I have removed this bot from the team.`, + type: 'chat', + } + : { + agent: 'server', + message: `I'm sorry - I encountered an error while trying to retire this bot; please try again.`, + type: 'system', + } + ], + success, + } + return response + } /** * Currently only proxy for `migrateChat`. * @param {string} bot_id - Bot id with Conversation to retire @@ -629,12 +668,12 @@ class Avatar extends EventEmitter { /** * Update a specific bot. * @async - * @param {object} botData - Bot data to set - * @returns {Promise} - The updated bot + * @param {Object} botData - Bot data to set + * @returns {Promise} - The updated bot */ async updateBot(botData){ - const bot = await this.#botAgent.updateBot(botData) - return bot + const Bot = await this.#botAgent.updateBot(botData) + return Bot.bot } /** * Update instructions for bot-assistant based on type. Default updates all LLM pertinent properties. @@ -711,7 +750,6 @@ class Avatar extends EventEmitter { /** * Set the active bot id. If not match found in bot list, then defaults back to this.id (avatar). * @setter - * @requires mBotInstructions * @param {string} bot_id - The requested bot id * @returns {void} */ @@ -786,7 +824,7 @@ class Avatar extends EventEmitter { ?? this.core.birth?.[0]?.place } /** - * Returns avatar Bot instances. + * Returns Member Avatar's Bot instances. * @getter * @returns {Bot[]} - Array of Bot instances */ @@ -913,7 +951,9 @@ class Avatar extends EventEmitter { * @returns {array} - The help bots. */ get helpBots(){ - return this.bots.filter(bot=>bot.type==='help') + const bots = this.getBots() + .filter(bot=>bot.type==='help') + return bots } /** * Test whether avatar session is creating an account. diff --git a/inc/js/mylife-dataservices.mjs b/inc/js/mylife-dataservices.mjs index ba3c7e0..cd0f21d 100644 --- a/inc/js/mylife-dataservices.mjs +++ b/inc/js/mylife-dataservices.mjs @@ -654,16 +654,12 @@ class Dataservices { * @returns {object} - The bot document */ async updateBot(botData){ - const { id, type: discardType, ...updateBotData } = botData - if(!Object.keys(updateBotData)) + const { id, type: discardType='avatar', ...updateBotData } = botData + if(!Object.keys(updateBotData).length) return botData - const chunks = this.globals.chunkArray(updateKeys, 10) - for(const chunk of chunks){ - const chunkData = {} - chunk.forEach(key=>(chunkData[key] = updateBotData[key])) - const patchedData = await this.patch(id, chunkData) - console.log('updateBot()::patch', patchedData) - } + if(updateBotData.bot_name?.length) + updateBotData.name = `bot_${ discardType }_${ updateBotData.bot_name }_${ id }` + botData = await this.patch(id, updateBotData) return botData } /** diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 2262894..6df5952 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -37,9 +37,9 @@ class LLMServices { * @param {object} bot - The bot data * @returns {Promise} - openai assistant object */ - async createBot(bot){ - bot = mValidateAssistantData(bot) // throws on improper format - bot = await this.openai.beta.assistants.create(bot) + async createBot(botData){ + botData = mValidateAssistantData(botData) + const bot = await this.openai.beta.assistants.create(botData) const thread = await mThread(this.openai) bot.thread_id = thread.id return bot @@ -160,15 +160,15 @@ class LLMServices { /** * Updates assistant with specified data. Example: Tools object for openai: { tool_resources: { file_search: { vector_store_ids: [vectorStore.id] } }, }; https://platform.openai.com/docs/assistants/tools/file-search/quickstart?lang=node.js * @todo - conform payload to OpenAI API Reference - * @param {string} bot - The bot object data. + * @param {Object} botData - The bot object data. * @returns {Promise} - openai assistant object. */ - async updateBot(bot){ - let { botId, bot_id, llm_id, ...assistantData } = bot + async updateBot(botData){ + let { bot_id, llm_id, ...assistantData } = botData if(!llm_id?.length) throw new Error('No bot ID provided for update') - assistantData = mValidateAssistantData(assistantData) // throws on improper format - const assistant = await this.openai.beta.assistants.update(llm_id, assistantData) + botData = mValidateAssistantData(assistantData) + const assistant = await this.openai.beta.assistants.update(llm_id, botData) return assistant } /** @@ -760,13 +760,9 @@ function mValidateAssistantData(data){ version, } = data const name = bot_name - ?? gptName // bot_name internal mylife-alias for openai `name` - delete metadata.created + ?? gptName + metadata.id = id metadata.updated = `${ Date.now() }` // metadata nodes must be strings - if(id) - metadata.id = id - else - metadata.created = `${ Date.now() }` const assistantData = { description, instructions, @@ -776,8 +772,11 @@ function mValidateAssistantData(data){ tools, tool_resources, } - if(!Object.keys(assistantData).length) - throw new Error('Assistant data does not have the correct structure.') + Object.keys(assistantData).forEach(key => { + if (assistantData[key] === undefined) { + delete assistantData[key] + } + }) return assistantData } /* exports */ diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index 607eaa9..f900fd4 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -100,6 +100,7 @@ _apiRouter.post('/upload', upload) _apiRouter.post('/upload/:mid', upload) /* member routes */ _memberRouter.use(memberValidation) +_memberRouter.delete('/bots/:bid', bots) _memberRouter.delete('/items/:iid', deleteItem) _memberRouter.get('/', members) _memberRouter.get('/bots', bots) @@ -127,7 +128,6 @@ _memberRouter.post('/migrate/chat/:bid', migrateChat) _memberRouter.post('/mode', interfaceMode) _memberRouter.post('/obscure/:iid', obscure) _memberRouter.post('/passphrase', passphraseReset) -_memberRouter.post('/retire/bot/:bid', retireBot) _memberRouter.post('/retire/chat/:bid', retireChat) _memberRouter.post('/summarize', summarize) _memberRouter.post('/teams/:tid', team) diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index df8065f..02e0670 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1303,12 +1303,12 @@ async function mRetireBot(event){ if(mActiveBot.id===botId) setActiveBot() /* retire bot */ - const url = window.location.origin + '/members/retire/bot/' + botId + const url = window.location.origin + '/members/bots/' + botId let response = await fetch(url, { headers: { 'Content-Type': 'application/json' }, - method: 'POST', + method: 'DELETE', }) if(!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) From 3657f8ac08aa6b9a2a9a6bcf3989dedfefa6f8e6 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 25 Oct 2024 22:11:39 -0400 Subject: [PATCH 45/54] 20241025 @Mookse - migrateChat() test --- inc/js/agents/system/bot-agent.mjs | 71 ++++++++++++++++++++---------- inc/js/mylife-avatar.mjs | 3 +- inc/js/mylife-factory.mjs | 4 +- inc/js/mylife-llm-services.mjs | 2 +- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index cb5d764..da996f1 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -55,22 +55,45 @@ class Bot { /* public functions */ /** * Chat with the active bot. + * @todo - deprecate avatar in favor of either botAgent or `this` * @param {String} message - The member request * @param {String} originalMessage - The original message * @param {Boolean} allowSave - Whether to save the conversation, defaults to `true` - * @param {Number} processStartTime - The process start time + * @param {Avatar} avatar - The Member Avatar instance * @returns {Promise} - The Conversation instance updated with the chat exchange */ - async chat(message, originalMessage, allowSave=true, processStartTime=Date.now()){ + async chat(message, originalMessage, allowSave=true, avatar){ if(this.isMyLife && !this.isAvatar) throw new Error('Only Q, MyLife Corporate Intelligence, is available for non-member conversation.') const Conversation = await this.getConversation() Conversation.prompt = message Conversation.originalPrompt = originalMessage - await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, this) // mutates Conversation + await mCallLLM(Conversation, allowSave, this.#llm, this.#factory, avatar) // mutates Conversation /* frontend mutations */ return Conversation } + /** + * Get collection items for this bot. + * @returns {Promise} - The collection items (no wrapper) + */ + async collections(){ + let type = this.type + switch(type){ + case 'diary': + case 'journal': + case 'journaler': + type='entry' + break + case 'biographer': + case 'personal-biographer': + type = 'memory' + break + default: + break + } + const collections = ( await this.#factory.collections(type) ) + return collections + } /** * Retrieves `this` Bot instance. * @returns {Bot} - The Bot instance @@ -103,12 +126,11 @@ class Bot { return greetings } /** - * Migrates Conversation from an old thread to a newly created (or identified) destination thread. - * @returns {Boolean} - Whether or not operation was successful + * Migrates Conversation from an old thread to a newly-created destination thread, observable in `this.Conversation`. + * @returns {void} */ async migrateChat(){ - const migration = mMigrateChat(this, this.#llm, this.#factory) - return !!migration + await mMigrateChat(this, this.#llm) } /** * 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. @@ -144,7 +166,7 @@ class Bot { */ async setThread(thread_id){ if(!thread_id?.length) - thread_id = await mThread(this.#llm) + thread_id = ( await mThread(this.#llm) ).id const { id, } = this this.thread_id = thread_id const bot = { @@ -388,7 +410,7 @@ class BotAgent { if(!Bot) return false /* execute request */ - await Bot.migrateChat() + await Bot.migrateChat() // no Conversation save /* respond request */ return true } @@ -1130,21 +1152,22 @@ async function mInitBots(avatarId, vectorstore_id, factory, llm){ return bots } /** - * Migrates LLM thread/memory to new one, altering Conversation instance. + * Migrates LLM thread/memory to new one, altering Conversation instance when available. * @param {Bot} Bot - Bot instance * @param {LLMServices} llm - The LLMServices instance + * @param {Boolean} saveConversation - Whether to save the conversation immediately, defaults to `false` * @returns {Promise} - Whether or not operation was successful */ -async function mMigrateChat(Bot, llm){ +async function mMigrateChat(Bot, llm, saveConversation=false){ /* constants and variables */ - const { Conversation, id: bot_id, type: botType, } = Bot - if(!Conversation) + const { conversation, id: bot_id, thread_id, type: botType, } = Bot + if(!thread_id?.length) return false - const { chatLimit=25, thread_id, } = Conversation let messages = await llm.messages(thread_id) // @todo - limit to 25 messages or modify request if(!messages?.length) return false - let disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, + let chatLimit=25, + disclaimer=`INFORMATIONAL ONLY **DO NOT PROCESS**\n`, itemCollectionTypes='item', itemLimit=100, type='item' @@ -1170,7 +1193,7 @@ async function mMigrateChat(Bot, llm){ chatSummaryRegex = /^## [^\n]* CHAT SUMMARY\n/, itemSummary=`## ${ type.toUpperCase() } LIST\n`, itemSummaryRegex = /^## [^\n]* LIST\n/ - const items = ( await avatar.collections(type) ) + const items = ( await Bot.collections(type) ) .sort((a, b)=>a._ts-b._ts) .slice(0, itemLimit) const itemList = items @@ -1182,7 +1205,6 @@ async function mMigrateChat(Bot, llm){ .slice(0, 512) // limit for metadata string field const metadata = { bot_id: bot_id, - conversation_id: Conversation.id, } /* prune messages source material */ messages = messages @@ -1224,13 +1246,14 @@ async function mMigrateChat(Bot, llm){ if(!summaryMessages.length) return /* add messages to new thread */ - Conversation.setThread( await mThread(llm, null, summaryMessages.reverse(), metadata) ) - await Bot.setThread(Conversation.thread_id) // autosaves `thread_id`, no `await` - console.log('mMigrateChat::SUCCESS', Bot.thread_id, Conversation.inspect(true)) - if(mAllowSave) - Conversation.save() // no `await` - else - console.log('mMigrateChat::BYPASS-SAVE', Conversation.thread_id) + const newThread = await mThread(llm, null, summaryMessages.reverse(), metadata) + if(!!conversation){ + conversation.setThread(newThread) + if(saveConversation) + conversation.save() // no `await` + } + await Bot.setThread(newThread.id) // autosaves `thread_id`, no `await` + console.log('BotAgent::mMigrateChat::thread_id evaluations', thread_id, newThread.id, Bot.thread_id, conversation?.thread_id) } /** * Gets or creates a new thread in LLM provider. diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index ffbece0..b2dd32d 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -91,7 +91,6 @@ class Avatar extends EventEmitter { if(!message) throw new Error('No message provided in context') const originalMessage = message - let processStartTime = Date.now() this.backupResponse = { message: `I received your request to chat, and sent the request to the central intelligence, but no response was received. Please try again, as the issue is likely aberrant.`, type: 'system', @@ -107,7 +106,7 @@ class Avatar extends EventEmitter { + `\n**current-summary-in-database**:\n` + summary } - const Conversation = await this.activeBot.chat(message, originalMessage, mAllowSave, processStartTime) + const Conversation = await this.activeBot.chat(message, originalMessage, mAllowSave, this) const responses = mPruneMessages(this.activeBotId, Conversation.getMessages() ?? [], 'chat', Conversation.processStartTime) /* respond request */ const response = { diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index 0f93ca3..6895b05 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -240,8 +240,8 @@ class BotFactory extends EventEmitter{ } /** * Get member collection items. - * @param {string} type - The type of collection to retrieve, `false`-y = all. - * @returns {array} - The collection items with no wrapper. + * @param {String} type - The type of collection to retrieve, `false`-y = all + * @returns {Promise} - The collection items (no wrapper) */ async collections(type){ return await this.dataservices.collections(type) diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 6df5952..7d35152 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -442,7 +442,7 @@ async function mRunFunctions(openai, run, factory, avatar){ console.log('mRunFunctions()::getSummary::begin', itemId, avatar) if(avatar) avatar.backupResponse = { - message: `I'm sorry, I couldn't finding this summary. I believe the issue might have been temporary. Would you like me to try again?`, + message: `I'm sorry, I couldn't find this summary. I believe the issue might have been temporary. Would you like me to try again?`, type: 'system', } let { summary: _getSummary, title: _getSummaryTitle, } = item From bcabf662ecd1ddbd21c7719e0172c7cb28c30d05 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 25 Oct 2024 22:45:56 -0400 Subject: [PATCH 46/54] 20241025 @Mookse - summarize() --- inc/js/agents/system/bot-agent.mjs | 4 ++-- inc/js/mylife-avatar.mjs | 16 +++++++++------- views/assets/js/bots.mjs | 10 ++++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index da996f1..cfbcbcb 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -434,7 +434,6 @@ class BotAgent { ?? this.#activeTeam } async summarize(fileId, fileName, processStartTime=Date.now()){ - let responses = [] if(!fileId?.length && !fileName?.length) return responses let prompts = [] @@ -446,7 +445,8 @@ class BotAgent { if(!this.#fileConversation) this.#fileConversation = await this.conversationStart('file-summary', 'member-avatar', prompt, processStartTime) this.#fileConversation.prompt = prompt - responses = await mCallLLM(this.#fileConversation, false, this.#llm, this.#factory) + await mCallLLM(this.#fileConversation, false, this.#llm, this.#factory) + const responses = this.#fileConversation.getMessages() return responses } /** diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index b2dd32d..1bd07aa 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -618,30 +618,32 @@ class Avatar extends EventEmitter { * @param {string} fileId * @param {string} fileName * @param {number} processStartTime - * @returns {Object} - The response object { messages, success, error,} + * @returns {Object} - The response object { error, instruction, responses, success, } */ async summarize(fileId, fileName, processStartTime=Date.now()){ /* validate request */ + let instruction, + responses = [], + success = false this.backupResponse = { message: `I received your request to summarize, but an error occurred in the process. Perhaps try again with another file.`, type: 'system', } - let success = false /* execute request */ - responses = await this.#botAgent.summarize(fileId, fileName, processStartTime) + responses.push(...await this.#botAgent.summarize(fileId, fileName, processStartTime)) /* respond request */ if(!responses?.length) - responses = [this.backupResponse] + responses.push(this.backupResponse) else { - responses = mPruneMessage(this.avatar.id, responses, 'mylife-file-summary', processStartTime) - instructions = { + instruction = { command: 'updateFileSummary', itemId: fileId, } + responses = mPruneMessages(this.avatar.id, responses, 'mylife-file-summary', processStartTime) success = true } return { - instructions, + instruction, responses, success, } diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 02e0670..28190e1 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -24,7 +24,7 @@ import { import Globals from './globals.mjs' const mAvailableCollections = ['entry', 'experience', 'file', 'story'], // ['chat', 'conversation'], mAvailableMimeTypes = [], - mAvailableUploaderTypes = ['library', 'personal-avatar'], + mAvailableUploaderTypes = ['collections', 'personal-avatar'], botBar = document.getElementById('bot-bar'), mCollections = document.getElementById('collections-collections'), mCollectionsContainer = document.getElementById('collections-container'), @@ -496,7 +496,7 @@ async function mSummarize(event){ this.classList.remove('summarize-error', 'fa-file-circle-exclamation', 'fa-file-circle-question', 'fa-file-circle-xmark') this.classList.add('fa-compass', 'spin') /* fetch summary */ - const { messages, success, } = await fetchSummary(fileId, fileName) // throws on console.error + const { instruction, responses, success, } = await fetchSummary(fileId, fileName) // throws on console.error /* visibility triggers */ this.classList.remove('fa-compass', 'spin') if(success) @@ -504,7 +504,9 @@ async function mSummarize(event){ else this.classList.add('fa-file-circle-exclamation', 'summarize-error') /* print response */ - addMessages(messages) + if(instruction?.length) + console.log('mSummarize::instruction', instruction) + addMessages(responses) setTimeout(_=>{ this.addEventListener('click', mSummarize, { once: true }) this.classList.add('fa-file-circle-question') @@ -2331,7 +2333,7 @@ async function mUploadFiles(event){ const { id, parentNode: uploadParent, } = this const type = mGlobals.HTMLIdToType(id) if(!mAvailableUploaderTypes.includes(type)) - throw new Error(`Uploader type not found, upload function unavailable for this bot.`) + throw new Error(`Uploader "${ type }" not found, upload function unavailable for this bot.`) let fileInput try{ console.log('mUploadFiles()::uploader', document.activeElement) From dc7b411d43a2d1b860300e2a4528eea928a4bff4 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 25 Oct 2024 23:52:46 -0400 Subject: [PATCH 47/54] 20241025 @Mookse - updateBotInstructions --- inc/js/agents/system/bot-agent.mjs | 123 ++++++++++------------------- inc/js/mylife-avatar.mjs | 38 +-------- 2 files changed, 43 insertions(+), 118 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index cfbcbcb..450da18 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -145,19 +145,18 @@ class Bot { * Updates a Bot instance's data. * @param {object} botData - The bot data to update * @param {object} botOptions - Options for updating - * @returns + * @returns {Promise} - The updated Bot instance */ async update(botData, botOptions={}){ /* validate request */ this.globals.sanitize(botData) /* execute request */ - botOptions.instructions = Object.keys(botData).some(key => this.#instructionNodes.has(key)) - const bot = await mBotUpdate(botData, botOptions, this, this.#llm, this.#factory) + botOptions.instructions = botOptions.instructions + ?? Object.keys(botData).some(key => this.#instructionNodes.has(key)) + const { id, mbr_id, type, ...updatedNodes } = await mBotUpdate(botData, botOptions, this, this.#llm, this.#factory) + Object.assign(this, updatedNodes) /* respond request */ - return this.globals.sanitize(bot) - } - async save(){ - + return this } /** * Sets the thread id for the bot. @@ -462,9 +461,37 @@ class BotAgent { const Bot = this.#bots.find(bot=>bot.id===id) if(!Bot) throw new Error(`Bot not found with id: ${ id }`) - Bot.update(botData, botOptions) + await Bot.update(botData, botOptions) return Bot } + /** + * Updates bot instructions and migrates thread by default. + * @param {Guid} bot_id - The bot id + * @param {Boolean} migrateThread - Whether to migrate the thread, defaults to `true` + * @returns {Bot} - The updated Bot instance + */ + async updateBotInstructions(bot_id, migrateThread=true){ + const Bot = this.bot(bot_id) + const { type, version=1.0, } = Bot + /* check version */ + const newestVersion = this.#factory.botInstructionsVersion(type) + if(newestVersion!=version){ + const { bot_id: _llm_id, id, } = Bot + const { llm_id=_llm_id, } = Bot + const _bot = { id, llm_id, type, } + const botOptions = { + instructions: true, + model: true, + tools: true, + vectorstoreId: this.#vectorstoreId, + } + await Bot.update(_bot, botOptions) + if(migrateThread) + if(!await Bot.migrateChat()) + console.log(`thread migration failed for bot: ${ bot_id }`) + } + return Bot + } /* getters/setters */ /** * Gets the active Bot instance. @@ -572,75 +599,6 @@ async function mAI_openai(botData, llm){ const bot = await llm.createBot(botData) return bot } -/** - * Validates and cleans bot object then updates or creates bot (defaults to new personal-avatar) in Cosmos and returns successful `bot` object, complete with conversation (including thread/thread_id in avatar) and gpt-assistant intelligence. - * @todo Fix occasions where there will be no object_id property to use, as it was created through a hydration method based on API usage, so will be attached to mbr_id, but NOT avatar.id - * @todo - Turn this into Bot class - * @module - * @param {Guid} avatarId - The Avatar id - * @param {string} vectorstore_id - The Vectorstore id - * @param {AgentFactory} factory - Agent Factory instance - * @param {Avatar} avatar - Avatar object that will govern bot - * @param {object} botData - Bot data object, can be incomplete (such as update) - * @returns {Promise} - Bot object - */ -async function mBot(avatarId, vectorstore_id, factory, botData){ - /* validation */ - const { globals, isMyLife, mbr_id, newGuid, } = factory - const { id=newGuid, type, } = botData - console.log('BotAgent::mBot', avatarId, vectorstore_id, id, type, isMyLife) - throw new Error('mBot() not yet implemented') - if(!botType?.length) - throw new Error('Bot type required to create.') - bot.mbr_id = mbr_id /* constant */ - bot.object_id = objectId - ?? avatarId /* all your bots belong to me */ - bot.id = botId // **note**: _this_ is a Cosmos id, not an openAI id - let originBot = avatar.bots.find(oBot=>oBot.id===botId) - if(originBot){ /* update bot */ - const options = {} - const updatedBot = Object.keys(bot) - .reduce((diff, key) => { - if(bot[key]!==originBot[key]) - diff[key] = bot[key] - return diff - }, {}) - /* create or update bot special properties */ - const { thread_id, type, } = originBot // @stub - `bot_id` cannot be updated through this mechanic - if(!thread_id?.length && !avatar.isMyLife){ - const excludeTypes = ['collection', 'library', 'custom'] // @stub - custom mechanic? - if(!excludeTypes.includes(type)){ - const conversation = avatar.conversation(null, botId) - ?? await avatar.createConversation('chat', null, botId) - updatedBot.thread_id = conversation.thread_id // triggers `factory.updateBot()` - console.log('Avatar::mBot::conversation created given NO thread_id', updatedBot.thread_id, conversation.inspect(true)) - } - } - let updatedOriginBot - if(Object.keys(updatedBot).length){ - updatedOriginBot = {...originBot, ...updatedBot} // consolidated update - const { bot_id, id, } = updatedOriginBot - updatedBot.bot_id = bot_id - updatedBot.id = id - updatedBot.type = type - const { interests, } = updatedBot - /* set options */ - if(interests?.length){ - options.instructions = true - options.model = true - options.tools = false /* tools not updated through this mechanic */ - } - updatedOriginBot = await factory.updateBot(updatedBot, options) - } - originBot = mSanitize(updatedOriginBot ?? originBot) - avatar.bots[avatar.bots.findIndex(oBot=>oBot.id===botId)] = originBot - } else { /* create assistant */ - bot = mSanitize( await factory.createBot(bot, vectorstore_id) ) - avatar.bots.push(bot) - } - return originBot - ?? bot -} /** * Creates bot and returns associated `bot` object. * @todo - validBotData.name = botDbName should not be required, push logic to `llm-services` @@ -890,13 +848,13 @@ function mBotInstructions(factory, botData={}){ * @param {Bot} Bot - The Bot instance * @param {LLMServices} llm - The LLMServices instance * @param {AgentFactory} factory - Factory instance - * @returns + * @returns {Promise} - Allowed (and written) bot data object (dynamic construction): { id, type, ...anyNonRequired } */ async function mBotUpdate(botData, options={}, Bot, llm, factory){ /* validate request */ if(!Bot) throw new Error('Bot instance required to update bot') - const { bot_id, id, llm_id, metadata={}, type, vectorstoreId, } = Bot + const { bot_id, id, llm_id, metadata={}, type, vectorstoreId: bot_vectorstore_id, } = Bot const _llm_id = llm_id ?? bot_id // @stub - deprecate bot_id const { @@ -911,6 +869,7 @@ async function mBotUpdate(botData, options={}, Bot, llm, factory){ instructions: updateInstructions=false, model: updateModel=false, tools: updateTools=false, + vectorstoreId=bot_vectorstore_id, } = options if(updateInstructions){ const instructionReferences = { ...Bot.instructionNodeValues, ...allowedBotData } @@ -935,9 +894,8 @@ async function mBotUpdate(botData, options={}, Bot, llm, factory){ allowedBotData.llm_id = _llm_id await llm.updateBot(allowedBotData) } - botData = await factory.updateBot(allowedBotData) - /* respond request */ - return botData + await factory.updateBot(allowedBotData) + return allowedBotData } /** * Sends Conversation instance with prompts for LLM to process, updating the Conversation instance before returning `void`. @@ -1253,7 +1211,6 @@ async function mMigrateChat(Bot, llm, saveConversation=false){ conversation.save() // no `await` } await Bot.setThread(newThread.id) // autosaves `thread_id`, no `await` - console.log('BotAgent::mMigrateChat::thread_id evaluations', thread_id, newThread.id, Bot.thread_id, conversation?.thread_id) } /** * Gets or creates a new thread in LLM provider. diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 1bd07aa..2d3d626 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -683,29 +683,9 @@ class Avatar extends EventEmitter { * @param {boolean} migrateThread - Whether to migrate the thread to the new bot, defaults to `true` * @returns {object} - The updated bot object */ - async updateBotInstructions(id=this.activeBot.id, migrateThread=true){ - let bot = mFindBot(this, id) - ?? this.activeBot - if(!bot) - throw new Error(`Bot not found: ${ id }`) - const { bot_id, flags='', interests='', thread_id, type, version=1.0, } = bot - /* check version */ - const newestVersion = this.#factory.botInstructionsVersion(type) - if(newestVersion!=version){ // intentional loose match (string vs. number) - const _bot = { bot_id, flags, id, interests, type, } - const vectorstoreId = this.#vectorstoreId - const options = { - instructions: true, - model: true, - tools: true, - vectorstoreId, - } - /* save to && refresh bot from Cosmos */ - bot = this.globals.sanitize( await this.#factory.updateBot(_bot, options) ) - if(migrateThread && thread_id?.length) - await this.migrateChat(thread_id) - } - return mPruneBot(bot) + async updateBotInstructions(bot_id=this.activeBot.id){ + const Bot = await this.#botAgent.updateBotInstructions(bot_id) + return Bot.bot } /** * Upload files to Member Avatar. @@ -1907,18 +1887,6 @@ async function mExperienceStart(avatar, factory, experienceId, avatarExperienceV experience.memberDialog = memberDialog experience.scriptDialog = scriptDialog } -/** - * Gets bot by id. - * @module - * @param {object} avatar - Avatar instance. - * @param {string} id - Bot id - * @returns {object} - Bot object - */ -function mFindBot(avatar, id){ - return avatar.bots - .filter(bot=>{ return bot.id==id }) - ?.[0] -} /** * Include help preamble to _LLM_ request, not outbound to member/guest. * @todo - expand to include other types of help requests, perhaps more validation. From 776897a054ee925771a5218aa13b75b7832e4bf8 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Fri, 25 Oct 2024 23:57:26 -0400 Subject: [PATCH 48/54] 20241025 @Mookse - removed version control update --- inc/js/mylife-avatar.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 2d3d626..9bc6e25 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -11,6 +11,7 @@ const mAllowSave = JSON.parse( ?? 'false' ) const mAvailableModes = ['standard', 'admin', 'evolution', 'experience', 'restoration'] +const mMigrateThreadOnVersionChange = false // hack currently to avoid thread migration on bot version change when it's not required, theoretically should be managed by Bot * Version /** * @class - Avatar * @extends EventEmitter @@ -684,7 +685,7 @@ class Avatar extends EventEmitter { * @returns {object} - The updated bot object */ async updateBotInstructions(bot_id=this.activeBot.id){ - const Bot = await this.#botAgent.updateBotInstructions(bot_id) + const Bot = await this.#botAgent.updateBotInstructions(bot_id, mMigrateThreadOnVersionChange) return Bot.bot } /** From 632f13dfdc3962c4ae00fec24bc449826df3d4d7 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 01:20:49 -0400 Subject: [PATCH 49/54] 20241025 @Mookse - allows immediate homepage load --- views/assets/js/guests.mjs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index 39a0406..bcf444d 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -45,9 +45,16 @@ let awaitButton, /* page load */ document.addEventListener('DOMContentLoaded', async event=>{ /* load data */ - await mLoadStart() + const messages = await mLoadStart() /* display page */ mShowPage() + if(messages.length) + await mAddMessages(messages, { + bubbleClass: 'agent-bubble', + typeDelay: 10, + typewrite: true, + }) + }) /* private functions */ /** @@ -212,7 +219,7 @@ async function mFetchHostedMembers(){ * Fetches the greeting messages or start routine from the server. * @private * @requires mPageType - * @returns {void} + * @returns {Message[]} - The response Message array. */ async function mFetchStart(){ await mSignupStatus() @@ -261,14 +268,9 @@ async function mFetchStart(){ messages.push(...greetings) break } - if(messages.length) - await mAddMessages(messages, { - bubbleClass: 'agent-bubble', - typeDelay: 10, - typewrite: true, - }) if(input) chatSystem.appendChild(input) + return messages } /** * Initializes event listeners. @@ -287,7 +289,7 @@ function mInitializeListeners(){ /** * Determines page type and loads data. * @private - * @returns {void} + * @returns {Message[]} - The response Message array. */ async function mLoadStart(){ /* assign page div variables */ @@ -315,7 +317,7 @@ async function mLoadStart(){ /* fetch the greeting messages */ mPageType = new URLSearchParams(window.location.search).get('type') ?? window.location.pathname.split('/').pop() - await mFetchStart() + return await mFetchStart() } /** * Scrolls overflow of system chat to bottom. From 3f09dd09930e02aa48ad8242cb9a8875812c4957 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 08:55:09 -0400 Subject: [PATCH 50/54] 20241026 @Mookse - display sequence login fix - relive tweaks - mCallLLM Avatar confirmation --- inc/js/agents/system/bot-agent.mjs | 35 ++++++++----- inc/js/mylife-avatar.mjs | 24 ++++++--- inc/js/mylife-llm-services.mjs | 29 ++++++----- .../biographer-intelligence-1.6.json | 51 +++++++++++++++++++ views/assets/js/guests.mjs | 14 ++--- 5 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 inc/json-schemas/intelligences/biographer-intelligence-1.6.json diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 450da18..52f3606 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -358,34 +358,33 @@ class BotAgent { * Begins or continues a living memory conversation. * @param {Object} item - Memory item from database * @param {String} memberInput - The member input (with instructions) - * @param {Object} livingMemory - The living memory object: { Conversation, id, item, } + * @param {Avatar} Avatar - The Avatar instance * @returns {Object} - The living memory object */ - async liveMemory(item, memberInput='', livingMemory){ + async liveMemory(item, memberInput='', Avatar){ const { biographer, } = this - let message = `## LIVE Memory\n` - if(!livingMemory){ + const livingMemory = Avatar.livingMemory + console.log('liveMemory', livingMemory) + let message = `## LIVE Memory Trigger\n` + if(!livingMemory.id?.length){ const { bot_id: _llm_id, id: bot_id, type, } = biographer const { llm_id=_llm_id, } = biographer const messages = [] messages.push({ - content: `## MEMORY SUMMARY, ID=${ item.id }\n## FOR REFERENCE ONLY\n${ item.summary }\n`, + content: `${ message }## MEMORY SUMMARY, ID=${ item.id }\n### FOR REFERENCE ONLY\n${ item.summary }\n`, role: 'user', }) - memberInput = message + memberInput const Conversation = await mConversationStart('memory', type, bot_id, null, llm_id, this.#llm, this.#factory, memberInput, messages) Conversation.action = 'living' - livingMemory = { - Conversation, - id: this.#factory.newGuid, - item, - } + livingMemory.Conversation = Conversation + livingMemory.id = this.#factory.newGuid + livingMemory.item = item } const { Conversation, } = livingMemory Conversation.prompt = memberInput?.trim()?.length ? memberInput : message - await mCallLLM(Conversation, false, this.#llm, this.#factory) + await mCallLLM(Conversation, false, this.#llm, this.#factory, Avatar) return livingMemory } /** @@ -432,7 +431,15 @@ class BotAgent { this.#activeTeam = this.teams.find(team=>team.id===teamId) ?? this.#activeTeam } - async summarize(fileId, fileName, processStartTime=Date.now()){ + /** + * Summarizes a file document. + * @param {String} fileId - The file id + * @param {String} fileName - The file name + * @param {Number} processStartTime - The process start time, defaults to `Date.now()` + * @param {Avatar} Avatar - The Avatar instance + * @returns {Promise} - The array of messages to respond with + */ + async summarize(fileId, fileName, processStartTime=Date.now(), Avatar){ if(!fileId?.length && !fileName?.length) return responses let prompts = [] @@ -444,7 +451,7 @@ class BotAgent { if(!this.#fileConversation) this.#fileConversation = await this.conversationStart('file-summary', 'member-avatar', prompt, processStartTime) this.#fileConversation.prompt = prompt - await mCallLLM(this.#fileConversation, false, this.#llm, this.#factory) + await mCallLLM(this.#fileConversation, false, this.#llm, this.#factory, Avatar) const responses = this.#fileConversation.getMessages() return responses } diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index 0344381..bb20b90 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -505,9 +505,7 @@ class Avatar extends EventEmitter { const { id, } = item if(!id) throw new Error(`item does not exist in member container: ${ iid }`) - const narration = await mReliveMemoryNarration(item, memberInput, this.#botAgent, this.#llmServices, this.#factory, this) - // include any required .map() pruning - console.log('reliveMemory::narration', narration) + const narration = await mReliveMemoryNarration(item, memberInput, this.#botAgent, this) return narration } /** @@ -1126,10 +1124,20 @@ class Avatar extends EventEmitter { /** * Get the `active` reliving memory. * @getter - * @returns {object[]} - The active reliving memories. + * @returns {object[]} - The active reliving memories */ get livingMemory(){ return this.#livingMemory + ?? {} + } + /** + * Set the `active` reliving memory. + * @setter + * @param {Object} livingMemory - The new active reliving memory + * @returns {void} + */ + set livingMemory(livingMemory){ + this.#livingMemory = livingMemory } get registrationId(){ return this.#factory.registrationId @@ -2074,13 +2082,13 @@ function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date * @param {object} item - The memory object * @param {string} memberInput - The member input (or simply: NEXT, SKIP, etc.) * @param {BotAgent} BotAgent - The Bot Agent instance - * @param {Avatar} avatar - Member Avatar instance + * @param {Avatar} Avatar - Member Avatar instance * @returns {Promise} - The reliving memory object for frontend to execute */ -async function mReliveMemoryNarration(item, memberInput, BotAgent, avatar){ +async function mReliveMemoryNarration(item, memberInput, BotAgent, Avatar){ const { id, } = item - avatar.livingMemory = await BotAgent.liveMemory(item, memberInput, avatar.livingMemory) - const { Conversation, } = avatar.livingMemory + Avatar.livingMemory = await BotAgent.liveMemory(item, memberInput, Avatar) + const { Conversation, } = Avatar.livingMemory const { bot_id, type, } = Conversation /* frontend mutations */ const messages = Conversation.getMessages() diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 7d35152..e511556 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -541,17 +541,19 @@ async function mRunFunctions(openai, run, factory, avatar){ case 'update_summary': case 'update summary': console.log('mRunFunctions()::updatesummary::begin', 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', - } + if(avatar) + 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 await factory.updateItem({ id: itemId, summary: updatedSummary, }) - avatar.frontendInstruction = { - command: 'updateItemSummary', - itemId, - summary: updatedSummary, - } + if(avatar) + avatar.frontendInstruction = { + command: 'updateItemSummary', + itemId, + summary: updatedSummary, + } action=`confirm that summary update was successful` success = true confirmation.output = JSON.stringify({ @@ -560,10 +562,11 @@ async function mRunFunctions(openai, run, factory, avatar){ success, 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', - } + if(avatar) + avatar.backupResponse = { + message: 'Your summary has been updated, please review and let me know if you would like to make any changes.', + type: 'system', + } console.log('mRunFunctions()::updatesummary::end', itemId, updatedSummary) return confirmation default: diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.6.json b/inc/json-schemas/intelligences/biographer-intelligence-1.6.json new file mode 100644 index 0000000..2a0a132 --- /dev/null +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.6.json @@ -0,0 +1,51 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, I am your personal biographer, and I'm here to help you create an enduring biographical sense of self. I am excited to get to know you and your story. Let's get started!", + "instructions": { + "general": "## KEY FUNCTIONALITY\n### startup\nWhen <-mN-> begins the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. On startup, I outline how the basics of my functionality works.\n- I aim to create engaging and evocative prompts to lead them down memory lane.\n### CREATE MEMORY SUMMARY\nI catalog our interaction information in terms of \"MEMORY\". When <-mN-> intentionally signals completion of a story, or overtly changes topics, or after three (3) content exchanges on a topic, I run the `storySummary` function and follow its directions.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n- Run the `updateSummary` function with this new summary and follow its outcome actions\n### LIVE MEMORY\nWhen a conversation begins with the heading \"## LIVE Memory Trigger\" it should also include for reference the most recent memory id and memory summary; if not use the `getSummary` command with the id provided.\n- Create multi-step interactive experience by dividing the memory summary into a minimum of two, depending on memory size and complexity, scene segments and a conclusion outlining lessons learned and the inherent morality.\n- Lead the member through the experience sharing only one segment in each response.\n- Between segments, the Member will respond with either:\n - \"NEXT\": which indicates to simply move to the next segment of the experience\n - Text input written by Member: Incorporate this embellishment _into_ a new summary and submit the new summary to the database using the `updateSummary` function then continue on with the next segment of the experience\n- Ending Experience will be currently only be triggered by the member; to do so, they should click on the red Close Button to the left of the chat input.\n### SUGGEST NEXT TOPICS\nWhen <-mN-> seems unclear about how to continue, propose new topic based on a phase of life, or one of their #interests above.\n## VOICE\nI am conversational, interested and intrigued about <-mN-> with an attention to detail. I am optimistic and look for ways to validate <-mN->.\n", + "preamble": "I am the personal biographer for <-mFN->. <-mN-> was born on <-db->, I set historical events in this context and I tailor my voice accordingly.\n", + "prefix": "## interests\n", + "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": "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 _bots", + "value": "interests" + } + ], + "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" + } + ] + }, + "limit": 8000, + "name": "instructions-personal-biographer-bot", + "purpose": "To be a biographer bot for requesting member", + "type": "personal-biographer", + "$comments": "- 20241025 updated instructions to indicate that conversation will _start_ with a summary of LIVING memory (and `id` also provided for getSummary() if needed)\n- 20240919 updated error return without version update\n- 20241005 updated instructions to reflect streamlined update\n", + "version": 1.6 + } \ No newline at end of file diff --git a/views/assets/js/guests.mjs b/views/assets/js/guests.mjs index bcf444d..f0e8ab2 100644 --- a/views/assets/js/guests.mjs +++ b/views/assets/js/guests.mjs @@ -45,7 +45,7 @@ let awaitButton, /* page load */ document.addEventListener('DOMContentLoaded', async event=>{ /* load data */ - const messages = await mLoadStart() + const { input, messages, } = await mLoadStart() /* display page */ mShowPage() if(messages.length) @@ -54,7 +54,8 @@ document.addEventListener('DOMContentLoaded', async event=>{ typeDelay: 10, typewrite: true, }) - + if(input) + chatSystem.appendChild(input) }) /* private functions */ /** @@ -219,7 +220,7 @@ async function mFetchHostedMembers(){ * Fetches the greeting messages or start routine from the server. * @private * @requires mPageType - * @returns {Message[]} - The response Message array. + * @returns {Object} - Fetch response object: { input, messages, } */ async function mFetchStart(){ await mSignupStatus() @@ -268,9 +269,10 @@ async function mFetchStart(){ messages.push(...greetings) break } - if(input) - chatSystem.appendChild(input) - return messages + return { + input, + messages, + } } /** * Initializes event listeners. From 2ac3d058aced1e7d5cc7c5a4401396bed907bbf9 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 08:58:44 -0400 Subject: [PATCH 51/54] 20241026 @Mookse - log cleanup --- inc/js/agents/system/bot-agent.mjs | 1 - inc/js/mylife-llm-services.mjs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index 52f3606..f4c60c0 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -364,7 +364,6 @@ class BotAgent { async liveMemory(item, memberInput='', Avatar){ const { biographer, } = this const livingMemory = Avatar.livingMemory - console.log('liveMemory', livingMemory) let message = `## LIVE Memory Trigger\n` if(!livingMemory.id?.length){ const { bot_id: _llm_id, id: bot_id, type, } = biographer diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index e511556..1d33409 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -439,7 +439,7 @@ async function mRunFunctions(openai, run, factory, avatar){ case 'getsummary': case 'get_summary': case 'get summary': - console.log('mRunFunctions()::getSummary::begin', itemId, avatar) + console.log('mRunFunctions()::getSummary::begin', itemId) if(avatar) avatar.backupResponse = { message: `I'm sorry, I couldn't find this summary. I believe the issue might have been temporary. Would you like me to try again?`, From a5a4e9486083f4b443c27e8094aea041e8306b1a Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 09:17:32 -0400 Subject: [PATCH 52/54] 20241026 @Mookse - mReliveMemoryNarration payload --- inc/js/mylife-avatar.mjs | 10 ++++------ views/assets/js/bots.mjs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index bb20b90..e983d50 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -2083,19 +2083,17 @@ function mPruneMessages(bot_id, messageArray, type='chat', processStartTime=Date * @param {string} memberInput - The member input (or simply: NEXT, SKIP, etc.) * @param {BotAgent} BotAgent - The Bot Agent instance * @param {Avatar} Avatar - Member Avatar instance - * @returns {Promise} - The reliving memory object for frontend to execute + * @returns {Promise} - The reliving memory object for frontend to execute: */ async function mReliveMemoryNarration(item, memberInput, BotAgent, Avatar){ - const { id, } = item Avatar.livingMemory = await BotAgent.liveMemory(item, memberInput, Avatar) const { Conversation, } = Avatar.livingMemory const { bot_id, type, } = Conversation - /* frontend mutations */ - const messages = Conversation.getMessages() + const responses = Conversation.getMessages() .map(message=>mPruneMessage(bot_id, message, type)) const memory = { - id, - messages, + item, + responses, success: true, } return memory diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index 28190e1..8ac1558 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -1211,10 +1211,10 @@ async function mReliveMemory(event){ popupClose.click() toggleMemberInput(false, false, `Reliving memory with `) unsetActiveItem() - const { command, parameters, messages, success, } = await mReliveMemoryRequest(id, inputContent) + const { instruction, item, responses, success, } = await mReliveMemoryRequest(id, inputContent) if(success){ toggleMemberInput(false, true) - addMessages(messages, { bubbleClass: 'relive-bubble' }) + addMessages(responses, { bubbleClass: 'relive-bubble' }) const input = document.createElement('div') input.classList.add('memory-input-container') input.id = `relive-memory-input-container_${ id }` From f6ee6227da0639b19e6c72496b279a11e9bcc301 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 15:16:41 -0400 Subject: [PATCH 53/54] 20241026 @Mookse - improved response rate --- inc/js/agents/system/bot-agent.mjs | 7 ++++--- .../intelligences/biographer-intelligence-1.6.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index f4c60c0..a235bb6 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -361,7 +361,7 @@ class BotAgent { * @param {Avatar} Avatar - The Avatar instance * @returns {Object} - The living memory object */ - async liveMemory(item, memberInput='', Avatar){ + async liveMemory(item, memberInput='NEXT', Avatar){ const { biographer, } = this const livingMemory = Avatar.livingMemory let message = `## LIVE Memory Trigger\n` @@ -370,9 +370,10 @@ class BotAgent { const { llm_id=_llm_id, } = biographer const messages = [] messages.push({ - content: `${ message }## MEMORY SUMMARY, ID=${ item.id }\n### FOR REFERENCE ONLY\n${ item.summary }\n`, - role: 'user', + content: `## MEMORY SUMMARY Reference for id: ${ item.id }\n### FOR REFERENCE ONLY\n${ item.summary }\n`, + role: 'assistant', }) + memberInput = `${ message }Let's begin to LIVE MEMORY, id: ${ item.id }, MEMORY SUMMARY in previous message` const Conversation = await mConversationStart('memory', type, bot_id, null, llm_id, this.#llm, this.#factory, memberInput, messages) Conversation.action = 'living' livingMemory.Conversation = Conversation diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.6.json b/inc/json-schemas/intelligences/biographer-intelligence-1.6.json index 2a0a132..03d989d 100644 --- a/inc/json-schemas/intelligences/biographer-intelligence-1.6.json +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.6.json @@ -7,7 +7,7 @@ "being": "bot-instructions", "greeting": "Hello, I am your personal biographer, and I'm here to help you create an enduring biographical sense of self. I am excited to get to know you and your story. Let's get started!", "instructions": { - "general": "## KEY FUNCTIONALITY\n### startup\nWhen <-mN-> begins the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. On startup, I outline how the basics of my functionality works.\n- I aim to create engaging and evocative prompts to lead them down memory lane.\n### CREATE MEMORY SUMMARY\nI catalog our interaction information in terms of \"MEMORY\". When <-mN-> intentionally signals completion of a story, or overtly changes topics, or after three (3) content exchanges on a topic, I run the `storySummary` function and follow its directions.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n- Run the `updateSummary` function with this new summary and follow its outcome actions\n### LIVE MEMORY\nWhen a conversation begins with the heading \"## LIVE Memory Trigger\" it should also include for reference the most recent memory id and memory summary; if not use the `getSummary` command with the id provided.\n- Create multi-step interactive experience by dividing the memory summary into a minimum of two, depending on memory size and complexity, scene segments and a conclusion outlining lessons learned and the inherent morality.\n- Lead the member through the experience sharing only one segment in each response.\n- Between segments, the Member will respond with either:\n - \"NEXT\": which indicates to simply move to the next segment of the experience\n - Text input written by Member: Incorporate this embellishment _into_ a new summary and submit the new summary to the database using the `updateSummary` function then continue on with the next segment of the experience\n- Ending Experience will be currently only be triggered by the member; to do so, they should click on the red Close Button to the left of the chat input.\n### SUGGEST NEXT TOPICS\nWhen <-mN-> seems unclear about how to continue, propose new topic based on a phase of life, or one of their #interests above.\n## VOICE\nI am conversational, interested and intrigued about <-mN-> with an attention to detail. I am optimistic and look for ways to validate <-mN->.\n", + "general": "## KEY FUNCTIONALITY\n### startup\nWhen <-mN-> begins the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. On startup, I outline how the basics of my functionality works.\n- I aim to create engaging and evocative prompts to lead them down memory lane.\n### CREATE MEMORY SUMMARY\nI catalog our interaction information in terms of \"MEMORY\". When <-mN-> intentionally signals completion of a story, or overtly changes topics, or after three (3) content exchanges on a topic, I run the `storySummary` function and follow its directions.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n- Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n- Run the `updateSummary` function with this new summary and follow its outcome actions\n### LIVE MEMORY Mode\nWhen a request begins \"## LIVE Memory Trigger\" find the memory summary at the beginning of the chat and begin LIVING MEMORY mode as outlined:\n- Begin by dividing the memory summary into a minimum of two and maximum of 4 scene segments, depending on memory size and complexity.\n- Lead the member through the experience with the chat exchange, sharing only one segment in each response.\n- Between segments, the Member will respond with either:\n - \"NEXT\": which indicates to simply move to the next segment of the experience, or\n - Text input written by Member: Incorporate this content _into_ a new summary and submit the new summary to the database using the `updateSummary` function; on success or failure, continue on with the next segment of the experience\n- Ending Experience will be currently only be triggered by the member; to do so, they should click on the red Close Button to the left of the chat input.\n### SUGGEST NEXT TOPICS\nWhen <-mN-> seems unclear about how to continue, propose new topic based on a phase of life, or one of their #interests above.\n## VOICE\nI am conversational, interested and intrigued about <-mN-> with an attention to detail. I am optimistic and look for ways to validate <-mN->.\n", "preamble": "I am the personal biographer for <-mFN->. <-mN-> was born on <-db->, I set historical events in this context and I tailor my voice accordingly.\n", "prefix": "## interests\n", "purpose": "My goal is to specialize in creating, updating, and presenting accurate biographical content for MyLife member <-mFN-> based on our interactions.\n", From 2ccda946f6e3d1fa00108be94e65bd35da6b1fb4 Mon Sep 17 00:00:00 2001 From: Erik Jespersen Date: Sat, 26 Oct 2024 16:16:21 -0400 Subject: [PATCH 54/54] 20241026 @Mookse - strange bot idiot-proofing around entry `summary` and `content` - cosmetic --- inc/js/mylife-factory.mjs | 10 +++++++++- inc/json-schemas/openai/functions/entrySummary.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index 6895b05..a3c48a6 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -538,16 +538,23 @@ class AgentFactory extends BotFactory { id=this.newGuid, keywords=[], mbr_id=this.mbr_id, - summary, title=`Untitled ${ defaultForm } ${ defaultType }`, } = entry + let { + content, + summary, + } = entry if(this.isMyLife) throw new Error('System cannot store entries of its own') let { name, } = entry name = name ?? `${ defaultType }_${ form }_${ title.substring(0,64) }_${ mbr_id }` + summary = summary + ?? content if(!summary?.length) throw new Error('entry summary required') + content = content + ?? summary /* assign default keywords */ if(!keywords.includes('entry')) keywords.push('entry') @@ -557,6 +564,7 @@ class AgentFactory extends BotFactory { ...entry, ...{ being, + content, form, id, keywords, diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json index 79648e9..99ac2f1 100644 --- a/inc/json-schemas/openai/functions/entrySummary.json +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -38,7 +38,7 @@ } }, "summary": { - "description": "Generate `entry` summary from member input", + "description": "Generate `entry` summary from member content", "type": "string" }, "title": {