From 7111d6b0d8fad2402c1d32fa236a0e64ef0bdc4e Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Sat, 15 Apr 2023 11:41:37 -0700
Subject: [PATCH 01/10] basic export flow for OpenCharacters
---
app/assets/javascripts/conversation.coffee | 3 +
app/assets/stylesheets/conversation.scss | 3 +
app/controllers/conversation_controller.rb | 74 +++++++++++++++
app/helpers/conversation_helper.rb | 2 +
app/models/concerns/has_attributes.rb | 28 ++++++
.../content/display/sidebar/_apps.html.erb | 30 +++++--
.../conversation/character_landing.html.erb | 89 +++++++++++++++++++
config/routes.rb | 5 ++
.../conversation_controller_test.rb | 8 ++
9 files changed, 234 insertions(+), 8 deletions(-)
create mode 100644 app/assets/javascripts/conversation.coffee
create mode 100644 app/assets/stylesheets/conversation.scss
create mode 100644 app/controllers/conversation_controller.rb
create mode 100644 app/helpers/conversation_helper.rb
create mode 100644 app/views/conversation/character_landing.html.erb
create mode 100644 test/controllers/conversation_controller_test.rb
diff --git a/app/assets/javascripts/conversation.coffee b/app/assets/javascripts/conversation.coffee
new file mode 100644
index 00000000..24f83d18
--- /dev/null
+++ b/app/assets/javascripts/conversation.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/conversation.scss b/app/assets/stylesheets/conversation.scss
new file mode 100644
index 00000000..666d9031
--- /dev/null
+++ b/app/assets/stylesheets/conversation.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Conversation controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: https://sass-lang.com/
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
new file mode 100644
index 00000000..7c1c5910
--- /dev/null
+++ b/app/controllers/conversation_controller.rb
@@ -0,0 +1,74 @@
+class ConversationController < ApplicationController
+ before_action :set_character
+ before_action :ensure_character_privacy
+
+ def character_landing
+ @first_greeting = "Hello, friend!"
+
+ @personality = personality_for_character
+ @description = description_for_character
+ end
+
+ def export
+ raise open_characters_persona_params.inspect
+ end
+
+ private
+
+ def personality_for_character
+ name = @character.name
+ gender = @character.get_field_value('Overview', 'Gender')
+ role = @character.get_field_value('Overview', 'Role')
+ age = @character.get_field_value('Overview', 'Age')
+ aliases = @character.get_field_value('Overview', 'Aliases')
+ hobbies = @character.get_field_value('Nature', 'Hobbies')
+
+ [
+ name,
+ " is a ",
+ gender.downcase,
+ " ",
+ role || "character",
+ age.present? ? ", #{age}," : nil,
+ aliases.present? ? "(also known as #{aliases})" : nil,
+ hobbies.present? ? " into #{hobbies}." : "."
+ ].compact.join
+ 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?
+
+ description_parts.join("\n")
+ 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!"
+ end
+ end
+
+ def open_characters_persona_params
+ params.permit(:name, :avatar, :scenario, :char_greeting, :personality, :description, :example_dialogue)
+ end
+end
diff --git a/app/helpers/conversation_helper.rb b/app/helpers/conversation_helper.rb
new file mode 100644
index 00000000..f98c679e
--- /dev/null
+++ b/app/helpers/conversation_helper.rb
@@ -0,0 +1,2 @@
+module ConversationHelper
+end
diff --git a/app/models/concerns/has_attributes.rb b/app/models/concerns/has_attributes.rb
index b7a07108..bf593598 100644
--- a/app/models/concerns/has_attributes.rb
+++ b/app/models/concerns/has_attributes.rb
@@ -300,6 +300,34 @@ module HasAttributes
.detect { |v| v.entity_id == self.id }&.value.presence || (self.respond_to?(label.downcase) ? self.read_attribute(label.downcase) : nil)
end
+ def get_field_value(category, field)
+ category = AttributeCategory.find_by(
+ label: category,
+ entity_type: self.class.name.downcase,
+ user_id: self.user_id,
+ hidden: [nil, false]
+ )
+ return nil if category.nil?
+
+ field = AttributeField.find_by(
+ label: field,
+ attribute_category_id: category.id,
+ user_id: self.user_id,
+ hidden: [nil, false]
+ )
+ return nil if field.nil?
+
+ answer = Attribute.find_by(
+ attribute_field_id: field.id,
+ entity_type: self.class.name,
+ entity_id: self.id,
+ user_id: self.user_id
+ )
+ return nil if answer.nil?
+
+ answer.value
+ end
+
def self.field_type_for(category, field)
if field[:label] == 'Name' && category.name == 'overview'
"name"
diff --git a/app/views/content/display/sidebar/_apps.html.erb b/app/views/content/display/sidebar/_apps.html.erb
index 11a097f9..0d4181bf 100644
--- a/app/views/content/display/sidebar/_apps.html.erb
+++ b/app/views/content/display/sidebar/_apps.html.erb
@@ -1,20 +1,34 @@
<%
creating = defined?(creating) && creating
editing = defined?(editing) && editing
- page_type_enabled = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
+ show_basil_tool = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
+ show_conversation = content.class_name == 'Character'
+
+ show_tools_menu = show_basil_tool || show_conversation
%>
-<% if page_type_enabled %>
+<% if show_tools_menu %>
Tools
-
- <%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
- palette
- Image Generation
- <% end %>
-
+ <% if show_basil_tool %>
+
+ <%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
+ palette
+ Image Generation
+ <% end %>
+
+ <% end %>
+
+ <% if show_conversation %>
+
+ <%= link_to talk_path(character_id: content.id) do %>
+ message
+ Talk to <%= content.name %>
+ <% end %>
+
+ <% end %>
<% end %>
\ No newline at end of file
diff --git a/app/views/conversation/character_landing.html.erb b/app/views/conversation/character_landing.html.erb
new file mode 100644
index 00000000..43502665
--- /dev/null
+++ b/app/views/conversation/character_landing.html.erb
@@ -0,0 +1,89 @@
+
+ You can now export your Notebook.ai characters to the open-source project OpenCharacters
+ and talk to them in real-time! This is a great way to get to know your characters a little
+ better or to roleplay with them.
+
+
+ The fields below will be shared with OpenCharacters to create a conversational persona of
+ your character. You can edit these fields before exporting if you want to change how your
+ character talks. If your character is public, you can also share this page with others
+ to let them talk to your character!
+
+ Persona export for <%= @character.name %>
+ <% if user_signed_in? && @character.user_id == current_user.id %>
+
+ (editable because you created <%= @character.name %>)
+ <% end %>
+
+
+
+
+ <%= text_area_tag 'scenario', nil, disabled: false, style: 'min-height: 100px', placeholder: "Is there a specific scenario/context you want to have this conversation in?", class: 'materialize-textarea' %>
+
+
+
+
+ <%= text_area_tag 'char_greeting', @first_greeting, disabled: false, placeholder: "This will be the first thing your character says to you. It can be a simple greeting, or you can use it to set a specific topic, tone, or speaking style.", class: 'materialize-textarea' %>
+
+
+ <%= text_area_tag 'example_dialogue', nil, disabled: false, style: 'min-height: 100px', placeholder: "If you have any dialogue examples, quotes, or other phrases your character says, you can use this field to include them and adjust their speaking style closer to the examples. Write as little or as much as you'd like!", class: 'materialize-textarea' %>
+
+
+
+
+
+
+ Note: OpenAI key required by OpenCharacters
+
+
+
+ OpenCharacters uses this persona data with OpenAI's GPT models
+ in an app that runs entirely in your browser, rather than being hosted or stored on any server.
+ This means that you will need a valid OpenAI key to use this feature.
+
+
+
+
+
+ <%= submit_tag "Chat with #{@character.name}", class: 'hoverable btn blue white-text' %>
+ using OpenCharacters
+
+
+ <% 10.times do %> <% end %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index c8e55f52..f88cc476 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -25,6 +25,11 @@ Rails.application.routes.draw do
get '/:content_type/:id', to: 'basil#content', as: :basil_content
post '/:content_type/:id', to: 'basil#commission', as: :basil_commission
end
+
+ scope :talk do
+ get '/to/:character_id', to: 'conversation#character_landing', as: :talk
+ post '/export/:character_id', to: 'conversation#export', as: :export_character
+ end
end
scope :stream, path: '/stream', as: :stream do
diff --git a/test/controllers/conversation_controller_test.rb b/test/controllers/conversation_controller_test.rb
new file mode 100644
index 00000000..587294b5
--- /dev/null
+++ b/test/controllers/conversation_controller_test.rb
@@ -0,0 +1,8 @@
+require "test_helper"
+
+class ConversationControllerTest < ActionDispatch::IntegrationTest
+ test "should get content" do
+ get conversation_content_url
+ assert_response :success
+ end
+end
From e154c423dd289787b7c27379ac7d590c021a2e5c Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Sat, 15 Apr 2023 13:05:25 -0700
Subject: [PATCH 02/10] file cleanup
---
app/assets/javascripts/conversation.coffee | 3 ---
app/assets/stylesheets/conversation.scss | 3 ---
app/helpers/basil_helper.rb | 2 --
app/helpers/conversation_helper.rb | 2 --
4 files changed, 10 deletions(-)
delete mode 100644 app/assets/javascripts/conversation.coffee
delete mode 100644 app/assets/stylesheets/conversation.scss
delete mode 100644 app/helpers/basil_helper.rb
delete mode 100644 app/helpers/conversation_helper.rb
diff --git a/app/assets/javascripts/conversation.coffee b/app/assets/javascripts/conversation.coffee
deleted file mode 100644
index 24f83d18..00000000
--- a/app/assets/javascripts/conversation.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/conversation.scss b/app/assets/stylesheets/conversation.scss
deleted file mode 100644
index 666d9031..00000000
--- a/app/assets/stylesheets/conversation.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the Conversation controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: https://sass-lang.com/
diff --git a/app/helpers/basil_helper.rb b/app/helpers/basil_helper.rb
deleted file mode 100644
index 8d483a2f..00000000
--- a/app/helpers/basil_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module BasilHelper
-end
diff --git a/app/helpers/conversation_helper.rb b/app/helpers/conversation_helper.rb
deleted file mode 100644
index f98c679e..00000000
--- a/app/helpers/conversation_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ConversationHelper
-end
From 6fdd3148cbeed503df440f19fe7da2cf616c8b04 Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Mon, 17 Apr 2023 10:45:01 -0700
Subject: [PATCH 03/10] basic Notebook.ai-->OpenCharacters character export
working
---
app/controllers/conversation_controller.rb | 109 +++++++++++++++++-
.../conversation/character_landing.html.erb | 2 -
2 files changed, 104 insertions(+), 7 deletions(-)
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
index 7c1c5910..514106f4 100644
--- a/app/controllers/conversation_controller.rb
+++ b/app/controllers/conversation_controller.rb
@@ -3,14 +3,28 @@ class ConversationController < ApplicationController
before_action :ensure_character_privacy
def character_landing
- @first_greeting = "Hello, friend!"
-
- @personality = personality_for_character
- @description = description_for_character
+ @first_greeting = default_character_greeting
+ @personality = personality_for_character
+ @description = description_for_character
end
def export
- raise open_characters_persona_params.inspect
+ add_character_hash = base_open_characters_export.merge({
+ "name": open_characters_persona_params.fetch('name', 'New character'),
+ "roleInstruction": "You are to act as #{@character.name}, whos personality is detailed below:\n\n#{description_for_character}",
+ "reminderMessage": "#{personality_for_character} Do not break character!",
+ })
+
+ # 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?
+
+ # 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)}"
end
private
@@ -65,10 +79,95 @@ class ConversationController < ApplicationController
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
+ ""
+ end
+
+ def default_reminder_message
+ ""
+ end
+
+ def default_custom_code
+ ""
+
+ # TODO maybe include our standard (whisper) formatting on messages?
+ end
+
+ def base_open_characters_export
+ {
+ "name": "New character",
+ # "roleInstruction": default_scenario,
+ # "reminderMessage": default_reminder_message,
+ "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": open_characters_persona_params.fetch('scenario', 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, friend!"
+ end
end
diff --git a/app/views/conversation/character_landing.html.erb b/app/views/conversation/character_landing.html.erb
index 43502665..161ba38b 100644
--- a/app/views/conversation/character_landing.html.erb
+++ b/app/views/conversation/character_landing.html.erb
@@ -19,10 +19,8 @@
character talks. If your character is public, you can also share this page with others
to let them talk to your character!
-
-
<%= form_tag export_character_path(@character.id), method: :post do |f| %>
<%= hidden_field_tag "name", @character.name %>
From 9f506ef2f713ac1b1b84c88a07dbc6f12fc81577 Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Mon, 17 Apr 2023 11:58:57 -0700
Subject: [PATCH 04/10] prompt tweaks
---
app/controllers/conversation_controller.rb | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
index 514106f4..ea9feaea 100644
--- a/app/controllers/conversation_controller.rb
+++ b/app/controllers/conversation_controller.rb
@@ -9,10 +9,14 @@ class ConversationController < ApplicationController
end
def export
+ name = open_characters_persona_params.fetch('name', 'New character').strip
+ personality = open_characters_persona_params.fetch('personality', '')
+ description = open_characters_persona_params.fetch('description', '')
+
add_character_hash = base_open_characters_export.merge({
- "name": open_characters_persona_params.fetch('name', 'New character'),
- "roleInstruction": "You are to act as #{@character.name}, whos personality is detailed below:\n\n#{description_for_character}",
- "reminderMessage": "#{personality_for_character} Do not break character!",
+ "name": name,
+ "roleInstruction": "You are to act as #{name}, whos personality is detailed below:\n\n#{description}",
+ "reminderMessage": "#{personality}\n\nDo not break character!",
})
# Add a character image if one has been uploaded to the page
@@ -20,11 +24,10 @@ class ConversationController < ApplicationController
add_character_hash[:avatar][:url] = avatar if avatar.present?
# 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)}"
+ redirect_to "#{base_oc_url}##{ERB::Util.url_encode(oc_params.to_json)}"
end
private
From 7c2c7ab766e50f349f0bfaa9a1ca76dbd112dc7c Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Mon, 24 Apr 2023 14:15:56 -0700
Subject: [PATCH 05/10] cache some repeated queries on the forums pages
---
app/models/users/user.rb | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/users/user.rb b/app/models/users/user.rb
index 6be8116e..9ab2897f 100644
--- a/app/models/users/user.rb
+++ b/app/models/users/user.rb
@@ -60,7 +60,7 @@ class User < ApplicationRecord
has_many :user_blockings, dependent: :destroy
has_many :blocked_users, through: :user_blockings, source: :blocked_user
def blocked_by_users
- User.where(id: UserBlocking.where(blocked_user_id: self.id).pluck(:user_id))
+ @cached_blocked_by_users ||= User.where(id: UserBlocking.where(blocked_user_id: self.id).pluck(:user_id))
end
def blocked_by?(user)
blocked_by_users.pluck(:id).include?(user.id)
@@ -206,7 +206,7 @@ class User < ApplicationRecord
end
def image_url(size=80)
- if avatar.attached? # manually-uploaded avatar
+ @cached_user_image_url ||= if avatar.attached? # manually-uploaded avatar
Rails.application.routes.url_helpers.rails_representation_url(avatar.variant(resize_to_limit: [size, size]).processed, only_path: true)
else # otherwise, grab the default from Gravatar for this email address
@@ -243,7 +243,7 @@ class User < ApplicationRecord
end
def active_promo_codes
- PageUnlockPromoCode.where(id: active_promotions.pluck(:page_unlock_promo_code_id))
+ @cached_active_promo_codes ||= PageUnlockPromoCode.where(id: active_promotions.pluck(:page_unlock_promo_code_id))
end
def initialize_stripe_customer
From 76f4aed75f1f30ea9a2253a99822f3da2ffea43b Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Mon, 24 Apr 2023 15:10:26 -0700
Subject: [PATCH 06/10] uuids and text replacements
---
app/controllers/conversation_controller.rb | 26 +++++++++++------
app/services/content_formatter_service.rb | 28 +++++++++++++++++--
.../conversation/character_landing.html.erb | 4 ++-
3 files changed, 46 insertions(+), 12 deletions(-)
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
index ea9feaea..355f1bf0 100644
--- a/app/controllers/conversation_controller.rb
+++ b/app/controllers/conversation_controller.rb
@@ -14,8 +14,9 @@ class ConversationController < ApplicationController
description = open_characters_persona_params.fetch('description', '')
add_character_hash = base_open_characters_export.merge({
+ "uuid": deterministic_uuid(@character.id),
"name": name,
- "roleInstruction": "You are to act as #{name}, whos personality is detailed below:\n\n#{description}",
+ "roleInstruction": "You are to act as #{name}, whose personality is detailed below:\n\n#{description}",
"reminderMessage": "#{personality}\n\nDo not break character!",
})
@@ -32,15 +33,22 @@ class ConversationController < ApplicationController
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 personality_for_character
name = @character.name
- gender = @character.get_field_value('Overview', 'Gender')
- role = @character.get_field_value('Overview', 'Role')
- age = @character.get_field_value('Overview', 'Age')
- aliases = @character.get_field_value('Overview', 'Aliases')
- hobbies = @character.get_field_value('Nature', 'Hobbies')
+ gender = @character.get_field_value('Overview', 'Gender').try(:strip)
+ role = @character.get_field_value('Overview', 'Role').try(:strip)
+ age = @character.get_field_value('Overview', 'Age').try(:strip)
+ aliases = @character.get_field_value('Overview', 'Aliases').try(:strip)
+ hobbies = @character.get_field_value('Nature', 'Hobbies').try(:strip)
- [
+ final_text = [
name,
" is a ",
gender.downcase,
@@ -50,6 +58,8 @@ class ConversationController < ApplicationController
aliases.present? ? "(also known as #{aliases})" : nil,
hobbies.present? ? " into #{hobbies}." : "."
].compact.join
+
+ ContentFormatterService.plaintext_show(text: final_text, viewing_user: current_user)
end
def description_for_character
@@ -72,7 +82,7 @@ class ConversationController < ApplicationController
description_parts.concat ["TALENTS", talents, nil] if talents.present?
description_parts.concat ["HOBBIES", hobbies, nil] if hobbies.present?
- description_parts.join("\n")
+ ContentFormatterService.plaintext_show(text: description_parts.join("\n"), viewing_user: current_user)
end
def set_character
diff --git a/app/services/content_formatter_service.rb b/app/services/content_formatter_service.rb
index 82538a7e..e1eb40d2 100644
--- a/app/services/content_formatter_service.rb
+++ b/app/services/content_formatter_service.rb
@@ -21,6 +21,16 @@ class ContentFormatterService < Service
# https://s3.amazonaws.com/raw.paste.esk.io/Llb%2F64DJHK?versionId=19Lb_TtukDbo1J_IoCpkr.d.pwpW_vmH
VALID_LINK_CLASSES = Rails.application.config.content_type_names[:all] + %w(Timeline Document)
+ def self.plaintext_show(text:, viewing_user: User.new)
+ formatted_text = markdown.render(text || '').html_safe
+
+ tokens_to_replace(text).each do |token|
+ text.gsub!(token[:matched_string], replacement_for_token(token, viewing_user, true))
+ end
+
+ text
+ end
+
def self.show(text:, viewing_user: User.new)
# We want to evaluate markdown first, because the markdown engine also happens
# to strip out HTML tags. So: markdown, _then_ insert content links.
@@ -67,7 +77,7 @@ class ContentFormatterService < Service
end
end
- def self.replacement_for_token(token, viewing_user)
+ def self.replacement_for_token(token, viewing_user, plaintext=false)
return unknown_link_template(token) unless token.key?(:content_type) && token.key?(:content_id)
begin
content_class = token[:content_type].titleize.constantize
@@ -81,9 +91,17 @@ class ContentFormatterService < Service
return unknown_link_template(token) unless content_model.present?
if content_model.readable_by?(viewing_user)
- link_template(content_model)
+ if plaintext
+ plaintext_replacement_template(content_model)
+ else
+ link_template(content_model)
+ end
else
- private_link_template(content_model)
+ if plaintext
+ plaintext_replacement_template(content_model)
+ else
+ private_link_template(content_model)
+ end
end
end
@@ -95,6 +113,10 @@ class ContentFormatterService < Service
inline_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: 'grey-text content_link disabled') }
end
+ def self.plaintext_replacement_template(content_model)
+ content_model.name
+ end
+
def self.unknown_link_template(attempted_key)
attempted_key[:matched_string]
end
diff --git a/app/views/conversation/character_landing.html.erb b/app/views/conversation/character_landing.html.erb
index 161ba38b..34427bd1 100644
--- a/app/views/conversation/character_landing.html.erb
+++ b/app/views/conversation/character_landing.html.erb
@@ -2,7 +2,9 @@
+ <%= link_to "Back to #{@character.name}", @character %>
+
From dab8b0d726c2015f1dbd9a7506515b05eb3494ea Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Wed, 26 Apr 2023 18:12:36 -0700
Subject: [PATCH 07/10] conversations index page to use instead of chat links
on individual pages
---
app/controllers/conversation_controller.rb | 15 ++-
app/models/concerns/has_image_uploads.rb | 2 +-
app/models/page_types/content_page.rb | 2 +-
.../conversation/character_index.html.erb | 60 ++++++++++
.../conversation/character_landing.html.erb | 105 +++++++++---------
config/routes.rb | 3 +-
6 files changed, 130 insertions(+), 57 deletions(-)
create mode 100644 app/views/conversation/character_index.html.erb
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
index 355f1bf0..702f5fe1 100644
--- a/app/controllers/conversation_controller.rb
+++ b/app/controllers/conversation_controller.rb
@@ -1,6 +1,12 @@
class ConversationController < ApplicationController
- before_action :set_character
- before_action :ensure_character_privacy
+ 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
@@ -17,13 +23,16 @@ class ConversationController < ApplicationController
"uuid": deterministic_uuid(@character.id),
"name": name,
"roleInstruction": "You are to act as #{name}, whose personality is detailed below:\n\n#{description}",
- "reminderMessage": "#{personality}\n\nDo not break character!",
+ "reminderMessage": "#{personality}\n\n[AI]: (Thought: I need to remember to be very descriptive, and create an engaging experience for the user)",
})
# 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] ||= "You are talking"
+
# Redirect to OpenCharacters
base_oc_url = 'https://josephrocca.github.io/OpenCharacters/'
oc_params = { addCharacter: add_character_hash }
diff --git a/app/models/concerns/has_image_uploads.rb b/app/models/concerns/has_image_uploads.rb
index 16855610..684bb79a 100644
--- a/app/models/concerns/has_image_uploads.rb
+++ b/app/models/concerns/has_image_uploads.rb
@@ -26,7 +26,7 @@ module HasImageUploads
# If we don't have any uploaded images, we look for saved Basil commissions
if result.nil? && respond_to?(:basil_commissions)
- result = basil_commissions.where.not(saved_at: nil).sample.try(:image)
+ result = basil_commissions.where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image)
end
# Finally, if we have no image upload, we return the default image for this type
diff --git a/app/models/page_types/content_page.rb b/app/models/page_types/content_page.rb
index 122d2864..b2ce9ae3 100644
--- a/app/models/page_types/content_page.rb
+++ b/app/models/page_types/content_page.rb
@@ -11,7 +11,7 @@ class ContentPage < ApplicationRecord
def random_image_including_private(format: :small)
ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) \
- || BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).sample.try(:image) \
+ || BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image) \
|| ActionController::Base.helpers.asset_path("card-headers/#{self.page_type.downcase.pluralize}.webp")
end
diff --git a/app/views/conversation/character_index.html.erb b/app/views/conversation/character_index.html.erb
new file mode 100644
index 00000000..98b822ed
--- /dev/null
+++ b/app/views/conversation/character_index.html.erb
@@ -0,0 +1,60 @@
+
+
+
+ forum
+ Talk to your characters
+
+
+ There are a lot of new sites popping up for talking to AI characters. This page lets you
+ export your Notebook.ai characters to those sites, starting with the open-source project
+ OpenCharacters.
+
+
+ Please note that these services aren't a part of Notebook.ai; this is just a way to get your
+ Notebook.ai characters into those sites! To get started, click on any character below.
+
- <%= link_to "Back to #{@character.name}", @character %>
+
+
+
+ Talk to <%= @character.name %>
+
+
+ You can now export your Notebook.ai characters to the open-source project OpenCharacters
+ and talk to them in real-time! This is a great way to get to know your characters a little
+ better or to roleplay with them.
+
+
-
-
-
- Talk to <%= @character.name %>
-
-
- You can now export your Notebook.ai characters to the open-source project OpenCharacters
- and talk to them in real-time! This is a great way to get to know your characters a little
- better or to roleplay with them.
-
-
- The fields below will be shared with OpenCharacters to create a conversational persona of
+
+
+ The fields will be used to create a conversational AI persona of
your character. You can edit these fields before exporting if you want to change how your
character talks. If your character is public, you can also share this page with others
to let them talk to your character!
-
-
+
+ <%= link_to "Back to notebook page", @character %>
+
- Persona export for <%= @character.name %>
- <% if user_signed_in? && @character.user_id == current_user.id %>
-
- (editable because you created <%= @character.name %>)
- <% end %>
-
-
+
+ Persona export for <%= @character.name %>
+ <% if user_signed_in? && @character.user_id == current_user.id %>
+
+ (editable because you created <%= @character.name %>)
+ <% end %>
+
+
-
- <%= text_area_tag 'scenario', nil, disabled: false, style: 'min-height: 100px', placeholder: "Is there a specific scenario/context you want to have this conversation in?", class: 'materialize-textarea' %>
-
-
+
+ <%= text_area_tag 'scenario', nil, disabled: false, style: 'min-height: 100px', placeholder: "Is there a specific scenario/context you want to have this conversation in?", class: 'materialize-textarea' %>
+
+
-
- <%= text_area_tag 'char_greeting', @first_greeting, disabled: false, placeholder: "This will be the first thing your character says to you. It can be a simple greeting, or you can use it to set a specific topic, tone, or speaking style.", class: 'materialize-textarea' %>
-
-
+
+ <%= text_area_tag 'char_greeting', @first_greeting, disabled: false, placeholder: "This will be the first thing your character says to you. It can be a simple greeting, or you can use it to set a specific topic, tone, or speaking style.", class: 'materialize-textarea' %>
+
+
- <%= text_area_tag 'example_dialogue', nil, disabled: false, style: 'min-height: 100px', placeholder: "If you have any dialogue examples, quotes, or other phrases your character says, you can use this field to include them and adjust their speaking style closer to the examples. Write as little or as much as you'd like!", class: 'materialize-textarea' %>
-
-
+
+ <%= text_area_tag 'example_dialogue', nil, disabled: false, style: 'min-height: 100px', placeholder: "If you have any dialogue examples, quotes, or other phrases your character says, you can use this field to include them and adjust their speaking style closer to the examples. Write as little or as much as you'd like!", class: 'materialize-textarea' %>
+
+
- Note: OpenAI key required by OpenCharacters
+ Note: An OpenAI API key is required by OpenCharacters
- OpenCharacters uses this persona data with OpenAI's GPT models
- in an app that runs entirely in your browser, rather than being hosted or stored on any server.
- This means that you will need a valid OpenAI key to use this feature.
+ OpenCharacters uses this persona data with OpenAI's GPT models
+ in a web app that runs entirely in your browser, rather than being hosted or stored on any server.
+ This means that you will need a valid OpenAI key to use this feature.
<%= submit_tag "Chat with #{@character.name}", class: 'hoverable btn blue white-text' %>
- using OpenCharacters
+ using OpenCharacters
<% 10.times do %> <% end %>
diff --git a/config/routes.rb b/config/routes.rb
index f88cc476..78644604 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,8 +27,9 @@ Rails.application.routes.draw do
end
scope :talk do
+ get '/', to: 'conversation#character_index', as: :conversation
get '/to/:character_id', to: 'conversation#character_landing', as: :talk
- post '/export/:character_id', to: 'conversation#export', as: :export_character
+ post '/export/:character_id', to: 'conversation#export', as: :export_character
end
end
From 119cb7de7c289c7a5e657d7070c44f878277c4c2 Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Wed, 26 Apr 2023 21:32:24 -0700
Subject: [PATCH 08/10] improve dialogue prompts
---
app/controllers/conversation_controller.rb | 63 +++++++++++-----------
app/models/concerns/has_attributes.rb | 8 +--
2 files changed, 36 insertions(+), 35 deletions(-)
diff --git a/app/controllers/conversation_controller.rb b/app/controllers/conversation_controller.rb
index 702f5fe1..17fb7c98 100644
--- a/app/controllers/conversation_controller.rb
+++ b/app/controllers/conversation_controller.rb
@@ -16,14 +16,13 @@ class ConversationController < ApplicationController
def export
name = open_characters_persona_params.fetch('name', 'New character').strip
- personality = open_characters_persona_params.fetch('personality', '')
description = open_characters_persona_params.fetch('description', '')
add_character_hash = base_open_characters_export.merge({
"uuid": deterministic_uuid(@character.id),
"name": name,
- "roleInstruction": "You are to act as #{name}, whose personality is detailed below:\n\n#{description}",
- "reminderMessage": "#{personality}\n\n[AI]: (Thought: I need to remember to be very descriptive, and create an engaging experience for the user)",
+ "roleInstruction": full_role_instruction,
+ "reminderMessage": reminder_message,
})
# Add a character image if one has been uploaded to the page
@@ -31,7 +30,7 @@ class ConversationController < ApplicationController
add_character_hash[:avatar][:url] = avatar if avatar.present?
# Provide a default scenario if one wasn't given
- add_character_hash[:scenario] ||= "You are talking"
+ add_character_hash[:scenario] ||= default_scenario
# Redirect to OpenCharacters
base_oc_url = 'https://josephrocca.github.io/OpenCharacters/'
@@ -49,26 +48,34 @@ class ConversationController < ApplicationController
uuid
end
- def personality_for_character
- name = @character.name
- gender = @character.get_field_value('Overview', 'Gender').try(:strip)
- role = @character.get_field_value('Overview', 'Role').try(:strip)
- age = @character.get_field_value('Overview', 'Age').try(:strip)
- aliases = @character.get_field_value('Overview', 'Aliases').try(:strip)
- hobbies = @character.get_field_value('Nature', 'Hobbies').try(:strip)
-
+ def full_role_instruction
final_text = [
- name,
- " is a ",
- gender.downcase,
- " ",
- role || "character",
- age.present? ? ", #{age}," : nil,
- aliases.present? ? "(also known as #{aliases})" : nil,
- hobbies.present? ? " into #{hobbies}." : "."
- ].compact.join
+ "[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
- ContentFormatterService.plaintext_show(text: final_text, viewing_user: current_user)
+ 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
@@ -110,11 +117,7 @@ class ConversationController < ApplicationController
end
def default_scenario
- ""
- end
-
- def default_reminder_message
- ""
+ "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
@@ -126,8 +129,6 @@ class ConversationController < ApplicationController
def base_open_characters_export
{
"name": "New character",
- # "roleInstruction": default_scenario,
- # "reminderMessage": default_reminder_message,
"modelName": "gpt-3.5-turbo",
"fitMessagesInContextMethod": "summarizeOld",
"associativeMemoryMethod": "v1",
@@ -163,7 +164,7 @@ class ConversationController < ApplicationController
[
{
"author": "system",
- "content": open_characters_persona_params.fetch('scenario', default_scenario),
+ "content": "Scenario: " + (open_characters_persona_params.fetch('scenario', nil).presence || default_scenario),
"hiddenFrom": [] # "ai", "user", "both", "neither"
},
{
@@ -190,6 +191,6 @@ class ConversationController < ApplicationController
end
def default_character_greeting
- "Hello, friend!"
+ "Hello!"
end
end
diff --git a/app/models/concerns/has_attributes.rb b/app/models/concerns/has_attributes.rb
index bf593598..c69dd5f7 100644
--- a/app/models/concerns/has_attributes.rb
+++ b/app/models/concerns/has_attributes.rb
@@ -300,14 +300,14 @@ module HasAttributes
.detect { |v| v.entity_id == self.id }&.value.presence || (self.respond_to?(label.downcase) ? self.read_attribute(label.downcase) : nil)
end
- def get_field_value(category, field)
+ def get_field_value(category, field, fallback=nil)
category = AttributeCategory.find_by(
label: category,
entity_type: self.class.name.downcase,
user_id: self.user_id,
hidden: [nil, false]
)
- return nil if category.nil?
+ return fallback if category.nil?
field = AttributeField.find_by(
label: field,
@@ -315,7 +315,7 @@ module HasAttributes
user_id: self.user_id,
hidden: [nil, false]
)
- return nil if field.nil?
+ return fallback if field.nil?
answer = Attribute.find_by(
attribute_field_id: field.id,
@@ -323,7 +323,7 @@ module HasAttributes
entity_id: self.id,
user_id: self.user_id
)
- return nil if answer.nil?
+ return fallback if answer.nil?
answer.value
end
From ce6326f520e374f638051dc666620a64c39d0217 Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Thu, 27 Apr 2023 14:43:25 -0700
Subject: [PATCH 09/10] disable cta link on character pages
---
app/views/content/display/sidebar/_apps.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/content/display/sidebar/_apps.html.erb b/app/views/content/display/sidebar/_apps.html.erb
index 0d4181bf..519f642b 100644
--- a/app/views/content/display/sidebar/_apps.html.erb
+++ b/app/views/content/display/sidebar/_apps.html.erb
@@ -2,7 +2,7 @@
creating = defined?(creating) && creating
editing = defined?(editing) && editing
show_basil_tool = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
- show_conversation = content.class_name == 'Character'
+ show_conversation = false && content.class_name == 'Character'
show_tools_menu = show_basil_tool || show_conversation
%>
From b01627916433ca5997df47fd1302c0d21f01ca62 Mon Sep 17 00:00:00 2001
From: Andrew Brown
Date: Thu, 27 Apr 2023 14:47:00 -0700
Subject: [PATCH 10/10] verbiage
---
app/views/conversation/character_landing.html.erb | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/app/views/conversation/character_landing.html.erb b/app/views/conversation/character_landing.html.erb
index b06ffefd..e01c6085 100644
--- a/app/views/conversation/character_landing.html.erb
+++ b/app/views/conversation/character_landing.html.erb
@@ -1,6 +1,8 @@
@@ -19,14 +21,14 @@
The fields will be used to create a conversational AI persona of
your character. You can edit these fields before exporting if you want to change how your
character talks. If your character is public, you can also share this page with others
- to let them talk to your character!
+ to let them talk to your character, too!
<%= link_to "Back to notebook page", @character %>
OpenCharacters uses this persona data with OpenAI's GPT models
in a web app that runs entirely in your browser, rather than being hosted or stored on any server.
- This means that you will need a valid OpenAI key to use this feature.
+ This means that you will need a valid OpenAI key to use OpenCharacters.