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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user