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>
This commit is contained in:
julio
2026-10-09 10:36:32 -03:00
co-authored by Claude Opus 5.5
parent 24625453f7
commit 935bf8d586
8 changed files with 804 additions and 103 deletions
+137
View File
@@ -8,6 +8,7 @@ import tempfile
import threading
import time
import unittest
from datetime import date, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
@@ -201,6 +202,142 @@ class VidaComAprendizadoTest(unittest.TestCase):
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: