Skip to content

Commit aa2fb8c

Browse files
committed
Merge branch 'master' of github.com:indentlabs/notebook
2 parents 7c2c7ab + cde7a9f commit aa2fb8c

File tree

11 files changed

+441
-15
lines changed

11 files changed

+441
-15
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
class ConversationController < ApplicationController
2+
before_action :authenticate_user!, only: [:character_index]
3+
4+
before_action :set_character, only: [:character_landing, :export]
5+
before_action :ensure_character_privacy, only: [:character_landing, :export]
6+
7+
def character_index
8+
@characters = @current_user_content.fetch('Character', [])
9+
end
10+
11+
def character_landing
12+
@first_greeting = default_character_greeting
13+
@personality = personality_for_character
14+
@description = description_for_character
15+
end
16+
17+
def export
18+
name = open_characters_persona_params.fetch('name', 'New character').strip
19+
description = open_characters_persona_params.fetch('description', '')
20+
21+
add_character_hash = base_open_characters_export.merge({
22+
"uuid": deterministic_uuid(@character.id),
23+
"name": name,
24+
"roleInstruction": full_role_instruction,
25+
"reminderMessage": reminder_message,
26+
})
27+
28+
# Add a character image if one has been uploaded to the page
29+
avatar = @character.random_image_including_private
30+
add_character_hash[:avatar][:url] = avatar if avatar.present?
31+
32+
# Provide a default scenario if one wasn't given
33+
add_character_hash[:scenario] ||= default_scenario
34+
35+
# Redirect to OpenCharacters
36+
base_oc_url = 'https://josephrocca.github.io/OpenCharacters/'
37+
oc_params = { addCharacter: add_character_hash }
38+
39+
redirect_to "#{base_oc_url}##{ERB::Util.url_encode(oc_params.to_json)}"
40+
end
41+
42+
private
43+
44+
def deterministic_uuid(id)
45+
static_prefix = "notebook-"
46+
hashed_id = Digest::SHA1.hexdigest(static_prefix + id.to_s)
47+
uuid = "#{hashed_id[0..7]}-#{hashed_id[8..11]}-#{hashed_id[12..15]}-#{hashed_id[16..19]}-#{hashed_id[20..31]}"
48+
uuid
49+
end
50+
51+
def full_role_instruction
52+
final_text = [
53+
"[SYSTEM]: You are roleplaying as #{@character.name}, #{personality_for_character}",
54+
"",
55+
"Follow this pattern:",
56+
"\"Hello!\" - dialogue",
57+
"*He jumps out of the bushes.* - action",
58+
"",
59+
"#{@character.name}'s personality is below:",
60+
"#{open_characters_persona_params.fetch('description', '(not included)')}",
61+
"",
62+
"#{@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.",
63+
].join("\n")
64+
end
65+
66+
def reminder_message
67+
"[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!)"
68+
end
69+
70+
def personality_for_character
71+
hobbies = @character.get_field_value('Nature', 'Hobbies')
72+
73+
personality_parts = [
74+
"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')}."
75+
]
76+
personality_parts << "Their hobbies include #{hobbies}." if hobbies.present?
77+
78+
ContentFormatterService.plaintext_show(text: personality_parts.join(' '), viewing_user: current_user)
79+
end
80+
81+
def description_for_character
82+
occupation = @character.get_field_value('Social', 'Occupation')
83+
background = @character.get_field_value('History', 'Background')
84+
motivations = @character.get_field_value('Nature', 'Motivations')
85+
mannerisms = @character.get_field_value('Nature', 'Mannerisms')
86+
flaws = @character.get_field_value('Nature', 'Flaws')
87+
prejudices = @character.get_field_value('Nature', 'Prejudices')
88+
talents = @character.get_field_value('Nature', 'Talents')
89+
hobbies = @character.get_field_value('Nature', 'Hobbies')
90+
91+
description_parts = []
92+
description_parts.concat ["OCCUPATION", occupation, nil] if occupation.present?
93+
description_parts.concat ["BACKGROUND", background, nil] if background.present?
94+
description_parts.concat ["MOTIVATIONS", motivations, nil] if motivations.present?
95+
description_parts.concat ["MANNERISMS", mannerisms, nil] if mannerisms.present?
96+
description_parts.concat ["FLAWS", flaws, nil] if flaws.present?
97+
description_parts.concat ["PREJUDICES", prejudices, nil] if prejudices.present?
98+
description_parts.concat ["TALENTS", talents, nil] if talents.present?
99+
description_parts.concat ["HOBBIES", hobbies, nil] if hobbies.present?
100+
101+
ContentFormatterService.plaintext_show(text: description_parts.join("\n"), viewing_user: current_user)
102+
end
103+
104+
def set_character
105+
@character = Character.find(params[:character_id].to_i)
106+
end
107+
108+
def ensure_character_privacy
109+
unless (user_signed_in? && @character.user == current_user) || @character.privacy == 'public'
110+
redirect_to root_path, notice: "That character is private!"
111+
return
112+
end
113+
end
114+
115+
def open_characters_persona_params
116+
params.permit(:name, :avatar, :scenario, :char_greeting, :personality, :description, :example_dialogue)
117+
end
118+
119+
def default_scenario
120+
"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."
121+
end
122+
123+
def default_custom_code
124+
""
125+
126+
# TODO maybe include our standard (whisper) formatting on messages?
127+
end
128+
129+
def base_open_characters_export
130+
{
131+
"name": "New character",
132+
"modelName": "gpt-3.5-turbo",
133+
"fitMessagesInContextMethod": "summarizeOld",
134+
"associativeMemoryMethod": "v1",
135+
"associativeMemoryEmbeddingModelName": "text-embedding-ada-002",
136+
"temperature": 0.7,
137+
"customCode": default_custom_code,
138+
"initialMessages": default_initial_messages,
139+
"avatar": {
140+
"url": "",
141+
"size": 1,
142+
"shape": "square"
143+
},
144+
"scene": {
145+
"background": {
146+
"url": ""
147+
},
148+
"music": {
149+
"url": ""
150+
}
151+
},
152+
"userCharacter": {
153+
"avatar": {
154+
"url": current_user.avatar.url,
155+
"size": 1,
156+
"shape": "circle"
157+
}
158+
},
159+
"streamingResponse": true
160+
}
161+
end
162+
163+
def default_initial_messages
164+
[
165+
{
166+
"author": "system",
167+
"content": "Scenario: " + (open_characters_persona_params.fetch('scenario', nil).presence || default_scenario),
168+
"hiddenFrom": [] # "ai", "user", "both", "neither"
169+
},
170+
{
171+
"author": "ai",
172+
"content": open_characters_persona_params.fetch('char_greeting', default_character_greeting),
173+
"hiddenFrom": []
174+
}
175+
]
176+
end
177+
178+
def default_export_metadata
179+
{
180+
"version": 1,
181+
"created": @content.created_at.to_i,
182+
"modified": @content.updated_at.to_i,
183+
"ncid": @content.id,
184+
"source": "https://www.notebook.ai/plan/characters#{@content.id}",
185+
"tool": {
186+
"name": "Notebook.ai Persona Export",
187+
"version": "1.0.0",
188+
"url": "https://www.notebook.ai"
189+
}
190+
}
191+
end
192+
193+
def default_character_greeting
194+
"Hello!"
195+
end
196+
end

app/helpers/basil_helper.rb

Lines changed: 0 additions & 2 deletions
This file was deleted.

app/models/concerns/has_attributes.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,34 @@ def overview_field_value(label)
300300
.detect { |v| v.entity_id == self.id }&.value.presence || (self.respond_to?(label.downcase) ? self.read_attribute(label.downcase) : nil)
301301
end
302302

303+
def get_field_value(category, field, fallback=nil)
304+
category = AttributeCategory.find_by(
305+
label: category,
306+
entity_type: self.class.name.downcase,
307+
user_id: self.user_id,
308+
hidden: [nil, false]
309+
)
310+
return fallback if category.nil?
311+
312+
field = AttributeField.find_by(
313+
label: field,
314+
attribute_category_id: category.id,
315+
user_id: self.user_id,
316+
hidden: [nil, false]
317+
)
318+
return fallback if field.nil?
319+
320+
answer = Attribute.find_by(
321+
attribute_field_id: field.id,
322+
entity_type: self.class.name,
323+
entity_id: self.id,
324+
user_id: self.user_id
325+
)
326+
return fallback if answer.nil?
327+
328+
answer.value
329+
end
330+
303331
def self.field_type_for(category, field)
304332
if field[:label] == 'Name' && category.name == 'overview'
305333
"name"

app/models/concerns/has_image_uploads.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def random_image_including_private(format: :medium)
2626

2727
# If we don't have any uploaded images, we look for saved Basil commissions
2828
if result.nil? && respond_to?(:basil_commissions)
29-
result = basil_commissions.where.not(saved_at: nil).sample.try(:image)
29+
result = basil_commissions.where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image)
3030
end
3131

3232
# Finally, if we have no image upload, we return the default image for this type

app/models/page_types/content_page.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ContentPage < ApplicationRecord
1111

1212
def random_image_including_private(format: :small)
1313
ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) \
14-
|| BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).sample.try(:image) \
14+
|| BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image) \
1515
|| ActionController::Base.helpers.asset_path("card-headers/#{self.page_type.downcase.pluralize}.webp")
1616
end
1717

app/services/content_formatter_service.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ class ContentFormatterService < Service
2121
# https://s3.amazonaws.com/raw.paste.esk.io/Llb%2F64DJHK?versionId=19Lb_TtukDbo1J_IoCpkr.d.pwpW_vmH
2222
VALID_LINK_CLASSES = Rails.application.config.content_type_names[:all] + %w(Timeline Document)
2323

24+
def self.plaintext_show(text:, viewing_user: User.new)
25+
formatted_text = markdown.render(text || '').html_safe
26+
27+
tokens_to_replace(text).each do |token|
28+
text.gsub!(token[:matched_string], replacement_for_token(token, viewing_user, true))
29+
end
30+
31+
text
32+
end
33+
2434
def self.show(text:, viewing_user: User.new)
2535
# We want to evaluate markdown first, because the markdown engine also happens
2636
# to strip out HTML tags. So: markdown, _then_ insert content links.
@@ -67,7 +77,7 @@ def self.links_to_replace(text)
6777
end
6878
end
6979

70-
def self.replacement_for_token(token, viewing_user)
80+
def self.replacement_for_token(token, viewing_user, plaintext=false)
7181
return unknown_link_template(token) unless token.key?(:content_type) && token.key?(:content_id)
7282
begin
7383
content_class = token[:content_type].titleize.constantize
@@ -81,9 +91,17 @@ def self.replacement_for_token(token, viewing_user)
8191
return unknown_link_template(token) unless content_model.present?
8292

8393
if content_model.readable_by?(viewing_user)
84-
link_template(content_model)
94+
if plaintext
95+
plaintext_replacement_template(content_model)
96+
else
97+
link_template(content_model)
98+
end
8599
else
86-
private_link_template(content_model)
100+
if plaintext
101+
plaintext_replacement_template(content_model)
102+
else
103+
private_link_template(content_model)
104+
end
87105
end
88106
end
89107

@@ -95,6 +113,10 @@ def self.private_link_template(content_model)
95113
inline_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: 'grey-text content_link disabled') }
96114
end
97115

116+
def self.plaintext_replacement_template(content_model)
117+
content_model.name
118+
end
119+
98120
def self.unknown_link_template(attempted_key)
99121
attempted_key[:matched_string]
100122
end
Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
<%
22
creating = defined?(creating) && creating
33
editing = defined?(editing) && editing
4-
page_type_enabled = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
4+
show_basil_tool = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
5+
show_conversation = false && content.class_name == 'Character'
6+
7+
show_tools_menu = show_basil_tool || show_conversation
58
%>
69

7-
<% if page_type_enabled %>
10+
<% if show_tools_menu %>
811
<ul class="collection content-tabs">
912
<li class="active center grey-text uppercase">
1013
Tools
1114
</li>
1215

13-
<li class="collection-item">
14-
<%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
15-
<i class="material-icons left">palette</i>
16-
Image Generation
17-
<% end %>
18-
</li>
16+
<% if show_basil_tool %>
17+
<li class="collection-item">
18+
<%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
19+
<i class="material-icons left">palette</i>
20+
Image Generation
21+
<% end %>
22+
</li>
23+
<% end %>
24+
25+
<% if show_conversation %>
26+
<li class="collection-item">
27+
<%= link_to talk_path(character_id: content.id) do %>
28+
<i class="material-icons left">message</i>
29+
Talk to <%= content.name %>
30+
<% end %>
31+
</li>
32+
<% end %>
1933
</ul>
2034
<% end %>

0 commit comments

Comments
 (0)