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