### Key Changes:
**AI Equipment Proposal (`functions/aiEquipmentProposal.js`):**
- Updated Gemini model to `gemini-3.1-flash-lite-preview` and updated the API key.
- Increased `MAX_TOOL_ITERATIONS` from 12 to 20.
- Added a new tool `list_equipment_by_category` to allow the AI to browse equipment when specific searches fail.
- Enhanced the system prompt with instructions to handle typos via category exploration and authorized more creative equipment suggestions based on event descriptions.
- Improved the user prompt to include more event context (name, location, notes, and options).
- Set `responseMimeType: 'application/json'` in the generation config for better reliability.
- Improved error logging and user-facing error messages for timeouts.
**UI & Pagination (`lib/views/`):**
- **ContainerFormPage**: Replaced `StreamBuilder` with a paginated list using `DataService` for equipment selection. Added a scroll controller to support infinite scrolling and updated UI colors to use the newer `withValues` API.
- **EquipmentSelectionDialog**:
- Increased pagination limit from 25 to 50 items.
- Implemented `_checkIfMoreItemsNeeded` logic to automatically fetch more pages if filters (like hiding conflicting items) leave the view too empty.
- Added a `NotificationListener` to the `ListView` to trigger pagination on scroll.
- Fixed minor encoding issues in comments.
---
### Proposed Commit Message:
feat: Mise à jour du modèle Gemini et optimisation de la sélection du matériel avec pagination
- Mise à jour du modèle d'IA vers `gemini-3.1-flash-lite-preview` et augmentation de la limite d'itérations des outils à 20.
- Ajout de l'outil `list_equipment_by_category` pour permettre à l'IA d'explorer les alternatives en cas d'échec de recherche textuelle.
- Enrichissement du prompt système et du contexte envoyé à l'IA (nom, lieu, notes et options de l'événement).
- Implémentation de la pagination dans `ContainerFormPage` pour la sélection d'équipements afin d'améliorer les performances.
- Optimisation de `EquipmentSelectionDialog` avec chargement automatique des pages suivantes si les filtres réduisent trop la liste visible.
- Passage à `withValues` pour la gestion des couleurs et amélioration de la gestion des erreurs et du logging.
This commit is contained in:
@@ -9,9 +9,9 @@ const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
const admin = require('firebase-admin');
|
||||
const logger = require('firebase-functions/logger');
|
||||
|
||||
const GEMINI_MODEL = 'gemini-2.5-flash';
|
||||
const GEMINI_API_KEY = 'AIzaSyBdBdjFLma2pLenmFBlqZHArS4GVF-mclo';
|
||||
const MAX_TOOL_ITERATIONS = 12;
|
||||
const GEMINI_MODEL = 'gemini-3.1-flash-lite-preview';
|
||||
const GEMINI_API_KEY = 'AIzaSyB0hOvBjWeWjdrxVARzfErZ_uGuArlvmQc';
|
||||
const MAX_TOOL_ITERATIONS = 20;
|
||||
const PAST_EVENTS_LIMIT = 5;
|
||||
const SEARCH_RESULTS_LIMIT = 20;
|
||||
const EVENT_SEARCH_SCAN_LIMIT = 100;
|
||||
@@ -141,6 +141,25 @@ const AI_TOOLS = [
|
||||
required: ['equipmentIds'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_equipment_by_category',
|
||||
description: 'Liste le materiel d\'une categorie ou sous-categorie specifique. Utile si search_equipment ne donne rien a cause d\'une faute de frappe ou pour explorer les alternatives.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Nom de la categorie.',
|
||||
nullable: true,
|
||||
},
|
||||
subCategory: {
|
||||
type: 'string',
|
||||
description: 'Nom de la sous-categorie.',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -151,8 +170,10 @@ Tu dois proposer une liste de materiel et de flight cases adaptee a l evenement
|
||||
Regles absolues :
|
||||
- Tu ne dois JAMAIS ecrire en base de donnees.
|
||||
- Utilise search_equipment pour trouver du materiel, check_availability_batch en priorite pour verifier la disponibilite, check_availability pour un cas isole, get_past_events pour t inspirer.
|
||||
- Si une recherche precise echoue, utilise list_equipment_by_category pour explorer les categories ou trouver corriger d'eventuelles fautes de frappe de l'utilisateur.
|
||||
- Si l utilisateur cite un evenement precis (nom/date), appelle d abord search_event_reference pour retrouver cet evenement et reutiliser son materiel ET ses flight cases.
|
||||
- La sous-categorie du materiel est tres importante. Prends-la en compte en priorite.
|
||||
- Sois libre d'ajouter du materiel pertinent par rapport aux options ou a la description de l'evenement si cela te semble justifie. Explique tes choix dans rationale.
|
||||
|
||||
Regles sur les flight cases (PRIORITAIRES) :
|
||||
- Apres avoir identifie les equipements necessaires, appelle TOUJOURS search_containers avec la liste de leurs IDs.
|
||||
@@ -811,7 +832,44 @@ async function toolSearchEventReference(query, dateHint) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un tool Gemini et retourne le résultat.
|
||||
* Recherche des équipements par catégorie et sous-catégorie.
|
||||
*/
|
||||
async function toolListEquipmentByCategory(category, subCategory) {
|
||||
let queryDb = getDb().collection('equipments');
|
||||
|
||||
if (category) {
|
||||
queryDb = queryDb.where('category', '==', category);
|
||||
}
|
||||
|
||||
if (subCategory) {
|
||||
queryDb = queryDb.where('subCategory', '==', subCategory);
|
||||
}
|
||||
|
||||
const snapshot = await queryDb.limit(50).get();
|
||||
|
||||
const results = snapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
name: data.name || doc.id,
|
||||
category: data.category || '',
|
||||
subCategory: data.subCategory || '',
|
||||
brand: data.brand || null,
|
||||
model: data.model || null,
|
||||
status: data.status || '',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
category: category || 'all',
|
||||
subCategory: subCategory || 'all',
|
||||
count: results.length,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un tool Gemini et retourne le résultat.
|
||||
*/
|
||||
async function executeTool(toolCall, excludeEventId, sharedContext) {
|
||||
const { name, args } = toolCall;
|
||||
@@ -849,6 +907,9 @@ async function executeTool(toolCall, excludeEventId, sharedContext) {
|
||||
case 'search_containers':
|
||||
return await toolSearchContainers(args.equipmentIds, args.query || null);
|
||||
|
||||
case 'list_equipment_by_category':
|
||||
return await toolListEquipmentByCategory(args.category || null, args.subCategory || null);
|
||||
|
||||
default:
|
||||
return { error: `Tool inconnu: ${name}` };
|
||||
}
|
||||
@@ -867,9 +928,13 @@ async function executeTool(toolCall, excludeEventId, sharedContext) {
|
||||
*/
|
||||
function buildUserPrompt({
|
||||
userMessage,
|
||||
eventName,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
endDate,
|
||||
location,
|
||||
notes,
|
||||
eventOptions,
|
||||
currentEquipment,
|
||||
workingProposal,
|
||||
}) {
|
||||
@@ -882,14 +947,18 @@ function buildUserPrompt({
|
||||
|
||||
const isAutoMode = !userMessage || userMessage.trim().length === 0;
|
||||
const finalMessage = isAutoMode
|
||||
? 'Génère automatiquement une proposition de matériel adaptée à cet événement, basée sur les événements similaires passés.'
|
||||
? 'Génère automatiquement une proposition de matériel adaptée à cet événement, en tenant compte des options et des détails fournis, et basée sur les événements similaires passés.'
|
||||
: userMessage.trim();
|
||||
|
||||
return [
|
||||
'Contexte de l\'événement :',
|
||||
`- Nom : ${eventName || 'non renseigné'}`,
|
||||
`- Type d'événement (ID): ${eventTypeId || 'non renseigné'}`,
|
||||
`- Date de début : ${startDate}`,
|
||||
`- Date de fin : ${endDate}`,
|
||||
`- Lieu : ${location || 'non renseigné'}`,
|
||||
`- Notes/Description : ${notes || 'aucune'}`,
|
||||
`- Options de l'événement : ${eventOptions || 'aucune'}`,
|
||||
`- Matériel déjà assigné : ${currentEquipmentStr}`,
|
||||
`- Proposition courante à modifier : ${workingProposalStr}`,
|
||||
'',
|
||||
@@ -981,9 +1050,13 @@ function parseAiResponse(rawText) {
|
||||
*/
|
||||
async function handleAiEquipmentProposal(req, res) {
|
||||
const {
|
||||
eventName,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
endDate,
|
||||
location,
|
||||
notes,
|
||||
eventOptions,
|
||||
userMessage,
|
||||
history = [],
|
||||
currentEquipment = [],
|
||||
@@ -1002,7 +1075,7 @@ async function handleAiEquipmentProposal(req, res) {
|
||||
systemInstruction: SYSTEM_PROMPT,
|
||||
tools: AI_TOOLS,
|
||||
toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
|
||||
generationConfig: { temperature: 0.2 },
|
||||
generationConfig: { temperature: 0.2, responseMimeType: 'application/json' },
|
||||
});
|
||||
|
||||
// Reconstruire l'historique de conversation
|
||||
@@ -1021,9 +1094,13 @@ async function handleAiEquipmentProposal(req, res) {
|
||||
|
||||
const userPrompt = buildUserPrompt({
|
||||
userMessage,
|
||||
eventName,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
endDate,
|
||||
location,
|
||||
notes,
|
||||
eventOptions,
|
||||
currentEquipment,
|
||||
workingProposal,
|
||||
});
|
||||
@@ -1151,9 +1228,10 @@ async function handleAiEquipmentProposal(req, res) {
|
||||
} catch (error) {
|
||||
logger.error('[AI] Conversation timeout/error', {
|
||||
message: error?.message || 'unknown',
|
||||
stack: error?.stack
|
||||
});
|
||||
res.status(200).json({
|
||||
assistantMessage: 'La generation IA a rencontre une erreur technique. Reessaie dans quelques secondes.',
|
||||
assistantMessage: 'Je suis désolé, je n\'ai pas réussi à traiter votre demande en raison d\'un temps trop long ou d\'une erreur technique. Veuillez réessayer avec des requêtes plus spécifiques.',
|
||||
proposal: null,
|
||||
});
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user