IA Funcinonal
This commit is contained in:
@@ -1,128 +1,116 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class AiRecommendationService {
|
||||
Future<String> recommendForOccasion(String prompt) async {
|
||||
final input = prompt.trim();
|
||||
if (input.isEmpty) {
|
||||
return 'Diz-me a ocasião ou destino e eu ajudo-te a preparar uma lista. Por exemplo: "uma viagem para Itália de 5 dias".';
|
||||
}
|
||||
static const String _apiUrl = 'https://apichat.epvc.pt/api/chat';
|
||||
static const String _model = 'llama3.2:3b';
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
static const String _systemPrompt =
|
||||
'voce é uma agente de ia que tem como objetivo ajudar o utilizador a formar uma especie de outfit e acessorios como consolas e ate documentacao que é preciso para seu dia ou viagem. voce usa uma linguagem descontraida mas sem usar emojis ou afins. para saber oque escolher voce vai usar as tags que estao nos itens ou suas notas. responde sempre em portugues.';
|
||||
|
||||
final lower = input.toLowerCase();
|
||||
final items = <String>{};
|
||||
final tips = <String>[];
|
||||
final List<Map<String, String>> _history = [];
|
||||
|
||||
items.addAll([
|
||||
'Documento de identificação ou passaporte',
|
||||
'Carteira/cartões e algum dinheiro',
|
||||
'Telemóvel e carregador',
|
||||
'Power bank',
|
||||
'Produtos de higiene pessoal',
|
||||
'Roupa interior e meias suficientes',
|
||||
'Medicamentos pessoais',
|
||||
]);
|
||||
|
||||
if (_containsAny(lower, [
|
||||
'viagem',
|
||||
'viajar',
|
||||
'italia',
|
||||
'itália',
|
||||
'paris',
|
||||
'espanha',
|
||||
'frança',
|
||||
'fim de semana',
|
||||
])) {
|
||||
items.addAll([
|
||||
'Adaptador de tomada se necessário',
|
||||
'Mochila pequena para passeios',
|
||||
'Garrafa de água reutilizável',
|
||||
'Cópia digital dos documentos',
|
||||
'Seguro/cartão europeu de saúde se aplicável',
|
||||
]);
|
||||
tips.add('Confirma o clima e as regras de bagagem antes de sair.');
|
||||
}
|
||||
|
||||
if (_containsAny(lower, [
|
||||
'italia',
|
||||
'itália',
|
||||
'roma',
|
||||
'milão',
|
||||
'veneza',
|
||||
'florença',
|
||||
])) {
|
||||
items.addAll([
|
||||
'Calçado muito confortável para caminhar',
|
||||
'Óculos de sol',
|
||||
'Roupa leve e versátil',
|
||||
'Casaco leve para a noite',
|
||||
'Roupa mais composta para igrejas ou locais religiosos',
|
||||
]);
|
||||
tips.add(
|
||||
'Em Itália vais provavelmente caminhar bastante; prioriza conforto no calçado.',
|
||||
);
|
||||
tips.add(
|
||||
'Para visitar igrejas, é útil levar roupa que cubra ombros/joelhos.',
|
||||
Future<String> _itemsContext() async {
|
||||
try {
|
||||
final user = Supabase.instance.client.auth.currentUser;
|
||||
if (user == null) return '';
|
||||
final rows = await Supabase.instance.client
|
||||
.from('items')
|
||||
.select()
|
||||
.eq('user_id', user.id);
|
||||
if (rows.isEmpty) return '';
|
||||
final buf = StringBuffer(
|
||||
'Itens disponiveis no inventario do utilizador:\n',
|
||||
);
|
||||
for (final it in rows) {
|
||||
final nome = it['nome'] ?? '';
|
||||
final cat = it['categoria'] ?? '';
|
||||
final tags = (it['tags'] as List?)?.join(', ') ?? '';
|
||||
final nota = it['nota'] ?? it['notes'] ?? '';
|
||||
buf.write('- $nome');
|
||||
if (cat.toString().isNotEmpty) buf.write(' (categoria: $cat)');
|
||||
if (tags.isNotEmpty) buf.write(' [tags: $tags]');
|
||||
if (nota.toString().isNotEmpty) buf.write(' {nota: $nota}');
|
||||
buf.writeln();
|
||||
}
|
||||
return buf.toString();
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (_containsAny(lower, ['praia', 'piscina', 'verão', 'calor', 'quente'])) {
|
||||
items.addAll([
|
||||
'Protetor solar',
|
||||
'Fato de banho',
|
||||
'Toalha de praia',
|
||||
'Chinelos',
|
||||
'Chapéu ou boné',
|
||||
]);
|
||||
}
|
||||
|
||||
if (_containsAny(lower, ['frio', 'inverno', 'neve', 'montanha'])) {
|
||||
items.addAll([
|
||||
'Casaco quente',
|
||||
'Cachecol',
|
||||
'Luvas',
|
||||
'Camisolas térmicas',
|
||||
'Calçado impermeável',
|
||||
]);
|
||||
}
|
||||
|
||||
if (_containsAny(lower, [
|
||||
'trabalho',
|
||||
'reunião',
|
||||
'conferência',
|
||||
'evento profissional',
|
||||
])) {
|
||||
items.addAll([
|
||||
'Portátil e carregador',
|
||||
'Roupa formal ou smart casual',
|
||||
'Bloco de notas',
|
||||
'Caneta',
|
||||
'Cartões/documentos profissionais',
|
||||
]);
|
||||
}
|
||||
|
||||
if (_containsAny(lower, [
|
||||
'casamento',
|
||||
'cerimónia',
|
||||
'formal',
|
||||
'jantar elegante',
|
||||
])) {
|
||||
items.addAll([
|
||||
'Roupa formal',
|
||||
'Sapatos formais',
|
||||
'Acessórios',
|
||||
'Perfume',
|
||||
'Kit pequeno de emergência para roupa',
|
||||
]);
|
||||
}
|
||||
|
||||
final itemLines = items.take(18).map((item) => '- $item').join('\n');
|
||||
final tipLines = tips.isEmpty
|
||||
? ''
|
||||
: '\n\nDicas:\n${tips.map((tip) => '- $tip').join('\n')}';
|
||||
|
||||
return 'Para $input, eu levaria:\n\n$itemLines$tipLines\n\nSe me disseres duração, clima e tipo de viagem, consigo ajustar melhor a lista.';
|
||||
}
|
||||
|
||||
bool _containsAny(String text, List<String> terms) {
|
||||
return terms.any(text.contains);
|
||||
Future<String> sendMessage(String userMessage, {bool silent = false}) async {
|
||||
final ctx = await _itemsContext();
|
||||
final systemContent = ctx.isNotEmpty
|
||||
? '$_systemPrompt\n\n$ctx'
|
||||
: _systemPrompt;
|
||||
|
||||
final messages = <Map<String, String>>[
|
||||
{'role': 'system', 'content': systemContent},
|
||||
..._history,
|
||||
];
|
||||
|
||||
final userContent = silent
|
||||
? '$userMessage\n\n[Instrucao: nao expliques nem comentes. Devolve apenas a lista de itens (do meu inventario quando possivel) que sugeres para esta ocasiao, em formato de lista simples.]'
|
||||
: userMessage;
|
||||
|
||||
messages.add({'role': 'user', 'content': userContent});
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse(_apiUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'model': _model,
|
||||
'messages': messages,
|
||||
'stream': false,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 60));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return 'Erro a contactar a IA (${response.statusCode}). Tenta de novo.';
|
||||
}
|
||||
|
||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
final aiText = _extract(data);
|
||||
if (aiText.isEmpty) {
|
||||
return 'Nao recebi resposta da IA. Tenta de novo.';
|
||||
}
|
||||
|
||||
// guardar no historico (mensagem do user "limpa", sem instrucoes silenciosas)
|
||||
_history.add({'role': 'user', 'content': userMessage});
|
||||
_history.add({'role': 'assistant', 'content': aiText});
|
||||
return aiText;
|
||||
} catch (_) {
|
||||
return 'Nao consegui ligar ao servidor. Verifica a tua internet e tenta de novo.';
|
||||
}
|
||||
}
|
||||
|
||||
String _extract(dynamic data) {
|
||||
if (data is Map) {
|
||||
// Ollama /api/chat -> { message: { role, content }, done, ... }
|
||||
final msg = data['message'];
|
||||
if (msg is Map && msg['content'] != null) {
|
||||
return msg['content'].toString().trim();
|
||||
}
|
||||
// Ollama /api/generate -> { response: "..." }
|
||||
if (data['response'] != null) return data['response'].toString().trim();
|
||||
// OpenAI-style fallback
|
||||
final choices = data['choices'];
|
||||
if (choices is List && choices.isNotEmpty) {
|
||||
final m = choices[0]['message'];
|
||||
if (m is Map && m['content'] != null) {
|
||||
return m['content'].toString().trim();
|
||||
}
|
||||
}
|
||||
if (data['content'] != null) return data['content'].toString().trim();
|
||||
}
|
||||
if (data is String) return data.trim();
|
||||
return '';
|
||||
}
|
||||
|
||||
void clearHistory() => _history.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user