Files
carinha-control-panel/tests/test_aprendizado.py
T
julioandClaude Opus 5.5 935bf8d586 feat(vida): semana da casa com análise por IA e rotina contada
O aprendizado passa a entender a logística da semana, não só a hora:
- presença por dia da semana × hora (7×24, média móvel por dia) e
  histórico dos últimos 42 dias;
- depois da meia-noite, com 7 dias observados, a IA escolhida (Gemini,
  IA local ou Qwen do S10, via Criacao.chamar) cruza os dias e devolve
  padrões em frases e um plano da semana (casa / fora / dormindo);
- o plano da IA só é adotado se conferir com 70% das horas medidas;
  senão fica como sugestão ("Usar este plano");
- "Conte sua rotina": texto livre vira plano na hora e vale sobre o da
  IA; o painel mostra o quanto confere com o que os sensores veem;
- o plano guia o comportamento: acorda do cochilo 15 min antes da
  chegada naquele dia da semana e, com a casa vazia, tira cochilos
  longos para guardar energia;
- lição da rotina só quando muda 3+ horas ou depois de uma semana, e
  some quando já existe plano da semana;
- painel: quadro da semana (o que eu vi / plano), a rotina entendida,
  sugestão, contar rotina, analisar agora e a IA da análise diária.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:36:32 -03:00

374 lines
17 KiB
Python

"""Aprendizado com o dia a dia: gosto pelos atos, rotina da casa, sono e o fechamento do dia."""
import http.client
import importlib.util
import json
import random
import sys
import tempfile
import threading
import time
import unittest
from datetime import date, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from aprendizado import Aprendizado # noqa: E402
from vida import Vida # noqa: E402
spec = importlib.util.spec_from_file_location('kiosk_aprendizado', ROOT / 'kiosk-server.py')
kiosk = importlib.util.module_from_spec(spec)
spec.loader.exec_module(kiosk)
def em(dia, hora, minuto=0, segundo=0):
return time.mktime((2026, 10, dia, hora, minuto, segundo, 0, 0, -1))
class AprendizadoTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.caminho = Path(self.tmp.name) / 'aprendizado.json'
self.a = Aprendizado(self.caminho)
self.a.hoje = self.a._dia_vazio('2026-10-06')
self.diario = []
self.a.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto))
def tearDown(self):
self.tmp.cleanup()
def fazer(self, nome, vezes, resposta=None, inicio=None):
t = inicio or em(6, 15)
for i in range(vezes):
self.a.ato(nome, t + i * 120, perto=True)
if resposta:
self.a.resposta(resposta, t + i * 120 + 20)
def test_gosto_sobe_com_carinho_e_desce_quando_ignorado(self):
self.fazer('rir', 5, 'carinho')
self.fazer('bocejar', 5, inicio=em(6, 16))
self.assertGreater(self.a.peso('rir'), 1.0)
self.assertLess(self.a.peso('bocejar'), 1.0)
self.assertLessEqual(self.a.peso('rir'), 2.0)
self.assertGreaterEqual(self.a.peso('bocejar'), 0.5)
def test_sem_opiniao_com_poucas_tentativas_ou_sozinho(self):
self.fazer('rir', 2, 'carinho')
self.assertEqual(self.a.peso('rir'), 1.0)
for i in range(10):
self.a.ato('pensar', em(6, 15) + i, perto=False) # sem plateia ninguém pode gostar
self.assertEqual(self.a.peso('pensar'), 1.0)
self.assertNotIn('pensar', self.a.itens)
def test_resposta_fora_da_janela_nao_conta(self):
for i in range(4):
self.a.ato('acenar', em(6, 15) + i * 300, perto=True)
self.a.resposta('carinho', em(6, 15) + i * 300 + 200) # 200 s depois: já era outra coisa
self.assertEqual(self.a.itens['acenar']['s'], 0)
def test_cada_ato_agrada_uma_vez(self):
self.a.ato('rir', em(6, 15), perto=True)
for _ in range(5):
self.a.resposta('carinho', em(6, 15) + 10)
self.assertEqual(self.a.itens['rir']['s'], 1.0)
def test_fechar_o_dia_gera_licoes_e_esquece_um_pouco(self):
self.fazer('rir', 4, 'carinho')
self.fazer('bocejar', 5, inicio=em(6, 17))
t_antes = self.a.itens['rir']['t']
self.a.observar(em(7, 0, 1), perto=False) # meia-noite: fecha o dia 6
textos = [t for _, t in self.diario]
self.assertTrue(any('gostam de me ver dar risada' in t for t in textos), textos)
self.assertTrue(any('Tentei bocejar 5 vezes e ninguém ligou' in t for t in textos), textos)
self.assertAlmostEqual(self.a.itens['rir']['t'], t_antes * 0.85, places=2)
self.assertEqual(self.a.hoje['dia'], '2026-10-07')
self.assertTrue(self.caminho.exists())
def test_rotina_da_casa_e_acordar_antes_de_chegarem(self):
for dia in (6, 7, 8):
for minuto in range(60):
self.a.observar(em(dia, 17, minuto), perto=False) # 17h: casa vazia
self.a.observar(em(dia, 18, minuto), perto=True) # 18h: chegou gente
self.a.observar(em(9, 0, 1), perto=False)
self.assertGreaterEqual(self.a.rotina[18], 0.5)
self.assertLess(self.a.rotina[17], 0.3)
self.assertTrue(any('costumam estar por perto das 18h às 19h' in t for _, t in self.diario), self.diario)
self.assertTrue(self.a.chega_logo(em(9, 17, 50)))
self.assertFalse(self.a.chega_logo(em(9, 17, 10))) # ainda falta muito
self.assertFalse(self.a.chega_logo(em(9, 12, 50)))
def test_acordar_a_toa_pede_mais_movimento_naquela_hora(self):
for dia in (6, 7, 8, 9):
for vez in range(2):
t = em(dia, 22, vez * 20)
self.a.acordou_pelo_sensor(t)
for s in range(0, 200, 5):
self.a.observar(t + s, perto=s < 10) # mexeu e sumiu: foi à toa
self.a.observar(em(dia + 1, 0, 1), perto=False)
self.assertGreaterEqual(self.a.extra_pir(em(10, 22, 30)), 1)
self.assertEqual(self.a.extra_pir(em(10, 15)), 0)
self.assertTrue(any('Acordei à toa 2 vezes por volta das 22h' in t for _, t in self.diario), self.diario)
def test_acordar_com_carinho_valeu_a_pena(self):
t = em(6, 22)
self.a.acordou_pelo_sensor(t)
self.a.observar(t + 5, perto=True)
self.a.resposta('carinho', t + 30)
for s in range(35, 200, 5):
self.a.observar(t + s, perto=False)
self.assertEqual(self.a.hoje['falsos'][22], 0)
def test_desligado_nao_aprende_nem_muda_nada(self):
self.a.configurar(dict(ligado=False))
self.fazer('rir', 6, 'carinho')
self.assertEqual(self.a.peso('rir'), 1.0)
self.assertEqual(self.a.extra_pir(em(6, 22)), 0)
self.assertFalse(self.a.chega_logo(em(6, 17, 50)))
def test_esquecer_e_persistencia(self):
self.fazer('rir', 5, 'carinho')
self.fazer('bocejar', 5, inicio=em(6, 17)) # o gosto é relativo: precisa de algo menos querido
self.a.salvar()
outro = Aprendizado(self.caminho)
self.assertGreater(outro.peso('rir'), 1.0)
outro.anotar = lambda *a, **k: None
outro.esquecer()
self.assertEqual(outro.peso('rir'), 1.0)
self.assertEqual(Aprendizado(self.caminho).itens, {})
def test_validar(self):
with self.assertRaises(ValueError):
self.a.configurar(dict(ligado='sim'))
with self.assertRaises(ValueError):
self.a.configurar(dict(outro=True))
class VidaComAprendizadoTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.a = Aprendizado(Path(self.tmp.name) / 'aprendizado.json')
self.a.hoje = self.a._dia_vazio('2026-10-06')
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3))
self.v.aprendizado = self.a
self.ordens = []
self.v.enfileirar = self.ordens.append
self.v.ultimo_tick = em(6, 15)
def tearDown(self):
self.tmp.cleanup()
def test_gesto_que_agrada_sai_mais(self):
for i in range(8):
self.a.ato('rir', em(6, 15) + i * 100, perto=True)
self.a.resposta('carinho', em(6, 15) + i * 100 + 5)
self.a.ato('feliz', em(6, 15) + i * 100 + 50, perto=True)
opcoes = [('rir', 1, ''), ('feliz', 1, '')]
saiu = [self.v.escolher(opcoes)[0] for _ in range(400)]
self.assertGreater(saiu.count('rir'), saiu.count('feliz') * 2)
def test_carinho_e_atos_com_plateia_chegam_ao_aprendizado(self):
self.v.alguem_perto = lambda: True
self.v.celular_online = lambda: True
self.v.proximo_ato = 0
self.v.necessidades.update(energia=90, social=90, diversao=90, alegria=90)
ato = None
for i in range(30):
ato = self.v.tick(em(6, 15) + i * 200)
if ato in self.a.itens:
break
self.assertIn(ato, self.a.itens)
self.v.estimulo('carinho', em(6, 15) + i * 200 + 10)
self.assertGreater(self.a.itens[ato]['s'], 0)
def test_pir_pede_mais_quando_acordou_a_toa_naquela_hora(self):
self.a.falsos[22] = 1.0
self.assertFalse(self.v.pir_acorda(em(6, 22)))
self.assertTrue(self.v.pir_acorda(em(6, 22) + 10))
self.assertEqual(self.a.acordou_em, em(6, 22) + 10)
self.assertTrue(self.v.pir_acorda(em(6, 15)))
def test_acorda_do_cochilo_antes_de_chegarem(self):
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
self.v.dormindo_proprio = True
self.v.necessidades['energia'] = 70
self.assertEqual(self.v.agir(em(6, 17, 50)), 'acordar')
self.assertIn('acordar', self.ordens)
def test_sono_profundo_nao_e_interrompido_pela_rotina(self):
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
self.v.dormindo_proprio = True
self.v.sono_profundo = True
self.v.necessidades['energia'] = 70
self.assertIsNone(self.v.agir(em(6, 17, 50)))
def semana_tipica(a, semanas=2):
"""Segunda, sexta e fim de semana em casa (7h-23h); terça a quinta fora das 8h às 18h. Começa em 05/10 (segunda)."""
inicio = date(2026, 10, 5)
for i in range(semanas * 7):
d = inicio + timedelta(days=i)
a.hoje = a._dia_vazio(d.isoformat())
for h in range(24):
for m in range(0, 60, 2):
fora = d.weekday() in (1, 2, 3) and 8 <= h < 18
perto = 7 <= h < 23 and not fora and m < 40
a.minuto = None
a.observar(time.mktime((d.year, d.month, d.day, h, m, 0, 0, 0, -1)), perto)
fim = d + timedelta(days=1)
a.observar(time.mktime((fim.year, fim.month, fim.day, 0, 0, 30, 0, 0, -1)), False)
def plano_certo():
dias = []
for dia in range(7):
if dia in (1, 2, 3):
periodos = [dict(ini=0, fim=7, tipo='dormindo'), dict(ini=7, fim=8, tipo='casa'),
dict(ini=8, fim=18, tipo='fora'), dict(ini=18, fim=23, tipo='casa'), dict(ini=23, fim=24, tipo='dormindo')]
else:
periodos = [dict(ini=0, fim=7, tipo='dormindo'), dict(ini=7, fim=23, tipo='casa'), dict(ini=23, fim=24, tipo='dormindo')]
dias.append(dict(dia=dia, periodos=periodos))
return dict(padroes=['Percebi que de terça a quinta a casa fica vazia das 8h às 18h'], dias=dias)
class SemanaTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.a = Aprendizado(Path(self.tmp.name) / 'ap.json')
self.diario = []
self.a.anotar = lambda tipo, texto, agora=None: self.diario.append(texto)
self.pedidos = []
self.resposta = plano_certo()
def ia(provedor, prompt, schema):
self.pedidos.append((provedor, prompt))
return self.resposta
self.a.ia, self.a.pode_ia = ia, lambda provedor: True
self.a.config['analise_ia'] = False # nos testes a análise é chamada direto
def tearDown(self):
self.tmp.cleanup()
def test_quadro_da_semana_separa_os_dias(self):
semana_tipica(self.a)
self.assertEqual(len(self.a.historico), 14)
self.assertLess(self.a.semana[2][10], 0.3) # quarta 10h: fora
self.assertGreater(self.a.semana[0][10], 0.5) # segunda 10h: em casa
self.assertGreater(self.a.semana[4][10], 0.5) # sexta 10h: em casa
self.assertEqual(self.a.esperado(em(14, 10)), 'fora') # 14/10 é quarta
self.assertEqual(self.a.esperado(em(12, 10)), 'casa') # 12/10 é segunda
self.assertTrue(self.a.chega_logo(em(14, 17, 50))) # quarta: chegam às 18h
self.assertFalse(self.a.chega_logo(em(12, 17, 50))) # segunda: já tem gente em casa
self.assertTrue(self.a.casa_vazia(em(14, 10)))
def test_plano_da_ia_que_bate_com_os_dados_e_usado(self):
semana_tipica(self.a)
self.a.analisar(em(19, 1))
self.assertIn('quarta', self.pedidos[0][1])
self.assertIn('sexta (2 dias medidos)', self.pedidos[0][1])
self.assertEqual(self.a.plano['origem'], 'ia')
self.assertGreaterEqual(self.a.conferir(self.a.plano), 0.7)
self.assertIsNone(self.a.sugestao)
self.assertEqual(self.a.esperado(em(14, 10)), 'fora')
self.assertEqual(self.a.esperado(em(14, 2)), 'dormindo')
self.assertTrue(any('terça a quinta' in t for t in self.diario))
def test_plano_da_ia_que_nao_bate_vira_so_sugestao(self):
semana_tipica(self.a)
errado = plano_certo()
for d in errado['dias']:
d['periodos'] = [dict(ini=0, fim=24, tipo='fora')] # "a casa vive vazia": falso
self.resposta = errado
self.a.analisar(em(19, 1))
self.assertIsNone(self.a.plano)
self.assertIsNotNone(self.a.sugestao)
self.assertLess(self.a.conferir(self.a.sugestao), 0.7)
self.assertTrue(any(t.startswith('Acho que') for t in self.diario))
self.a.usar_sugestao()
self.assertEqual(self.a.plano['origem'], 'voce')
def test_rotina_contada_vale_na_hora_e_a_ia_nao_atropela(self):
self.a.informar('Fico fora de terça a quinta, das 8h às 18h')
self.assertIn('Fico fora de terça a quinta', self.pedidos[0][1])
self.assertEqual(self.a.plano['origem'], 'voce')
self.assertIsNone(self.a.conferir(self.a.plano)) # ainda sem dados para conferir
semana_tipica(self.a)
self.assertGreaterEqual(self.a.conferir(self.a.plano), 0.7)
self.a.analisar(em(19, 1))
self.assertEqual(self.a.plano['origem'], 'voce') # o que a pessoa disse continua valendo
self.assertIsNotNone(self.a.sugestao)
def test_validar_plano_conserta_o_que_a_ia_erra(self):
p = Aprendizado.validar_plano(dict(padroes=['ok'] * 9, dias=[
dict(dia=0, periodos=[dict(ini=8, fim=20, tipo='casa'), dict(ini=18, fim=30, tipo='fora'),
dict(ini=5, fim=5, tipo='casa'), dict(ini=1, fim=3, tipo='festa')]),
dict(dia=9, periodos=[dict(ini=0, fim=24, tipo='casa')]), 'lixo']), 'ia')
self.assertEqual(p['dias'][0], [dict(ini=8, fim=18, tipo='casa'), dict(ini=18, fim=24, tipo='fora')])
self.assertEqual(len(p['padroes']), 5)
with self.assertRaises(ValueError):
Aprendizado.validar_plano(dict(padroes=[], dias=[]), 'ia')
def test_analise_roda_sozinha_a_meia_noite_com_uma_semana(self):
self.a.config['analise_ia'] = True
semana_tipica(self.a, semanas=1)
for _ in range(50):
if not self.a.analise['ocupado'] and (self.a.plano or self.a.sugestao):
break
time.sleep(0.05)
self.assertEqual(len(self.pedidos), 1) # só na 7ª meia-noite
self.assertIsNotNone(self.a.plano or self.a.sugestao)
def test_persistencia_da_semana_e_do_plano(self):
semana_tipica(self.a, semanas=1)
self.a.informar('Fico fora de terça a quinta, das 8h às 18h')
self.a.salvar()
outro = Aprendizado(Path(self.tmp.name) / 'ap.json')
self.assertEqual(outro.semana, self.a.semana)
self.assertEqual(outro.plano['dias'], self.a.plano['dias'])
self.assertEqual(len(outro.historico), 7)
def test_casa_vazia_faz_ele_cochilar_para_guardar_pique(self):
v = Vida(Path(self.tmp.name) / 'v.json', iniciar=False, rng=random.Random(5))
v.aprendizado = self.a
v.enfileirar = lambda ordem: None
v.celular_online = lambda: True
v.necessidades.update(energia=70, diversao=80, social=80, alegria=80)
self.a.informar('Fico fora de terça a quinta, das 8h às 18h')
v.visto_em = em(14, 9)
atos = [v.agir(em(14, 10) + i * 120) for i in range(10)]
self.assertIn('atividade dormir', atos)
class RotasTest(unittest.TestCase):
def test_status_config_e_esquecer(self):
with tempfile.TemporaryDirectory() as tmp:
kiosk.ARQUIVO_APARENCIA = str(Path(tmp) / 'a.json')
s = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
s.aprendizado = Aprendizado(Path(tmp) / 'ap.json')
kiosk.ligar_eventos(s)
threading.Thread(target=s.serve_forever, daemon=True).start()
try:
def pedir(metodo, rota, corpo=None):
c = http.client.HTTPConnection('127.0.0.1', s.server_port, timeout=5)
dados = json.dumps(corpo).encode() if corpo is not None else None
c.request(metodo, rota, body=dados, headers={'Content-Type': 'application/json'} if dados else {})
r = c.getresponse()
return r.status, json.loads(r.read())
cod, st = pedir('GET', '/aprendizado/status')
self.assertEqual(cod, 200)
self.assertEqual(len(st['rotina']), 24)
cod, st = pedir('POST', '/aprendizado/config', dict(ligado=False))
self.assertEqual((cod, st['config']['ligado']), (200, False))
cod, st = pedir('POST', '/aprendizado/config', dict(ligado='x'))
self.assertEqual(cod, 400)
cod, st = pedir('POST', '/aprendizado/esquecer', {})
self.assertEqual((cod, st['dias']), (200, 0))
cod, est = pedir('GET', '/estado')
self.assertIn('aprendizado', est)
finally:
s.shutdown()
kiosk.EVENTOS.fechar()
if __name__ == '__main__':
unittest.main()