File size: 19,604 Bytes
886c92a
da7ef42
886c92a
4533086
 
886c92a
c253059
886c92a
b4c693d
dfc948e
19c5a64
 
 
 
 
 
 
 
 
 
 
 
c23cd74
da7ef42
886c92a
b4c693d
dfc948e
19c5a64
 
 
 
db31ff7
 
 
 
 
 
19c5a64
db31ff7
 
19c5a64
db31ff7
 
886c92a
b4c693d
db31ff7
ac5ae77
fd036de
19c5a64
 
fd036de
 
 
 
19c5a64
fd036de
19c5a64
fd036de
4533086
886c92a
 
 
 
 
 
 
5ad49c1
c23cd74
886c92a
 
 
fd036de
886c92a
5ad49c1
dfc948e
886c92a
dfc948e
 
5853bf1
dfc948e
 
 
 
 
 
 
 
 
 
5853bf1
dfc948e
 
 
 
5853bf1
 
 
 
dfc948e
6d14961
5ad49c1
 
886c92a
5ad49c1
5fd4540
dfc948e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
886c92a
 
 
fd036de
 
 
886c92a
 
 
 
fd036de
19c5a64
fd036de
dfc948e
 
fd036de
886c92a
 
7319f0c
886c92a
0a19f08
fd036de
 
 
 
 
 
886c92a
 
 
 
 
 
5ad49c1
 
886c92a
 
 
 
 
 
fd036de
 
 
 
19c5a64
886c92a
 
 
 
fd036de
886c92a
 
fd036de
fd7ef5f
886c92a
fd036de
19c5a64
bd93f15
 
 
 
 
 
938aa20
 
 
fd7ef5f
19c5a64
fd036de
dfc948e
 
 
fd036de
886c92a
 
fd036de
886c92a
19c5a64
fd036de
9f6e9e1
 
 
 
 
 
 
886c92a
 
 
5ad49c1
886c92a
5ad49c1
 
 
886c92a
89db25a
5ad49c1
 
886c92a
89db25a
886c92a
 
 
19c5a64
89db25a
886c92a
 
89db25a
886c92a
 
 
 
 
89db25a
984f26b
5ad49c1
 
886c92a
 
 
fd036de
 
 
886c92a
 
89db25a
886c92a
 
89db25a
19c5a64
fd036de
89db25a
 
 
 
 
 
 
 
 
 
fd036de
886c92a
 
 
7319f0c
0a19f08
fd036de
 
 
 
 
 
886c92a
 
 
 
 
 
5ad49c1
 
886c92a
 
 
89db25a
886c92a
89db25a
 
886c92a
 
 
 
 
 
 
 
 
 
fd036de
 
 
 
19c5a64
886c92a
 
 
 
fd036de
dfc948e
fd7ef5f
fd036de
fd7ef5f
886c92a
fd036de
19c5a64
886c92a
 
 
db31ff7
fd7ef5f
938aa20
 
 
fd7ef5f
19c5a64
fd036de
dfc948e
 
 
fd036de
 
886c92a
fd036de
886c92a
19c5a64
fd036de
9f6e9e1
 
 
 
 
 
 
886c92a
 
 
10ad7a5
886c92a
 
5ad49c1
886c92a
 
5ad49c1
886c92a
 
19c5a64
dfc948e
 
 
 
19c5a64
 
dfc948e
 
 
 
 
 
 
 
 
 
 
19c5a64
dfc948e
 
19c5a64
 
5ad49c1
dfc948e
 
 
 
 
 
 
 
 
 
 
19c5a64
 
dfc948e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd036de
dfc948e
 
 
 
 
fd036de
dfc948e
fd036de
dfc948e
fd036de
dfc948e
 
 
fd036de
dfc948e
 
 
 
 
 
 
 
fd036de
dfc948e
 
 
 
 
 
 
5ad49c1
 
ccfefd3
fd036de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c253059
19c5a64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
from flask import Flask, request, render_template, jsonify, Response
import json
import os
from google import genai
from google.genai import types
import base64
from werkzeug.utils import secure_filename
import mimetypes
from dotenv import load_dotenv
from datetime import datetime
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(),
        logging.FileHandler('app.log', mode='a', encoding='utf-8')
    ]
)
logger = logging.getLogger(__name__)

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size
load_dotenv()

@app.before_request
def log_request_info():
    logger.info(f'Request: {request.method} {request.url} from {request.remote_addr}')

def load_system_instruction():
    """Charge les instructions système depuis le fichier Markdown"""
    try:
        with open('instructions/system_instruction.md', 'r', encoding='utf-8') as f:
            return f.read().strip()
    except FileNotFoundError:
        logger.error("Fichier d'instructions système non trouvé.")
        return "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir."
    except Exception as e:
        logger.exception("Erreur lors du chargement des instructions système")
        return "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir."

# Configuration du client Gemini
API_KEY = os.getenv("GOOGLE_API_KEY")
SYSTEM_INSTRUCTION = load_system_instruction()

if not API_KEY:
    logger.warning("GOOGLE_API_KEY non définie dans les variables d'environnement")
    logger.warning("L'application démarrera mais les fonctionnalités de chat seront limitées")
    client = None
else:
    try:
        client = genai.Client(api_key=API_KEY)
        logger.info("Client Gemini initialisé avec succès")
    except Exception as e:
        logger.exception("Erreur lors de l'initialisation du client Gemini")
        client = None

# Configuration par défaut
MODEL = "gemini-2.5-flash"
DEFAULT_CONFIG = {
    "temperature": 0.7,
    "max_output_tokens": 8192,
    "top_p": 0.9,
    "top_k": 40
}

# Outils activés par défaut
DEFAULT_TOOLS = [
    types.Tool(code_execution=types.ToolCodeExecution()),
    types.Tool(google_search=types.GoogleSearch())
]

# Stockage des conversations avec métadonnées (en production, utilisez une base de données)
conversations = {}
conversation_metadata = {}

def add_message_to_history(conversation_id, role, content, has_file=False, file_data=None):
    """Ajoute un message à l'historique de la conversation"""
    if conversation_id not in conversation_metadata:
        conversation_metadata[conversation_id] = {
            'id': conversation_id,
            'created_at': datetime.now().isoformat(),
            'last_activity': datetime.now().isoformat(),
            'messages': [],
            'status': 'active'
        }
    
    message_data = {
        'role': role,
        'content': content,
        'timestamp': datetime.now().isoformat(),
        'hasFile': has_file
    }
    if file_data:
        message_data['fileData'] = file_data
    conversation_metadata[conversation_id]['messages'].append(message_data)
    conversation_metadata[conversation_id]['last_activity'] = datetime.now().isoformat()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/admin1')
def admin():
    """Page d'administration"""
    return render_template('admin.html')

@app.route('/admin/conversations')
def get_conversations():
    """API pour récupérer les conversations pour l'admin"""
    try:
        # Calculer les statistiques
        total_conversations = len(conversation_metadata)
        total_messages = sum(len(conv['messages']) for conv in conversation_metadata.values())
        active_conversations = sum(1 for conv in conversation_metadata.values() if conv.get('status') == 'active')
        conversations_with_files = sum(1 for conv in conversation_metadata.values() 
                                     if any(msg.get('hasFile') for msg in conv['messages']))
        
        # Préparer les données des conversations
        conversations_data = []
        for conv_id, conv_data in conversation_metadata.items():
            conversations_data.append({
                'id': conv_id,
                'createdAt': conv_data.get('created_at'),
                'lastActivity': conv_data.get('last_activity'),
                'status': conv_data.get('status', 'active'),
                'messages': conv_data.get('messages', [])
            })
        
        # Trier par dernière activité (plus récent en premier)
        conversations_data.sort(key=lambda x: x.get('lastActivity', ''), reverse=True)
        
        return jsonify({
            'conversations': conversations_data,
            'stats': {
                'total': total_conversations,
                'totalMessages': total_messages,
                'active': active_conversations,
                'withFiles': conversations_with_files
            }
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/chat', methods=['POST'])
def chat():
    try:
        if not client:
            return jsonify({'error': 'Client Gemini non initialisé. Vérifiez GOOGLE_API_KEY.'}), 500

        data = request.get_json()
        message = data.get('message', '')
        thinking_enabled = data.get('thinking_enabled', True)
        conversation_id = data.get('conversation_id', 'default')

        logger.info(f"Requête chat reçue: message='{message[:50]}...', conversation_id={conversation_id}")

        # Ajouter le message de l'utilisateur à l'historique
        add_message_to_history(conversation_id, 'user', message)

        # Configuration du thinking
        config_dict = DEFAULT_CONFIG.copy()
        config_dict["system_instruction"] = SYSTEM_INSTRUCTION
        config_dict["tools"] = DEFAULT_TOOLS

        # Activer thinking si demandé
        if thinking_enabled:
            config_dict["thinking_config"] = types.ThinkingConfig(
                thinking_budget=-1,  # Dynamic thinking
                include_thoughts=True
            )
        generation_config = types.GenerateContentConfig(**config_dict)
        
        # Gestion de la conversation
        if conversation_id not in conversations:
            conversations[conversation_id] = client.chats.create(
                model=MODEL,
                config=generation_config
            )
        
        chat = conversations[conversation_id]
        
        # Génération de la réponse avec streaming
        def generate():
            try:
                if not client:
                    yield f"data: {json.dumps({'type': 'error', 'content': 'API Gemini non configurée. Définissez GOOGLE_API_KEY.'})}\n\n"
                    return

                logger.info(f"Démarrage du streaming pour conversation {conversation_id}")
                response_stream = chat.send_message_stream(
                    message,
                    config=generation_config
                )

                full_response = ""
                thoughts = ""
                chunk_count = 0

                for chunk in response_stream:
                    chunk_count += 1
                    logger.debug(f"Chunk {chunk_count} reçu")
                    if chunk.candidates and chunk.candidates[0].content:
                        for part in chunk.candidates[0].content.parts:
                            if part.text:
                                if part.thought and thinking_enabled:
                                    thoughts += part.text
                                    yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n"
                                else:
                                    full_response += part.text
                                    yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n"

                logger.info(f"Streaming terminé, réponse complète: {len(full_response)} caractères")

                # Ajouter la réponse de l'assistant à l'historique
                if full_response:
                    add_message_to_history(conversation_id, 'assistant', full_response)

                # Signal de fin
                yield f"data: {json.dumps({'type': 'end'})}\n\n"

            except Exception as e:
                logger.exception("Erreur lors du streaming")
                yield f"data: {json.dumps({'type': 'error', 'content': f'Erreur API: {str(e)}'})}\n\n"

        return Response(generate(), mimetype='text/event-stream', headers={
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Content-Type',
        })
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/upload', methods=['POST'])
def upload_file():
    try:
        if 'file' not in request.files:
            return jsonify({'error': 'No file uploaded'}), 400

        file = request.files['file']
        if file.filename == '':
            return jsonify({'error': 'No file selected'}), 400

        # Lire le fichier
        file_bytes = file.read()
        mime_type = file.content_type or mimetypes.guess_type(file.filename)[0]
        logger.info(f"Fichier uploadé: {file.filename}, taille: {len(file_bytes)} bytes, type: {mime_type}")

        # Encoder en base64 pour le stockage temporaire
        file_b64 = base64.b64encode(file_bytes).decode()

        return jsonify({
            'success': True,
            'mime_type': mime_type,
            'data': file_b64
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/chat_with_file', methods=['POST'])
def chat_with_file():
    try:
        if not client:
            return jsonify({'error': 'Client Gemini non initialisé. Vérifiez GOOGLE_API_KEY.'}), 500

        data = request.get_json()
        message = data.get('message', '')
        file_data_list = data.get('file_data', [])
        thinking_enabled = data.get('thinking_enabled', True)
        conversation_id = data.get('conversation_id', 'default')

        logger.info(f"Requête chat_with_file reçue: message='{message[:50]}...', fichiers={len(file_data_list)}, conversation_id={conversation_id}")

        # Ensure file_data_list is a list
        if not isinstance(file_data_list, list):
            file_data_list = [file_data_list]

        # Ajouter le message de l'utilisateur à l'historique (avec indication de fichiers)
        display_message = message if message else 'Analyse ces fichiers'
        if file_data_list:
            file_count = len(file_data_list)
            display_message += f" [{file_count} fichier{'s' if file_count > 1 else ''}]"
        add_message_to_history(conversation_id, 'user', display_message, has_file=len(file_data_list) > 0, file_data=file_data_list)

        # Configuration du thinking
        config_dict = DEFAULT_CONFIG.copy()
        config_dict["tools"] = DEFAULT_TOOLS
        config_dict["system_instruction"] = SYSTEM_INSTRUCTION

        # Activer thinking si demandé
        if thinking_enabled:
            config_dict["thinking_config"] = types.ThinkingConfig(
                thinking_budget=-1,
                include_thoughts=True
            )
        generation_config = types.GenerateContentConfig(**config_dict)
        
        # Gestion de la conversation
        if conversation_id not in conversations:
            conversations[conversation_id] = client.chats.create(
                model=MODEL,
                config=generation_config
            )
        
        chat = conversations[conversation_id]
        
        # Préparation du contenu avec fichiers
        contents = [message]

        for file_data in file_data_list:
            file_bytes = base64.b64decode(file_data['data'])
            file_part = types.Part.from_bytes(
                data=file_bytes,
                mime_type=file_data['mime_type']
            )
            contents.append(file_part)
        
        # Génération de la réponse avec streaming
        def generate():
            try:
                if not client:
                    yield f"data: {json.dumps({'type': 'error', 'content': 'API Gemini non configurée. Définissez GOOGLE_API_KEY.'})}\n\n"
                    return

                logger.info(f"Démarrage du streaming avec fichiers pour conversation {conversation_id}")
                response_stream = chat.send_message_stream(
                    contents,
                    config=generation_config
                )

                full_response = ""
                thoughts = ""
                chunk_count = 0

                for chunk in response_stream:
                    chunk_count += 1
                    logger.debug(f"Chunk {chunk_count} reçu (avec fichiers)")
                    for part in chunk.candidates[0].content.parts:
                        if part.text:
                            if part.thought and thinking_enabled:
                                thoughts += part.text
                                yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n"
                            else:
                                full_response += part.text
                                yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n"

                logger.info(f"Streaming avec fichiers terminé, réponse complète: {len(full_response)} caractères")

                # Ajouter la réponse de l'assistant à l'historique
                if full_response:
                    add_message_to_history(conversation_id, 'assistant', full_response)

                # Signal de fin
                yield f"data: {json.dumps({'type': 'end'})}\n\n"

            except Exception as e:
                logger.exception("Erreur lors du streaming avec fichiers")
                yield f"data: {json.dumps({'type': 'error', 'content': f'Erreur API avec fichiers: {str(e)}'})}\n\n"

        return Response(generate(), mimetype='text/event-stream', headers={
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Content-Type',
        })
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/reset_conversation', methods=['POST'])
def reset_conversation():
    try:
        data = request.get_json()
        conversation_id = data.get('conversation_id', 'default')
        
        if conversation_id in conversations:
            del conversations[conversation_id]

        # Marquer la conversation comme terminée dans les métadonnées
        if conversation_id in conversation_metadata:
            conversation_metadata[conversation_id]['status'] = 'reset'
            conversation_metadata[conversation_id]['last_activity'] = datetime.now().isoformat()

        logger.info(f"Conversation {conversation_id} réinitialisée")
        return jsonify({'success': True})
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/admin/conversations/<conversation_id>', methods=['DELETE'])
def delete_conversation(conversation_id):
    """Supprimer une conversation (pour l'admin)"""
    try:
        if conversation_id in conversations:
            del conversations[conversation_id]

        if conversation_id in conversation_metadata:
            del conversation_metadata[conversation_id]

        logger.info(f"Conversation {conversation_id} supprimée")
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/admin/conversations/<conversation_id>/export')
def export_conversation(conversation_id):
    """Exporter une conversation en JSON"""
    try:
        if conversation_id not in conversation_metadata:
            return jsonify({'error': 'Conversation non trouvée'}), 404
        
        conversation_data = conversation_metadata[conversation_id]

        logger.info(f"Conversation {conversation_id} exportée")
        return jsonify({
            'conversation_id': conversation_id,
            'export_date': datetime.now().isoformat(),
            'data': conversation_data
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/admin/stats')
def get_admin_stats():
    """Statistiques détaillées pour l'admin"""
    try:
        # Statistiques générales
        total_conversations = len(conversation_metadata)
        total_messages = sum(len(conv['messages']) for conv in conversation_metadata.values())

        # Statistiques par statut
        status_stats = {}
        for conv in conversation_metadata.values():
            status = conv.get('status', 'active')
            status_stats[status] = status_stats.get(status, 0) + 1

        # Conversations avec fichiers
        conversations_with_files = sum(1 for conv in conversation_metadata.values()
                                     if any(msg.get('hasFile') for msg in conv['messages']))

        # Activité par jour (derniers 7 jours)
        from collections import defaultdict
        daily_activity = defaultdict(int)

        for conv in conversation_metadata.values():
            for message in conv['messages']:
                if message.get('timestamp'):
                    try:
                        date = datetime.fromisoformat(message['timestamp']).date()
                        daily_activity[date.isoformat()] += 1
                    except:
                        continue

        return jsonify({
            'total_conversations': total_conversations,
            'total_messages': total_messages,
            'status_distribution': status_stats,
            'conversations_with_files': conversations_with_files,
            'daily_activity': dict(daily_activity)
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/debug/api_test')
def debug_api_test():
    """Endpoint de debug pour tester la connectivité API"""
    try:
        if not client:
            return jsonify({
                'status': 'error',
                'message': 'Client Gemini non initialisé',
                'api_key_set': bool(API_KEY)
            })

        # Test simple de l'API
        response = client.models.generate_content(
            model=MODEL,
            contents="Hello",
            config=types.GenerateContentConfig(
                max_output_tokens=10,
                system_instruction="Réponds brièvement."
            )
        )

        return jsonify({
            'status': 'success',
            'message': 'API Gemini fonctionnelle',
            'model': MODEL,
            'response_length': len(response.text) if response.text else 0,
            'sample_response': response.text[:100] if response.text else None
        })

    except Exception as e:
        return jsonify({
            'status': 'error',
            'message': f'Erreur API: {str(e)}',
            'api_key_set': bool(API_KEY)
        })

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=7860)