Skip to content

Commit efa0bda

Browse files
authored
Merge pull request #19 from MVP-Psicologia-Positiva/initial-sentiment-analysis
Sentiment Analysis, Memory Refactor, and RAG-Based Long-Term Memory for Lulu Assistant
2 parents f5d3eeb + 32c2940 commit efa0bda

File tree

9 files changed

+264
-45
lines changed

9 files changed

+264
-45
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.5 on 2025-05-18 17:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0014_alter_chat_facts_feedback_bot_message_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='chat_memories',
15+
name='user_message_sentiment',
16+
field=models.CharField(blank=True, max_length=20, null=True),
17+
),
18+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.5 on 2025-05-23 03:15
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0015_chat_memories_user_message_sentiment'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='chat_memories',
16+
name='embedding',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(), blank=True, null=True, size=None),
18+
),
19+
]

happy-kids/api/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.db import models
22
from django.contrib.auth.models import User
3+
from django.contrib.postgres.fields import ArrayField
34

45
class historicalSessions(models.Model):
56
sessionDate = models.DateTimeField("session date", default="")
@@ -20,8 +21,10 @@ class UsersMilestones(models.Model):
2021
class chat_memories(models.Model):
2122
user_id = models.CharField(max_length=100)
2223
user_message = models.TextField()
24+
user_message_sentiment = models.CharField(max_length=20, blank=True, null=True)
2325
chat_message = models.TextField()
2426
date_time = models.DateTimeField(auto_now_add=True)
27+
embedding = ArrayField(models.FloatField(), blank=True, null=True)
2528

2629
class dim_question_type(models.Model):
2730
question_type = models.TextField(unique=True)

happy-kids/frontend/templates/frontend/dashboard.html

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,33 @@
1919

2020
<div class="dashboard-grid" style="width: 100%; max-width: 100%; margin: 0 auto; padding: 20px;">
2121
<div class="dashboard-card">
22-
<h3>Mensagens por Dia</h3>
22+
<h3>Messages per day</h3>
2323
<div id="chat-messages-graph" style="height:300px; width: 100%;"></div>
2424
</div>
2525

2626
<div class="dashboard-card">
27-
<h3>Usuários Únicos por Dia</h3>
27+
<h3>Unique users per day</h3>
2828
<div id="unique-users-graph" style="height:300px; width: 100%;"></div>
2929
</div>
3030

3131
<div class="dashboard-card">
32-
<h3>Top 5 Usuários</h3>
32+
<h3>Top 5 Users</h3>
3333
<div id="top-users-graph" style="height:300px; width: 100%;"></div>
3434
</div>
3535

3636
<div class="dashboard-card">
3737
<h3>Feedback Like/Dislike</h3>
3838
<div id="feedback-pie" style="height:300px; width: 100%;"></div>
3939
</div>
40+
41+
<div class="dashboard-card">
42+
<h3>Sentiments by Day</h3>
43+
<div id="sentiment-graph" style="height:350px; width: 100%;"></div>
44+
</div>
45+
<div class="dashboard-card">
46+
<h3>Sentiments Rank</h3>
47+
<div id="sentiment-bar-graph" style="height:300px; width: 100%;"></div>
48+
</div>
4049
</div>
4150
</div>
4251
</main>
@@ -46,14 +55,13 @@ <h3>Feedback Like/Dislike</h3>
4655
const plotConfig = {
4756
responsive: true,
4857
displaylogo: false,
49-
displayModeBar: true // ou 'hover' se quiser só ao passar o mouse
58+
displayModeBar: true
5059
};
5160

52-
// Paleta personalizada mais próxima do CSS da aplicação
5361
const customLayout = {
54-
paper_bgcolor: "#faf8ff", // fundo lilás super claro
55-
plot_bgcolor: "#ffffff", // fundo interno branco
56-
font: {color: "#4a2d7c"}, // roxo institucional
62+
paper_bgcolor: "#faf8ff",
63+
plot_bgcolor: "#ffffff",
64+
font: {color: "#4a2d7c"},
5765
xaxis: {
5866
gridcolor: '#ddd',
5967
zerolinecolor: '#ccc',
@@ -70,45 +78,41 @@ <h3>Feedback Like/Dislike</h3>
7078
title: {font: {color: "#4a2d7c"}}
7179
};
7280

73-
// Dados vindos do Django
7481
const chatDates = {{ chat_dates_json|safe }};
7582
const minDate = chatDates.length > 0 ? chatDates[0] : null;
7683

77-
// Mensagens por dia
84+
// Messages per day
7885
Plotly.newPlot('chat-messages-graph', [{
7986
x: chatDates,
8087
y: {{ chat_counts_json|safe }},
8188
type: 'scatter',
8289
mode: 'lines+markers',
83-
marker: {color: '#8c30f5'} // roxo vibrante
90+
marker: {color: '#8c30f5'}
8491
}], Object.assign({}, customLayout, {
85-
title: 'Mensagens por Dia',
86-
xaxis: Object.assign({}, customLayout.xaxis, {title: 'Data', range: [minDate, null]}),
87-
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Mensagens'})
92+
xaxis: Object.assign({}, customLayout.xaxis, {range: [minDate, null]}),
93+
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Messages'})
8894
}), plotConfig);
8995

90-
// Usuários únicos por dia
96+
//Unique users per day
9197
Plotly.newPlot('unique-users-graph', [{
9298
x: {{ unique_user_dates_json|safe }},
9399
y: {{ unique_user_counts_json|safe }},
94100
type: 'bar',
95101
marker: {color: '#8c30f5'}
96102
}], Object.assign({}, customLayout, {
97-
title: 'Usuários Únicos por Dia',
98-
xaxis: Object.assign({}, customLayout.xaxis, {title: 'Data'}),
99-
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Usuários'})
103+
xaxis: Object.assign({}, customLayout.xaxis),
104+
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Users'})
100105
}), plotConfig);
101106

102-
// Top usuários
107+
// Top 5 Users
103108
Plotly.newPlot('top-users-graph', [{
104109
x: {{ top_usernames_json|safe }},
105110
y: {{ top_user_counts_json|safe }},
106111
type: 'bar',
107-
marker: {color: '#3bace2'} // azul institucional
112+
marker: {color: '#3bace2'}
108113
}], Object.assign({}, customLayout, {
109-
title: 'Top 5 Usuários',
110-
xaxis: Object.assign({}, customLayout.xaxis, {title: 'Usuário'}),
111-
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Mensagens'})
114+
xaxis: Object.assign({}, customLayout.xaxis),
115+
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Messages'})
112116
}), plotConfig);
113117

114118
// Feedback Pie
@@ -118,9 +122,37 @@ <h3>Feedback Like/Dislike</h3>
118122
type: 'pie',
119123
marker: {colors: ['#81d4af', '#f78da7']},
120124
}], Object.assign({}, customLayout, {
121-
title: 'Likes vs Dislikes',
122125
legend: {orientation: "h", x: 0.5, xanchor: 'center', font: {color: '#4a2d7c'}}
123126
}), plotConfig);
127+
128+
// Sentiments by Day
129+
fetch("{% url 'sentiment_over_time' %}")
130+
.then(response => response.json())
131+
.then(data => {
132+
const layout = Object.assign({}, customLayout, {
133+
xaxis: Object.assign({}, customLayout.xaxis),
134+
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Messages', rangemode: 'tozero'}),
135+
legend: {orientation: "h", x: 0.5, xanchor: 'center', font: {color: '#4a2d7c'}}
136+
});
137+
Plotly.newPlot('sentiment-graph', data.datasets, layout, plotConfig);
138+
});
139+
140+
// Sentiments Rank
141+
fetch("{% url 'sentiment_ranking' %}")
142+
.then(response => response.json())
143+
.then(data => {
144+
const barLayout = Object.assign({}, customLayout, {
145+
xaxis: Object.assign({}, customLayout.xaxis),
146+
yaxis: Object.assign({}, customLayout.yaxis, {title: 'Messages', rangemode: 'tozero'}),
147+
margin: { t: 50, l: 40, r: 20, b: 40 }
148+
});
149+
Plotly.newPlot('sentiment-bar-graph', [{
150+
x: data.sentiments,
151+
y: data.counts,
152+
type: 'bar',
153+
marker: {color: '#8c30f5'}
154+
}], barLayout, plotConfig);
155+
});
124156
</script>
125157

126158
</body>

happy-kids/frontend/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
path('create_diary/', views.create_diary, name='create_diary'),
3636
path("generate_suggestions/", views.generate_suggestions, name="generate_suggestions"),
37-
path('dashboard', views.dashboard_view, name='dashboard')
37+
path('dashboard', views.dashboard_view, name='dashboard'),
38+
path('sentiment-over-time/', views.sentiment_over_time, name='sentiment_over_time'),
39+
path('sentiment-ranking/', views.sentiment_ranking, name='sentiment_ranking'),
3840

3941
]

happy-kids/frontend/utils.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,90 @@
44
from django.utils import timezone
55
from datetime import timedelta
66
from api import models
7+
import openai
8+
import numpy as np
79

810
def get_short_term_memory(user_id):
9-
"""Recupera a memória de curto prazo do banco de dados."""
11+
"""Recupera a memória de curto prazo do banco de dados"""
1012
time_threshold = timezone.now() - timedelta(minutes=30)
1113

1214
conversations = (
1315
models.chat_memories.objects
1416
.filter(user_id=user_id, date_time__gte=time_threshold)
15-
.order_by('-date_time')[:20]
17+
.order_by('date_time')
1618
)
1719

18-
messages = [
19-
f"User: {conv.user_message}\nChat: {conv.chat_message}"
20-
for conv in conversations
21-
]
22-
23-
return messages
20+
memory = []
21+
for conv in conversations:
22+
23+
if conv.user_message:
24+
memory.append({"role": "user", "content": conv.user_message})
25+
26+
if conv.chat_message:
27+
memory.append({"role": "assistant", "content": conv.chat_message})
28+
29+
return memory
30+
31+
32+
def classify_sentiment(text):
33+
system_prompt = (
34+
"You are an expert assistant in human emotion and sentiment analysis.\n"
35+
"Your task is to read any provided text and classify the predominant emotion expressed, choosing only one category from the following list.\n"
36+
"Consider the context, nuance, and tone. If the text does not clearly express any of these emotions, respond with \"Neutral\".\n\n"
37+
"Possible sentiment categories:\n"
38+
"- Anger\n"
39+
"- Fear\n"
40+
"- Sadness\n"
41+
"- Joy\n"
42+
"- Disgust\n"
43+
"- Surprise\n"
44+
"- Neutral\n\n"
45+
"Respond only with the category, no explanation."
46+
)
47+
user_prompt = f'Text: "{text}"\nCategory:'
48+
try:
49+
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
50+
51+
response = client.chat.completions.create(
52+
model="gpt-4o-mini",
53+
messages=[
54+
{"role": "system", "content": system_prompt},
55+
{"role": "user", "content": user_prompt}
56+
],
57+
max_tokens=10,
58+
temperature=0
59+
)
60+
category = response.choices[0].message.content.strip()
61+
return category
62+
except Exception as e:
63+
print(f"Error during sentiment analysis: {e}")
64+
return None
65+
66+
67+
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
68+
69+
def get_embedding(text):
70+
response = client.embeddings.create(
71+
model="text-embedding-3-small",
72+
input=text,
73+
)
74+
return response.data[0].embedding
75+
76+
77+
78+
def cosine_similarity(a, b):
79+
a = np.array(a)
80+
b = np.array(b)
81+
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
2482

83+
def get_relevant_memories(user_id, question, top_k=3):
84+
question_emb = get_embedding(question)
2585

86+
history = models.chat_memories.objects.filter(user_id=user_id).exclude(embedding=None)
87+
scored = []
88+
for h in history:
89+
sim = cosine_similarity(question_emb, h.embedding)
90+
scored.append((sim, h))
91+
scored.sort(reverse=True, key=lambda x: x[0])
92+
top_memories = [x[1] for x in scored[:top_k]]
93+
return top_memories

0 commit comments

Comments
 (0)