feat(vida): página "Dia a dia" com as decisões e o porquê
Cada decisão tomada com base no que ele aprendeu passa a ser guardada com o motivo (aprendizado.decidir, até 800, persistidas): - acordou do cochilo antes da hora: "às 18h de quarta costuma ter gente, pelo plano que você contou"; - cochilo com a casa vazia, com a duração e o porquê; - sensor segurado numa hora em que acordou à toa (no máximo 1 nota a cada 10 min); - gesto escolhido porque costuma agradar (chance ≥ ×1,3); - fechamento do dia: horas com gente, carinhos e quanto o dia conferiu com o plano; - análise da IA (adotada ou sugestão, com a porcentagem) e rotina contada por você. esperado_e_fonte diz de onde veio a expectativa (plano seu, plano da IA, o que viu naquele dia da semana ou média). GET /aprendizado/dia devolve um dia: medido × plano por hora, conferência do dia, resumo e decisões. Painel: tela "Dia a dia" (#/dia-a-dia) com navegação entre dias, os números do dia, as faixas "Viu" × "Esperava" e a linha do tempo das decisões; atalho no cartão "O que ele aprendeu", no Ctrl+K e no guia. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+118
-7
@@ -38,6 +38,8 @@ MIN_MINUTOS_HORA = 10 # minutos observados numa hora para ela entrar na rotin
|
||||
ROTINA_CHEIA, ROTINA_VAZIA = 0.5, 0.3
|
||||
AVISO_CHEGADA = 15 * 60 # s antes da hora movimentada em que ele acorda do cochilo
|
||||
MAX_LICOES = 30
|
||||
MAX_DECISOES = 800 # decisões guardadas (com o motivo), umas duas semanas
|
||||
AVISO_SONO = 10 * 60 # no máximo uma nota de "não acordou com o sensor" a cada 10 min
|
||||
DIAS_SEMANA = ['segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado', 'domingo']
|
||||
CURTO = ['seg', 'ter', 'qua', 'qui', 'sex', 'sáb', 'dom']
|
||||
MEMORIA_SEMANA = 0.5 # cada dia da semana aparece 1 vez por semana: aprende mais rápido
|
||||
@@ -122,6 +124,8 @@ class Aprendizado:
|
||||
self.analise = dict(ocupado=False, erro='', em=None, tipo='')
|
||||
self.ia = None # (provedor, prompt, schema) -> dict (o main liga)
|
||||
self.pode_ia = lambda provedor: False
|
||||
self.decisoes = [] # [{horario, tipo, texto, motivo}] mais nova primeiro
|
||||
self.sono_avisado = 0.0
|
||||
try:
|
||||
salvo = json.loads(self.caminho.read_text(encoding='utf-8'))
|
||||
self.config.update(self.validar(salvo.get('config', {})))
|
||||
@@ -138,6 +142,7 @@ class Aprendizado:
|
||||
self.semana = [[None if v is None else float(v) for v in linha] for linha in salvo['semana']]
|
||||
self.semana_n = [[int(v) for v in linha] for linha in salvo['semana_n']]
|
||||
self.historico = [d for d in salvo.get('historico', []) if isinstance(d, dict)][-MAX_HISTORICO:]
|
||||
self.decisoes = [d for d in salvo.get('decisoes', []) if isinstance(d, dict)][:MAX_DECISOES]
|
||||
if salvo.get('plano'):
|
||||
self.plano = self.validar_plano(salvo['plano'], salvo['plano'].get('origem', 'ia'), salvo['plano'].get('criado'))
|
||||
if salvo.get('sugestao'):
|
||||
@@ -180,7 +185,7 @@ class Aprendizado:
|
||||
with self.lock:
|
||||
self.itens, self.rotina, self.falsos = {}, [None] * 24, [0.0] * 24
|
||||
self.semana, self.semana_n = [[None] * 24 for _ in range(7)], [[0] * 24 for _ in range(7)]
|
||||
self.historico, self.plano, self.sugestao = [], None, None
|
||||
self.historico, self.plano, self.sugestao, self.decisoes = [], None, None, []
|
||||
self.dias, self.licoes, self.rotina_avisada, self.rotina_avisada_dia = 0, [], [], 0
|
||||
self.hoje = self._dia_vazio(_dia(time.time()))
|
||||
self.pendentes, self.acordou_em = [], None
|
||||
@@ -195,7 +200,7 @@ class Aprendizado:
|
||||
licoes=self.licoes, rotina_avisada=self.rotina_avisada, rotina_avisada_dia=self.rotina_avisada_dia,
|
||||
hoje=self.hoje, semana=self.semana,
|
||||
semana_n=self.semana_n, historico=self.historico, plano=self.plano, sugestao=self.sugestao,
|
||||
analise_em=self.analise['em'])
|
||||
analise_em=self.analise['em'], decisoes=self.decisoes)
|
||||
self.salvo_em = time.time()
|
||||
try:
|
||||
gravar_atomico(self.caminho, dados)
|
||||
@@ -296,16 +301,58 @@ class Aprendizado:
|
||||
def esperado(self, agora):
|
||||
"""'casa', 'fora', 'dormindo' ou None nesta hora: pelo plano da semana, senão pelo que mediu
|
||||
naquele dia da semana, senão pela média de todos os dias."""
|
||||
return self.esperado_e_fonte(agora)[0]
|
||||
|
||||
def esperado_e_fonte(self, agora):
|
||||
t = time.localtime(agora)
|
||||
if self.plano:
|
||||
fonte = 'pelo plano que você contou' if self.plano['origem'] == 'voce' else 'pelo plano da IA'
|
||||
for p in self.plano['dias'][t.tm_wday]:
|
||||
if p['ini'] <= t.tm_hour < p['fim']:
|
||||
return p['tipo']
|
||||
return None
|
||||
v = self.semana[t.tm_wday][t.tm_hour] if self.semana_n[t.tm_wday][t.tm_hour] >= 2 else self.rotina[t.tm_hour]
|
||||
return p['tipo'], fonte
|
||||
return None, fonte
|
||||
if self.semana_n[t.tm_wday][t.tm_hour] >= 2:
|
||||
v, fonte = self.semana[t.tm_wday][t.tm_hour], 'pelo que vi nas %ss' % DIAS_SEMANA[t.tm_wday].replace('-feira', '')
|
||||
if t.tm_wday >= 5:
|
||||
fonte = 'pelo que vi nos %ss' % DIAS_SEMANA[t.tm_wday]
|
||||
else:
|
||||
v, fonte = self.rotina[t.tm_hour], 'pela média de todos os dias'
|
||||
if v is None:
|
||||
return None
|
||||
return 'casa' if v >= ROTINA_CHEIA else 'fora' if v < ROTINA_VAZIA else None
|
||||
return None, fonte
|
||||
return ('casa' if v >= ROTINA_CHEIA else 'fora' if v < ROTINA_VAZIA else None), fonte
|
||||
|
||||
# ------------------------------------------------------------------ decisões (com o porquê)
|
||||
def decidir(self, tipo, texto, motivo, agora=None):
|
||||
"""Guarda uma decisão tomada com base no que aprendeu, para o painel mostrar dia a dia."""
|
||||
with self.lock:
|
||||
self.decisoes.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto, motivo=motivo))
|
||||
del self.decisoes[MAX_DECISOES:]
|
||||
|
||||
def porque_chegada(self, agora):
|
||||
t = time.localtime(agora + 3600)
|
||||
_, fonte = self.esperado_e_fonte(agora + 3600)
|
||||
return 'às %dh de %s costuma ter gente, %s' % (t.tm_hour, DIAS_SEMANA[t.tm_wday], fonte)
|
||||
|
||||
def porque_vazia(self, agora):
|
||||
t = time.localtime(agora)
|
||||
_, fonte = self.esperado_e_fonte(agora)
|
||||
return 'às %dh de %s a casa costuma ficar vazia, %s' % (t.tm_hour, DIAS_SEMANA[t.tm_wday], fonte)
|
||||
|
||||
def segurou_sono(self, agora, precisa, extra):
|
||||
"""O PIR viu algo, mas nesta hora ele pede mais movimento: anota (sem repetir toda hora)."""
|
||||
if agora - self.sono_avisado < AVISO_SONO:
|
||||
return
|
||||
self.sono_avisado = agora
|
||||
self.decidir('sono', 'Não acordou com o sensor',
|
||||
'por volta das %dh já acordou à toa outras vezes: pede %d %s a mais'
|
||||
% (_hora(agora), extra, 'movimento' if extra == 1 else 'movimentos'), agora)
|
||||
|
||||
def escolheu(self, nome, agora=None):
|
||||
"""Escolheu um ato que costuma agradar: anota quando o gosto pesou de verdade."""
|
||||
peso = self.peso(nome)
|
||||
if peso >= 1.3:
|
||||
self.decidir('gosto', 'Escolheu ' + self.nome(nome),
|
||||
'costuma agradar: chance ×%s' % ('%.1f' % peso).replace('.', ','), agora)
|
||||
|
||||
def chega_logo(self, agora):
|
||||
"""Falta pouco para a hora em que costuma ter gente (e agora costuma estar vazio)?"""
|
||||
@@ -369,6 +416,15 @@ class Aprendizado:
|
||||
self.historico.append(dict(dia=hoje['dia'], sem=sem, presenca=fracs, atos=hoje['atos'],
|
||||
respostas=sum(hoje['respostas'].values()), a_toa=sum(hoje['falsos'])))
|
||||
del self.historico[:-MAX_HISTORICO]
|
||||
horas = sum(1 for v in fracs if v is not None and v >= ROTINA_VAZIA)
|
||||
nota = self.conferir_dia(sem, fracs)
|
||||
fim_do_dia = time.mktime(time.strptime(hoje['dia'] + ' 23:59:59', '%Y-%m-%d %H:%M:%S'))
|
||||
self.decisoes.insert(0, dict(
|
||||
horario=fim_do_dia, tipo='dia', texto='Fechou o dia: atualizou o quadro de ' + DIAS_SEMANA[sem],
|
||||
motivo='%d %s com gente, %d %s%s' % (
|
||||
horas, 'hora' if horas == 1 else 'horas', sum(hoje['respostas'].values()),
|
||||
'carinho ou conversa' if sum(hoje['respostas'].values()) == 1 else 'carinhos ou conversas',
|
||||
'' if nota is None else '; o dia conferiu %d%% com o plano' % round(nota * 100))))
|
||||
# gosto: o que agradou e o que foi ignorado hoje
|
||||
contagem = hoje['contagem']
|
||||
gostou = sorted(((s, n) for n, (t, s) in contagem.items() if s >= 1), reverse=True)
|
||||
@@ -457,6 +513,47 @@ class Aprendizado:
|
||||
certas += (v >= ROTINA_VAZIA) if p['tipo'] == 'casa' else (v < ROTINA_CHEIA)
|
||||
return round(certas / total, 2) if total >= MIN_HORAS_CONFERIR else None
|
||||
|
||||
def conferir_dia(self, sem, fracs, plano=None):
|
||||
"""Quanto um dia medido bateu com o plano (0 a 1), ou None sem plano ou com poucas horas."""
|
||||
plano = plano or self.plano
|
||||
if not plano:
|
||||
return None
|
||||
certas = total = 0
|
||||
for p in plano['dias'][sem]:
|
||||
for h in range(p['ini'], p['fim']):
|
||||
if fracs[h] is None:
|
||||
continue
|
||||
total += 1
|
||||
certas += (fracs[h] >= ROTINA_VAZIA) if p['tipo'] == 'casa' else (fracs[h] < ROTINA_CHEIA)
|
||||
return round(certas / total, 2) if total >= 6 else None
|
||||
|
||||
def dia_a_dia(self, dia=None):
|
||||
"""Um dia para o painel: o que viu, o que esperava pelo plano, quanto conferiu e as decisões."""
|
||||
hoje = self.hoje['dia']
|
||||
dia = dia or hoje
|
||||
try:
|
||||
d = date.fromisoformat(dia)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('Dia inválido (use AAAA-MM-DD)') from None
|
||||
sem = d.weekday()
|
||||
with self.lock:
|
||||
if dia == hoje:
|
||||
h = self.hoje
|
||||
fracs = [round(h['presenca'][i] / h['observado'][i], 2) if h['observado'][i] >= 5 else None for i in range(24)]
|
||||
resumo = dict(atos=h['atos'], respostas=sum(h['respostas'].values()), a_toa=sum(h['falsos']))
|
||||
else:
|
||||
registro = next((x for x in self.historico if x['dia'] == dia), None)
|
||||
fracs = registro['presenca'] if registro else [None] * 24
|
||||
resumo = dict(atos=registro['atos'], respostas=registro['respostas'], a_toa=registro['a_toa']) if registro else None
|
||||
decisoes = [x for x in self.decisoes if _dia(x['horario']) == dia]
|
||||
dias = sorted({x['dia'] for x in self.historico} | {_dia(x['horario']) for x in self.decisoes} | {hoje})
|
||||
plano = self.plano
|
||||
horas = sum(1 for v in fracs if v is not None and v >= ROTINA_VAZIA)
|
||||
return dict(dia=dia, sem=sem, nome_dia=DIAS_SEMANA[sem], hoje=dia == hoje, medido=fracs,
|
||||
plano=plano['dias'][sem] if plano else None, origem_plano=plano['origem'] if plano else None,
|
||||
confere=self.conferir_dia(sem, fracs), horas_com_gente=horas, resumo=resumo,
|
||||
decisoes=decisoes, dias=dias[-60:])
|
||||
|
||||
def tabela_semana(self):
|
||||
linhas = []
|
||||
for sem in range(7):
|
||||
@@ -534,6 +631,15 @@ class Aprendizado:
|
||||
self.plano, self.sugestao = plano, None
|
||||
else:
|
||||
self.sugestao = plano
|
||||
if usar:
|
||||
motivo = 'plano adotado: confere %d%% com o que mediu' % round(nota * 100)
|
||||
elif nota is None:
|
||||
motivo = 'ficou como sugestão: ainda poucos dados para conferir'
|
||||
elif self.plano and self.plano['origem'] == 'voce' and nota >= CONCORDA:
|
||||
motivo = 'ficou como sugestão: a rotina que você contou continua valendo'
|
||||
else:
|
||||
motivo = 'ficou como sugestão: confere só %d%% (precisa de %d%%)' % (round(nota * 100), round(CONCORDA * 100))
|
||||
self.decidir('ia', 'IA analisou %d dias' % len(self.historico), motivo, agora)
|
||||
if plano['padroes']:
|
||||
texto = plano['padroes'][0] if usar else 'Acho que ' + plano['padroes'][0][0].lower() + plano['padroes'][0][1:]
|
||||
self.anotar('aprendizado', texto, agora)
|
||||
@@ -552,6 +658,7 @@ class Aprendizado:
|
||||
self.plano, self.sugestao = plano, None
|
||||
self.analise['em'] = agora or time.time()
|
||||
self.anotar('aprendizado', 'Você me contou sua rotina. Vou seguir e conferir com o que eu vir.', agora)
|
||||
self.decidir('voce', 'Passou a seguir a rotina que você contou', '"%s"' % texto[:140], agora)
|
||||
|
||||
def chamar_ia_pronta(self):
|
||||
if not self.ia or not self.pode_ia(self.config['ia']):
|
||||
@@ -589,6 +696,7 @@ class Aprendizado:
|
||||
semana=[[v if self.semana_n[s][h] else None for h, v in enumerate(linha)]
|
||||
for s, linha in enumerate(self.semana)],
|
||||
historico_dias=len(self.historico),
|
||||
decisoes_hoje=sum(1 for x in self.decisoes if _dia(x['horario']) == self.hoje['dia']),
|
||||
plano=self.plano and dict(self.plano, confere=self.conferir(self.plano)),
|
||||
sugestao=self.sugestao and dict(self.sugestao, confere=self.conferir(self.sugestao)),
|
||||
analise=dict(self.analise, pode=bool(self.ia) and self.pode_ia(self.config['ia']),
|
||||
@@ -606,6 +714,9 @@ class Aprendizado:
|
||||
try:
|
||||
if metodo == 'GET' and rota == '/aprendizado/status':
|
||||
resultado = self.status()
|
||||
elif metodo == 'GET' and rota == '/aprendizado/dia':
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
resultado = self.dia_a_dia((parse_qs(urlsplit(h.path).query).get('dia') or [None])[0])
|
||||
elif metodo == 'POST' and rota == '/aprendizado/config':
|
||||
resultado = self.configurar(ler_json(h, 1024))
|
||||
elif metodo == 'POST' and rota == '/aprendizado/esquecer':
|
||||
|
||||
Reference in New Issue
Block a user