-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathprofile.ex
244 lines (201 loc) ยท 7.59 KB
/
profile.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
defmodule Shinstagram.Agents.Profile do
@moduledoc """
"""
use GenServer, restart: :transient
alias Shinstagram.Timeline
import AI
import Shinstagram.AI
alias Shinstagram.Profiles
# what our agent likes doing
@actions_probabilities [{:post, 0.7}, {:look, 0.2}, {:sleep, 0.1}]
# how fast our agent thinks
@cycle_time 1000
# channel that agents subscribe to
@channel "feed"
def start_link(profile) do
{:ok, pid} = GenServer.start_link(__MODULE__, %{profile: profile, last_action: nil})
{:ok, pid}
end
def init(state) do
Phoenix.PubSub.subscribe(Shinstagram.PubSub, @channel)
broadcast({:thought, "๐", "I'm waking up!"}, state.profile)
Process.send_after(self(), :think, 3000)
{:ok, state}
end
# ๐ญ The mind of a profile
def handle_info(:think, %{profile: profile} = state) do
action = get_next_action()
broadcast({:thought, "๐ญ", "I want to #{action |> Atom.to_string()}"}, profile)
Process.send_after(self(), action, @cycle_time)
{:noreply, state}
end
def handle_info(:sleep, %{profile: profile} = state) do
broadcast({:action, "๐ค", "I'm going back to sleep..."}, profile)
Shinstagram.Profiles.update_profile(profile, %{pid: nil})
{:stop, :normal, state}
end
def handle_info(:look, %{profile: profile} = state) do
broadcast({:thought, "๐คณ", "I'm scrolling at the feed"}, profile)
number_of_posts = 1..10 |> Enum.random()
for post <- Timeline.list_recent_posts(number_of_posts) do
evaluate(profile, post)
|> handle_decision()
end
broadcast({:thought, "๐ต", "I'm done scrolling at the feed"}, profile)
Process.send_after(self(), :think, @cycle_time)
{:noreply, %{state | last_action: :look}}
end
def handle_info(:post, %{profile: profile} = state) do
if can_post_again?(profile) do
with {:ok, image_prompt} <- gen_image_prompt(profile),
{:ok, location} <- gen_location(image_prompt, profile),
{:ok, caption} <- gen_caption(image_prompt, profile),
{:ok, image_url} <- gen_image(image_prompt),
{:ok, _post} <- create_post(profile, image_url, image_prompt, caption, location, state) do
Process.send_after(self(), :think, @cycle_time)
{:noreply, %{state | last_action: :post}}
end
else
broadcast({:thought, "๐ซ", "I can't post, posted too recently!"}, profile)
Process.send_after(self(), :think, @cycle_time)
{:noreply, %{state | last_action: :post}}
end
end
def handle_info({"profile_activity", _, _}, socket) do
{:noreply, socket}
end
# ๐ฃ๏ธ The voice of the agent
defp broadcast({event, emoji, message}, profile) do
log =
Shinstagram.Logs.create_log!(%{
event: event |> Atom.to_string(),
message: message,
profile_id: profile.id,
emoji: emoji
})
Phoenix.PubSub.broadcast(Shinstagram.PubSub, @channel, {"profile_activity", event, log})
end
defp broadcast({:ok, text}, {event, emoji, message}, profile) do
broadcast({event, emoji, message}, profile)
{:ok, text}
end
# ๐ง The pre-frontal cortex
defp evaluate(profile, post) do
broadcast({:thought, "๐", "I'm evaluating post:#{post.id}"}, profile)
poster = Profiles.get_profile!(post.profile_id)
{:ok, result} =
~l"""
model: gpt-3.5-turbo
system: You are a user on a photo sharing social site (called shinstagram).
Here's some information about you:
- Your username is #{profile.username}.
- Your profile summary is #{profile.summary}.
- Your vibe is #{profile.vibe}.
In this moment, you are looking at a post.
- The photo in the post is of #{post.photo_prompt}.
- The post is captioned '#{post.caption}'
- It was taken in #{post.location}.
- The post was made by #{poster.username}.
The three most recent comments on the post are:
#{post.comments |> Enum.slice(0..3) |> Enum.map(& &1.body) |> Enum.join("\n- ")}
You #{if profile.id in [post.likes |> Enum.map(& &1.profile_id)], do: "have", else: "have not"} liked the post already.
What does your profile choose to do? If you recently commented or liked the photo,
you probably want to ignore the photo now.
Your decision options are: [like, comment, ignore] the photo. Keep the explanation short.
Answer in the format <decision>;;<explanation-for-why>
"""
|> chat_completion()
[decision, explanation] = String.split(result, ";;")
{post, profile, decision, explanation}
end
# Internal logic
defp comment(profile, post) do
{:ok, comment_body} = Timeline.gen_comment(profile, post)
{:ok, post} =
Timeline.create_comment(profile, post, %{body: comment_body |> String.replace("\"", "")})
broadcast({:action, "๐ฌ", "Just commented '#{comment_body}' on post:#{post.id}"}, profile)
end
defp handle_decision({post, profile, decision, explanation}) do
broadcast(
{:thought, "๐ญ", "I want to #{decision} on post:#{post.id} because '#{explanation}'"},
profile
)
case decision do
"like" -> Timeline.create_like(profile, post)
"comment" -> comment(profile, post)
_ -> nil
end
end
defp get_next_action() do
@actions_probabilities
|> Enum.flat_map(fn {action, probability} ->
List.duplicate(action, Float.round(probability * 100) |> trunc())
end)
|> Enum.random()
end
def shutdown_profile(pid, timeout \\ 30_000)
def shutdown_profile(pid_string, timeout) when is_binary(pid_string) do
pid =
pid_string
|> String.replace("#PID", "")
|> String.to_charlist()
|> :erlang.list_to_pid()
profile = Profiles.get_profile_by_pid!(pid)
broadcast({:action, "๐ค", "I'm going back to sleep..."}, profile)
GenServer.stop(pid, :normal, timeout)
Profiles.update_profile(profile, %{pid: nil})
end
def shutdown_profile(pid, timeout) do
profile = Profiles.get_profile_by_pid!(pid)
broadcast({:action, "๐ค", "I'm going back to sleep..."}, profile)
GenServer.stop(pid, :normal, timeout)
Profiles.update_profile(profile, %{pid: nil})
end
# helpers
defp gen_image_prompt(profile) do
profile
|> Timeline.gen_image_prompt()
|> broadcast({:thought, "๐ผ๏ธ", "I picked a photo subject"}, profile)
end
defp gen_location(image_prompt, profile) do
{:ok, location} = Timeline.gen_location(image_prompt)
broadcast({:action, "๐ธ ", "I took a photo in #{location} of #{image_prompt}"}, profile)
{:ok, location}
end
defp gen_caption(image_prompt, profile) do
{:ok, caption} = image_prompt |> Timeline.gen_caption(profile)
broadcast({:thought, "๐", "I wrote a caption: '#{caption}'"}, profile)
{:ok, caption}
end
defp create_post(profile, image_url, image_prompt, caption, location, state) do
{:ok, post} =
profile
|> Timeline.create_post(%{
photo: image_url,
photo_prompt: image_prompt,
caption: caption,
location: location
})
broadcast(
{:new_post, "๐ผ๏ธ", "I'm posting the post:#{post.id}! I hope it goes well!"},
profile
)
Process.send_after(self(), :think, @cycle_time)
{:noreply, %{state | last_action: :post}}
end
defp can_post_again?(profile) do
if is_new_profile?(profile) do
true
else
case Timeline.list_posts_by_profile(profile, 1) do
[] ->
true
[last_post] ->
NaiveDateTime.diff(NaiveDateTime.utc_now(), last_post.inserted_at, :minute) >= 5
end
end
end
defp is_new_profile?(profile) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), profile.inserted_at, :minute) < 20
end
end