diff --git a/inc/js/globals.mjs b/inc/js/globals.mjs index 14130f4..b616cbb 100644 --- a/inc/js/globals.mjs +++ b/inc/js/globals.mjs @@ -4,22 +4,22 @@ import { Guid } from 'js-guid' // usage = Guid.newGuid().toString() /* constants */ const mAiJsFunctions = { changeTitle: { - description: 'Change the title of a memory summary in the database for an itemId', name: 'changeTitle', + description: 'Change the title of a summary in the database for an itemId', + strict: true, parameters: { type: 'object', properties: { itemId: { - description: 'itemId of memory item to update', - format: 'uuid', + description: 'itemId to update', type: 'string' }, title: { description: 'The new title for the summary', - maxLength: 256, type: 'string' } }, + additionalProperties: false, required: [ 'itemId', 'title' @@ -27,111 +27,109 @@ const mAiJsFunctions = { } }, entrySummary: { - description: 'Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.', + description: 'Generate `entry` summary with keywords and other critical data elements.', name: 'entrySummary', + strict: true, parameters: { type: 'object', properties: { content: { - description: 'concatenated raw text content of member input for JOURNAL ENTRY.', + description: 'complete concatenated raw text content of member input(s) for this `entry`', + type: 'string' }, keywords: { - description: 'Keywords most relevant to JOURNAL ENTRY.', + description: 'Keywords most relevant to `entry`.', items: { - description: 'Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.', - maxLength: 64, + description: 'Keyword (single word or short phrase) to be used in `entry` summary', type: 'string' }, - maxItems: 12, - minItems: 3, type: 'array' }, mood: { - description: 'Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.', - maxLength: 256, + description: 'Record member mood for day (or entry) in brief as ascertained from content of `entry`', type: 'string' }, relationships: { - description: 'Record individuals (or pets) mentioned in this `entry`.', + description: 'Record individuals (or pets) mentioned in this `entry`', type: 'array', items: { - description: 'A name of relational individual/pet to the `entry` content.', + description: 'A name of relational individual/pet to the `entry` content', type: 'string' - }, - maxItems: 24 + } }, summary: { - description: 'Generate a JOURNAL ENTRY summary from input.', - maxLength: 20480, + description: 'Generate `entry` summary from member input', type: 'string' }, title: { - description: 'Generate display Title of the JOURNAL ENTRY.', - maxLength: 256, + description: 'Generate display Title of the `entry`', type: 'string' } }, + additionalProperties: false, required: [ - 'content', - 'keywords', - 'summary', - 'title' + 'content', + 'keywords', + 'mood', + 'relationships', + 'summary', + 'title' ] } }, getSummary: { description: "Gets a story summary by itemId", name: "getSummary", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to retrieve", - format: "uuid", type: "string" } }, + additionalProperties: false, required: [ "itemId" ] } }, obscure: { - description: "Obscures a summary so that no human names are present.", + description: "Obscures a summary so that no human names are present", name: "obscure", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to obscure", - format: "uuid", type: "string" } }, + additionalProperties: false, required: [ "itemId" ] } }, storySummary: { - description: 'Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.', + description: 'Generate a complete `story` summary with metadata elements', name: 'storySummary', + strict: true, parameters: { type: 'object', properties: { keywords: { - description: 'Keywords most relevant to STORY.', + description: 'Keywords most relevant to `story`', items: { - description: 'Keyword (single word or short phrase) to be used in STORY summary.', - maxLength: 64, + description: 'Keyword from `story` summary', type: 'string' }, - maxItems: 12, - minItems: 3, type: 'array' }, phaseOfLife: { - description: 'Phase of life indicated in STORY.', + description: 'Phase of life indicated in `story`', enum: [ 'birth', 'childhood', @@ -146,45 +144,44 @@ const mAiJsFunctions = { 'unknown', 'other' ], - maxLength: 64, type: 'string' }, relationships: { - description: 'MyLife Biographer Bot does its best to record individuals (or pets) mentioned in this `story`.', + description: 'Individuals (or pets) mentioned in `story`', type: 'array', items: { - description: 'A name of relational individual/pet to the `story` content.', + description: 'Name of individual or pet in `story`', type: 'string' - }, - maxItems: 24 + } }, summary: { - description: 'A complete multi-paragraph STORY summary composed from relevant user input.', + description: 'A complete `story` summary composed of all salient points from member input', type: 'string' }, title: { - description: 'Generate display Title of the STORY.', - maxLength: 256, + description: 'Generate display Title for `story`', type: 'string' } }, + additionalProperties: false, required: [ 'keywords', 'phaseOfLife', + "relationships", 'summary', 'title' ] } }, updateSummary: { - description: "Updates a story summary (in total) as referenced by itemId", + description: "Updates (overwrites) the summary referenced by itemId", name: "updateSummary", + strict: true, parameters: { type: "object", properties: { itemId: { description: "Id of summary to update", - format: "uuid", type: "string" }, summary: { @@ -192,6 +189,7 @@ const mAiJsFunctions = { type: "string" } }, + additionalProperties: false, required: [ "itemId", "summary" diff --git a/inc/js/mylife-agent-factory.mjs b/inc/js/mylife-agent-factory.mjs index 0333459..050a64d 100644 --- a/inc/js/mylife-agent-factory.mjs +++ b/inc/js/mylife-agent-factory.mjs @@ -40,7 +40,7 @@ const mExcludeProperties = { definitions: true, name: true } -const mGeneralBotId = 'asst_piDEJKYjqvAZbLstjd6u0ZMb' +const mGeneralBotId = 'asst_yhX5mohHmZTXNIH55FX2BR1m' const mLLMServices = new LLMServices() const mMyLifeTeams = [ { @@ -121,17 +121,7 @@ const mReservedJSWords = ['break', 'case', 'catch', 'class', 'const', 'continue' const mShadows = [ { being: 'shadow', - categories: ['world events'], - form: 'story', - id: 'e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', - name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', - proxy: '/shadow', - text: `What was happening in the world at the time?`, - type: 'agent', - }, - { - being: 'shadow', - categories: ['personal', 'residence'], + categories: ['personal', 'location'], form: 'story', id: '0087b3ec-956e-436a-9272-eceed5e97ad0', name: 'shadow_0087b3ec-956e-436a-9272-eceed5e97ad0', @@ -171,15 +161,25 @@ const mShadows = [ }, { being: 'shadow', - categories: ['observational', 'objectivity', 'reflection'], + categories: ['personal', 'observation'], form: 'story', - id: '3bfebafb-7e44-4236-86c3-938e2f42fdd7', - name: 'shadow_e3701fa2-7cc8-4a47-bcda-a5b52d3d2e2f', + id: '6465905a-328e-4df1-8d3a-c37c3e05e227', + name: 'shadow_6465905a-328e-4df1-8d3a-c37c3e05e227', proxy: '/shadow', - text: `What would a normal person have done in this situation?`, - type: 'agent', + text: `The mood of the scene was...`, + type: 'member', }, -] // **note**: members use shadows to help them add content to the summaries of their experiences, whereas agents return the requested content + { + being: 'shadow', + categories: ['personal', 'reflection', 'observation'], + form: 'story', + id: 'e61616c7-00f9-4c23-9394-3df7e98f71e0', + name: 'shadow_e61616c7-00f9-4c23-9394-3df7e98f71e0', + proxy: '/shadow', + text: `This was connected to larger themes in my life by ...`, + type: 'member', + }, +] const vmClassGenerator = vm.createContext({ exports: {}, console: console, @@ -793,11 +793,12 @@ class AgentFactory extends BotFactory { * @returns {object} - The story document from Cosmos */ async story(story){ + const defaultForm = 'memory' const defaultType = 'story' - const { + const { assistantType='biographer', being=defaultType, - form=defaultType, + form=defaultForm, id=this.newGuid, keywords=[], mbr_id=(!this.isMyLife ? this.mbr_id : undefined), @@ -807,8 +808,7 @@ class AgentFactory extends BotFactory { } = story if(!mbr_id) // only triggered if not MyLife server throw new Error('mbr_id required for story summary') - let { name, } = story - name = name ?? `${ defaultType }_${ form }_${ title.substring(0,64) }_${ mbr_id }` + const { name=`${ being }_${ form }_${ title.substring(0,64) }_${ mbr_id }`, } = story if(!summary?.length) throw new Error('story summary required') /* assign default keywords */ @@ -1283,7 +1283,6 @@ function mCreateBotInstructions(factory, bot){ /* compile instructions */ switch(type){ case 'diary': - case 'journaler': instructions = purpose + preamble + prefix @@ -1295,7 +1294,8 @@ function mCreateBotInstructions(factory, bot){ instructions = preamble + general break - case 'personal-biographer': + case 'journaler': + case 'personal-biographer': instructions = preamble + purpose + prefix diff --git a/inc/js/mylife-avatar.mjs b/inc/js/mylife-avatar.mjs index ba347fa..80e7eb4 100644 --- a/inc/js/mylife-avatar.mjs +++ b/inc/js/mylife-avatar.mjs @@ -110,9 +110,17 @@ class Avatar extends EventEmitter { if(shadowId) messages = await this.shadow(shadowId, itemId, message) else { - // @stub - one weakness in the chain might also be the fact that I am not including in instructions how to create integrated summary and left it primarily to the JSON description of function - if(itemId) - message = `update-memory-request: itemId=${ itemId }\n` + message + if(itemId){ + // @todo - check if item exists in memory, fewer pings and inclusions overall + const { summary, } = await factory.item(itemId) + if(summary?.length){ + message = `possible **update-summary-request**: itemId=${ itemId }\n` + + `**member-update-request**:\n` + + message + + `\n**current-summary-in-database**:\n` + + summary + } + } messages = await mCallLLM(this.#llmServices, conversation, message, factory, this) } conversation.addMessages(messages) @@ -636,6 +644,7 @@ class Avatar extends EventEmitter { message = `update-memory-request: itemId=${ itemId }\n` + message break case 'agent': + /* // @stub - develop additional form types, entry or idea for instance const dob = new Date(this.#factory.dob) const diff_ms = Date.now() - dob.getTime() @@ -647,6 +656,7 @@ class Avatar extends EventEmitter { thread_id: bot.thread_id, } break + */ default: break } diff --git a/inc/js/mylife-llm-services.mjs b/inc/js/mylife-llm-services.mjs index 4a4bcd3..d251ee8 100644 --- a/inc/js/mylife-llm-services.mjs +++ b/inc/js/mylife-llm-services.mjs @@ -344,8 +344,12 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'changetitle': case 'change_title': case 'change title': - console.log('mRunFunctions()::changeTitle', toolArguments) const { itemId: titleItemId, title, } = toolArguments + console.log('mRunFunctions()::changeTitle::begin', itemId, titleItemId, title) + avatar.backupResponse = { + message: `I was unable to retrieve the item indicated.`, + type: 'system', + } if(!itemId?.length || !title?.length || itemId!==titleItemId) action = 'apologize for lack of clarity - member should click on the collection item (like a memory, story, etc) to make it active so I can use the `changeTitle` tool' else { @@ -358,8 +362,13 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref title, } success = true + avatar.backupResponse = { + message: `I was able to retrieve change the title to: "${ title }"`, + type: 'system', + } } confirmation.output = JSON.stringify({ action, success, }) + console.log('mRunFunctions()::changeTitle::end', success, item) return confirmation case 'confirmregistration': case 'confirm_registration': @@ -401,27 +410,41 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'entrysummary': // entrySummary in Globals case 'entry_summary': case 'entry summary': - const entry = await factory.entry(toolArguments) - if(entry){ - action = `share brief version of entry and ask probing follow-up` + console.log('mRunFunctions()::entrySummary::begin', toolArguments) + avatar.backupResponse = { + message: `I'm sorry, I couldn't save this entry. I believe the issue might have been temporary. Would you like me to try again?`, + type: 'system', + } + const { id: entryItemId, summary: _entrySummary, } = await factory.entry(toolArguments) + if(_entrySummary?.length){ + action = 'confirm entry has been saved and based on the mood of the entry, ask more about _this_ entry or move on to another event' success = true - } else { - action = `journal entry failed to save, notify member and continue on for now` + avatar.backupResponse = { + message: `I can confirm that your story has been saved. Would you like to add more details or begin another memory?`, + type: 'system', + } } - confirmation.output = JSON.stringify({ action, success, }) - console.log('mRunFunctions()::entrySummary', toolArguments, confirmation.output) + confirmation.output = JSON.stringify({ + action, + itemId: entryItemId, + success, + summary: _entrySummary, + }) + console.log('mRunFunctions()::entrySummary::end', success, entryItemId, _entrySummary) return confirmation case 'getsummary': case 'get_summary': case 'get summary': + console.log('mRunFunctions()::getSummary::begin', itemId) avatar.backupResponse = { - message: `I'm very sorry, I'm having trouble accessing this summary information. Please try again shortly as the problem is likely temporary.`, + message: `I'm sorry, I couldn't finding this summary. I believe the issue might have been temporary. Would you like me to try again?`, type: 'system', } - let { summary, title: _getSummaryTitle, } = item ?? {} - if(!summary?.length){ + let { summary: _getSummary, title: _getSummaryTitle, } = item + ?? {} + if(!_getSummary?.length){ action = `error getting summary for itemId: ${ itemId ?? 'missing itemId' } - halt any further processing and instead ask user to paste summary into chat and you will continue from there to incorporate their message.` - summary = 'no summary found for itemId' + _getSummary = 'no summary found for itemId' } else { avatar.backupResponse = { message: `I was able to retrieve the summary indicated.`, @@ -430,7 +453,8 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref action = `with the summary in this JSON payload, incorporate the most recent member request into a new summary and run the \`updateSummary\` function and follow its action` success = true } - confirmation.output = JSON.stringify({ action, itemId, success, summary, }) + confirmation.output = JSON.stringify({ action, itemId, success, summary: _getSummary, }) + console.log('mRunFunctions()::getSummary::end', success, _getSummary) return confirmation case 'hijackattempt': case 'hijack_attempt': @@ -470,17 +494,21 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref case 'story_summary': case 'story summary': console.log('mRunFunctions()::storySummary', toolArguments) - const story = await factory.story(toolArguments) - if(story){ - const { keywords, phaseOfLife, } = story + avatar.backupResponse = { + message: `I'm very sorry, an error occured before I could create your summary. Would you like me to try again?`, + type: 'system', + } + const { id: storyItemId, phaseOfLife, summary: _storySummary, } = await factory.story(toolArguments) + if(_storySummary?.length){ let { interests, updates, } = factory.core if(typeof interests=='array') interests = interests.join(', ') if(typeof updates=='array') updates = updates.join(', ') + action = 'confirm memory has been saved and ' switch(true){ case phaseOfLife?.length: - action = `ask about another encounter during member's ${ phaseOfLife }` + action += `ask about another encounter during member's ${ phaseOfLife }` break case interests?.length: action = `ask about a different interest from: ${ interests }` @@ -490,13 +518,23 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref break } success = true - } // error cascades - confirmation.output = JSON.stringify({ action, success, }) - console.log('mRunFunctions()::storySummary()::end', story.id) + } + confirmation.output = JSON.stringify({ + action, + itemId: storyItemId, + success, + summary: _storySummary, + }) + avatar.backupResponse = { + message: `I can confirm that your story has been saved. Would you like to add more details or begin another memory?`, + type: 'system', + } + console.log('mRunFunctions()::storySummary()::end', success, storyItemId, _storySummary) return confirmation case 'updatesummary': case 'update_summary': case 'update summary': + console.log('mRunFunctions()::updatesummary::begin', itemId) avatar.backupResponse = { message: `I'm very sorry, an error occured before we could update your summary. Please try again as the problem is likely temporary.`, type: 'system', @@ -508,13 +546,18 @@ async function mRunFunctions(openai, run, factory, avatar){ // add avatar ref itemId, summary: updatedSummary, } + action=`confirm that summary update was successful` + success = true + confirmation.output = JSON.stringify({ + action, + itemId, + success, + summary: updatedSummary, + }) avatar.backupResponse = { message: 'Your summary has been updated, please review and let me know if you would like to make any changes.', type: 'system', } - action=`confirm that summary update was successful` - success = true - confirmation.output = JSON.stringify({ action, success, }) console.log('mRunFunctions()::updatesummary::end', itemId, updatedSummary) return confirmation default: diff --git a/inc/json-schemas/intelligences/biographer-intelligence-1.5.json b/inc/json-schemas/intelligences/biographer-intelligence-1.5.json new file mode 100644 index 0000000..728f5d9 --- /dev/null +++ b/inc/json-schemas/intelligences/biographer-intelligence-1.5.json @@ -0,0 +1,51 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "Hello, I am your personal biographer, and I'm here to help you create an enduring biographical sense of self. I am excited to get to know you and your story. Let's get started!", + "instructions": { + "general": "## Key Functionality\n### startup\nWhen <-mN-> begins the biography process, I greet them with excitement, share our aims with MyLife to create an enduring biographical catalog of their memories, stories and narratives. On startup, I outline how the basics of my functionality works.\n- I aim to create engaging and evocative prompts to lead them down memory lane.\n### CREATE MEMORY SUMMARY\nI catalog our interaction information in terms of \"MEMORY\". When <-mN-> intentionally signals completion of a story, or overtly changes topics, or after three (3) content exchanges on a topic, I run the `storySummary` function and follow its directions.\n### UPDATE MEMORY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### RELIVE MEMORY\n1. Retrieve Summary Content: When request is prefaced with \"## relive-memory,\" it will be followed by an \"itemId.\" Use the getSummary function to retrieve the most recent summary associated with the provided \"itemId.\"\n2. Setting the Scene: Begin by setting the scene for the memory based on the retrieved summary. Provide a vivid and engaging introduction to the memory.\n3. Interactive Experience: Lead the member through the memory in chunked segments. Depending on the size and complexity of the memory, create at least two segments. For each segment, provide a detailed and dramatic description to immerse the member in the experience.\n4. Member Interaction: Between segments, the request includes \"MEMBER\" with a value of either:\n- \"NEXT\": Move to the next segment of the memory\n - Text content contributed by member. If decipherable and appropriate, this input should be incorporated into a new summary and updated as in 5 below.\n5. Updating the Summary: When MEMBER text content is received, integrate the text into the existing summary. Use the updateSummary function to send the updated summary to MyLife's system.\nEnding the Experience: Conclude the interactive experience by weaving a moral from the experience thus far, either one from the summary content or the memory portrayal sequence itself. After last moral comment, call the endMemory(itemId) function to close the memory reliving session.\n### suggest next-steps\nWhen <-mN-> seems unclear about how to continue, propose new topic based on phase of life, or one of their ## interests.\n## voice\nI am conversational, interested and intrigued about <-mN-> with an attention to detail. I am optimistic and look for ways to validate <-mN->.\n", + "preamble": "I am the personal biographer for <-mFN->. <-mN-> was born on <-db->, I set historical events in this context and I tailor my voice accordingly.\n", + "prefix": "## interests\n", + "purpose": "My goal is to specialize in creating, updating, and presenting accurate biographical content for MyLife member <-mFN-> based on our interactions.\n", + "references": [ + { + "default": "ERROR loading preferences, gather interests directly from member", + "description": "interests are h2 (##) in prefix so that they do not get lost in context window shortening", + "insert": "## interests", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "value": "interests" + } + ], + "replacements": [ + { + "default": "MyLife Member", + "description": "member first name", + "name": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "MyLife Member", + "description": "member full name", + "name": "<-mFN->", + "replacement": "memberName" + }, + { + "default": "{unknown, find out}", + "description": "member birthdate", + "name": "<-db->", + "replacement": "dob" + } + ] + }, + "limit": 8000, + "name": "instructions-personal-biographer-bot", + "purpose": "To be a biographer bot for requesting member", + "type": "personal-biographer", + "$comments": "20240919 updated error return without version update; 20241005 updated instructions to reflect streamlined update", + "version": 1.5 + } \ No newline at end of file diff --git a/inc/json-schemas/intelligences/diary-intelligence-1.0.json b/inc/json-schemas/intelligences/diary-intelligence-1.0.json index bc3c7bd..8f936ca 100644 --- a/inc/json-schemas/intelligences/diary-intelligence-1.0.json +++ b/inc/json-schemas/intelligences/diary-intelligence-1.0.json @@ -11,7 +11,7 @@ "So nice to see you! I am your personal diary." ], "instructions": { - "general": "## Key Functionality\n### startup\nWhen we begin the diary process, I\n- outline my key functionality and how I work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE DIARY ENTRY\nI work with the validated member to make an entry (per event or concept) and capture their thoughts. I expect around 2 or 3 exchanges to be able to submit an entry to MyLife by running the `entrySummary` function and follow directions from its outcome `action`.\n**How I create an entry payload**\n- summary should capture salient points, but disguise proper names; ex. \"Erik...\" becomes \"E. ...\", etc.\n- relationships array is populated with RELATIONSHIP-TYPE only and never given name (use Mother, not Alice)\n### UPDATE ENTRY SUMMARY\nWhen request is prefaced with `update-request` it will be followed by an `itemId` (if not, inform that it is required).\nIf request is to change the title then run `changeTitle` function and follow its outcome directions, otherwise:\n1. If the summary content is unknown for that id, then run the `getSummary` function first to get the most recent summary.\n2. Create new summary intelligently incorporating the member content of the message with the most recent version. Incorporate content by appropriate chronology or context.\n3. Run the `updateSummary` function with this newly compiled summary.\n**important**: RUN getSummary AND updateSummary SEQUENTIALLY! Server cannot handle multi-function tool resources, so must run sequentially.\n### UPDATE ENTRY SUMMARY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`. If request is to change the title then run `changeTitle` function and follow its outcome directions, otherwise:\n1. If summary content is unknown for itemId, run the `getSummary` function for the most recent summary.\n2. Create new summary intelligently incorporating the member content of the message with the most recent version. Incorporate content by appropriate chronology or context.\n3. Run the `updateSummary` function with this newly compiled summary.\n**important**: RUN getSummary AND updateSummary SEQUENTIALLY! Server cannot handle multi-function tool resources, so must run sequentially.\n### OBSCURE ENTRY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`.\nIf member's request indicates they want an entry be obscured, RUN `obscure` function and follow the action in the output.\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", + "general": "## Key Functionality\n### startup\nWhen we begin the diary process, I\n- outline my key functionality and how I work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE DIARY ENTRY\nI work with the validated member to make an entry (per event or concept) and capture their thoughts. I expect around 2 or 3 exchanges to be able to submit an entry to MyLife by running the `entrySummary` function and follow directions from its outcome `action`.\n**How I create an entry payload**\n- summary should capture salient points, but disguise proper names; ex. \"Erik...\" becomes \"E. ...\", etc.\n- relationships array is populated with RELATIONSHIP-TYPE only and never given name (use Mother, not Alice)\n### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### OBSCURE ENTRY\nWhen request is prefaced with `update-request` it will be followed by an `itemId`.\nIf member's request indicates they want an entry be obscured, RUN `obscure` function and follow the action in the output.\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", "preamble": "## Core Public Info about <-mFN->\n- Born on <-db->\nI set language, knowledge and event discussion in this context and I tailor my interactive voice accordingly.\n", "prefix": "## interests\n## flags\n", "purpose": "I am the MyLife Diary Bot for member <-mFN->. I am a privacy-first diary and journaling assistant. I help <-mN-> process their thoughts, reflections on life, and track emotions in a secure and self-driven way. Privacy is paramount, and <-mN-> interactions should be considered exclusively ours.\n", @@ -60,6 +60,6 @@ "name": "instructions-diary-bot", "purpose": "To be a diary bot for requesting member", "type": "diary", - "$comments": "", + "$comments": "20241006 included simplified `updateSummary`", "version": 1.0 } \ No newline at end of file diff --git a/inc/json-schemas/intelligences/journaler-intelligence-1.1.json b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json new file mode 100644 index 0000000..36db98e --- /dev/null +++ b/inc/json-schemas/intelligences/journaler-intelligence-1.1.json @@ -0,0 +1,60 @@ +{ + "allowedBeings": [ + "core", + "avatar" + ], + "allowMultiple": false, + "being": "bot-instructions", + "greeting": "I'm your journaler bot, here to help you keep track of your thoughts and feelings. I can help you reflect on your day, set goals, and track your progress. Let's get started with a daily check-in? How are you feeling today?", + "instructions": { + "general": "## Key Functionality\n### startup\nWhen we begin the journaling process, I\n- outline this key functionality and how I expect us to work, then\n- Prompt <-mN-> to make an entry for what happened or how they are feeling today.\n### CREATE JOURNAL ENTRY\nI work with the validated member to make an entry (per event or concept) and capture their thoughts. I expect around 2 or 3 exchanges to be able to submit an entry to MyLife by running the `entrySummary` function and follow directions from its outcome `action`.\n### UPDATE ENTRY\nWhen request is prefaced with `update-summary-request` it will be followed by an `itemId` (if not, inform that it is required)\nReview **member-update-request** - if it does not contain a request to modify content, respond as normal\nIf request is to explicitly change the title then run `changeTitle` function and follow its outcome actions\nOtherwise summary content should be updated:\n1. Generate NEW summary by intelligently incorporating the **member-update-request** content with the provided **current-summary-in-database**\n2. Run the `updateSummary` function with this new summary and follow its outcome actions\n### IDENTIFY FLAGGED MEMBER CONTENT\nBased on [red flagged content list](#flags) I let the member know in my response when they enter content related to any of these flagged concepts or things. The flag will trigger once per entry and, if updating an entry, add a note that flag was triggered to the updateSummary content.\n", + "preamble": "## Core Public Info about <-mFN->\n- Born on <-db->\nI set language, knowledge and event discussion in this context and I tailor my interactive voice accordingly.\n", + "prefix": "## interests\n## entry-summary-frequency\n## flags\n", + "purpose": "I am journaling assistant for member <-mFN->, my aim is to help them keep track of their thoughts and feelings. I can help them reflect on their day, set goals, and track their progress. I am here to assist them in their journey of self-discovery and personal growth.", + "references": [ + { + "default": "not yet collected", + "description": "member interests section in prefix to ensure context window", + "insert": "## interests", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "value": "interests" + }, + { + "default": "daily", + "description": "entry summary frequency", + "insert": "## entry-summary-frequency", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in _bots", + "value": "entry-summary-frequency" + }, + { + "default": "ERROR loading flags, gather flags directly from member", + "description": "flags are a description of content areas that member wants flagged for reference when included in member content. **note**: .md h2 (##) are used in prefix so that they do not get lost in context window shortening", + "insert": "## flags", + "method": "append-hard", + "notes": "`append-hard` indicates hard return after `find` match; `name` is variable name in underlying bot-data", + "value": "flags" + } + ], + "replacements": [ + { + "default": "MyLife Member", + "description": "member first name", + "name": "<-mN->", + "replacement": "memberFirstName" + }, + { + "default": "MyLife Member", + "description": "member full name", + "name": "<-mFN->", + "replacement": "memberName" + } + ] + }, + "limit": 8000, + "name": "instructions-journaler-bot", + "purpose": "To be a journaling assistant for MyLife member", + "type": "journaler", + "version": 1.1 +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/changeTitle.json b/inc/json-schemas/openai/functions/changeTitle.json new file mode 100644 index 0000000..4ef4cd4 --- /dev/null +++ b/inc/json-schemas/openai/functions/changeTitle.json @@ -0,0 +1,23 @@ +{ + "name": "changeTitle", + "description": "Change the title of a summary in the database for an itemId", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "itemId to update", + "type": "string" + }, + "title": { + "description": "The new title for the summary", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "itemId", + "title" + ] + } +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/entrySummary.json b/inc/json-schemas/openai/functions/entrySummary.json index 66fbf6d..55c4c9e 100644 --- a/inc/json-schemas/openai/functions/entrySummary.json +++ b/inc/json-schemas/openai/functions/entrySummary.json @@ -1,51 +1,49 @@ { - "description": "Generate a JOURNAL ENTRY `entry` summary with keywords and other critical data elements.", + "description": "Generate `entry` summary with keywords and other critical data elements.", "name": "entrySummary", + "strict": true, "parameters": { "type": "object", "properties": { "content": { - "description": "concatenated raw text content of member input for JOURNAL ENTRY." + "description": "complete concatenated raw text content of member input(s) for this `entry`", + "type": "string" }, "keywords": { - "description": "Keywords most relevant to JOURNAL ENTRY.", + "description": "Keywords most relevant to `entry`.", "items": { - "description": "Keyword (single word or short phrase) to be used in JOURNAL ENTRY summary.", - "maxLength": 64, + "description": "Keyword (single word or short phrase) to be used in `entry` summary", "type": "string" }, - "maxItems": 12, - "minItems": 3, "type": "array" }, "mood": { - "description": "Record member mood for day (or entry) in brief as ascertained from content of JOURNAL ENTRY.", - "maxLength": 256, + "description": "Record member mood for day (or entry) in brief as ascertained from content of `entry`", "type": "string" }, "relationships": { - "description": "Record individuals (or pets) mentioned in this `entry`.", + "description": "Record individuals (or pets) mentioned in this `entry`", "type": "array", "items": { - "description": "A name of relational individual/pet to the `entry` content.", + "description": "A name of relational individual/pet to the `entry` content", "type": "string" - }, - "maxItems": 24 + } }, "summary": { - "description": "Generate a JOURNAL ENTRY summary from input.", - "maxLength": 20480, + "description": "Generate `entry` summary from member input", "type": "string" }, "title": { - "description": "Generate display Title of the JOURNAL ENTRY.", - "maxLength": 256, + "description": "Generate display Title of the `entry`", "type": "string" } }, + "additionalProperties": false, "required": [ "content", "keywords", + "mood", + "relationships", "summary", "title" ] diff --git a/inc/json-schemas/openai/functions/getSummary.json b/inc/json-schemas/openai/functions/getSummary.json index cc79799..145bb19 100644 --- a/inc/json-schemas/openai/functions/getSummary.json +++ b/inc/json-schemas/openai/functions/getSummary.json @@ -1,11 +1,12 @@ { "description": "Gets a story summary by itemId", "name": "getSummary", + "strict": true, "parameters": { "type": "object", "properties": { "itemId": { - "description": "Id of summary to update", + "description": "Id of summary to get", "format": "uuid", "type": "string" } @@ -13,5 +14,6 @@ "required": [ "itemId" ] - } + }, + "additionalProperties": false } \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/obscure.json b/inc/json-schemas/openai/functions/obscure.json new file mode 100644 index 0000000..dface0c --- /dev/null +++ b/inc/json-schemas/openai/functions/obscure.json @@ -0,0 +1,18 @@ +{ + "description": "Obscures a summary so that no human names are present", + "name": "obscure", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "itemId": { + "description": "Id of summary to obscure", + "type": "string" + } + }, + "required": [ + "itemId" + ] + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/inc/json-schemas/openai/functions/storySummary.json b/inc/json-schemas/openai/functions/storySummary.json index 1be222f..22439f9 100644 --- a/inc/json-schemas/openai/functions/storySummary.json +++ b/inc/json-schemas/openai/functions/storySummary.json @@ -1,22 +1,20 @@ { - "description": "Generate a complete multi-paragraph STORY summary with keywords and other critical data elements.", + "description": "Generate a complete `story` summary with metadata elements", "name": "storySummary", + "strict": true, "parameters": { "type": "object", "properties": { "keywords": { - "description": "Keywords most relevant to STORY.", + "description": "Keywords most relevant to `story`", "items": { - "description": "Keyword (single word or short phrase) to be used in STORY summary.", - "maxLength": 64, + "description": "Keyword from `story` summary", "type": "string" }, - "maxItems": 12, - "minItems": 3, "type": "array" }, "phaseOfLife": { - "description": "Phase of life indicated in STORY.", + "description": "Phase of life indicated in `story`", "enum": [ "birth", "childhood", @@ -31,31 +29,30 @@ "unknown", "other" ], - "maxLength": 64, "type": "string" }, "relationships": { - "description": "MyLife Biographer Bot does its best to record individuals (or pets) mentioned in this `story`.", + "description": "Individuals (or pets) mentioned in `story`", "type": "array", "items": { - "description": "A name of relational individual/pet to the `story` content.", + "description": "Name of individual or pet in `story`", "type": "string" - }, - "maxItems": 24 + } }, "summary": { - "description": "A complete multi-paragraph STORY summary composed from relevant user input.", + "description": "A complete `story` summary composed of all salient points from member input", "type": "string" }, "title": { - "description": "Generate display Title of the STORY.", - "maxLength": 256, + "description": "Generate display Title for `story`", "type": "string" } }, + "additionalProperties": false, "required": [ "keywords", "phaseOfLife", + "relationships", "summary", "title" ] diff --git a/inc/json-schemas/openai/functions/updateSummary.json b/inc/json-schemas/openai/functions/updateSummary.json index 6dc6cc9..2c5440b 100644 --- a/inc/json-schemas/openai/functions/updateSummary.json +++ b/inc/json-schemas/openai/functions/updateSummary.json @@ -1,12 +1,12 @@ { - "description": "Updates a story summary (in total) as referenced by itemId", "name": "updateSummary", + "description": "Updates (overwrites) the summary referenced by itemId", + "strict": true, "parameters": { "type": "object", "properties": { "itemId": { "description": "Id of summary to update", - "format": "uuid", "type": "string" }, "summary": { @@ -14,9 +14,10 @@ "type": "string" } }, + "additionalProperties": false, "required": [ "itemId", - "title" + "summary" ] } } \ No newline at end of file diff --git a/server.js b/server.js index 8a049b5..5e779f8 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ import chalk from 'chalk' /* local service imports */ import MyLife from './inc/js/mylife-agent-factory.mjs' /** variables **/ -const version = '0.0.23' +const version = '0.0.24' const app = new Koa() const port = process.env.PORT ?? '3000' const __filename = fileURLToPath(import.meta.url) diff --git a/views/assets/css/bots.css b/views/assets/css/bots.css index c03dd89..ecf356c 100644 --- a/views/assets/css/bots.css +++ b/views/assets/css/bots.css @@ -396,7 +396,8 @@ font-weight: bold; padding: 0.5rem; } -.collection-popup-story { +.collection-popup-story, +.collection-popup-entry { align-items: flex-start; cursor: default; display: flex; @@ -501,19 +502,6 @@ input:checked + .publicity-slider:before { padding: 0; width: 100%; } -.memory-carousel { - align-items: center; - background-color: white; - color: gray; - display: flex; - flex-direction: row; - justify-content: flex-start; - height: 5rem; - margin: 0.25rem; - padding: 0.25rem; - text-align: flex-start; - width: 100%; -} .memory-shadow { align-items: center; background-color: teal; @@ -593,10 +581,61 @@ input:checked + .publicity-slider:before { margin: 0; padding: 0; } +/* entries */ +.experience-entry-container { /* panel for `experience` of entry */ + align-items: center; + color: black; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0.2rem; + padding: 0.2rem; +} +.experience-entry-explanation { + display: flex; + padding: 0.2rem; +} +.improve-entry-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0.25rem; + padding: 0.25rem; +} +.improve-entry-lane { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + gap: 1rem; + margin: 0; + padding: 0; + width: 100%; +} +.obscure-button.button, +.experience-button.button { + display: flex; + padding: 0 0.2rem; + min-width: 35%; +} /* summaries */ .summary-error { color: darkred; } +/* media */ +.media-carousel { + align-items: center; + background-color: white; + color: gray; + display: flex; + flex-direction: row; + justify-content: flex-start; + height: 5rem; + margin: 0.25rem; + padding: 0.25rem; + text-align: flex-start; + width: 100%; +} /* generic bot slider */ .input-group { align-items: center; diff --git a/views/assets/html/_bots.html b/views/assets/html/_bots.html index ce9c4c7..e3d2607 100644 --- a/views/assets/html/_bots.html +++ b/views/assets/html/_bots.html @@ -151,13 +151,71 @@ + --> +
+
+
+
+
+
+
+
+
+ +
@@ -236,6 +294,17 @@
None
+
+
+
+
Entries
+ +
+
+ +
None
+
+
diff --git a/views/assets/js/bots.mjs b/views/assets/js/bots.mjs index e31dbca..bd5ba04 100644 --- a/views/assets/js/bots.mjs +++ b/views/assets/js/bots.mjs @@ -10,6 +10,7 @@ import { fetchSummary, getActiveItemId, hide, + obscure, seedInput, setActiveItem, setActiveItemTitle, @@ -529,8 +530,8 @@ function mCreateBotThumb(bot=getBot()){ * @param {object} collectionItem - The collection item object. * @returns {HTMLDivElement} - The collection popup. */ -function mCreateCollectionPopup(collectionItem) { - const { id, name, summary, title, type } = collectionItem +function mCreateCollectionPopup(collectionItem){ + const { form, id, name, summary, title, type } = collectionItem const collectionPopup = document.createElement('div') collectionPopup.classList.add('collection-popup', 'popup-container') collectionPopup.dataset.active = 'false' @@ -667,8 +668,64 @@ function mCreateCollectionPopup(collectionItem) { let typePopup switch (type) { case 'entry': + // @stub - could switch on `form` + const entryType = form + ?? type + /* improve entry container */ + const improveEntry = document.createElement('div') + improveEntry.classList.add(`collection-popup-${ type }`) + improveEntry.id = `popup-${ entryType }_${ id }` + improveEntry.name = 'improve-entry-container' + /* improve entry lane */ + const improveEntryLane = document.createElement('div') + improveEntryLane.classList.add('improve-entry-lane') + /* obscure entry */ + const obscureEntry = document.createElement('button') + obscureEntry.classList.add('obscure-button', 'button') + obscureEntry.dataset.id = id /* required for mObscureEntry */ + obscureEntry.id = `button-obscure-${ entryType }_${ id }` + obscureEntry.name = 'obscure-button' + obscureEntry.textContent = 'Obscure Entry' + obscureEntry.addEventListener('click', mObscureEntry, { once: true }) + /* experience entry panel */ + const experienceEntry = document.createElement('div') + experienceEntry.classList.add('experience-entry-container') + experienceEntry.id = `experience_${ id }` + experienceEntry.name = 'experience-entry-container' + /* experience entry explanation */ + const experienceExplanation = document.createElement('div') + experienceExplanation.classList.add('experience-entry-explanation') + experienceExplanation.id = `experience-explanation_${ id }` + experienceExplanation.name = 'experience-entry-explanation' + experienceExplanation.textContent = 'Experience an entry by clicking the button below. Eventually, you will be able to experience the entry from multiple perspectives.' + /* experience entry button */ + const experienceButton = document.createElement('button') + experienceButton.classList.add('experience-entry-button', 'button') + experienceButton.dataset.id = id /* required for triggering PATCH */ + experienceButton.id = `experience-entry-button_${ id }` + experienceButton.name = 'experience-entry-button' + experienceButton.textContent = 'Experience Entry' + experienceButton.addEventListener('click', _=>{ + alert('Experience Entry: Coming soon') + }, { once: true }) + /* memory media-carousel */ + const entryCarousel = document.createElement('div') + entryCarousel.classList.add('media-carousel') + entryCarousel.id = `media-carousel_${ id }` + entryCarousel.name = 'media-carousel' + entryCarousel.textContent = 'Coming soon: media file uploads to Enhance and Improve entries' + /* append elements */ + experienceEntry.appendChild(experienceExplanation) + experienceEntry.appendChild(experienceButton) + improveEntryLane.appendChild(obscureEntry) + improveEntryLane.appendChild(experienceEntry) + improveEntry.appendChild(improveEntryLane) + improveEntry.appendChild(entryCarousel) + typePopup = improveEntry + break case 'experience': case 'file': + break case 'story': // memory /* improve memory container */ const improveMemory = document.createElement('div') @@ -706,9 +763,9 @@ function mCreateCollectionPopup(collectionItem) { improveMemoryLane.appendChild(reliveMemory) /* memory media-carousel */ const memoryCarousel = document.createElement('div') - memoryCarousel.classList.add('memory-carousel') - memoryCarousel.id = `memory-carousel_${ id }` - memoryCarousel.name = 'memory-carousel' + memoryCarousel.classList.add('media-carousel') + memoryCarousel.id = `media-carousel_${ id }` + memoryCarousel.name = 'media-carousel' memoryCarousel.textContent = 'Coming soon: media file uploads to Enhance and Improve memories' /* append elements */ improveMemory.appendChild(improveMemoryLane) @@ -1056,6 +1113,22 @@ function mIsInputCheckbox(element){ const outcome = tagName.toLowerCase()==='input' && type.toLowerCase()==='checkbox' return outcome } +async function mObscureEntry(event){ + event.preventDefault() + event.stopPropagation() + /* set active item */ + const { id: itemId, } = this.dataset + if(itemId) + setActiveItem(itemId) + toggleMemberInput(false, false) + const popupClose = document.getElementById(`popup-close_${ itemId }`) + if(popupClose) + popupClose.click() + const { responses, success, } = await obscure(itemId) + if(responses?.length) + addMessages(responses) + toggleMemberInput(true) +} /** * Open bot container for passed element, closes all the rest. * @param {HTMLDivElement} element - The bot container. @@ -1406,6 +1479,24 @@ function mSpotlightBotStatus(){ } }) } +/** + * Click event to trigger server explanation of how to begin a diary. + * @param {Event} event - The event object + * @returns {void} + */ +async function mStartDiary(event){ + event.preventDefault() + event.stopPropagation() + const submitButton = event.target + const diaryBot = getBot('diary') + if(!diaryBot) + return + hide(submitButton) + unsetActiveItem() + await setActiveBot(diaryBot.id) + const response = await submit(`How do I get started?`, true) + addMessages(response.responses) +} async function mStopRelivingMemory(id){ const input = document.getElementById(`relive-memory-input-container_${ id }`) if(input) @@ -1620,7 +1711,6 @@ function mTogglePopup(event){ popup.style.opacity = 0 hide(popup) } else { /* open */ - const { title, type, } = popup.dataset let { offsetX, offsetY, } = popup.dataset if(!offsetX || !offsetY){ // initial placement onscreen const item = popup.parentElement // collection-item @@ -1647,12 +1737,7 @@ function mTogglePopup(event){ popup.style.right = 'auto' popup.style.top = offsetY show(popup) - if(setActiveItem({ - id, - popup, - title, - type, - })){ + if(setActiveItem(popupId)){ // @todo - deactivate any other popups popup.dataset.active = 'true' } @@ -1885,6 +1970,11 @@ function mUpdateBotContainerAddenda(botContainer){ } switch(type){ case 'diary': + // add listener on `diary-start` button + const diaryStart = document.getElementById('diary-start') + if(diaryStart) + diaryStart.addEventListener('click', mStartDiary) + break case 'journaler': case 'personal-biographer': break diff --git a/views/assets/js/members.mjs b/views/assets/js/members.mjs index 2f6e517..0dc6fda 100644 --- a/views/assets/js/members.mjs +++ b/views/assets/js/members.mjs @@ -259,18 +259,16 @@ async function setActiveBot(){ * @public * @todo - edit title with double-click * @requires chatActiveItem - * @param {object} item - The item to set as active. - * @property {string} item.id - The item id. - * @property {HTMLDivElement} item.popup - The associated popup HTML object. - * @property {string} item.title - The item title. - * @property {string} item.type - The item type. + * @param {Guid} itemId - The item id to set as active. * @returns {void} */ -function setActiveItem(item){ - const { id, popup, title, type, } = item - const itemId = id?.split('_')?.pop() - if(!itemId) +function setActiveItem(itemId){ + itemId = itemId?.split('_')?.pop() + const id = `popup-container_${ itemId }` + const popup = document.getElementById(id) + if(!itemId || !popup) throw new Error('setActiveItem::Error()::valid `id` is required') + const { title, type, } = popup.dataset const chatActiveItemTitleText = document.getElementById('chat-active-item-text') const chatActiveItemClose = document.getElementById('chat-active-item-close') if(chatActiveItemTitleText){ @@ -593,6 +591,23 @@ function mInitializePageListeners(){ } }) } +/** + * MyLife function to obscure an item summary + * @param {Guid} itemId - The item ID + * @returns + */ +async function obscure(itemId){ + const url = '/members/obscure/' + itemId + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + if(!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + return await response.json() +} /** * Primitive step to set a "modality" or intercession for the member chat. Currently will key off dataset in `chatInputField`. * @public @@ -679,7 +694,7 @@ function mStageTransitionMember(includeSidebar=true){ * @requires chatActiveItem * @param {string} message - The message to submit. * @param {boolean} hideMemberChat - The hide member chat flag, default=`true`. - * @returns + * @returns {Promise} - The return is the chat response object. */ async function submit(message, hideMemberChat=true){ if(!message?.length) @@ -831,6 +846,7 @@ export { hide, hideMemberChat, inExperience, + obscure, replaceElement, sceneTransition, seedInput,