File size: 16,189 Bytes
3866cc2
e2e99c1
b8367c7
84b5e3f
3866cc2
 
 
 
 
9130085
 
 
3866cc2
e2e99c1
3866cc2
84b5e3f
e2e99c1
 
 
3866cc2
 
 
4c6c469
 
 
 
3866cc2
 
 
 
 
 
 
 
a49a0e2
 
3866cc2
 
a49a0e2
99b85a0
 
c91e0f2
 
db7649c
d422dea
 
 
 
 
 
 
 
 
 
9f88566
 
 
 
 
 
 
 
 
 
3866cc2
 
 
4c6c469
3866cc2
e6037bb
 
 
 
 
 
528b765
 
 
e6037bb
 
 
99b85a0
e6037bb
 
 
 
 
 
 
528b765
 
 
 
e6037bb
 
 
 
 
 
 
 
3866cc2
 
 
4c6c469
91ad91f
c91e0f2
 
99b85a0
c91e0f2
 
db7649c
d422dea
 
 
c91e0f2
91ad91f
d422dea
 
3866cc2
e6037bb
 
 
 
 
3866cc2
 
1fdfc51
 
 
3866cc2
 
a19ba3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db7649c
 
 
4eded8c
db7649c
 
4eded8c
db7649c
 
91ad91f
db7649c
 
 
 
ccc0517
 
db7649c
91ad91f
4eded8c
 
 
 
 
db7649c
1fdfc51
4eded8c
db7649c
 
4eded8c
ae35c80
d422dea
 
ae35c80
 
 
 
 
 
d422dea
 
ae35c80
 
 
 
e2e99c1
d422dea
 
c0109e1
e2e99c1
b4314c0
c0109e1
e2e99c1
b4314c0
 
 
 
 
 
 
 
9130085
e2e99c1
b4314c0
e2e99c1
b4314c0
 
 
 
 
 
 
e2e99c1
99b85a0
 
ccc0517
 
 
 
 
 
b4314c0
ccc0517
e2e99c1
 
 
 
db7649c
d422dea
 
b4314c0
db7649c
b4314c0
 
 
 
 
 
 
db7649c
46f5a9a
 
db7649c
 
 
e2e99c1
 
 
 
b4314c0
 
 
 
 
4eded8c
 
 
 
 
 
 
 
 
 
e2e99c1
4eded8c
 
 
 
 
 
 
 
 
 
 
e2e99c1
 
 
4eded8c
 
 
 
 
 
 
 
 
 
 
 
 
 
e2e99c1
 
b4314c0
e2e99c1
b4314c0
 
 
 
 
e2e99c1
 
 
 
d422dea
 
 
ccc0517
 
 
 
 
 
 
 
 
 
d422dea
 
 
ccc0517
d422dea
ccc0517
 
 
 
 
 
 
d422dea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f88566
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a19ba3d
9f88566
 
 
 
a19ba3d
 
 
 
9f88566
 
 
 
 
 
3866cc2
 
7e6f170
 
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
import os
import json
# pyrefly: ignore [missing-import]
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
import sys
from pathlib import Path
BASE_DIR = Path(__file__).parent.absolute()
env_path = BASE_DIR / ".env"
load_dotenv(dotenv_path=env_path)

# Import du moteur Jarvis
try:
    from jarvis_engine import JarvisEngine, memory_manager
except ImportError as e:
    print(f"Erreur critique : Impossible d'importer jarvis_engine.py ({e})")
    sys.exit(1)

app = FastAPI()

# Instance globale du moteur Jarvis
# On le crée une fois au démarrage pour garder l'historique (ou on peut le recréer par session)
jarvis = JarvisEngine()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

from typing import Optional

class ChatRequest(BaseModel):
    prompt: str
    google_token: Optional[str] = None
    user_id: str = "default_user"
    user_name: Optional[str] = "Utilisateur"
    lat: Optional[float] = None
    lng: Optional[float] = None
    thread_id: Optional[str] = "main"
    mode: Optional[str] = None
    image_base64: Optional[str] = None


class ModeRequest(BaseModel):
    name: str
    instruction: str
    icon: Optional[str] = "💎"
    color: Optional[str] = "#4285F4"

class TaskRequest(BaseModel):
    name: str
    urgency: int = 5
    importance: int = 5
    duration: int = 5
    envy: int = 5
    energy: int = 5
    status: Optional[str] = "pending"
    score: Optional[float] = 0.0


@app.get("/")
async def root():
    return {"status": "Online", "message": "Jarvis Native Server is running!"}

from fastapi.responses import StreamingResponse

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    async def event_generator():
        try:
            # Envoi du sentiment détecté dès le début
            yield f"data: {json.dumps({'sentiment': jarvis.last_sentiment})}\n\n"

            async for chunk in jarvis.process_query_stream(
                prompt=request.prompt,
                google_token=request.google_token,
                user_id=request.user_id,
                user_name=request.user_name,
                lat=request.lat,
                lng=request.lng,
                thread_id=request.thread_id,
                mode=request.mode,
                image_base64=request.image_base64
            ):
                if isinstance(chunk, dict) and "tool_use" in chunk:
                    yield f"data: {json.dumps({'tool_use': chunk['tool_use']})}\n\n"
                elif isinstance(chunk, str):
                    yield f"data: {json.dumps({'chunk': chunk})}\n\n"
            
            # Envoi final avec métadonnées (image_result, sentiment)
            yield f"data: {json.dumps({'done': True, 'image_result': jarvis.last_image_result, 'sentiment': jarvis.last_sentiment})}\n\n"
        except Exception as e:
            yield f"data: {json.dumps({'error': str(e)})}\n\n"

    return StreamingResponse(event_generator(), media_type="text/event-stream")

@app.post("/chat")
async def chat(request: ChatRequest):
    try:
        # On passe le token reçu de l'application Android au moteur Jarvis
        response_text = await jarvis.process_query(
            request.prompt, 
            google_token=request.google_token, 
            user_id=request.user_id,
            user_name=request.user_name,
            lat=request.lat,
            lng=request.lng,
            thread_id=request.thread_id or "main",
            mode=request.mode,
            image_base64=request.image_base64
        )



        
        return {
            "response": response_text, 
            "image_result": jarvis.last_image_result,
            "sentiment": jarvis.last_sentiment
        }
    
    except Exception as e:
        import traceback
        error_trace = traceback.format_exc()
        print(f"!!! ERREUR CRITIQUE API JARVIS (/chat) !!!\n{error_trace}")
        raise HTTPException(status_code=500, detail=str(e))

        raise HTTPException(status_code=500, detail=str(e))

@app.post("/chat/{user_id}/delete")
async def delete_chat_message(user_id: str, request: Request):
    try:
        data = await request.json()
        thread_id = data.get("thread_id", "main")
        content = data.get("content")
        from jarvis_engine import memory_manager
        memory_manager.delete_message(user_id, thread_id, content)
        return {"status": "deleted"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/chat/{user_id}/clear")
async def clear_chat_thread(user_id: str, request: Request):
    try:
        data = await request.json()
        thread_id = data.get("thread_id", "main")
        from jarvis_engine import memory_manager
        memory_manager.clear_thread(user_id, thread_id)
        return {"status": "cleared"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/anticipate")
async def anticipate(request: ChatRequest):
    try:
        # 1. Analyse cognitive proactive (Jarvis réfléchit)
        prompt = (
            "Fais un auto-examen de ma situation actuelle (calendrier, mails, trafic). "
            "Si tu détectes un besoin d'action ou une info capitale, utilise 'schedule_smart_reminder' pour plus tard ou 'send_notification' pour tout de suite. "
            "Si tout est sous contrôle, réponds simplement 'RAS'."
        )
        await jarvis.process_query(
            prompt, 
            google_token=request.google_token, 
            user_name=request.user_name,
            lat=request.lat,
            lng=request.lng,
            save_to_history=False
        )

        # 2. Récupération des notifications à envoyer maintenant (programmées précédemment)
        from jarvis_engine import memory_manager
        pending = memory_manager.get_pending_notifications(request.user_name.lower() if request.user_name else "antoine")

        return {"status": "Analysis complete", "notifications": pending}
    except Exception as e:
        import traceback
        print(f"!!! ERREUR CRITIQUE API JARVIS (/anticipate) !!!\n{traceback.format_exc()}")
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/notifications")
async def get_notifications(user_id: str):

    # On importe context ici pour accéder aux notifs stockées dans jarvis_engine
    from jarvis_engine import context
    notifs = context.pending_notifications.copy()
    return {"notifications": notifs}

@app.post("/notifications/clear")
async def clear_notifications(user_id: str):

    from jarvis_engine import context
    context.pending_notifications.clear()
    return {"status": "Cleared"}

@app.get("/history/{thread_id}")
async def get_thread_history(thread_id: str, user_id: str):

    """Récupère l'historique complet d'un thread spécifique pour un utilisateur donné."""
    try:
        from jarvis_engine import memory_manager
        safe_user_id = user_id.lower().replace(" ", "_")
        
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute(
                    "SELECT role, content FROM conversation_history WHERE user_id = %s AND thread_id = %s ORDER BY timestamp ASC",
                    (safe_user_id, thread_id)
                )
                rows = cursor.fetchall()
                
        import re
        formatted_history = []
        for role, content_json in rows:
            text = ""
            try:
                parts = json.loads(content_json)
                for p in parts:
                    if isinstance(p, dict) and "text" in p:
                        text += p["text"]
            except Exception:
                pass
            
            # Nettoyage de tout le contexte injecté au début du message (incluant émotion et lieu)
            text = re.sub(r'^\[CONTEXTE :.*?\n\n', '', text, flags=re.DOTALL)
            if "Génère un briefing matinal" in text or "Analyse mes notifications" in text:
                continue
                
            if text.strip():
                formatted_history.append({
                    "text": text.strip(),
                    "isUser": role == "user"
                })
        return {"history": formatted_history}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/threads")
async def list_threads(user_id: str):

    """Liste tous les threads de discussion disponibles depuis Supabase."""
    try:
        from jarvis_engine import memory_manager
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute("SELECT DISTINCT thread_id FROM conversation_history")
                rows = cursor.fetchall()
                
        threads = {row[0] for row in rows}
        threads.add("main")
        sorted_threads = sorted(list(threads), key=lambda x: (x != "main", x))
        return {"threads": sorted_threads}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/bridge")
async def get_bridge_notes():
    """Récupère les notes laissées par Jarvis pour le développeur."""
    try:
        from jarvis_engine import memory_manager
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute("SELECT id, title, message, category, timestamp FROM bridge_notes ORDER BY timestamp DESC")
                rows = cursor.fetchall()
        
        notes = []
        for r in rows:
            notes.append({
                "id": r[0],
                "title": r[1],
                "message": r[2],
                "category": r[3],
                "timestamp": str(r[4])
            })
        return {"notes": notes, "count": len(notes)}
    except Exception as e:
        import traceback
        print(f"Erreur API /bridge : {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/preferences/{user_id}")
async def get_preferences(user_id: str):
    try:
        from jarvis_engine import memory_manager
        prefs = memory_manager.get_user_preferences(user_id)
        return {"preferences": prefs}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

class PreferenceRequest(BaseModel):
    preference_key: str
    preference_value: str

@app.post("/preferences/{user_id}")
async def set_preference(user_id: str, request: PreferenceRequest):
    try:
        from jarvis_engine import memory_manager
        memory_manager.set_user_preference(user_id, request.preference_key, request.preference_value)
        return {"status": "success"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/bridge/clear")
async def clear_bridge_notes():
    """Marque toutes les notes comme lues et vide la table bridge."""
    try:
        from jarvis_engine import memory_manager
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute("DELETE FROM bridge_notes")
            conn.commit()
        return {"status": "cleared"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/memory/{user_id}")
async def get_memory(user_id: str):

    try:
        facts = memory_manager.get_all_facts(user_id)
        return {"facts": facts}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/memory/delete")
async def delete_memory_fact(request: Request):
    try:
        data = await request.json()
        user_id = data.get("user_id")
        if not user_id:
            raise HTTPException(status_code=400, detail="user_id is required")
        fact = data.get("fact")

        if not fact:
            raise HTTPException(status_code=400, detail="Fact text is required")
        memory_manager.delete_fact(user_id, fact)
        return {"status": "deleted"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/modes/{user_id}")
async def get_modes(user_id: str):
    try:
        modes = memory_manager.get_user_modes(user_id)
        return {"modes": modes}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/modes/{user_id}")
async def set_mode(user_id: str, request: ModeRequest):
    try:
        memory_manager.set_user_mode(user_id, request.name, request.instruction, request.icon, request.color)
        return {"status": "success"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/modes/{user_id}/delete")
async def delete_mode(user_id: str, request: Request):
    try:
        data = await request.json()
        mode_name = data.get("name")
        memory_manager.delete_user_mode(user_id, mode_name)
        return {"status": "deleted"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/tasks/{user_id}")
async def get_tasks(user_id: str):
    try:
        from jarvis_engine import memory_manager
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute(
                    "SELECT name, score, urgency, importance, duration, envy, energy, status, id FROM user_tasks WHERE user_id = %s ORDER BY score DESC",
                    (user_id.lower(),)
                )
                rows = cursor.fetchall()
        
        tasks = []
        for r in rows:
            tasks.append({
                "name": r[0],
                "score": r[1] * 100, # Conversion en % pour l'app
                "urgency": r[2],
                "importance": r[3],
                "duration": r[4],
                "envy": r[5],
                "energy": r[6],
                "status": r[7],
                "id": r[8]
            })
        return {"tasks": tasks}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/tasks/{user_id}")
async def add_task(user_id: str, request: TaskRequest):
    try:
        from jarvis_engine import memory_manager
        # Si le score n'est pas fourni, on pourrait appeler le MLP prioritizer ici (à faire plus tard)
        # Pour l'instant on stocke ce qui vient de l'app ou de Jarvis
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                cursor.execute(
                    "INSERT INTO user_tasks (user_id, name, score, urgency, importance, duration, envy, energy, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
                    (user_id.lower(), request.name, request.score / 100.0, request.urgency, request.importance, request.duration, request.envy, request.energy, request.status)
                )
            conn.commit()
        return {"status": "success"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/tasks/{user_id}/delete")
async def delete_task(user_id: str, request: Request):
    try:
        data = await request.json()
        task_id = data.get("id")
        task_name = data.get("name")
        from jarvis_engine import memory_manager
        with memory_manager.get_conn() as conn:
            with conn.cursor() as cursor:
                if task_id:
                    cursor.execute("DELETE FROM user_tasks WHERE user_id = %s AND id = %s", (user_id.lower(), task_id))
                else:
                    cursor.execute("DELETE FROM user_tasks WHERE user_id = %s AND name = %s", (user_id.lower(), task_name))
            conn.commit()
        return {"status": "deleted"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


if __name__ == "__main__":
    import uvicorn
    # On augmente le timeout à 120 secondes pour laisser le temps aux agents de réfléchir
    uvicorn.run(app, host="0.0.0.0", port=7860, timeout_keep_alive=120)