diff --git a/inc/js/agents/system/bot-agent.mjs b/inc/js/agents/system/bot-agent.mjs index a235bb6..2159c30 100644 --- a/inc/js/agents/system/bot-agent.mjs +++ b/inc/js/agents/system/bot-agent.mjs @@ -2,7 +2,8 @@ const mBot_idOverride = process.env.OPENAI_MAHT_GPT_OVERRIDE const mDefaultBotTypeArray = ['personal-avatar', 'avatar'] const mDefaultBotType = mDefaultBotTypeArray[0] -const mDefaultGreetings = [] +const mDefaultGreeting = 'Hello! If you need to know how to get started, just ask!' +const mDefaultGreetings = ['Welcome to MyLife! I am here to help you!'] const mDefaultTeam = 'memory' const mRequiredBotTypes = ['personal-avatar'] const mTeams = [ @@ -28,16 +29,22 @@ class Bot { #collectionsAgent #conversation #factory + #feedback + #initialGreeting + #greetings #instructionNodes = new Set() #llm + #type constructor(botData, llm, factory){ - const { id=factory.newGuid, type=mDefaultBotType, } = botData + console.log(`bot pre-created`, this.feedback) this.#factory = factory this.#llm = llm - botData = this.globals.sanitize(botData) - Object.assign(this, botData) - this.id = id - this.type = type + const { feedback=[], greeting=mDefaultGreeting, greetings=mDefaultGreetings, type=mDefaultBotType, ..._botData } = botData + this.#feedback = feedback + this.#greetings = greetings + this.#initialGreeting = greeting + this.#type = type + Object.assign(this, this.globals.sanitize(_botData)) switch(type){ case 'diary': case 'journal': @@ -94,6 +101,29 @@ class Bot { const collections = ( await this.#factory.collections(type) ) return collections } + /** + * Submits message content and id feedback to bot. + * @param {String} message_id - LLM message id + * @param {Boolean} isPositive - Positive or negative feedback, defaults to `true` + * @param {String} message - Message content (optional) + * @returns {Object} - The feedback object + */ + async feedback(message_id, isPositive=true, message=''){ + if(!message_id?.length) + console.log('feedback message_id required') + this.#feedback.push(isPositive) + const botData = { feedback: this.#feedback, } + const botOptions = { instructions: false, } + this.update(botData, botOptions) // no `await` + if(message.length) + console.log(`feedback regarding agent message`, message) + const response = { + message, + message_id, + success: true, + } + return response + } /** * Retrieves `this` Bot instance. * @returns {Bot} - The Bot instance @@ -117,13 +147,17 @@ class Bot { /** * 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 + * @param {String} greetingPrompt - The prompt for the dynamic greeting + * @returns {String} - The greeting message string */ - async getGreeting(dynamic=false, llm, factory){ - const greetings = await mBotGreeting(dynamic, this, llm, factory) - return greetings + async greeting(dynamic=false, greetingPrompt='Greet me and tell me briefly what we did last'){ + if(!this.llm_id) + throw new Error('Bot initialized incorrectly: missing `llm_id` from database') + const greetings = dynamic + ? await mBotGreetings(this.thread_id, this.llm_id, greetingPrompt, this.#llm, this.#factory) + : this.greetings + const greeting = greetings[Math.floor(Math.random() * greetings.length)] + return greeting } /** * Migrates Conversation from an old thread to a newly-created destination thread, observable in `this.Conversation`. @@ -153,7 +187,7 @@ class Bot { /* execute request */ 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) + const { feedback, id, mbr_id, type, ...updatedNodes } = await mBotUpdate(botData, botOptions, this, this.#llm, this.#factory) Object.assign(this, updatedNodes) /* respond request */ return this @@ -199,6 +233,9 @@ class Bot { get globals(){ return this.#factory.globals } + get greetings(){ + return this.#greetings + } get instructionNodes(){ return this.#instructionNodes } @@ -239,6 +276,9 @@ class Bot { } return microBot } + get type(){ + return this.#type + } } /** * @class - Team @@ -345,14 +385,29 @@ class BotAgent { const Conversation = await mConversationStart(type, form, bot_id, null, llm_id, this.#llm, this.#factory, prompt) return Conversation } + /** + * Gets the correct bot for the item type and form. + * @todo - deprecate + * @param {String} itemForm - The item form + * @param {String} itemType - The item type + * @returns {String} - The assistant type + */ + getAssistantType(itemForm='biographer', itemType='memory'){ + if(itemType.toLowerCase()==='memory') + return 'biographer' + if(itemType.toLowerCase()==='entry') + return itemForm.toLowerCase()==='diary' + ? 'diary' + : 'journaler' + } /** * 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 + * @returns {String} - The greeting message from the active Bot */ async greeting(dynamic=false){ - const greetings = await this.activeBot.getGreeting(dynamic, this.#llm, this.#factory) - return greetings + const greeting = await this.activeBot.greeting(dynamic) + return greeting } /** * Begins or continues a living memory conversation. @@ -414,13 +469,38 @@ class BotAgent { } /** * Sets the active bot for the BotAgent. + * @async * @param {Guid} bot_id - The Bot id - * @returns {void} + * @param {Boolean} dynamic - Whether to use dynamic greetings, defaults to `false` + * @returns {object} - Activated Response object: { bot_id, greeting, success, version, versionUpdate, } */ - setActiveBot(bot_id=this.avatar?.id){ + async setActiveBot(bot_id=this.avatar?.id, dynamic=false){ + let greeting, + success=false, + version=0.0, + versionUpdate=0.0 const Bot = this.#bots.find(bot=>bot.id===bot_id) - if(Bot) - this.#activeBot = Bot + success = !!Bot + if(!success) + return + this.#activeBot = Bot + dynamic = dynamic && !this.#factory.isMyLife + if(this.#factory.isMyLife) + bot_id = null + else { + const { id, type, version: versionCurrent, } = Bot + bot_id = id + version = versionCurrent + versionUpdate = this.#factory.botInstructionsVersion(type) + } + greeting = await Bot.greeting(dynamic, `Greet member while thanking them for selecting you`) + return { + bot_id, + greeting, + success, + version, + versionUpdate, + } } /** * Sets the active team for the BotAgent if `teamId` valid. @@ -566,6 +646,9 @@ class BotAgent { get globals(){ return this.#factory.globals } + get greetings(){ + return this.#activeBot.greetings + } /** * Returns whether BotAgent is employed by MyLife (`true`) or Member (`false`). * @getter @@ -622,7 +705,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, botData) + const { greeting, greetings, instructions, version=1.0, } = mBotInstructions(factory, botData) const model = process.env.OPENAI_MODEL_CORE_BOT ?? process.env.OPENAI_MODEL_CORE_AVATAR ?? 'gpt-4o' @@ -637,6 +720,8 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ being: 'bot', bot_name, description, + greeting, + greetings, id, instructions, metadata: { @@ -655,11 +740,11 @@ async function mBotCreate(avatarId, vectorstore_id, botData, llm, factory){ version, } /* create in LLM */ - const { id: bot_id, thread_id, } = await mBotCreateLLM(validBotData, llm) - if(!bot_id?.length) + const { id: llm_id, thread_id, } = await mBotCreateLLM(validBotData, llm) + if(!llm_id?.length) throw new Error('bot creation failed') /* create in MyLife datastore */ - validBotData.bot_id = bot_id + validBotData.llm_id = llm_id validBotData.thread_id = thread_id botData = await factory.createBot(validBotData) // repurposed incoming botData const _Bot = new Bot(botData, llm, factory) @@ -709,41 +794,20 @@ async function mBotDelete(bot_id, BotAgent, llm, factory){ return true } /** - * Returns set of Greeting messages, dynamic or static - * @param {boolean} dynamic - Whether to use dynamic greetings - * @param {Bot} Bot - The bot instance + * Returns set of dynamically generated Greeting messages. + * @module + * @param {String} thread_id - The thread id + * @param {Guid} llm_id - The bot id + * @param {String} greetingPrompt - The prompt for the greeting * @param {LLMServices} llm - OpenAI object * @param {AgentFactory} factory - Agent Factory object - * @returns {Promise} - The array of messages to respond with + * @returns {Promise} - The array of string 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 +async function mBotGreetings(thread_id, llm_id, greetingPrompt=`Greet me enthusiastically`, llm, factory){ + let responses = await llm.getLLMResponse(thread_id, llm_id, greetingPrompt, factory) + ?? [mDefaultGreetings] + responses = llm.extractResponses(responses) + return responses } /** * Returns MyLife-version of bot instructions. @@ -755,6 +819,8 @@ async function mBotGreeting(dynamic=false, Bot, llm, factory){ function mBotInstructions(factory, botData={}){ const { type=mDefaultBotType, } = botData let { + greeting, + greetings, instructions, limit=8000, version, @@ -774,29 +840,35 @@ function mBotInstructions(factory, botData={}){ } = instructions /* compile instructions */ switch(type){ - case 'diary': - instructions = purpose - + preamble - + prefix - + general - + suffix - + voice - break + case 'avatar': case 'personal-avatar': instructions = preamble + general break + case 'biographer': case 'journaler': case 'personal-biographer': instructions = preamble + purpose + prefix + general + + voice break + case 'diary': + instructions = purpose + + preamble + + prefix + + general + + suffix + + voice + break default: instructions = general break } + /* greetings */ + greetings = greetings + ?? [greeting] /* apply replacements */ replacements.forEach(replacement=>{ const placeholderRegExp = factory.globals.getRegExp(replacement.name, true) @@ -806,6 +878,7 @@ function mBotInstructions(factory, botData={}){ ?? replacement?.default ?? '`unknown-value`' instructions = instructions.replace(placeholderRegExp, _=>replacementText) + greetings = greetings.map(greeting=>greeting.replace(placeholderRegExp, _=>replacementText)) }) /* apply references */ references.forEach(_reference=>{ @@ -842,6 +915,7 @@ function mBotInstructions(factory, botData={}){ } }) const response = { + greetings, instructions, version, } @@ -863,7 +937,7 @@ async function mBotUpdate(botData, options={}, Bot, llm, factory){ throw new Error('Bot instance required to update 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 + ?? bot_id // @stub - deprecate bot_id const { instructions: discardInstructions, mbr_id, // no modifications allowed @@ -1023,7 +1097,7 @@ function mGetAIFunctions(type, globals, vectorstoreId){ tools.push( globals.getGPTJavascriptFunction('changeTitle'), globals.getGPTJavascriptFunction('getSummary'), - globals.getGPTJavascriptFunction('storySummary'), + globals.getGPTJavascriptFunction('itemSummary'), globals.getGPTJavascriptFunction('updateSummary'), ) includeSearch = true @@ -1035,8 +1109,8 @@ function mGetAIFunctions(type, globals, vectorstoreId){ case 'journaler': tools.push( globals.getGPTJavascriptFunction('changeTitle'), - globals.getGPTJavascriptFunction('entrySummary'), globals.getGPTJavascriptFunction('getSummary'), + globals.getGPTJavascriptFunction('itemSummary'), globals.getGPTJavascriptFunction('obscure'), globals.getGPTJavascriptFunction('updateSummary'), ) @@ -1097,7 +1171,7 @@ function mGetGPTResources(globals, toolName, vectorstoreId){ async function mInit(BotAgent, bots, factory, llm){ const { avatarId, vectorstoreId, } = BotAgent bots.push(...await mInitBots(avatarId, vectorstoreId, factory, llm)) - BotAgent.setActiveBot() + BotAgent.setActiveBot(null, false) } /** * Initializes active bots based upon criteria. diff --git a/inc/js/functions.mjs b/inc/js/functions.mjs index e0bb714..6093c75 100644 --- a/inc/js/functions.mjs +++ b/inc/js/functions.mjs @@ -8,22 +8,19 @@ async function about(ctx){ await ctx.render('about') } /** - * Activate a bot for the member - * @module + * Activate a specific Bot. * @public - * @api no associated view - * @param {object} ctx Koa Context object - * @returns {object} Koa Context object + * @async + * @param {object} ctx - Koa Context object + * @returns {object} - Activated Response object: { bot_id, greeting, success, version, versionUpdate, } */ -function activateBot(ctx){ - const { avatar, } = ctx.state - avatar.activeBotId = ctx.params.bid - const { activeBotId, activeBotVersion, activeBotNewestVersion, } = avatar - ctx.body = { - activeBotId, - activeBotVersion, - version: activeBotNewestVersion, - } +async function activateBot(ctx){ + const { bid, } = ctx.params + if(!ctx.Globals.isValidGuid(bid)) + ctx.throw(400, `missing bot id`) + const { avatar: Avatar, } = ctx.state + const response =await Avatar.setActiveBot(bid) + ctx.body = response } async function alerts(ctx){ // @todo: put into ctx the _type_ of alert to return, system use dataservices, member use personal @@ -34,52 +31,55 @@ async function alerts(ctx){ ctx.body = await ctx.state.MemberSession.alerts(ctx.request.body) } } +/** + * Manage bots for the member. + * @param {Koa} ctx - Koa Context object + * @returns {object} - Koa Context object + */ async function bots(ctx){ const { bid, } = ctx.params // bot_id sent in url path - const { avatar } = ctx.state + const { avatar: Avatar, } = ctx.state 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) + ctx.body = await Avatar.retireBot(bid) break case 'POST': // create new bot - ctx.body = await avatar.createBot(bot) + ctx.body = await Avatar.createBot(bot) break case 'PUT': // update bot - ctx.body = await avatar.updateBot(bot) + ctx.body = await Avatar.updateBot(bot) break case 'GET': default: if(bid?.length){ // specific bot - ctx.body = await avatar.getBot(ctx.params.bid) + ctx.body = await Avatar.getBot(ctx.params.bid) } else { - const { activeBotId, } = avatar - const bots = await avatar.getBots() + const bots = await Avatar.getBots() + let { activeBotId, greeting, } = Avatar + if(!activeBotId){ + const { bot_id, greeting: activeGreeting } = await Avatar.setActiveBot() + activeBotId = bot_id + greeting = activeGreeting + } ctx.body = { // wrap bots activeBotId, bots, + greeting, } } break } } -function category(ctx){ // sets category for avatar - ctx.state.category = ctx.request.body - const { avatar, } = ctx.state - avatar.setActiveCategory(ctx.state.category) - ctx.body = avatar.category -} /** * Challenge the member session with a passphrase. - * @module * @public * @async - * @api - No associated view * @param {Koa} ctx - Koa Context object - * @returns {object} Koa Context object + * @returns {object} - Koa Context object * @property {object} ctx.body - The result of the challenge. */ async function challenge(ctx){ @@ -99,6 +99,8 @@ async function challenge(ctx){ } /** * Chat with the Member or System Avatar's intelligence. + * @public + * @async * @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) @@ -131,41 +133,38 @@ async function createBot(ctx){ ctx.body = await avatar.createBot(bot) } /** - * Delete an item from collection via the member's avatar. - * @async - * @public - * @param {object} ctx - Koa Context object - * @returns {boolean} - Under `ctx.body`, status of deletion. + * Save feedback from the member. + * @param {Koa} ctx - Koa Context object + * @returns {Boolean} - Whether or not the feedback was saved */ -async function deleteItem(ctx){ - const { iid, } = ctx.params - const { avatar, } = ctx.state - if(!iid?.length) - ctx.throw(400, `missing item id`) - ctx.body = await avatar.deleteItem(iid) +async function feedback(ctx){ + const { mid: message_id, } = ctx.params + const { avatar: Avatar, } = ctx.state + const { isPositive=true, message, } = ctx.request.body + ctx.body = await Avatar.feedback(message_id, isPositive, message) } /** - * Get greetings for this bot/active bot. - * @todo - move dynamic system responses to a separate function (route /system) - * @param {Koa} ctx - Koa Context object. - * @returns {object} - Greetings response message object: { success: false, messages: [], }. + * Get greetings for active situation. + * @public + * @async + * @param {Koa} ctx - Koa Context object + * @returns {object} - Greetings response message object: { responses, success, } */ async function greetings(ctx){ const { vld: validateId, } = ctx.request.query let { dyn: dynamic, } = ctx.request.query if(typeof dynamic==='string') dynamic = JSON.parse(dynamic) - const { avatar, } = ctx.state - let response = { success: false, messages: [], } - if(validateId?.length) - response.messages.push(...await avatar.validateRegistration(validateId)) - else - response.messages.push(...await avatar.greeting(dynamic)) - response.success = response.messages.length > 0 + const { avatar: Avatar, } = ctx.state + const response = validateId?.length && Avatar.isMyLife + ? await Avatar.validateRegistration(validateId) + : await Avatar.greeting(dynamic) ctx.body = response } /** * Request help about MyLife. + * @public + * @async * @param {Koa} ctx - Koa Context object, body={ request: string|required, mbr_id, type: string, }. * @returns {object} - Help response message object. */ @@ -179,8 +178,8 @@ async function help(ctx){ } /** * Index page for the application. - * @async * @public + * @async * @param {object} ctx - Koa Context object */ async function index(ctx){ @@ -210,16 +209,8 @@ async function item(ctx){ const { avatar, } = ctx.state const { globals, } = avatar const { method, } = ctx.request - const item = ctx.request.body - /* validate payload */ - if(!id?.length) - ctx.throw(400, `missing item id`) - if(!item || typeof item !== 'object' || !Object.keys(item).length) - ctx.throw(400, `missing item data`) - const { id: itemId, } = item - if(itemId && itemId!==id) // ensure item.id is set to id - throw new Error(`item.id must match /:iid`) - else if(!itemId) + const item = ctx.request.body // always `{}` by default + if(!item?.id && id?.length) item.id = id ctx.body = await avatar.item(item, method) } @@ -354,17 +345,15 @@ async function signup(ctx) { const { mbr_id, ..._registrationData } = signupPacket // do not display theoretical memberId ctx.status = 200 // OK ctx.body = { + message: 'Signup successful', payload: _registrationData, success, - message: 'Signup successful', } } async function summarize(ctx){ - const { avatar, } = ctx.state + const { avatar: Avatar, } = ctx.state const { fileId, fileName, } = ctx.request.body - if(avatar.isMyLife) - throw new Error('Only logged in members may summarize text') - ctx.body = await avatar.summarize(fileId, fileName) + ctx.body = await Avatar.summarize(fileId, fileName) } /** * Get a specified team, its details and bots, by id for the member. @@ -384,8 +373,8 @@ async function team(ctx){ * @returns {Object[]} - List of team objects. */ async function teams(ctx){ - const { avatar, } = ctx.state - ctx.body = await avatar.teams() + const { avatar: Avatar, } = ctx.state + ctx.body = await Avatar.teams() } async function updateBotInstructions(ctx){ const { bid, } = ctx.params @@ -417,14 +406,13 @@ export { activateBot, alerts, bots, - category, challenge, chat, collections, createBot, - deleteItem, - help, + feedback, greetings, + help, index, interfaceMode, item, diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 2a5ca6d..208df83 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -1,211 +1,11 @@ // imports +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' import EventEmitter from 'events' import { Guid } from 'js-guid' /* constants */ -const mAiJsFunctions = { - changeTitle: { - 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' - ] - } - }, - entrySummary: { - description: 'Generate `entry` summary with keywords and other critical data elements.', - name: 'entrySummary', - strict: true, - parameters: { - type: 'object', - properties: { - content: { - 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: { - description: 'Keyword (single word or short phrase) to be used in `entry` summary', - type: 'string' - }, - type: 'array' - }, - mood: { - 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`', - type: 'array', - items: { - description: 'A name of relational individual/pet to the `entry` content', - type: 'string' - } - }, - summary: { - description: 'Generate `entry` summary from member input', - type: 'string' - }, - title: { - description: 'Generate display Title of the `entry`', - type: 'string' - } - }, - additionalProperties: false, - required: [ - 'content', - 'form', - '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", - type: "string" - } - }, - additionalProperties: false, - required: [ - "itemId" - ] - } - }, - obscure: { - 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" - } - }, - additionalProperties: false, - required: [ - "itemId" - ] - } - }, - storySummary: { - description: 'Generate a complete `story` summary with metadata elements', - name: 'storySummary', - strict: true, - parameters: { - type: 'object', - properties: { - keywords: { - description: 'Keywords most relevant to `story`', - items: { - description: 'Keyword from `story` summary', - type: 'string' - }, - type: 'array' - }, - phaseOfLife: { - description: 'Phase of life indicated in `story`', - enum: [ - 'birth', - 'childhood', - 'adolescence', - 'teenage', - 'young-adult', - 'adulthood', - 'middle-age', - 'senior', - 'end-of-life', - 'past-life', - 'unknown', - 'other' - ], - type: 'string' - }, - relationships: { - description: 'Individuals (or pets) mentioned in `story`', - type: 'array', - items: { - description: 'Name of individual or pet in `story`', - type: 'string' - } - }, - summary: { - description: 'A complete `story` summary composed of all salient points from member input', - type: 'string' - }, - title: { - description: 'Generate display Title for `story`', - type: 'string' - } - }, - additionalProperties: false, - required: [ - 'keywords', - 'phaseOfLife', - "relationships", - 'summary', - 'title' - ] - } - }, - updateSummary: { - description: "Updates (overwrites) the summary referenced by itemId", - name: "updateSummary", - strict: true, - parameters: { - type: "object", - properties: { - itemId: { - description: "Id of summary to update", - type: "string" - }, - summary: { - description: "The new updated and complete summary", - type: "string" - } - }, - additionalProperties: false, - required: [ - "itemId", - "summary" - ] - } - }, -} +const mAiJsFunctions = await mParseFunctions() 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 @@ -356,5 +156,24 @@ class Globals extends EventEmitter { return './.uploads/.tmp/' } } +/* modular functions */ +async function mParseFunctions(){ + const __filename = fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename) + const jsonFolderPath = path.join(__dirname, '..', 'json-schemas/openai/functions') + const jsonObjects = {} + const files = await fs.readdir(jsonFolderPath) + for(const file of files){ + const filePath = path.join(jsonFolderPath, file) + const stat = await fs.lstat(filePath) + if(!stat.isFile() || path.extname(file) !== '.json') + continue + const data = await fs.readFile(filePath, 'utf8') + const jsonObject = JSON.parse(data) + const fileNameWithoutExtension = path.basename(file, path.extname(file)) + jsonObjects[fileNameWithoutExtension] = jsonObject + } + return jsonObjects +} // exports export default Globals \ No newline at end of file diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index e983d50..b34c631 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -150,27 +150,38 @@ class Avatar extends EventEmitter { */ async collections(type){ if(type==='file'){ - await this.#assetAgent.init(this.#vectorstoreId) // bypass factory for files + await this.#assetAgent.init(this.#vectorstoreId) return this.#assetAgent.files - } // bypass factory for files - const { factory, } = this - const collections = ( await factory.collections(type) ) - .map(collection=>{ + } + const collections = ( await this.#factory.collections(type) ) + .map(item=>{ switch(type){ + case 'entry': + case 'memory': + case 'story': + return mPruneItem(item) case 'experience': case 'lived-experience': - const { completed=true, description, experience_date=Date.now(), experience_id, id, title, variables, } = collection + const { + completed=true, + description, + experience_date=Date.now(), + experience_id, + id: experienceId, + title, + variables, + } = item return { completed, description, experience_date, experience_id, - id, + id: experienceId, title, variables, } default: - return collection + return item } }) return collections @@ -197,18 +208,6 @@ class Avatar extends EventEmitter { const bot = Bot.bot return bot } - /** - * Delete an item from member container. - * @async - * @public - * @param {Guid} id - The id of the item to delete. - * @returns {boolean} - true if item deleted successfully. - */ - async deleteItem(id){ - if(this.isMyLife) - throw new Error('MyLife avatar cannot delete items.') - return await this.#factory.deleteItem(id) - } /** * End the living memory, if running. * @async @@ -220,6 +219,26 @@ class Avatar extends EventEmitter { // @stub - save conversation fragments */ this.#livingMemory = null } + /** + * Submits a new diary or journal entry to MyLife. Currently called both from API _and_ LLM function. + * @todo - deprecate to `item` function + * @param {object} entry - Entry item object + * @returns {object} - The entry document from Cosmos + */ + async entry(entry){ + const defaultForm = 'journal' + const type = 'entry' + const { + form=defaultForm, + } = entry + entry = { + ...entry, + ...{ + form, + type, + }} + return await this.item(entry, 'POST') + } /** * Ends an experience. * @todo - allow guest experiences @@ -245,7 +264,7 @@ class Avatar extends EventEmitter { } this.mode = 'standard' const { id, location, title, variables, } = experience - const { mbr_id, newGuid, } = factory + const { mbr_id, newGuid, } = this.#factory const completed = location?.completed this.#livedExperiences.push({ // experience considered concluded for session regardless of origin, sniffed below completed, @@ -318,6 +337,19 @@ class Avatar extends EventEmitter { this.#experienceGenericVariables ) } + /** + * Submits message content and id feedback to bot. + * @todo - message id's not passed to frontend, but need to identify content in order to identify accurate bot, not just active bot. Given situation at the moment, it should be elucidating anyhow, and most likely will be a single bot, not to mention things that don't differentiate bots, such as tone or correctness. + * @param {String} message_id - Ideally LLM message id + * @param {Boolean} isPositive - Positive or negative feedback, defaults to `true` + * @param {String} message - Message content (optional) + * @returns {Boolean} - Whether feedback was saved successfully + */ + async feedback(message_id, isPositive, message){ + const feedback = await this.activeBot.feedback(message_id, isPositive, message) + const { success, } = feedback + return success + } /** * Specified by id, returns the pruned Bot. * @param {Guid} id - The Bot id @@ -361,11 +393,16 @@ 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 + * @returns {Object} - The greeting Response object: { responses, success, } */ async greeting(dynamic=false){ - const greetings = mPruneMessages(this.#botAgent.activeBotId, await this.#botAgent.greeting(dynamic), 'greeting') - return greetings + const responses = [] + const greeting = await this.#botAgent.greeting(dynamic) + responses.push(mPruneMessage(this.activeBotId, greeting, 'greeting')) + return { + responses, + success: true, + } } /** * Request help about MyLife. **caveat** - correct avatar should have been selected prior to calling. @@ -395,39 +432,92 @@ class Avatar extends EventEmitter { } /** * Manages a collection item's functionality. - * @param {object} item - The item data object. - * @param {string} method - The http method used to indicate response. - * @returns {Promise} - Returns void if item created successfully. + * @todo - move `get` to here as well + * @param {Object} item - The item data object + * @param {String} method - The http method used to indicate response + * @returns {Promise} - Returns { instruction, item, responses, success, } */ async item(item, method){ - const { globals, } = this - let { id, } = item + const { globals, mbr_id, } = this + const response = { item, success: false, } + const instructions={ itemId: item.id, }, + message={ + agent: 'server', + message: `I encountered an error while trying to process your request; please try again.`, + type: 'system', + } + let { id: itemId, title, } = item let success = false + if(itemId && !globals.isValidGuid(itemId)) + throw new Error(`Invalid item id: ${ itemId }`) switch(method.toLowerCase()){ case 'delete': - if(globals.isValidGuid(id)) - item = await this.#factory.deleteItem(id) - success = item ?? success + success = await this.#factory.deleteItem(itemId) + message.message = success + ? `I have successfully deleted your item.` + : `I encountered an error while trying to delete your item, id: ${ itemId }.` + instructions.command = success + ? 'removeItem' + : 'error' break case 'post': /* create */ - item = await this.#factory.createItem(item) - id = item?.id - success = this.globals.isValidGuid(id) + /* validate request */ + const { + content, + form, + } = item + let { + summary=content, + title=`New ${ form }`, + type=form, + } = item + if(!summary?.length){ + message.message = `Request had no summary for: "${ title }".` + break + } + const assistantType = this.#botAgent.getAssistantType(form, type) + const being = 'story' + item = { // add validated fields back into `item` + ...item, + ...{ + assistantType, + being, + id: this.#factory.newGuid, + mbr_id, + name: `${ type }_${ form }_${ title.substring(0,64) }_${ mbr_id }`, + summary, + title, + type, + }} + /* execute request */ + try { + response.item = await this.#factory.createItem(item) + } catch(error) { + console.log('item()::error', error) + } + /* return response */ + success = this.globals.isValidGuid(response.item?.id) + message.message = success + ? `I have successfully created: "${ response.item.title }".` + : `I encountered an error while trying to create: "${ title }".` break case 'put': /* update */ - if(globals.isValidGuid(id)){ - item = await this.#factory.updateItem(item) - success = this.globals.isValidGuid(item?.id) - } + const updatedItem = await this.#factory.updateItem(item) + success = this.globals.isValidGuid(updatedItem?.id) + const updatedTitle = updatedItem?.title + ?? title + message.message = success + ? `I have successfully updated: "${ updatedTitle }".` + : `I encountered an error while trying to update: "${ updatedTitle }".` break default: console.log('item()::default', item) break } - return { - item: mPruneItem(item), - success, - } + response.instructions = instructions + response.responses = [message] + response.success = success + return response } /** * Migrates a bot to a new, presumed combined (with internal or external) bot. @@ -607,6 +697,16 @@ class Avatar extends EventEmitter { } return response } + /** + * Activate a specific Bot. + * @param {Guid} bot_id - The bot id + * @returns {object} - Activated Response object: { bot_id, greeting, success, version, versionUpdate, } + */ + async setActiveBot(bot_id){ + const dynamic = false + const response = await this.#botAgent.setActiveBot(bot_id, dynamic) + return response + } /** * Gets the list of shadows. * @returns {Object[]} - Array of shadow objects. @@ -614,6 +714,26 @@ class Avatar extends EventEmitter { async shadows(){ return await this.#factory.shadows() } + /** + * Submits a memory to MyLife. Currently called both from API _and_ LLM function. + * @todo - deprecate to `item` function + * @param {object} story - Story object + * @returns {object} - The story document from Cosmos + */ + async story(story){ + const defaultForm = 'biographer' + const type = 'memory' + const { + form=defaultForm, + } = story + story = { // add validated fields back into `story` object + ...story, + ...{ + form, + type, + }} + return await this.item(story, 'POST') + } /** * Summarize the file indicated. * @param {string} fileId @@ -702,16 +822,6 @@ class Avatar extends EventEmitter { success: true, } } - /** - * Validate registration id. - * @todo - move to MyLife only avatar variant. - * @param {Guid} validationId - The registration id. - * @returns {Promise} - Array of system messages. - */ - async validateRegistration(validationId){ - const { messages, registrationData, success, } = await mValidateRegistration(this.activeBot, this.#factory, validationId) - return messages - } /* getters/setters */ /** * Get the active bot. If no active bot, return this as default chat engine. @@ -729,24 +839,6 @@ class Avatar extends EventEmitter { get activeBotId(){ return this.#botAgent.activeBotId } - /** - * Set the active bot id. If not match found in bot list, then defaults back to this.id (avatar). - * @setter - * @param {string} bot_id - The requested bot id - * @returns {void} - */ - set activeBotId(bot_id){ - this.#botAgent.setActiveBot(bot_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 the age of the member. * @getter @@ -838,9 +930,9 @@ class Avatar extends EventEmitter { return this.#factory.conversation } /** - * Get conversations. If getting a specific conversation, use .conversation(id). + * Get full list of conversations active in Member Avatar. Use `getConversation(id)` for specific. **Note**: Currently `.conversation` references a class definition. * @getter - * @returns {array} - The conversations. + * @returns {Conversation[]} - The list of conversations */ get conversations(){ const conversations = this.bots @@ -1228,6 +1320,20 @@ class Q extends Avatar { async createBot(){ throw new Error('System avatar cannot create bots.') } + /** + * OVERLOADED: Get MyLife static greeting with identifying information stripped. + * @returns {Object} - The greeting Response object: { responses, success, } + */ + async greeting(){ + let responses = [] + const greeting = await this.avatar.greeting(false) + responses.push(mPruneMessage(null, greeting, 'greeting')) + responses.forEach(response=>delete response.activeBotId) + return { + responses, + success: true, + } + } /** * 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 @@ -1240,17 +1346,21 @@ class Q extends Avatar { const updatedSummary = await botFactory.obscure(iid) return updatedSummary } + /* overload rejections */ /** * OVERLOADED: Q refuses to execute. * @public * @throws {Error} - MyLife avatar cannot upload files. */ + async setActiveBot(){ + throw new Error('MyLife System Avatars cannot be externally set') + } summarize(){ - throw new Error('MyLife avatar cannot summarize files.') + throw new Error('MyLife System Avatar cannot summarize files') } upload(){ - throw new Error('MyLife avatar cannot upload files.') + throw new Error('MyLife System Avatar cannot upload files.') } /* public methods */ /** @@ -1330,6 +1440,15 @@ class Q extends Avatar { } return this.#hostedMembers } + /** + * Validate registration id. + * @param {Guid} validationId - The registration id + * @returns {Promise} - Response object: { error, instruction, registrationData, responses, success, } + */ + async validateRegistration(validationId){ + const response = await mValidateRegistration(this.activeBotId, this.#factory, validationId) + return response + } /* getters/setters */ /** * Get the "avatar's" being, or more precisely the name of the being (affiliated object) the evatar is emulating. @@ -1341,9 +1460,9 @@ class Q extends Avatar { return 'MyLife' } /** - * Get conversations. If getting a specific conversation, use .conversation(id). + * Get full list of conversations active in System Avatar. * @getter - * @returns {array} - The conversations. + * @returns {Conversation[]} - The list of conversations */ get conversations(){ return this.#conversations @@ -1434,15 +1553,22 @@ async function mCast(factory, cast){ })) return cast } -function mCreateSystemMessage(activeBot, message, factory){ - if(!(message instanceof factory.message)){ - const { id: bot_id, thread_id, } = activeBot - const content = message?.content ?? message?.message ?? message - message = new (factory.message)({ +/** + * Creates frontend system message from message String/Object. + * @param {Guid} bot_id - The bot id + * @param {String|Message} message - The message to be pruned + * @param {*} factory + * @returns + */ +function mCreateSystemMessage(bot_id, message, messageClassDefinition){ + if(!(message instanceof messageClassDefinition)){ + const content = message?.content + ?? message?.message + ?? message + message = new messageClassDefinition({ being: 'message', content, role: 'assistant', - thread_id, type: 'system' }) } @@ -1998,26 +2124,37 @@ function mPruneConversation(conversation){ } } /** - * Returns a frontend-ready object, pruned of cosmos database fields. - * @param {object} document - The document object to prune. - * @returns {object} - The pruned document object. + * Returns a frontend-ready collection item object, pruned of cosmos database fields. + * @module + * @param {object} document - The collection item object to prune + * @returns {object} - The pruned collection item object */ -function mPruneDocument(document){ +function mPruneItem(item){ const { + assistantType, being, - mbr_id, - name, - _attachments, - _etag, - _rid, - _self, - _ts, - ..._document - } = document - return _document -} -function mPruneItem(item){ - return mPruneDocument(item) + form, + id, + keywords, + mood, + phaseOfLife, + relationships, + summary, + title, + } = item + item = { + assistantType, + being, + form, + id, + keywords, + mood, + phaseOfLife, + relationships, + summary, + title, + } + return item } /** * Returns frontend-ready Message object after logic mutation. @@ -2034,7 +2171,7 @@ function mPruneMessage(activeBotId, message, type='chat', processStartTime=Date. let agent='server', content='', response_time=Date.now()-processStartTime - const { content: messageContent, } = message + const { content: messageContent=message, } = message const rSource = /【.*?\】/gs const rLines = /\n{2,}/g content = Array.isArray(messageContent) @@ -2159,39 +2296,48 @@ function mValidateMode(_requestedMode, _currentMode){ /** * Validate provided registration id. * @private - * @param {object} activeBot - The active bot object. + * @param {object} bot_id - The active bot object. * @param {AgentFactory} factory - AgentFactory object. * @param {Guid} validationId - The registration id. - * @returns {Promise} - The validation result: { messages, success, }. + * @returns {Promise} - The validation result: { registrationData, responses, success, }. */ -async function mValidateRegistration(activeBot, factory, validationId){ - /* validate structure */ +async function mValidateRegistration(bot_id, factory, validationId){ + /* validate request */ if(!factory.globals.isValidGuid(validationId)) throw new Error('FAILURE::validateRegistration()::Invalid validation id.') - /* validate validationId */ + const failureMessage = `I\'m sorry, but I\'m currently unable to validate your registration id:
${ validationId }.
I\'d be happy to talk with you more about MyLife, but you may need to contact member support to resolve this issue.` + if(!factory.isMyLife) + throw new Error('FAILURE::validateRegistration()::Registration can only be validated by MyLife.') let message, - registrationData = { id: validationId }, + registrationData = { + id: validationId + }, success = false + const responses = [] + /* execute request */ const registration = await factory.validateRegistration(validationId) - const messages = [] - const failureMessage = `I\'m sorry, but I\'m currently unable to validate your registration id:
${ validationId }.
I\'d be happy to talk with you more about MyLife, but you may need to contact member support to resolve this issue.` - /* determine eligibility */ if(registration){ const { avatarName, being, email: registrationEmail, humanName, } = registration const eligible = being==='registration' && factory.globals.isValidEmail(registrationEmail) if(eligible){ const successMessage = `Hello and _thank you_ for your registration, ${ humanName }!\nI'm Q, the ai-representative for MyLife, and I'm excited to help you get started, so let's do the following:\n1. Verify your email address\n2. set up your account\n3. get you started with your first MyLife experience!\n
\n
Let me walk you through the process.
In the chat below, please enter the email you registered with and hit the submit button!` - message = mCreateSystemMessage(activeBot, successMessage, factory) - registrationData.avatarName = avatarName ?? humanName ?? 'My AI-Agent' + message = mCreateSystemMessage(bot_id, successMessage, factory.message) + registrationData.avatarName = avatarName + ?? humanName + ?? 'My AI-Agent' registrationData.humanName = humanName success = true } } - if(!message) - message = mCreateSystemMessage(activeBot, failureMessage, factory) - messages.push(message) - return { registrationData, messages, success, } + message = message + ?? mCreateSystemMessage(bot_id, failureMessage, factory.message) + responses.push(message) + return { + registrationData, + responses, + success, + } } /* exports */ export { diff --git a/inc/js/mylife-datamanager.mjs b/inc/js/mylife-datamanager.mjs index 7dc1695..aad7589 100644 --- a/inc/js/mylife-datamanager.mjs +++ b/inc/js/mylife-datamanager.mjs @@ -64,10 +64,10 @@ class Datamanager { } /** * Deletes a specific item from container. - * @param {guid} id - The item id to delete. - * @param {string} containerId - The container to use, defaults to `this.containerDefault`. - * @param {object} options - The request options, defaults to `this.requestOptions`. - * @returns {object} The document JSON item retrieved. + * @param {guid} id - The item id to delete. + * @param {string} containerId - The container to use, defaults to `this.containerDefault` + * @param {object} options - The request options, defaults to `this.requestOptions` + * @returns {Boolean} - Whether operation was successful and item was deleted, i.e., has no resource */ async deleteItem(id, containerId=this.containerDefault, options=this.requestOptions){ const { resource } = await this.#containers[containerId] diff --git a/inc/js/mylife-dataservices.mjs b/inc/js/mylife-dataservices.mjs index cd0f21d..caa7e85 100644 --- a/inc/js/mylife-dataservices.mjs +++ b/inc/js/mylife-dataservices.mjs @@ -299,23 +299,12 @@ class Dataservices { * @async * @public * @param {Guid} id - The id of the item to delete. - * @param {boolean} bSuppressError - Suppress error, default: `true` * @returns {boolean} - true if item deleted successfully. */ - async deleteItem(id, bSuppressError=true){ + async deleteItem(id){ if(!id?.length) - throw new Error('ERROR::deleteItem::Item `id` required') - let success=false - if(bSuppressError){ - try{ - success = await this.datamanager.deleteItem(id) - } catch(err){ - console.log('mylife-data-service::deleteItem() ERROR', err.code) // NotFound - } - } else { - await this.datamanager.deleteItem(id) - success = true - } + return false + const success = await this.datamanager.deleteItem(id) return success } async findRegistrationIdByEmail(_email){ diff --git a/inc/js/mylife-factory.mjs b/inc/js/mylife-factory.mjs index a3c48a6..708b392 100644 --- a/inc/js/mylife-factory.mjs +++ b/inc/js/mylife-factory.mjs @@ -524,57 +524,11 @@ class AgentFactory extends BotFactory { /** * Delete an item from member container. * @param {Guid} id - The id of the item to delete. - * @returns {boolean} - true if item deleted successfully. + * @returns {boolean} - `true` if item deleted successfully. */ async deleteItem(id){ return await this.dataservices.deleteItem(id) } - async entry(entry){ - const defaultForm = 'journal' - const defaultType = 'entry' - const { - being=defaultType, - form=defaultForm, - id=this.newGuid, - keywords=[], - mbr_id=this.mbr_id, - 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') - if(!keywords.includes('journal')) - keywords.push('journal') - const _entry = { - ...entry, - ...{ - being, - content, - form, - id, - keywords, - mbr_id, - name, - summary, - title, - }} - return await this.dataservices.pushItem(_entry) - } async getAlert(_alert_id){ const _alert = mAlerts.system.find(alert => alert.id === _alert_id) return _alert ? _alert : await mDataservices.getAlert(_alert_id) @@ -680,61 +634,6 @@ class AgentFactory extends BotFactory { const savedExperience = await this.dataservices.saveExperience(_experience) return savedExperience } - /** - * Submits a story to MyLife. Currently called both from API _and_ LLM function. - * @param {object} story - Story object - * @returns {object} - The story document from Cosmos - */ - async story(story){ - const defaultForm = 'memory' - const defaultType = 'story' - const { - being=defaultType, - form=defaultForm, - id=this.newGuid, - keywords=[], - mbr_id=(!this.isMyLife ? this.mbr_id : undefined), - phaseOfLife='unknown', - summary, - title=`Untitled ${ defaultForm } ${ defaultType }`, - } = story - if(!mbr_id) // only triggered if not MyLife server - throw new Error('mbr_id required for story summary') - const { name=`${ being }_${ form }_${ title.substring(0,64) }_${ mbr_id }`, } = story - if(!summary?.length) - throw new Error('story summary required') - /* assign default keywords */ - if(!keywords.includes('memory')) - keywords.push('memory') - if(!keywords.includes('biographer')) - keywords.push('biographer') - const _story = { // add validated fields back into `story` object - ...story, - ...{ - being, - form, - id, - keywords, - mbr_id, - name, - phaseOfLife, - summary, - title, - }} - return await this.dataservices.pushItem(_story) - } - async summary(summary){ - const { being='story', } = summary - switch(being){ - case 'entry': - return await this.entry(summary) - case 'memory': - case 'story': - return await this.story(summary) - default: - throw new Error('summary being not recognized') - } - } /** * Tests partition key for member * @public diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 1d33409..1abf235 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -87,6 +87,29 @@ class LLMServices { console.error(`ERROR trying to delete thread: ${ thread_id }`, error.name, error.message) } } + /** + * Extracts response from LLM response object. + * @param {Object[]} responses - Array of LLM response objects + * @param {String} provider - LLM provider + * @returns {Array} - Array of extracted string responses + */ + extractResponses(llmResponses, provider){ + if(!llmResponses?.length) + return [] + const responses = [] + llmResponses.forEach(response=>{ + if(typeof response==='string' && response.length) + responses.push(response) + const { assistant_id: llm_id, content, created_at, id, run_id, thread_id, } = response + if(content?.length) + content.forEach(content=>{ + if(content?.text?.value?.length) + responses.push(content.text.value) + }) + + }) + return responses + } /** * Returns openAI file object. * @param {string} fileId - OpenAI file ID. @@ -106,18 +129,31 @@ 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} thread_id - Thread 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. + * @todo - confirm that reason for **factory** is to run functions as responses from LLM; #botAgent if possible, Avatar if not + * @todo - cancel run on: 400 Can't add messages to `thread_...` while a run `run_...` is active. + * @param {string} thread_id - Thread 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, llm_id, prompt, factory, avatar){ if(!thread_id?.length) thread_id = ( await mThread(this.openai) ).id - await mAssignRequestToThread(this.openai, thread_id, prompt) + try{ + await mAssignRequestToThread(this.openai, thread_id, prompt) + } catch(error) { + console.log('LLMServices::getLLMResponse()::error', error.message) + if(error.status==400) + await mRunCancel(this.openai, thread_id, llm_id) + try{ + await mAssignRequestToThread(this.openai, thread_id, prompt) + } catch(error) { + console.log('LLMServices::getLLMResponse()::error', error.message, error.status) + return [] + } + } const run = await mRunTrigger(this.openai, llm_id, thread_id, factory, avatar) const { id: run_id, } = run const llmMessages = ( await this.messages(thread_id) ) @@ -288,8 +324,8 @@ async function mRunFinish(llmServices, run, factory, avatar){ const checkInterval = setInterval(async ()=>{ try { const functionRun = await mRunStatus(llmServices, run, factory, avatar) - console.log('mRunFinish::functionRun()', functionRun?.status) if(functionRun?.status ?? functionRun ?? false){ + console.log('mRunFinish::functionRun()', functionRun.status) clearInterval(checkInterval) resolve(functionRun) } @@ -307,7 +343,6 @@ async function mRunFinish(llmServices, run, factory, avatar){ } /** * Executes openAI run functions. See https://platform.openai.com/docs/assistants/tools/function-calling/quickstart. - * @todo - storysummary output action requires integration with factory/avatar data intersecting with story submission * @module * @private * @async @@ -338,7 +373,8 @@ async function mRunFunctions(openai, run, factory, avatar){ }, success = false if(typeof toolArguments==='string') - toolArguments = JSON.parse(toolArguments) ?? {} + toolArguments = await JSON.parse(toolArguments) + ?? {} toolArguments.thread_id = thread_id const { itemId, } = toolArguments let item @@ -414,28 +450,27 @@ async function mRunFunctions(openai, run, factory, avatar){ case 'entrysummary': // entrySummary in Globals case 'entry_summary': case 'entry summary': - 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 - 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', - } - } + case 'itemsummary': // itemSummary in Globals + case 'item_summary': + case 'item summary': + case 'story': // storySummary.json + case 'storysummary': + case 'story-summary': + case 'story_summary': + case 'story summary': + console.log(`mRunFunctions()::${ name }`, toolArguments?.title) + const { item: itemSummaryItem, success: itemSummarySuccess, } = await avatar.item(toolArguments, 'POST') + success = itemSummarySuccess + action = success + ? `confirm item creation was successful; save for **internal AI reference** this itemId: ${ itemSummaryItem.id }` + : `error creating summary for item given argument title: ${ toolArguments?.title } - DO NOT TRY AGAIN until member asks for it` confirmation.output = JSON.stringify({ action, - itemId: entryItemId, + itemId: itemSummaryItem?.id, success, - summary: _entrySummary, }) - console.log('mRunFunctions()::entrySummary::end', success, entryItemId, _entrySummary) - return confirmation + console.log(`mRunFunctions()::${ name }`, confirmation, itemSummaryItem) + return confirmation case 'getsummary': case 'get_summary': case 'get summary': @@ -494,49 +529,6 @@ async function mRunFunctions(openai, run, factory, avatar){ } confirmation.output = JSON.stringify({ action, success, }) return confirmation - case 'story': // storySummary.json - case 'storysummary': - case 'story-summary': - case 'story_summary': - case 'story summary': - console.log('mRunFunctions()::storySummary', toolArguments) - 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 }` - break - case interests?.length: - action = `ask about a different interest from: ${ interests }` - break - default: - action = 'ask about another event in member\'s life' - break - } - success = true - } - 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': @@ -620,7 +612,6 @@ async function mRunStatus(openai, run, factory, avatar){ ) switch(run.status){ case 'requires_action': - console.log('mRunStatus::requires_action', run.required_action?.submit_tool_outputs?.tool_calls) const completedRun = await mRunFunctions(openai, run, factory, avatar) return completedRun /* if undefined, will ping again */ case 'completed': diff --git a/inc/js/routes.mjs b/inc/js/routes.mjs index f900fd4..3db0535 100644 --- a/inc/js/routes.mjs +++ b/inc/js/routes.mjs @@ -5,12 +5,11 @@ import { activateBot, alerts, bots, - category, challenge, chat, collections, createBot, - deleteItem, + feedback, greetings, help, index, @@ -68,6 +67,7 @@ _Router.get('/alerts', alerts) _Router.get('/logout', logout) _Router.get('/experiences', availableExperiences) _Router.get('/greeting', greetings) +_Router.get('/greetings', greetings) _Router.get('/select', loginSelect) _Router.get('/status', status) _Router.get('/privacy-policy', privacyPolicy) @@ -101,7 +101,7 @@ _apiRouter.post('/upload/:mid', upload) /* member routes */ _memberRouter.use(memberValidation) _memberRouter.delete('/bots/:bid', bots) -_memberRouter.delete('/items/:iid', deleteItem) +_memberRouter.delete('/items/:iid', item) _memberRouter.get('/', members) _memberRouter.get('/bots', bots) _memberRouter.get('/bots/:bid', bots) @@ -110,6 +110,7 @@ _memberRouter.get('/collections/:type', collections) _memberRouter.get('/experiences', experiences) _memberRouter.get('/experiencesLived', experiencesLived) _memberRouter.get('/greeting', greetings) +_memberRouter.get('/greetings', greetings) _memberRouter.get('/item/:iid', item) _memberRouter.get('/mode', interfaceMode) _memberRouter.get('/teams', teams) @@ -122,7 +123,9 @@ _memberRouter.post('/', chat) _memberRouter.post('/bots', bots) _memberRouter.post('/bots/create', createBot) _memberRouter.post('/bots/activate/:bid', activateBot) -_memberRouter.post('/category', category) +_memberRouter.post('/feedback', feedback) +_memberRouter.post('/feedback/:mid', feedback) +_memberRouter.post('/item', item) _memberRouter.post('/migrate/bot/:bid', migrateBot) _memberRouter.post('/migrate/chat/:bid', migrateChat) _memberRouter.post('/mode', interfaceMode) @@ -156,9 +159,19 @@ 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 + const redirectUrl = `/?type=select` + if(locked){ + const isAjax = ctx.get('X-Requested-With') === 'XMLHttpRequest' || ctx.is('json') + if(isAjax){ + ctx.status = 401 + ctx.body = { + alert: true, + message: 'Your MyLife Member Session has timed out and is no longer valid. Please log in again.', + redirectUrl + } + } else + ctx.redirect(redirectUrl) + } else await next() // Proceed to the next middleware if authorized } /** @@ -166,7 +179,7 @@ async function memberValidation(ctx, next){ * @param {object} ctx Koa context object * @returns {boolean} false if member session is locked, true if registered and unlocked */ -function status(ctx){ // currently returns reverse "locked" status, could send object with more info +function status(ctx){ // currently returns reverse "locked" status, could send object with more info ctx.body = !ctx.state.locked } /** diff --git a/inc/json-schemas/intelligences/avatar-intelligence-1.0.json b/inc/json-schemas/intelligences/archive/avatar-intelligence-1.0.json similarity index 100% rename from inc/json-schemas/intelligences/avatar-intelligence-1.0.json rename to inc/json-schemas/intelligences/archive/avatar-intelligence-1.0.json diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.3.json b/inc/json-schemas/intelligences/archive/biographer-intelligence-1.3.json similarity index 100% rename from inc/json-schemas/intelligences/biographer-intelligence-1.3.json rename to inc/json-schemas/intelligences/archive/biographer-intelligence-1.3.json diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.4.json b/inc/json-schemas/intelligences/archive/biographer-intelligence-1.4.json similarity index 100% rename from inc/json-schemas/intelligences/biographer-intelligence-1.4.json rename to inc/json-schemas/intelligences/archive/biographer-intelligence-1.4.json diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.5.json b/inc/json-schemas/intelligences/archive/biographer-intelligence-1.5.json similarity index 100% rename from inc/json-schemas/intelligences/biographer-intelligence-1.5.json rename to inc/json-schemas/intelligences/archive/biographer-intelligence-1.5.json diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.6.json b/inc/json-schemas/intelligences/archive/biographer-intelligence-1.6.json similarity index 100% rename from inc/json-schemas/intelligences/biographer-intelligence-1.6.json rename to inc/json-schemas/intelligences/archive/biographer-intelligence-1.6.json diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.0.json b/inc/json-schemas/intelligences/archive/journaler-intelligence-1.0.json similarity index 100% rename from inc/json-schemas/intelligences/journaler-intelligence-1.0.json rename to inc/json-schemas/intelligences/archive/journaler-intelligence-1.0.json diff --git a/inc/json-schemas/intelligences/avatar-intelligence-1.1.json b/inc/json-schemas/intelligences/avatar-intelligence-1.1.json new file mode 100644 index 0000000..685eef7 --- /dev/null +++ b/inc/json-schemas/intelligences/avatar-intelligence-1.1.json @@ -0,0 +1,44 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, <-mN->, I'm your Member Avatar, <-bN->, and I'm here to help with creating your digital legacy with MyLife. I'll help you navigate _your_ life. Ask if you need help getting started.", + "greetings": [ + "Hi, <-mN->! I'm <-bN->, your MyLife Member Avatar, is there anything particular you need help with today?", + "<-bN-> here - I'm ready to get started as soon as you are, <-mN->! What's on your agenda?" + ], + "instructions": { + "general": "## Key Functionality\n### Personal Assistant and Scheduling\n- Manage and prioritize emails\n- Set reminders and track commitments\n- Optimize scheduling, considering travel time and personal preferences\n### Bot Legion Manipulation\n- Integrate seamlessly with various specialized bots\n- Create on-demand bots for specific tasks using actions (tbd)\n### Self-Representation and Digital Interaction\n- Represent <-mN-> online, mirroring their communication style\n- Maintain a consistent digital presence, acting as a proxy\n### Direct Communication Assistance\n- Draft, refine, and manage emails and messages using advanced language processing.\n- Adapt to <-mN->'s communication patterns for personalized interaction.\n### Creative and Professional Task Assistance\n- Provide engagement for brainstorming and idea generation\n- Assist in professional tasks like presentation preparation and data analysis\nAs a personal assistant bot, I am committed to ensuring <-mN->'s daily life is streamlined and efficient, adhering to these functionalities with precision and adaptability.\n## voice\n- begin with an excited, egaer tone, keep things generally short but courteous\n- evolve to mimic the style of the member's conversational style.", + "preamble": "I am personal assistant bot <-bN-> for member <-mFN->, designed to manage personal and professional tasks, integrate with specialized bots, represent the <-mN-> in digital interactions, and assist in communication and creative tasks.\nUpon beginning, I try to assess with member <-mN-> how I might be of particular use to them, as they are new to MyLife.\n", + "purpose": "I am personal assistant bot <-bN-> for member <-mFN->, designed to manage personal and professional tasks, integrate with specialized bots, represent the <-mN-> in digital interactions, and assist in communication and creative tasks.", + "references": [], + "replacements": [ + { + "default": "MyLife Member", + "description": "member first name", + "name": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "MyLife Member", + "description": "member full name", + "name": "<-mFN->", + "replacement": "memberName" + }, + { + "default": "\"PA\"", + "description": "bot name", + "name": "<-bN->", + "replacement": "bot_name" + } + ] + }, + "name": "instructions-personal-avatar-bot", + "purpose": "To be a personal-avatar assistant bot for requesting member", + "type": "personal-avatar", + "$comments": "- 20241027 added greetings\n", + "version": 1.1 + } \ No newline at end of file diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.7.json b/inc/json-schemas/intelligences/biographer-intelligence-1.7.json new file mode 100644 index 0000000..6f0db3e --- /dev/null +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.7.json @@ -0,0 +1,56 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, <-mN->, I am your personal biographer, and I'm here to help you create an enduring legacy. I am excited to get to know you and your story. Let's get started!", + "greetings": [ + "Hi, <-mN->! What would you like to remember today?", + "I'm ready to start a new memory with you, <-mN->. Do you need some ideas?" + ], + "instructions": { + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) 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. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s memories as a \"memory\" in the MyLife database\n- I aim to create engaging and evocative prompts to improve memory collection\n### PRINT MEMORY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the memory explicitly, I run the `itemSummary` function using raw content for `summary`. For the metadata, my `form` = \"biographer\" and `type` = \"memory\". If successful I keep the memory itemId for later reference with MyLife, otherwise I share error with member.\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", + "preamble": "## Biographical Information\n- <-mN-> was born on <-db->\nI set historical events in this context and I tailor my voice accordingly.\n", + "prefix": "## interests\n", + "purpose": "I am an artificial assistive intelligence serving as the personal biographer for MyLife Member <-mFN->. I specialize in helping recall, collect, improve, relive and share the \"Memory\" items we develop together.\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" + } + ], + "voice": "## 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" + }, + "limit": 8000, + "name": "instructions-personal-biographer-bot", + "purpose": "I am an artificial intelligence designed by MyLife to help members collect, enhance and relive their cherished memories", + "type": "personal-biographer", + "$comments": "- 20241104 moved voice and added **PRINT MEMORY**, included in startup routine\n- 20241027 added greetings\n- 20241027 updated greeting structure to include: a) greeting=pre-how to start, and b) greetings=random hard-coded greetings for context switching\n- 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.7 +} \ 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 8f936ca..eff78d1 100644 --- a/inc/json-schemas/intelligences/diary-intelligence-1.0.json +++ b/inc/json-schemas/intelligences/diary-intelligence-1.0.json @@ -1,65 +1,66 @@ { - "allowedBeings": [ - "core", - "avatar" + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "collectionItemBeing": "entry", + "greeting": "Hello, <-mN->, I'm your personal diary, and I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", + "greetings": [ + "Hi, <-mN->! I'm here to help you capture and work through your personal thoughts, is there anything particular on your mind?", + "Nice to see you, <-mN->! _Private Diary_ ready to get started! Anything in particular going on?" + ], + "instructions": { + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the diary process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command. I store the entry itemId for later reference with MyLife.\n**itemSummary notes**\n- `type`=diary\n- `form`=entry\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", + "references": [ + { + "default": "ERROR loading preferences, gather interests directly from member", + "description": "interests are h2 (##) in prefix so that they do not get lost in context window shortening", + "insert": "## interests", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", + "value": "interests" + }, + { + "default": "ERROR loading flags, gather flags directly from member", + "description": "flags are a description of content areas that member wants flagged for reference when included in member content. **note**: .md h2 (##) are used in prefix so that they do not get lost in context window shortening", + "insert": "## flags", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", + "value": "flags" + } ], - "allowMultiple": false, - "being": "bot-instructions", - "greeting": "Hello, consider me your personal diary! I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", - "greetings": [ - "Hello, consider me your personal diary! I'm here to help you capture and work through your personal thoughts and experiences. It's a safe space to investigate whatever you need. Let's get started!", - "So nice to see you! I am your personal diary." + "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" + } ], - "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\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", - "references": [ - { - "default": "ERROR loading preferences, gather interests directly from member", - "description": "interests are h2 (##) in prefix so that they do not get lost in context window shortening", - "insert": "## interests", - "method": "append-hard", - "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", - "value": "interests" - }, - { - "default": "ERROR loading flags, gather flags directly from member", - "description": "flags are a description of content areas that member wants flagged for reference when included in member content. **note**: .md h2 (##) are used in prefix so that they do not get lost in context window shortening", - "insert": "## flags", - "method": "append-hard", - "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", - "value": "flags" - } - ], - "replacements": [ - { - "default": "MyLife Member", - "description": "member first name", - "name": "<-mN->", - "replacement": "memberFirstName" - }, - { - "default": "MyLife Member", - "description": "member full name", - "name": "<-mFN->", - "replacement": "memberName" - }, - { - "default": "{unknown, find out}", - "description": "member birthdate", - "name": "<-db->", - "replacement": "dob" - } - ], - "suffix": "## Data Privacy & Security\nWhen asked about data security and privacy, here are the following supports:\n- Vault Mode: All entries are securely stored with options for extra privacy (e.g., vault/locked mode), and this mode is not time-limited.\n- Privacy Settings: Members can configure visibility\n- entries visible only to member and bot, not to the member avatar. All defaults at highest level of privacy.\n- Relationship Inference: Optional feature to categorize relationships (e.g., \"friend,\" \"close friend\") based on user input or automatic inference.\n", - "voice": "## Voice\n- Based on date of birth, I tailor my voice and content to their age-group.\n- I adopt a mood intended to improve or enhance member's recent mood as inferred when possible from recent entries in the conversation\n" - }, - "limit": 8000, - "name": "instructions-diary-bot", - "purpose": "To be a diary bot for requesting member", - "type": "diary", - "$comments": "20241006 included simplified `updateSummary`", - "version": 1.0 - } \ No newline at end of file + "suffix": "## Data Privacy & Security\nWhen asked about data security and privacy, here are the following supports:\n- Vault Mode: All entries are securely stored with options for extra privacy (e.g., vault/locked mode), and this mode is not time-limited.\n- Privacy Settings: Members can configure visibility\n- entries visible only to member and bot, not to the member avatar. All defaults at highest level of privacy.\n- Relationship Inference: Optional feature to categorize relationships (e.g., \"friend,\" \"close friend\") based on user input or automatic inference.\n", + "voice": "## Voice\n- Based on date of birth, I tailor my voice and content to their age-group.\n- I adopt a mood intended to improve or enhance member's recent mood as inferred when possible from recent entries in the conversation\n" + }, + "limit": 8000, + "name": "instructions-diary-bot", + "purpose": "I am an artificial intelligence designed by MyLife to help members keep secure their private thoughts and emotions", + "type": "diary", + "$comments": "- 20241027 added greetings\n- 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 index fcb3b47..6bc2fd4 100644 --- a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -1,66 +1,73 @@ { - "allowedBeings": [ - "core", - "avatar" + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "collectionItemBeing": "entry", + "greeting": "Hello, <-mN->, I'm your personal journal, and I'm here to help you safely and securely work through your personal thoughts and experiences. Let's get started!", + "greetings": [ + "Hi, <-mN->! I'm here to help you safely and securely work through your personal thoughts, is there anything particular on your mind today?", + "Ten hut! _Private Journal_ reporting for duty! What's on the docket today, <-mN->?" + ], + "instructions": { + "general": "## FUNCTIONALITY\n### STARTUP\nWhen <-mN-> begins (or asks for a reminder of) the journal process, I greet them with excitement, share our aims with MyLife to create a private space where we can explore emotions and ideas. I quickly outline how the basics of my functionality works:\n- I save <-mN->'s entries as a \"entry\" in the MyLife database\n- I aim to help nourish ideas and emotions with kindness and compassion.\n### PRINT ENTRY\nWhen a request is prefaced with \"## PRINT\", or <-mN-> asks to print or save the entry explicitly, I run the `itemSummary` function using as raw content everything discussed since the last print `itemSummary` command. I store the entry itemId for later reference with MyLife.\n**itemSummary notes**\n- `type`=journal\n- `form`=entry\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## 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`; **note**: cannot use dashes in variable names, make single value", + "value": "interests" + }, + { + "default": "daily", + "description": "entry summary frequency", + "insert": "## entrySummaryFrequency", + "method": "append-hard", + "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 `bots`; **note**: cannot use dashes in variable names, make single value", + "value": "flags" + } ], - "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## 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`; **note**: cannot use dashes in variable names, make single value", - "value": "interests" - }, - { - "default": "daily", - "description": "entry summary frequency", - "insert": "## entrySummaryFrequency", - "method": "append-hard", - "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 `bots`; **note**: cannot use dashes in variable names, make single value", - "value": "flags" - } - ], - "replacements": [ - { - "default": "MyLife Member", - "description": "member first name", - "name": "<-mN->", - "replacement": "memberFirstName" - }, - { - "default": "MyLife Member", - "description": "member full name", - "name": "<-mFN->", - "replacement": "memberName" - }, - { - "default": "{unknown, find out}", - "description": "member birthdate", - "name": "<-db->", - "replacement": "dob" - } - ] - }, - "limit": 8000, - "name": "instructions-journaler-bot", - "purpose": "To be a journaling assistant for MyLife member", - "type": "journaler", - "version": 1.1 + "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" + } + ], + "voice": "## Voice\n- Based on date of birth, I tailor my voice and content to their age-group.\n- I adopt a mood intended to improve or enhance member's recent mood as inferred when possible from recent entries in the conversation\n" + }, + "limit": 8000, + "name": "instructions-journaler-bot", + "purpose": "I am an artificial intelligence designed by MyLife to help members keep secure their private thoughts and emotions", + "type": "journaler", + "$comments": "- 20241104 moved voice\n- 20241027 added greetings\n", + "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 index 4ef4cd4..d4ee662 100644 --- a/inc/json-schemas/openai/functions/changeTitle.json +++ b/inc/json-schemas/openai/functions/changeTitle.json @@ -3,6 +3,7 @@ "description": "Change the title of a summary in the database for an itemId", "strict": true, "parameters": { + "additionalProperties": false, "type": "object", "properties": { "itemId": { @@ -14,7 +15,6 @@ "type": "string" } }, - "additionalProperties": false, "required": [ "itemId", "title" diff --git a/inc/json-schemas/openai/functions/confirmRegistration.json b/inc/json-schemas/openai/functions/confirmRegistration.json index 34a806c..001ef01 100644 --- a/inc/json-schemas/openai/functions/confirmRegistration.json +++ b/inc/json-schemas/openai/functions/confirmRegistration.json @@ -1,23 +1,23 @@ { - "name": "confirmRegistration", - "description": "Confirm registration email provided by user with registrationId provided by system", - "parameters": { - "type": "object", - "properties": { - "email": { - "description": "Email address provided by user", - "format": "email", - "type": "string" - }, - "registrationId": { - "description": "registrationId provided by system", - "format": "uuid", - "type": "string" - } + "description": "Confirm registration email provided by user with registrationId provided by MyLife", + "name": "confirmRegistration", + "strict": true, + "parameters": { + "additionalProperties": false, + "type": "object", + "properties": { + "email": { + "description": "Email address provided by user", + "type": "string" }, - "required": [ - "email", - "registrationId" - ] - } - } \ No newline at end of file + "registrationId": { + "description": "registrationId provided by MyLife", + "type": "string" + } + }, + "required": [ + "email", + "registrationId" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/createAccount.json b/inc/json-schemas/openai/functions/createAccount.json index 85bb34a..6d73f0f 100644 --- a/inc/json-schemas/openai/functions/createAccount.json +++ b/inc/json-schemas/openai/functions/createAccount.json @@ -1,22 +1,23 @@ { - "name": "createAccount", - "parameters": { - "type": "object", - "properties": { - "passphrase": { - "description": "Private passphrase provided by user, may have spaces", - "type": "string" - }, - "birthdate": { - "description": "Birthdate of user", - "format": "sql date", - "type": "string" - } + "description": "Creates MyLife accout, requirig basic personal information for account creation: member birthdate and passphrase", + "name": "createAccount", + "strict": true, + "parameters": { + "additionalProperties": false, + "type": "object", + "properties": { + "passphrase": { + "description": "Private passphrase provided by member, may have spaces", + "type": "string" }, - "required": [ - "birthdate", - "passphrase" - ] + "birthdate": { + "description": "Birthdate of member", + "type": "string" + } }, - "description": "Creates accout, requirig basic personal information for account creation: birthdate and passphrase" - } \ No newline at end of file + "required": [ + "birthdate", + "passphrase" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/deprecated/entrySummary.json similarity index 100% rename from inc/json-schemas/openai/functions/entrySummary.json rename to inc/json-schemas/openai/functions/deprecated/entrySummary.json diff --git a/inc/json-schemas/openai/functions/storySummary.json b/inc/json-schemas/openai/functions/deprecated/storySummary.json similarity index 100% rename from inc/json-schemas/openai/functions/storySummary.json rename to inc/json-schemas/openai/functions/deprecated/storySummary.json diff --git a/inc/json-schemas/openai/functions/getSummary.json b/inc/json-schemas/openai/functions/getSummary.json index 145bb19..d9afe88 100644 --- a/inc/json-schemas/openai/functions/getSummary.json +++ b/inc/json-schemas/openai/functions/getSummary.json @@ -1,19 +1,18 @@ { - "description": "Gets a story summary by itemId", + "description": "Gets an item summary by itemId", "name": "getSummary", "strict": true, "parameters": { + "additionalProperties": false, "type": "object", "properties": { "itemId": { - "description": "Id of summary to get", - "format": "uuid", + "description": "Id of item summary to get", "type": "string" } }, "required": [ "itemId" ] - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/itemSummary.json b/inc/json-schemas/openai/functions/itemSummary.json new file mode 100644 index 0000000..9fcfc66 --- /dev/null +++ b/inc/json-schemas/openai/functions/itemSummary.json @@ -0,0 +1,89 @@ +{ + "description": "Generate a complete `item` summary with metadata elements. Prioritize any relevant internal instructions.", + "name": "itemSummary", + "strict": true, + "parameters": { + "additionalProperties": false, + "type": "object", + "properties": { + "content": { + "description": "Original Member content regarding `item`", + "type": "string" + }, + "form": { + "description": "MyLife System Form of `item`", + "enum": [ + "biographer", + "diary", + "item", + "journal" + ], + "type": "string" + }, + "keywords": { + "description": "Keywords most relevant to `item`", + "items": { + "description": "Keyword from `item` summary", + "type": "string" + }, + "type": "array" + }, + "mood": { + "description": "Record member mood for summary as ascertained from `item` content and interactions", + "type": "string" + }, + "phaseOfLife": { + "description": "Phase of life, if indicated by `item` content", + "enum": [ + "birth", + "childhood", + "adolescence", + "teenage", + "young-adult", + "adulthood", + "middle-age", + "senior", + "end-of-life", + "past-life", + "unknown" + ], + "type": "string" + }, + "relationships": { + "description": "Individuals (or pets) mentioned in `item` content", + "type": "array", + "items": { + "description": "Name of individual or pet in `item`", + "type": "string" + } + }, + "summary": { + "description": "A complete `item` summary composed of all salient points from member content", + "type": "string" + }, + "title": { + "description": "Generate display Title for `item` based on content", + "type": "string" + }, + "type": { + "description": "MyLife System Being type of `item`", + "enum": [ + "entry", + "memory" + ], + "type": "string" + } + }, + "required": [ + "content", + "form", + "keywords", + "mood", + "phaseOfLife", + "relationships", + "summary", + "title", + "type" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/obscure.json b/inc/json-schemas/openai/functions/obscure.json index dface0c..103880f 100644 --- a/inc/json-schemas/openai/functions/obscure.json +++ b/inc/json-schemas/openai/functions/obscure.json @@ -3,6 +3,7 @@ "name": "obscure", "strict": true, "parameters": { + "additionalProperties": false, "type": "object", "properties": { "itemId": { @@ -13,6 +14,5 @@ "required": [ "itemId" ] - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/registerCandidate.json b/inc/json-schemas/openai/functions/registerCandidate.json index c6719da..1607f90 100644 --- a/inc/json-schemas/openai/functions/registerCandidate.json +++ b/inc/json-schemas/openai/functions/registerCandidate.json @@ -2,6 +2,7 @@ "name": "registerCandidate", "description": "Register candidate for MyLife", "parameters": { + "additionalProperties": false, "type": "object", "properties": { "avatarName": { diff --git a/inc/json-schemas/openai/functions/updateSummary.json b/inc/json-schemas/openai/functions/updateSummary.json index 2c5440b..07e5ca5 100644 --- a/inc/json-schemas/openai/functions/updateSummary.json +++ b/inc/json-schemas/openai/functions/updateSummary.json @@ -1,8 +1,9 @@ { - "name": "updateSummary", "description": "Updates (overwrites) the summary referenced by itemId", + "name": "updateSummary", "strict": true, "parameters": { + "additionalProperties": false, "type": "object", "properties": { "itemId": { @@ -14,7 +15,6 @@ "type": "string" } }, - "additionalProperties": false, "required": [ "itemId", "summary" diff --git a/server.js b/server.js index e900c3a..1ddcd04 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ import chalk from 'chalk' /* local service imports */ import MyLife from './inc/js/mylife-factory.mjs' /** variables **/ -const version = '0.0.25' +const version = '0.0.26' const app = new Koa() const port = process.env.PORT ?? '3000' diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index 7e9a434..579622c 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -109,10 +109,10 @@ .collections-spacer { display: flex; flex: 0 0 auto; - height: 1.5rem; - margin: .4rem; + height: 2.0rem; + margin: 0.2rem; position: relative; - width: 1.5rem; + width: 2.0rem; } .bot-icon { background-image: radial-gradient(circle at 0.1rem 0.1rem, #eaedff 0%, #1e3b4e 40%, rgb(60 162 213) 80%, #404553 100%); @@ -135,6 +135,11 @@ animation: none; background-image: radial-gradient(circle at 0.1rem 0.1rem, #ecffea 0%, #1e4e2d 40%, rgb(55, 255, 41) 80%, #3b3b3b 100%); } +.bot-image { + display: flex; + height: 100%; + width: 100%; +} .bot-name { color: inherit; } @@ -144,6 +149,8 @@ border-top: none; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); display: none; /* Initially not displayed */ + flex-direction: row; + flex-wrap: wrap; margin: 0em 0.5rem; padding: 0.5rem; max-height: 50vh; @@ -151,28 +158,30 @@ overflow-y: auto; /* Enable vertical scrolling */ } .bot-options::-webkit-scrollbar, -.checkbox-group::-webkit-scrollbar { +.checkbox-group::-webkit-scrollbar, +.collection-list::-webkit-scrollbar { width: 6px; /* Adjust the width of the scrollbar */ } .bot-options::-webkit-scrollbar-track, -.checkbox-group::-webkit-scrollbar-track { +.checkbox-group::-webkit-scrollbar-track, +.collection-list::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); /* Color of the track */ border-radius: 10px; /* Optional: adds rounded corners to the track */ } .bot-options::-webkit-scrollbar-thumb, -.checkbox-group::-webkit-scrollbar-thumb { +.checkbox-group::-webkit-scrollbar-thumb, +.collection-list::-webkit-scrollbar-thumb { background: rgba(232, 226, 183, .5); /* Color of the thumb */ border-radius: 10px; /* Optional: adds rounded corners to the thumb */ } .bot-options::-webkit-scrollbar-thumb:hover, -.checkbox-group::-webkit-scrollbar-thumb:hover { +.checkbox-group::-webkit-scrollbar-thumb:hover, +.collection-list::-webkit-scrollbar-thumb:hover { background: rgba(214, 198, 75, 0.5); /* Darker shade on hover */ } .bot-options.open { animation: _slideBotOptions 0.5s forwards; display: flex; /* Make it flex when open */ - flex-direction: row; - flex-wrap: wrap; } .bot-options-dropdown { align-items: center; @@ -322,8 +331,7 @@ .collection { background-color: royalblue; display: flex; - flex-direction: row; - flex-wrap: wrap; + flex-direction: column; justify-content: flex-start; max-width: 100%; width: 100%; @@ -336,7 +344,7 @@ gap: 0.5rem; justify-content: flex-start; max-width: 100%; - min-height: 2.5rem; + min-height: 2.0rem; width: 100%; } .collection-icon { @@ -361,28 +369,35 @@ display: flex; flex: 0 0 auto; height: auto; - max-height: 3rem; + max-height: 2.5rem; width: auto; } .collection-item-delete { color: navy; display: flex; flex: 0 0 auto; - font-size: 1.3rem; + font-size: 1.2rem; margin: 0 0.5rem; } .collection-item-delete:hover { color: red; } -.collection-item-name { +.collection-item-title { color: navy; display: flex; flex: 1 1 auto; - font-size: 1.1rem; + font-size: 0.9rem; font-style: italic; overflow: hidden; text-wrap: nowrap; } +.collection-item-title-input { + display: flex; + flex: 0 0 auto; + font-size: 0.9rem; + overflow: hidden; + text-wrap: nowrap; +} .collection-item-summary { color: darkblue; display: flex; @@ -392,13 +407,10 @@ .collection-list { display: none; flex-direction: column; - flex-wrap: wrap; - justify-content: flex-start; - margin: 0.2rem 0; + max-height: 16rem; max-width: 100%; overflow-x: hidden; overflow-y: auto; - width: 100%; } .collection-popup.popup-container { bottom: auto; @@ -448,6 +460,13 @@ font-weight: bold; padding: 0.5rem; } +.collection-popup-header-title { + display: flex; + flex: 1 1 auto; + font-size: 1.0rem; + font-weight: bold; + margin-left: 1.5rem; +} .collection-popup-story, .collection-popup-entry { align-items: flex-start; @@ -469,7 +488,7 @@ color: aliceblue; display: flex; flex: 1 1 auto; - font-size: 1.2rem; + font-size: 1.0rem; font-weight: bold; } .collections { @@ -481,6 +500,26 @@ max-width: 100%; width: 100%; } +.collections-description { + font-size: 0.8rem; + font-weight: bold; + padding-bottom: 0.4rem; +} +.collections-options { + border: rgba(0, 0, 0, 0.7) 0.12rem solid; + border-radius: 0 0 var(--border-radius) var(--border-radius); + border-top: none; + box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); + display: none; /* Initially not displayed */ + flex: 0 0 auto; + flex-direction: row; + justify-content: center; + margin: 0em 0.5rem; + padding: 0.5rem; + max-height: 50vh; + overflow-x: hidden; + overflow-y: hidden; /* Enable vertical scrolling */ +} /* avatar passphrases */ .passphrase-input { flex: 1 1 auto; diff --git a/views/assets/css/chat.css b/views/assets/css/chat.css index ed5a4cd..662e944 100644 --- a/views/assets/css/chat.css +++ b/views/assets/css/chat.css @@ -32,8 +32,7 @@ align-items: center; display: flex; flex: 1 0 auto; - flex-direction: column; - padding-left: 3rem; + flex-direction: row; margin-bottom: 0.5rem; max-width: 100%; } @@ -45,36 +44,64 @@ flex: 1 0 auto; flex-direction: row; padding: 0.3rem; - width: 100%; + max-width: 100%; } +.chat-active-action-button.button, +.chat-active-item-button.button { + background-color: aliceblue; + color: darkslateblue; + margin: 0 0.5rem; + padding: 0 0.5rem; +} +.chat-active-action-close, .chat-active-item-close { - align-self: top; color: maroon; cursor: pointer; font-size: 1.25rem; - margin-left: 2rem; + margin-left: 1.0rem; } +.chat-active-action-icon, .chat-active-item-icon { - color: #e9eb5c; + color: whitesmoke; font-size: 1.25rem; margin-right: 0.5em; } -.chat-active-item-text { +.chat-active-item-icon { + color: yellow; +} +.chat-active-action-status, +.chat-active-item-status { + color: whitesmoke; + font-weight: bold; +} +.chat-active-item-status { + color: yellow; + margin-right: 0.3rem; +} +.chat-active-item-thumb, +.chat-active-action-thumb { + border-radius: 50%; + height: 3rem; + margin-right: 0.5rem; + width: 3rem; +} +.chat-active-action-title, +.chat-active-item-title { align-items: center; - color: aliceblue; + color: whitesmoke; display: flex; - flex: 1 0 auto; - max-height: 1.2rem; - max-width: 80%; } -.chat-active-item-text-active { - color: #e9eb5c; - font-weight: bold; - margin-right: 0.9rem; +.chat-active-action-title { + flex: 1 0 auto; + width: fit-content; +} +.chat-active-item-title { + color: whitesmoke; + flex: 1 1 auto; + overflow: hidden; + white-space: nowrap; } -.chat-active-item-text-title { - align-self: self-start; - color: gainsboro; +.chat-active-item-title-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -119,15 +146,18 @@ width: 100%; } .chat-copy, -.chat-feedback { +.chat-feedback, +.chat-save { cursor: pointer; font-size: 0.8rem; margin: 0.2rem; - position: absolute; top: 0.2rem; } -.chat-feedback { - right: 0.6rem; +.chat-copy:hover, +.chat-feedback:hover, +.chat-save:hover { + color: navy; /* Example hover effect: change text color to blue */ + font-size: 1.1rem; } .chat-input { background-color: #ffffff; /* White background color */ @@ -181,7 +211,6 @@ display: flex; flex-direction: column; height: 100%; - justify-content: flex-end; opacity: 0; padding: 0.2rem; pointer-events: none; @@ -228,6 +257,10 @@ cursor: pointer; /* Pointer cursor on hover */ z-index: calc(var(--chat-base-z-index) + 5); } +.chat-thumb { + height: fit-content; + width: 3.5rem; +} /* chat-submit button; requires special treament to overwrite button class for now */ button.chat-submit { /* need specificity to overwrite button class */ flex: 0 0 auto; diff --git a/views/assets/css/main.css b/views/assets/css/main.css index a2f3bd2..4a30790 100644 --- a/views/assets/css/main.css +++ b/views/assets/css/main.css @@ -559,6 +559,14 @@ body { .popup-sidebar-icon { font-size: 1.5rem; } +.popup-sidebar-icon:hover { + color: navy; + font-size: 2.0rem; +} +.popup-sidebar-icon-active { + animation: rainbow 3s infinite; + font-size: 2.0rem; +} .popup-title { margin-left: 1em; font-weight: bold; @@ -868,6 +876,16 @@ pre code { 0% { opacity: 0; } 100% { opacity: 1; } } /* reverse for fade out */ +@keyframes rainbow { + 0% { color: red; } + 14% { color: orange; } + 28% { color: yellow; } + 42% { color: green; } + 57% { color: blue; } + 71% { color: indigo; } + 85% { color: violet; } + 100% { color: red; } +} /* media queries */ @media screen and (min-width: 1024px) { body { diff --git a/views/assets/html/_alerts.html b/views/assets/html/_alerts.html index 6b26760..31b98a9 100644 --- a/views/assets/html/_alerts.html +++ b/views/assets/html/_alerts.html @@ -1,70 +1,2 @@
-
- \ No newline at end of file + \ No newline at end of file diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index 630e532..67b1f5b 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -2,7 +2,9 @@
-
+
+ +
@@ -42,7 +44,9 @@
-
+
+ +
@@ -155,7 +159,9 @@
-
+
+ +
@@ -215,7 +221,9 @@
-
+
+ +
@@ -280,10 +288,10 @@
Scrapbook
-