MVP
This commit is contained in:
@@ -1,187 +1,133 @@
|
||||
# 🎯 Motor de Recomendações
|
||||
# Motor de Recomendações e IA
|
||||
|
||||
## Filosofia
|
||||
## Estado atual
|
||||
|
||||
> "Não precisas de IA para parecer inteligente. Precisas de regras bem pensadas."
|
||||
O DayMaker usa uma abordagem baseada em **IA de linguagem com contexto do inventário**.
|
||||
|
||||
O motor de recomendações evolui em 3 fases:
|
||||
|
||||
| Fase | Tecnologia | Estado |
|
||||
|------|-----------|--------|
|
||||
| 1 — Regras estáticas | Lógica JS simples | ✅ MVP |
|
||||
| 2 — IA de linguagem | OpenAI API | 🔲 Fase 2 |
|
||||
| 3 — Recomendação personalizada | Histórico + ML | 🔲 Fase 3 |
|
||||
Não existe atualmente um motor de regras local como fonte principal das recomendações. A recomendação é gerada pelo serviço `AiRecommendationService`, que chama uma API Ollama remota.
|
||||
|
||||
---
|
||||
|
||||
## FASE 1 — Regras Estáticas (MVP)
|
||||
## Serviço principal
|
||||
|
||||
### Contextos disponíveis
|
||||
Ficheiro:
|
||||
|
||||
O utilizador escolhe um dos seguintes contextos:
|
||||
|
||||
```javascript
|
||||
// constants/contexts.js
|
||||
|
||||
export const CONTEXTS = [
|
||||
{
|
||||
id: 'travel_short',
|
||||
label: 'Viagem curta (1-3 dias)',
|
||||
icon: '✈️',
|
||||
tags: ['travel'],
|
||||
priorityCategories: ['documents', 'clothing', 'electronics', 'footwear'],
|
||||
maxItems: 20,
|
||||
},
|
||||
{
|
||||
id: 'travel_long',
|
||||
label: 'Viagem longa (4+ dias)',
|
||||
icon: '🧳',
|
||||
tags: ['travel'],
|
||||
priorityCategories: ['documents', 'clothing', 'electronics', 'footwear', 'accessories'],
|
||||
maxItems: 40,
|
||||
},
|
||||
{
|
||||
id: 'work',
|
||||
label: 'Dia de trabalho',
|
||||
icon: '💼',
|
||||
tags: ['work'],
|
||||
priorityCategories: ['electronics', 'clothing', 'footwear', 'accessories'],
|
||||
maxItems: 15,
|
||||
},
|
||||
{
|
||||
id: 'casual',
|
||||
label: 'Fim de semana casual',
|
||||
icon: '😎',
|
||||
tags: ['casual'],
|
||||
priorityCategories: ['clothing', 'footwear', 'accessories'],
|
||||
maxItems: 10,
|
||||
},
|
||||
{
|
||||
id: 'sport',
|
||||
label: 'Treino / Desporto',
|
||||
icon: '🏃',
|
||||
tags: ['sport'],
|
||||
priorityCategories: ['clothing', 'footwear'],
|
||||
maxItems: 8,
|
||||
},
|
||||
{
|
||||
id: 'outdoor',
|
||||
label: 'Saída para o exterior',
|
||||
icon: '🌲',
|
||||
tags: ['outdoor', 'travel'],
|
||||
priorityCategories: ['clothing', 'footwear', 'accessories'],
|
||||
maxItems: 12,
|
||||
},
|
||||
];
|
||||
```text
|
||||
lib/services/ai_recommendation_service.dart
|
||||
```
|
||||
|
||||
### Lógica de filtragem
|
||||
Responsabilidades:
|
||||
|
||||
```javascript
|
||||
// services/suggestions.js
|
||||
|
||||
export function getSuggestionsForContext(items, contextId) {
|
||||
const context = CONTEXTS.find(c => c.id === contextId);
|
||||
if (!context) return [];
|
||||
|
||||
// 1. Filtrar itens com pelo menos uma tag do contexto
|
||||
const relevantItems = items.filter(item =>
|
||||
item.contextTags.some(tag => context.tags.includes(tag))
|
||||
);
|
||||
|
||||
// 2. Ordenar por prioridade de categoria
|
||||
const sorted = relevantItems.sort((a, b) => {
|
||||
const aIndex = context.priorityCategories.indexOf(a.category);
|
||||
const bIndex = context.priorityCategories.indexOf(b.category);
|
||||
const aPriority = aIndex === -1 ? 999 : aIndex;
|
||||
const bPriority = bIndex === -1 ? 999 : bIndex;
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
|
||||
// 3. Limitar ao máximo de itens
|
||||
return sorted.slice(0, context.maxItems);
|
||||
}
|
||||
```
|
||||
|
||||
### Exemplos de output
|
||||
|
||||
**Contexto: "Viagem curta"**
|
||||
```
|
||||
✅ Passaporte (documents.identity → travel)
|
||||
✅ Portátil (electronics.computers → travel, work)
|
||||
✅ Carregador USB-C (electronics.cables → travel)
|
||||
✅ T-shirt preta (clothing.casual → casual, travel)
|
||||
✅ Jeans azul (clothing.casual → casual, travel)
|
||||
✅ Sapatilhas brancas (footwear.casual → casual, travel)
|
||||
❌ Fato de treino (clothing.sportswear → sport) ← não aparece
|
||||
❌ Consola PS5 (electronics.gaming → casual) ← não aparece
|
||||
```
|
||||
- Buscar itens do utilizador no Supabase.
|
||||
- Construir contexto textual do inventário.
|
||||
- Enviar mensagens para a API Ollama.
|
||||
- Manter histórico simples de conversa.
|
||||
- Suportar `silent: true` para sugestões estruturadas.
|
||||
- Buscar itens com imagens para a home.
|
||||
|
||||
---
|
||||
|
||||
## FASE 2 — IA de Linguagem (OpenAI API)
|
||||
## Endpoint e modelo
|
||||
|
||||
> **Não implementar no MVP. Esta secção é referência para a Fase 2.**
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Endpoint | `https://apichat.epvc.pt/api/chat` |
|
||||
| Modelo | `llama3.2:3b` |
|
||||
| Stream | `false` |
|
||||
| Formato | Ollama `/api/chat` |
|
||||
|
||||
### Objetivo
|
||||
Payload base:
|
||||
|
||||
Permitir ao utilizador escrever em linguagem natural:
|
||||
- "Vou viajar 4 horas de comboio"
|
||||
- "Tenho uma reunião importante amanhã de manhã"
|
||||
- "Fim de semana na praia"
|
||||
|
||||
### Arquitetura proposta
|
||||
|
||||
```
|
||||
Utilizador escreve frase
|
||||
↓
|
||||
OpenAI API (classificação de contexto)
|
||||
↓
|
||||
Retorna JSON estruturado:
|
||||
```json
|
||||
{
|
||||
"context": "travel",
|
||||
"duration": "short",
|
||||
"environment": "transit",
|
||||
"weather": "unknown",
|
||||
"formality": "casual"
|
||||
"model": "llama3.2:3b",
|
||||
"messages": [
|
||||
{"role": "system", "content": "..."},
|
||||
{"role": "user", "content": "..."}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
↓
|
||||
Motor de regras Fase 1 (com parâmetros enriquecidos)
|
||||
↓
|
||||
Lista de itens sugeridos
|
||||
```
|
||||
|
||||
### Prompt base para OpenAI
|
||||
|
||||
```
|
||||
System: És um assistente de inventário pessoal.
|
||||
Analisa a frase do utilizador e devolve APENAS um JSON com:
|
||||
- context: "travel" | "work" | "casual" | "sport" | "outdoor" | "formal"
|
||||
- duration: "short" | "medium" | "long" | null
|
||||
- environment: "indoor" | "outdoor" | "transit" | null
|
||||
- weather: "cold" | "hot" | "rainy" | null
|
||||
- formality: "casual" | "formal" | null
|
||||
|
||||
User: {frase do utilizador}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FASE 3 — Recomendação Personalizada
|
||||
## Prompt de sistema
|
||||
|
||||
> **Fase muito futura. Apenas visão.**
|
||||
A IA é instruída a:
|
||||
|
||||
- Sistema aprende quais itens o utilizador seleciona/ignora das sugestões
|
||||
- Pontuação de relevância por item por contexto
|
||||
- Integração com API de clima para filtros automáticos
|
||||
- Histórico de viagens para sugestões baseadas em padrões
|
||||
- Ajudar a montar outfits e escolher o que levar.
|
||||
- Usar linguagem simples e curta.
|
||||
- Não usar emojis.
|
||||
- Basear-se nas tags e notas dos itens.
|
||||
- Responder sempre em português.
|
||||
|
||||
---
|
||||
|
||||
## Notas para o agente IA
|
||||
## Contexto enviado à IA
|
||||
|
||||
- **No MVP, usar APENAS a Fase 1.** Não implementar OpenAI no MVP.
|
||||
- A lógica de `getSuggestionsForContext` deve ser pura (sem side effects)
|
||||
- Os contextos são fixos no MVP — não adicionar sem validar com o utilizador
|
||||
- A ordenação por `priorityCategories` é intencional e importante — respeitá-la
|
||||
- Quando a Fase 2 for implementada, o output do OpenAI deve mapear para os IDs de contexto da Fase 1
|
||||
O contexto inclui os itens do utilizador:
|
||||
|
||||
```text
|
||||
Itens disponiveis no inventario do utilizador:
|
||||
- Bota verde (categoria: Roupa) [tags: casual, outdoor]
|
||||
- Switch (categoria: Eletrónica) [tags: gaming, casual]
|
||||
```
|
||||
|
||||
Esse contexto é anexado à mensagem de sistema.
|
||||
|
||||
---
|
||||
|
||||
## Chat IA
|
||||
|
||||
No ecrã `AiChatScreen`:
|
||||
|
||||
- O utilizador escreve livremente.
|
||||
- Existem sugestões rápidas no topo.
|
||||
- A resposta é apresentada como conversa.
|
||||
- O histórico fica em memória no serviço enquanto a instância existir.
|
||||
|
||||
---
|
||||
|
||||
## Sugestão IA na Home
|
||||
|
||||
Fluxo:
|
||||
|
||||
1. Utilizador toca em `Pedir sugestão à IA`.
|
||||
2. App abre um diálogo e pede a ocasião.
|
||||
3. O utilizador pode escrever algo como `piquenique no parque` ou escolher chip rápido.
|
||||
4. A app envia a ocasião em modo `silent`.
|
||||
5. A IA deve devolver apenas nomes exatos dos itens, um por linha.
|
||||
6. A app cruza esses nomes com os itens reais do Supabase.
|
||||
7. A app mostra cards com imagem, nome e categoria.
|
||||
8. O utilizador pode exportar os itens para um dia da semana.
|
||||
|
||||
---
|
||||
|
||||
## Modo silencioso
|
||||
|
||||
Quando `silent: true`, a instrução acrescentada ao pedido é:
|
||||
|
||||
```text
|
||||
responde APENAS com os nomes exatos dos itens do meu inventario que sugeres, um por linha, sem numeracao, sem explicacao, sem comentarios.
|
||||
```
|
||||
|
||||
Isto permite transformar a resposta da IA numa lista de itens reais.
|
||||
|
||||
---
|
||||
|
||||
## Limitações conhecidas
|
||||
|
||||
- A correspondência depende da IA devolver nomes próximos aos nomes reais.
|
||||
- Se a IA devolver texto extra, a app tenta limpar linhas, mas pode falhar correspondência.
|
||||
- A IA não vê imagens, apenas nomes, categorias, tags e notas.
|
||||
- Não há ranking local por clima, cor ou histórico de uso.
|
||||
|
||||
---
|
||||
|
||||
## Melhorias futuras
|
||||
|
||||
- Resposta em JSON em vez de texto simples.
|
||||
- Validação local mais robusta por ID de item.
|
||||
- Sugestões com clima e duração.
|
||||
- Preferências aprendidas por histórico.
|
||||
- Integração com calendário.
|
||||
|
||||
Reference in New Issue
Block a user