app/controllers/conversation_controller.rb
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