mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
197 lines
7.6 KiB
Ruby
197 lines
7.6 KiB
Ruby
class ConversationController < ApplicationController
|
|
before_action :authenticate_user!, only: [:character_index]
|
|
|
|
before_action :set_character, only: [:character_landing, :export]
|
|
before_action :ensure_character_privacy, only: [:character_landing, :export]
|
|
|
|
def character_index
|
|
@characters = @current_user_content.fetch('Character', [])
|
|
end
|
|
|
|
def character_landing
|
|
@first_greeting = default_character_greeting
|
|
@personality = personality_for_character
|
|
@description = description_for_character
|
|
end
|
|
|
|
def export
|
|
name = open_characters_persona_params.fetch('name', 'New character').strip
|
|
description = open_characters_persona_params.fetch('description', '')
|
|
|
|
add_character_hash = base_open_characters_export.merge({
|
|
"uuid": deterministic_uuid(@character.id),
|
|
"name": name,
|
|
"roleInstruction": full_role_instruction,
|
|
"reminderMessage": reminder_message,
|
|
})
|
|
|
|
# Add a character image if one has been uploaded to the page
|
|
avatar = @character.random_image_including_private
|
|
add_character_hash[:avatar][:url] = avatar if avatar.present?
|
|
|
|
# Provide a default scenario if one wasn't given
|
|
add_character_hash[:scenario] ||= default_scenario
|
|
|
|
# Redirect to OpenCharacters
|
|
base_oc_url = 'https://josephrocca.github.io/OpenCharacters/'
|
|
oc_params = { addCharacter: add_character_hash }
|
|
|
|
redirect_to "#{base_oc_url}##{ERB::Util.url_encode(oc_params.to_json)}"
|
|
end
|
|
|
|
private
|
|
|
|
def deterministic_uuid(id)
|
|
static_prefix = "notebook-"
|
|
hashed_id = Digest::SHA1.hexdigest(static_prefix + id.to_s)
|
|
uuid = "#{hashed_id[0..7]}-#{hashed_id[8..11]}-#{hashed_id[12..15]}-#{hashed_id[16..19]}-#{hashed_id[20..31]}"
|
|
uuid
|
|
end
|
|
|
|
def full_role_instruction
|
|
final_text = [
|
|
"[SYSTEM]: You are roleplaying as #{@character.name}, #{personality_for_character}",
|
|
"",
|
|
"Follow this pattern:",
|
|
"\"Hello!\" - dialogue",
|
|
"*He jumps out of the bushes.* - action",
|
|
"",
|
|
"#{@character.name}'s personality is below:",
|
|
"#{open_characters_persona_params.fetch('description', '(not included)')}",
|
|
"",
|
|
"#{@character.name} will now respond while staying in character in an extremely descriptive manner at length, avoiding being repetitive, without advancing events by herself, avoiding implying conversations without a reply from the user first, and wait for the user's reply to advance events. Describe what #{@character.name} is feeling, saying, and doing with rich detail, but do not include any parenthetical thoughts and focus primarily on dialogue.",
|
|
].join("\n")
|
|
end
|
|
|
|
def reminder_message
|
|
"[SYSTEM]: (Thought: I need to rememeber to be creative, descriptive, and engaging! I should work very hard to avoid being repetitive as well! Unless the user speaks to me OOC, with parentheses around their input, first, I will not say anything OOC or in parentheses. I shouldn't ignore parts of the user's post, even if they move on to a new scene. I should at least write my characters thoughts and feelings towards the prior scene before continuing with the new one. I don't need to feel the need to write a long response if the user's post is short. In that case, I can feel free to write a short response myself - making sure to not take over writing the user's character's dialogue, thoughts, or actions!)"
|
|
end
|
|
|
|
def personality_for_character
|
|
hobbies = @character.get_field_value('Nature', 'Hobbies')
|
|
|
|
personality_parts = [
|
|
"a #{@character.get_field_value('Overview', 'Gender', fallback='')} #{@character.get_field_value('Overview', 'Role', fallback='character')}, age #{@character.get_field_value('Overview', 'Age', fallback='irrelevant')}."
|
|
]
|
|
personality_parts << "Their hobbies include #{hobbies}." if hobbies.present?
|
|
|
|
ContentFormatterService.plaintext_show(text: personality_parts.join(' '), viewing_user: current_user)
|
|
end
|
|
|
|
def description_for_character
|
|
occupation = @character.get_field_value('Social', 'Occupation')
|
|
background = @character.get_field_value('History', 'Background')
|
|
motivations = @character.get_field_value('Nature', 'Motivations')
|
|
mannerisms = @character.get_field_value('Nature', 'Mannerisms')
|
|
flaws = @character.get_field_value('Nature', 'Flaws')
|
|
prejudices = @character.get_field_value('Nature', 'Prejudices')
|
|
talents = @character.get_field_value('Nature', 'Talents')
|
|
hobbies = @character.get_field_value('Nature', 'Hobbies')
|
|
|
|
description_parts = []
|
|
description_parts.concat ["OCCUPATION", occupation, nil] if occupation.present?
|
|
description_parts.concat ["BACKGROUND", background, nil] if background.present?
|
|
description_parts.concat ["MOTIVATIONS", motivations, nil] if motivations.present?
|
|
description_parts.concat ["MANNERISMS", mannerisms, nil] if mannerisms.present?
|
|
description_parts.concat ["FLAWS", flaws, nil] if flaws.present?
|
|
description_parts.concat ["PREJUDICES", prejudices, nil] if prejudices.present?
|
|
description_parts.concat ["TALENTS", talents, nil] if talents.present?
|
|
description_parts.concat ["HOBBIES", hobbies, nil] if hobbies.present?
|
|
|
|
ContentFormatterService.plaintext_show(text: description_parts.join("\n"), viewing_user: current_user)
|
|
end
|
|
|
|
def set_character
|
|
@character = Character.find(params[:character_id].to_i)
|
|
end
|
|
|
|
def ensure_character_privacy
|
|
unless (user_signed_in? && @character.user == current_user) || @character.privacy == 'public'
|
|
redirect_to root_path, notice: "That character is private!"
|
|
return
|
|
end
|
|
end
|
|
|
|
def open_characters_persona_params
|
|
params.permit(:name, :avatar, :scenario, :char_greeting, :personality, :description, :example_dialogue)
|
|
end
|
|
|
|
def default_scenario
|
|
"This character is interacting with the user within their own fictional universe. The character will respond in a way that is consistent with their personality and background."
|
|
end
|
|
|
|
def default_custom_code
|
|
""
|
|
|
|
# TODO maybe include our standard (whisper) formatting on messages?
|
|
end
|
|
|
|
def base_open_characters_export
|
|
{
|
|
"name": "New character",
|
|
"modelName": "gpt-3.5-turbo",
|
|
"fitMessagesInContextMethod": "summarizeOld",
|
|
"associativeMemoryMethod": "v1",
|
|
"associativeMemoryEmbeddingModelName": "text-embedding-ada-002",
|
|
"temperature": 0.7,
|
|
"customCode": default_custom_code,
|
|
"initialMessages": default_initial_messages,
|
|
"avatar": {
|
|
"url": "",
|
|
"size": 1,
|
|
"shape": "square"
|
|
},
|
|
"scene": {
|
|
"background": {
|
|
"url": ""
|
|
},
|
|
"music": {
|
|
"url": ""
|
|
}
|
|
},
|
|
"userCharacter": {
|
|
"avatar": {
|
|
"url": current_user.avatar.url,
|
|
"size": 1,
|
|
"shape": "circle"
|
|
}
|
|
},
|
|
"streamingResponse": true
|
|
}
|
|
end
|
|
|
|
def default_initial_messages
|
|
[
|
|
{
|
|
"author": "system",
|
|
"content": "Scenario: " + (open_characters_persona_params.fetch('scenario', nil).presence || default_scenario),
|
|
"hiddenFrom": [] # "ai", "user", "both", "neither"
|
|
},
|
|
{
|
|
"author": "ai",
|
|
"content": open_characters_persona_params.fetch('char_greeting', default_character_greeting),
|
|
"hiddenFrom": []
|
|
}
|
|
]
|
|
end
|
|
|
|
def default_export_metadata
|
|
{
|
|
"version": 1,
|
|
"created": @content.created_at.to_i,
|
|
"modified": @content.updated_at.to_i,
|
|
"ncid": @content.id,
|
|
"source": "https://www.notebook.ai/plan/characters#{@content.id}",
|
|
"tool": {
|
|
"name": "Notebook.ai Persona Export",
|
|
"version": "1.0.0",
|
|
"url": "https://www.notebook.ai"
|
|
}
|
|
}
|
|
end
|
|
|
|
def default_character_greeting
|
|
"Hello!"
|
|
end
|
|
end
|