CocoonAI commited on
Commit
84ed4c5
·
1 Parent(s): f5d0e0b
Files changed (2) hide show
  1. app.py +319 -179
  2. requirements.txt +14 -29
app.py CHANGED
@@ -1,97 +1,185 @@
1
- # app.py - Version corrigée et simplifiée pour commencer
2
 
3
- # === IMPORTS ===
4
  import os
5
  import json
6
  import tempfile
7
  from datetime import datetime
8
  from typing import List, Dict, Optional
9
 
10
- # === CORRECTION : Import correct de dotenv ===
11
- from dotenv import load_dotenv
12
- load_dotenv()
13
-
14
- # === Imports FastAPI ===
15
- from fastapi import FastAPI, UploadFile, File, Form
 
 
 
 
 
16
  from fastapi.responses import JSONResponse
 
17
  from pydantic import BaseModel
18
 
19
- # === Imports pour l'IA (avec gestion d'erreurs) ===
 
 
 
 
20
  try:
21
  import openai
22
- from sentence_transformers import SentenceTransformer
 
 
 
 
 
23
  from supabase import create_client
24
- AI_AVAILABLE = True
25
- except ImportError as e:
26
- print(f"⚠️ Modules IA non disponibles: {e}")
27
- AI_AVAILABLE = False
 
 
 
 
 
 
 
28
 
29
  # === CONFIGURATION ===
30
  SUPABASE_URL = os.getenv("SUPABASE_URL")
31
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
32
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
33
 
34
- # Vérifications avec messages clairs
35
- if not SUPABASE_URL:
36
- print(" SUPABASE_URL manquant dans les variables d'environnement")
37
- if not SUPABASE_KEY:
38
- print("❌ SUPABASE_KEY manquant dans les variables d'environnement")
39
- if not OPENAI_API_KEY:
40
- print("❌ OPENAI_API_KEY manquant dans les variables d'environnement")
41
-
42
- # === INITIALISATION SERVICES (avec protection) ===
43
- app = FastAPI(title="Cocoon AI Assistant", description="API pour assistant créateur")
44
-
45
- # Initialisation conditionnelle
 
 
 
 
 
46
  supabase_client = None
47
  model = None
48
 
49
- if AI_AVAILABLE and SUPABASE_URL and SUPABASE_KEY:
50
  try:
51
  supabase_client = create_client(SUPABASE_URL, SUPABASE_KEY)
52
  print("✅ Supabase connecté")
53
  except Exception as e:
54
- print(f"❌ Erreur connexion Supabase: {e}")
55
 
56
- if AI_AVAILABLE and OPENAI_API_KEY:
57
  try:
58
  openai.api_key = OPENAI_API_KEY
59
  print("✅ OpenAI configuré")
60
  except Exception as e:
61
- print(f"❌ Erreur configuration OpenAI: {e}")
 
 
 
 
 
 
 
 
62
 
63
- # Modèle SentenceTransformer (optionnel au démarrage)
64
  def load_ai_model():
 
65
  global model
66
- if not model and AI_AVAILABLE:
67
  try:
68
  cache_dir = os.path.join(tempfile.gettempdir(), "hf_cache")
69
  os.makedirs(cache_dir, exist_ok=True)
70
  model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder=cache_dir)
71
  print("✅ Modèle IA chargé")
72
- return model
73
  except Exception as e:
74
- print(f" Erreur chargement modèle: {e}")
75
- return None
76
  return model
77
 
78
- # === FONCTIONS UTILITAIRES ===
79
- def get_user_vault_path(user_id: str) -> str:
80
- """Créer le chemin vers le dossier vault de l'utilisateur"""
81
- base_path = os.path.join(tempfile.gettempdir(), "vaults")
82
- user_path = os.path.join(base_path, f"user_{user_id}")
83
- os.makedirs(user_path, exist_ok=True)
84
- return user_path
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- def safe_json_response(data, status_code=200):
87
- """Retourner une réponse JSON sécurisée"""
88
- try:
89
- return JSONResponse(content=data, status_code=status_code)
90
- except Exception as e:
91
- return JSONResponse(
92
- content={"error": f"Erreur de sérialisation: {str(e)}"},
93
- status_code=500
94
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  # === MODÈLES DE DONNÉES ===
97
  class ProfileRequest(BaseModel):
@@ -110,14 +198,20 @@ class NoteRequest(BaseModel):
110
  # === ROUTES DE BASE ===
111
  @app.get("/")
112
  def root():
113
- """Page d'accueil de l'API"""
114
  return {
115
  "message": "🚀 Cocoon AI Assistant API",
116
  "status": "En ligne",
 
117
  "services": {
118
  "supabase": "✅" if supabase_client else "❌",
119
- "openai": "✅" if OPENAI_API_KEY else "❌",
120
- "ai_model": "✅" if model else "⏳ Non chargé"
 
 
 
 
 
121
  },
122
  "timestamp": datetime.now().isoformat()
123
  }
@@ -129,155 +223,94 @@ def ping():
129
 
130
  @app.get("/health")
131
  def health_check():
132
- """Vérification de santé détaillée"""
133
  health_status = {
134
  "status": "healthy",
135
  "timestamp": datetime.now().isoformat(),
136
  "services": {
137
  "api": "✅ Running",
138
- "supabase": "✅ Connected" if supabase_client else "❌ Not connected",
139
- "openai": "✅ Configured" if OPENAI_API_KEY else "❌ Not configured",
140
- "ai_model": "✅ Loaded" if model else "⏳ Not loaded"
141
  },
142
- "environment": {
143
- "python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}",
144
- "temp_dir": tempfile.gettempdir()
 
145
  }
146
  }
147
 
148
  # Déterminer le statut global
149
- if not supabase_client or not OPENAI_API_KEY:
 
150
  health_status["status"] = "degraded"
 
151
 
152
  return health_status
153
 
154
  # === ROUTES PRINCIPALES ===
155
  @app.post("/profile")
156
  async def save_profile(req: ProfileRequest):
157
- """Sauvegarder le profil utilisateur et créer le vault Obsidian"""
158
  try:
159
- print(f"📝 Sauvegarde profil pour utilisateur: {req.user_id}")
160
 
161
- # Créer le dossier utilisateur
162
- vault_path = get_user_vault_path(req.user_id)
163
 
164
- # Sauvegarder les données brutes en JSON
165
- profile_json_path = os.path.join(vault_path, "user_profile.json")
166
- with open(profile_json_path, "w", encoding="utf-8") as f:
167
  json.dump(req.profile_data, f, indent=2, ensure_ascii=False)
168
 
169
- # Créer une version markdown simple du profil
170
- profile_md_path = os.path.join(vault_path, "Profile")
171
- os.makedirs(profile_md_path, exist_ok=True)
172
-
173
- profile_content = f"""# 👤 Profil Utilisateur
174
-
175
- ## Informations de base
176
- - **Expérience**: {req.profile_data.get('experienceLevel', 'Non défini')}
177
- - **Objectif**: {req.profile_data.get('contentGoal', 'Non défini')}
178
- - **Niche**: {req.profile_data.get('niche', 'Non défini')}
179
- - **Localisation**: {req.profile_data.get('city', '')}, {req.profile_data.get('country', '')}
180
-
181
- ## Business
182
- - **Type**: {req.profile_data.get('businessType', 'Non défini')}
183
- - **Description**: {req.profile_data.get('businessDescription', 'Non défini')}
184
-
185
- ## Stratégie
186
- - **Plateformes**: {', '.join(req.profile_data.get('platforms', []))}
187
- - **Types de contenu**: {', '.join(req.profile_data.get('contentTypes', []))}
188
- - **Audience**: {req.profile_data.get('targetGeneration', 'Non défini')}
189
-
190
- ## Ressources
191
- - **Temps disponible**: {req.profile_data.get('timeAvailable', 'Non défini')}
192
- - **Ressources**: {req.profile_data.get('resources', 'Non défini')}
193
- - **Défis**: {req.profile_data.get('mainChallenges', 'Non défini')}
194
-
195
- ---
196
- Créé le: {datetime.now().strftime('%Y-%m-%d à %H:%M')}
197
- """
198
-
199
- with open(os.path.join(profile_md_path, "user_profile.md"), "w", encoding="utf-8") as f:
200
- f.write(profile_content)
201
-
202
  # Synchroniser avec Supabase si disponible
203
- files_created = 2
204
  if supabase_client:
205
  try:
206
  supabase_client.table("vault_files").upsert({
207
  "user_id": req.user_id,
208
  "path": "Profile/user_profile.md",
209
- "content": profile_content,
210
  "updated_at": datetime.now().isoformat()
211
  }).execute()
212
- print("✅ Profil synchronisé avec Supabase")
213
  except Exception as e:
214
  print(f"⚠️ Erreur sync Supabase: {e}")
 
215
 
216
  return {
217
  "status": "✅ Profil sauvegardé avec succès",
218
- "message": "Votre profil a été créé dans votre vault Obsidian",
219
  "vault_path": vault_path,
220
- "files_created": files_created,
 
221
  "timestamp": datetime.now().isoformat()
222
  }
223
 
224
  except Exception as e:
225
- print(f"❌ Erreur sauvegarde profil: {e}")
226
- return JSONResponse(
227
- status_code=500,
228
- content={
229
- "error": f"Erreur lors de la sauvegarde: {str(e)}",
230
- "user_id": req.user_id
231
- }
232
- )
233
-
234
- @app.post("/note")
235
- async def save_note(req: NoteRequest):
236
- """Sauvegarder une note simple"""
237
- try:
238
- vault_path = get_user_vault_path(req.user_id)
239
-
240
- # Créer le fichier note
241
- note_content = f"""# {req.title}
242
-
243
- {req.content}
244
-
245
- ---
246
- Créé le: {datetime.now().strftime('%Y-%m-%d à %H:%M')}
247
- """
248
-
249
- note_path = os.path.join(vault_path, f"{req.title.replace(' ', '_')}.md")
250
- with open(note_path, "w", encoding="utf-8") as f:
251
- f.write(note_content)
252
-
253
- return {
254
- "status": "✅ Note sauvegardée",
255
- "message": f"Note '{req.title}' créée avec succès"
256
- }
257
-
258
- except Exception as e:
259
- return JSONResponse(status_code=500, content={"error": str(e)})
260
 
261
  @app.post("/ask")
262
- async def ask_simple(req: AskRequest):
263
- """Version simplifiée de l'assistant IA"""
264
  try:
265
- if not OPENAI_API_KEY:
266
  return {
267
- "answer": "❌ Service IA non configuré. Veuillez configurer OPENAI_API_KEY.",
268
- "status": "error"
 
269
  }
270
 
271
- # Charger le modèle si nécessaire
272
  current_model = load_ai_model()
273
 
274
  # Réponse simple avec OpenAI
275
  response = openai.chat.completions.create(
276
- model="gpt-4",
277
  messages=[
278
  {
279
  "role": "system",
280
- "content": "Tu es un assistant pour créateurs de contenu. Réponds en français de manière utile et concise."
281
  },
282
  {"role": "user", "content": req.question}
283
  ],
@@ -288,18 +321,47 @@ async def ask_simple(req: AskRequest):
288
  return {
289
  "answer": response.choices[0].message.content,
290
  "status": "✅ Réponse générée",
291
- "model_loaded": current_model is not None
 
 
292
  }
293
 
294
  except Exception as e:
295
  print(f"❌ Erreur IA: {e}")
296
- return JSONResponse(
297
- status_code=500,
298
- content={
299
- "error": f"Erreur lors de la génération: {str(e)}",
300
- "answer": "Désolé, je ne peux pas répondre pour le moment."
301
- }
302
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  @app.get("/user/{user_id}/status")
305
  async def get_user_status(user_id: str):
@@ -308,52 +370,130 @@ async def get_user_status(user_id: str):
308
  vault_path = get_user_vault_path(user_id)
309
 
310
  # Compter les fichiers
311
- file_count = 0
312
- files_list = []
313
 
314
  if os.path.exists(vault_path):
315
  for root, dirs, files in os.walk(vault_path):
316
  for file in files:
317
- if file.endswith('.md') or file.endswith('.json'):
318
- file_count += 1
319
- rel_path = os.path.relpath(os.path.join(root, file), vault_path)
320
- files_list.append(rel_path)
 
321
 
322
- # Vérifier si le profil existe
323
- profile_exists = os.path.exists(os.path.join(vault_path, "Profile/user_profile.md"))
324
 
325
  return {
326
  "user_id": user_id,
327
- "vault_path": vault_path,
328
  "profile_exists": profile_exists,
329
- "total_files": file_count,
330
- "files": files_list[:10], # Premières 10 files
331
- "status": "✅ Statut récupéré",
 
332
  "timestamp": datetime.now().isoformat()
333
  }
334
 
335
  except Exception as e:
336
- return JSONResponse(status_code=500, content={"error": str(e)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- # === GESTION DES ERREURS GLOBALES ===
339
  @app.exception_handler(Exception)
340
  async def global_exception_handler(request, exc):
341
- """Gestionnaire d'erreurs global"""
342
  print(f"❌ Erreur globale: {exc}")
343
  return JSONResponse(
344
  status_code=500,
345
  content={
346
  "error": "Erreur interne du serveur",
347
  "message": str(exc),
348
- "timestamp": datetime.now().isoformat()
 
349
  }
350
  )
351
 
352
  # === DÉMARRAGE ===
353
  if __name__ == "__main__":
354
- print("🚀 Démarrage de Cocoon AI Assistant...")
355
- print(f"📖 Documentation: http://localhost:8000/docs")
356
- print(f"❤️ Health check: http://localhost:8000/health")
357
-
358
  import uvicorn
359
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ # app.py - Version optimisée pour Hugging Face Spaces
2
 
 
3
  import os
4
  import json
5
  import tempfile
6
  from datetime import datetime
7
  from typing import List, Dict, Optional
8
 
9
+ # === GESTION ROBUSTE DES IMPORTS ===
10
+ try:
11
+ from dotenv import load_dotenv
12
+ load_dotenv()
13
+ DOTENV_AVAILABLE = True
14
+ except ImportError:
15
+ print("⚠️ python-dotenv non disponible, utilisation des variables d'environnement système")
16
+ DOTENV_AVAILABLE = False
17
+
18
+ # === Imports FastAPI (obligatoires) ===
19
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
20
  from fastapi.responses import JSONResponse
21
+ from fastapi.middleware.cors import CORSMiddleware
22
  from pydantic import BaseModel
23
 
24
+ # === Imports IA (optionnels) ===
25
+ OPENAI_AVAILABLE = False
26
+ SUPABASE_AVAILABLE = False
27
+ SENTENCE_TRANSFORMERS_AVAILABLE = False
28
+
29
  try:
30
  import openai
31
+ OPENAI_AVAILABLE = True
32
+ print("✅ OpenAI disponible")
33
+ except ImportError:
34
+ print("⚠️ OpenAI non disponible")
35
+
36
+ try:
37
  from supabase import create_client
38
+ SUPABASE_AVAILABLE = True
39
+ print("✅ Supabase disponible")
40
+ except ImportError:
41
+ print("⚠️ Supabase non disponible")
42
+
43
+ try:
44
+ from sentence_transformers import SentenceTransformer
45
+ SENTENCE_TRANSFORMERS_AVAILABLE = True
46
+ print("✅ SentenceTransformers disponible")
47
+ except ImportError:
48
+ print("⚠️ SentenceTransformers non disponible")
49
 
50
  # === CONFIGURATION ===
51
  SUPABASE_URL = os.getenv("SUPABASE_URL")
52
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
53
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
54
 
55
+ # === INITIALISATION APP ===
56
+ app = FastAPI(
57
+ title="Cocoon AI Assistant",
58
+ description="API pour assistant créateur de contenu",
59
+ version="1.0.0"
60
+ )
61
+
62
+ # CORS pour permettre les requêtes depuis votre frontend
63
+ app.add_middleware(
64
+ CORSMiddleware,
65
+ allow_origins=["*"], # En production, limitez aux domaines autorisés
66
+ allow_credentials=True,
67
+ allow_methods=["*"],
68
+ allow_headers=["*"],
69
+ )
70
+
71
+ # === INITIALISATION SERVICES ===
72
  supabase_client = None
73
  model = None
74
 
75
+ if SUPABASE_AVAILABLE and SUPABASE_URL and SUPABASE_KEY:
76
  try:
77
  supabase_client = create_client(SUPABASE_URL, SUPABASE_KEY)
78
  print("✅ Supabase connecté")
79
  except Exception as e:
80
+ print(f"❌ Erreur Supabase: {e}")
81
 
82
+ if OPENAI_AVAILABLE and OPENAI_API_KEY:
83
  try:
84
  openai.api_key = OPENAI_API_KEY
85
  print("✅ OpenAI configuré")
86
  except Exception as e:
87
+ print(f"❌ Erreur OpenAI: {e}")
88
+
89
+ # === FONCTIONS UTILITAIRES ===
90
+ def get_user_vault_path(user_id: str) -> str:
91
+ """Créer le chemin vers le dossier vault de l'utilisateur"""
92
+ base_path = os.path.join(tempfile.gettempdir(), "vaults")
93
+ user_path = os.path.join(base_path, f"user_{user_id}")
94
+ os.makedirs(user_path, exist_ok=True)
95
+ return user_path
96
 
 
97
  def load_ai_model():
98
+ """Charger le modèle IA de manière paresseuse"""
99
  global model
100
+ if model is None and SENTENCE_TRANSFORMERS_AVAILABLE:
101
  try:
102
  cache_dir = os.path.join(tempfile.gettempdir(), "hf_cache")
103
  os.makedirs(cache_dir, exist_ok=True)
104
  model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder=cache_dir)
105
  print("✅ Modèle IA chargé")
 
106
  except Exception as e:
107
+ print(f"⚠️ Erreur chargement modèle: {e}")
 
108
  return model
109
 
110
+ def create_simple_obsidian_structure(user_id: str, profile_data: dict):
111
+ """Créer une structure Obsidian simple"""
112
+ vault_path = get_user_vault_path(user_id)
113
+
114
+ # Créer les dossiers principaux
115
+ folders = [
116
+ "Profile",
117
+ "Content_Strategy",
118
+ "Goals_and_Metrics",
119
+ "Resources_and_Skills",
120
+ "AI_Context"
121
+ ]
122
+
123
+ for folder in folders:
124
+ os.makedirs(os.path.join(vault_path, folder), exist_ok=True)
125
+
126
+ # Créer le profil principal
127
+ profile_content = f"""# 👤 Mon Profil Créateur
128
 
129
+ ## 🎯 Informations de base
130
+ - **Expérience**: {profile_data.get('experienceLevel', 'Non défini')}
131
+ - **Objectif**: {profile_data.get('contentGoal', 'Non défini')}
132
+ - **Niche**: {profile_data.get('niche', 'Non défini')}
133
+ - **Localisation**: {profile_data.get('city', '')}, {profile_data.get('country', '')}
134
+
135
+ ## 🏢 Business
136
+ - **Type**: {profile_data.get('businessType', 'Non défini')}
137
+ - **Description**: {profile_data.get('businessDescription', 'Non défini')}
138
+
139
+ ## 🎯 Stratégie
140
+ - **Plateformes**: {', '.join(profile_data.get('platforms', []))}
141
+ - **Types de contenu**: {', '.join(profile_data.get('contentTypes', []))}
142
+ - **Audience**: {profile_data.get('targetGeneration', 'Non défini')}
143
+
144
+ ## ⏰ Ressources
145
+ - **Temps disponible**: {profile_data.get('timeAvailable', 'Non défini')}
146
+ - **Ressources**: {profile_data.get('resources', 'Non défini')}
147
+ - **Défis**: {profile_data.get('mainChallenges', 'Non défini')}
148
+
149
+ ## 💰 Monétisation
150
+ - **Intention**: {profile_data.get('monetizationIntent', 'Non défini')}
151
+
152
+ ---
153
+ **Créé le**: {datetime.now().strftime('%Y-%m-%d à %H:%M')}
154
+ """
155
+
156
+ with open(os.path.join(vault_path, "Profile", "user_profile.md"), "w", encoding="utf-8") as f:
157
+ f.write(profile_content)
158
+
159
+ # Créer un dashboard simple
160
+ dashboard_content = f"""# 🏠 Mon Dashboard Créateur
161
+
162
+ ## 📊 Vue d'ensemble
163
+ - **Profil**: {profile_data.get('experienceLevel', 'Non défini')}
164
+ - **Objectif**: {profile_data.get('contentGoal', 'Non défini')}
165
+ - **Niche**: {profile_data.get('niche', 'Non défini')}
166
+
167
+ ## 🎯 Navigation rapide
168
+ - [[Profile/user_profile|👤 Mon Profil]]
169
+ - [[Content_Strategy/content_goals|🎯 Mes Objectifs]]
170
+ - [[Goals_and_Metrics/success_metrics|📊 Mes Métriques]]
171
+
172
+ ## 📈 Plateformes actives
173
+ {chr(10).join([f'- **{platform}**' for platform in profile_data.get('platforms', [])])}
174
+
175
+ ---
176
+ **Dernière mise à jour**: {datetime.now().strftime('%Y-%m-%d à %H:%M')}
177
+ """
178
+
179
+ with open(os.path.join(vault_path, "Dashboard.md"), "w", encoding="utf-8") as f:
180
+ f.write(dashboard_content)
181
+
182
+ return vault_path
183
 
184
  # === MODÈLES DE DONNÉES ===
185
  class ProfileRequest(BaseModel):
 
198
  # === ROUTES DE BASE ===
199
  @app.get("/")
200
  def root():
201
+ """Page d'accueil avec statut des services"""
202
  return {
203
  "message": "🚀 Cocoon AI Assistant API",
204
  "status": "En ligne",
205
+ "version": "1.0.0",
206
  "services": {
207
  "supabase": "✅" if supabase_client else "❌",
208
+ "openai": "✅" if OPENAI_AVAILABLE and OPENAI_API_KEY else "❌",
209
+ "ai_model": "✅" if model else "⏳ Non chargé",
210
+ "sentence_transformers": "✅" if SENTENCE_TRANSFORMERS_AVAILABLE else "❌"
211
+ },
212
+ "environment": {
213
+ "platform": "Hugging Face Spaces",
214
+ "python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}"
215
  },
216
  "timestamp": datetime.now().isoformat()
217
  }
 
223
 
224
  @app.get("/health")
225
  def health_check():
226
+ """Vérification de santé complète"""
227
  health_status = {
228
  "status": "healthy",
229
  "timestamp": datetime.now().isoformat(),
230
  "services": {
231
  "api": "✅ Running",
232
+ "supabase": "✅ Connected" if supabase_client else "❌ Not available",
233
+ "openai": "✅ Available" if OPENAI_AVAILABLE and OPENAI_API_KEY else "❌ Not available",
234
+ "ai_model": "✅ Ready" if model else "⏳ Not loaded"
235
  },
236
+ "dependencies": {
237
+ "supabase_lib": "" if SUPABASE_AVAILABLE else "❌",
238
+ "openai_lib": "✅" if OPENAI_AVAILABLE else "❌",
239
+ "transformers_lib": "✅" if SENTENCE_TRANSFORMERS_AVAILABLE else "❌"
240
  }
241
  }
242
 
243
  # Déterminer le statut global
244
+ critical_services = [OPENAI_AVAILABLE, SUPABASE_AVAILABLE]
245
+ if not any(critical_services):
246
  health_status["status"] = "degraded"
247
+ health_status["message"] = "Services IA non disponibles"
248
 
249
  return health_status
250
 
251
  # === ROUTES PRINCIPALES ===
252
  @app.post("/profile")
253
  async def save_profile(req: ProfileRequest):
254
+ """Sauvegarder le profil utilisateur"""
255
  try:
256
+ print(f"📝 Sauvegarde profil pour: {req.user_id}")
257
 
258
+ # Créer la structure Obsidian
259
+ vault_path = create_simple_obsidian_structure(req.user_id, req.profile_data)
260
 
261
+ # Sauvegarder les données brutes
262
+ with open(os.path.join(vault_path, "user_profile.json"), "w", encoding="utf-8") as f:
 
263
  json.dump(req.profile_data, f, indent=2, ensure_ascii=False)
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  # Synchroniser avec Supabase si disponible
266
+ sync_status = "disabled"
267
  if supabase_client:
268
  try:
269
  supabase_client.table("vault_files").upsert({
270
  "user_id": req.user_id,
271
  "path": "Profile/user_profile.md",
272
+ "content": "Profil créé",
273
  "updated_at": datetime.now().isoformat()
274
  }).execute()
275
+ sync_status = "synced"
276
  except Exception as e:
277
  print(f"⚠️ Erreur sync Supabase: {e}")
278
+ sync_status = "failed"
279
 
280
  return {
281
  "status": "✅ Profil sauvegardé avec succès",
282
+ "message": "Votre vault Obsidian a été créé",
283
  "vault_path": vault_path,
284
+ "sync_status": sync_status,
285
+ "files_created": 3,
286
  "timestamp": datetime.now().isoformat()
287
  }
288
 
289
  except Exception as e:
290
+ print(f"❌ Erreur sauvegarde: {e}")
291
+ raise HTTPException(status_code=500, detail=f"Erreur sauvegarde: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  @app.post("/ask")
294
+ async def ask_ai(req: AskRequest):
295
+ """Poser une question à l'IA"""
296
  try:
297
+ if not OPENAI_AVAILABLE or not OPENAI_API_KEY:
298
  return {
299
+ "answer": "❌ Service IA non disponible. OpenAI n'est pas configuré.",
300
+ "status": "error",
301
+ "suggestion": "Configurez OPENAI_API_KEY dans les variables d'environnement"
302
  }
303
 
304
+ # Charger le modèle si possible
305
  current_model = load_ai_model()
306
 
307
  # Réponse simple avec OpenAI
308
  response = openai.chat.completions.create(
309
+ model="gpt-4o-mini", # Modèle moins cher
310
  messages=[
311
  {
312
  "role": "system",
313
+ "content": "Tu es un assistant expert pour créateurs de contenu. Réponds en français de manière utile, concise et actionnable."
314
  },
315
  {"role": "user", "content": req.question}
316
  ],
 
321
  return {
322
  "answer": response.choices[0].message.content,
323
  "status": "✅ Réponse générée",
324
+ "model_used": "gpt-4o-mini",
325
+ "has_context": current_model is not None,
326
+ "timestamp": datetime.now().isoformat()
327
  }
328
 
329
  except Exception as e:
330
  print(f"❌ Erreur IA: {e}")
331
+ return {
332
+ "answer": "Désolé, je ne peux pas répondre pour le moment. Erreur technique.",
333
+ "status": "error",
334
+ "error_details": str(e)
335
+ }
336
+
337
+ @app.post("/note")
338
+ async def save_note(req: NoteRequest):
339
+ """Sauvegarder une note"""
340
+ try:
341
+ vault_path = get_user_vault_path(req.user_id)
342
+
343
+ note_content = f"""# {req.title}
344
+
345
+ {req.content}
346
+
347
+ ---
348
+ **Créé le**: {datetime.now().strftime('%Y-%m-%d à %H:%M')}
349
+ """
350
+
351
+ safe_filename = req.title.replace(" ", "_").replace("/", "_")
352
+ note_path = os.path.join(vault_path, f"{safe_filename}.md")
353
+
354
+ with open(note_path, "w", encoding="utf-8") as f:
355
+ f.write(note_content)
356
+
357
+ return {
358
+ "status": "✅ Note sauvegardée",
359
+ "filename": f"{safe_filename}.md",
360
+ "message": f"Note '{req.title}' créée avec succès"
361
+ }
362
+
363
+ except Exception as e:
364
+ raise HTTPException(status_code=500, detail=str(e))
365
 
366
  @app.get("/user/{user_id}/status")
367
  async def get_user_status(user_id: str):
 
370
  vault_path = get_user_vault_path(user_id)
371
 
372
  # Compter les fichiers
373
+ total_files = 0
374
+ markdown_files = []
375
 
376
  if os.path.exists(vault_path):
377
  for root, dirs, files in os.walk(vault_path):
378
  for file in files:
379
+ if file.endswith(('.md', '.json')):
380
+ total_files += 1
381
+ if file.endswith('.md'):
382
+ rel_path = os.path.relpath(os.path.join(root, file), vault_path)
383
+ markdown_files.append(rel_path)
384
 
385
+ profile_exists = os.path.exists(os.path.join(vault_path, "Profile", "user_profile.md"))
 
386
 
387
  return {
388
  "user_id": user_id,
389
+ "vault_exists": os.path.exists(vault_path),
390
  "profile_exists": profile_exists,
391
+ "total_files": total_files,
392
+ "markdown_files": markdown_files[:10], # Limiter l'affichage
393
+ "vault_path": vault_path,
394
+ "status": "✅ Utilisateur trouvé" if profile_exists else "⚠️ Profil non créé",
395
  "timestamp": datetime.now().isoformat()
396
  }
397
 
398
  except Exception as e:
399
+ raise HTTPException(status_code=500, detail=str(e))
400
+
401
+ @app.get("/user/{user_id}/vault_structure")
402
+ async def get_vault_structure(user_id: str):
403
+ """Récupérer la structure du vault utilisateur"""
404
+ try:
405
+ vault_path = get_user_vault_path(user_id)
406
+
407
+ if not os.path.exists(vault_path):
408
+ return {
409
+ "error": "Vault non trouvé",
410
+ "message": "Créez d'abord votre profil",
411
+ "user_id": user_id
412
+ }
413
+
414
+ structure = {}
415
+ for root, dirs, files in os.walk(vault_path):
416
+ rel_path = os.path.relpath(root, vault_path)
417
+ if rel_path == ".":
418
+ rel_path = "root"
419
+
420
+ structure[rel_path] = {
421
+ "folders": dirs,
422
+ "markdown_files": [f for f in files if f.endswith('.md')],
423
+ "other_files": [f for f in files if not f.endswith('.md')],
424
+ "total_files": len(files)
425
+ }
426
+
427
+ return {
428
+ "user_id": user_id,
429
+ "structure": structure,
430
+ "total_folders": len(structure),
431
+ "vault_path": vault_path,
432
+ "status": "✅ Structure récupérée"
433
+ }
434
+
435
+ except Exception as e:
436
+ raise HTTPException(status_code=500, detail=str(e))
437
+
438
+ # === ROUTES DE TEST ET DEBUG ===
439
+ @app.get("/test")
440
+ async def test_all_services():
441
+ """Tester tous les services"""
442
+ tests = {
443
+ "timestamp": datetime.now().isoformat(),
444
+ "results": {}
445
+ }
446
+
447
+ # Test création vault
448
+ try:
449
+ test_path = get_user_vault_path("test_user")
450
+ tests["results"]["vault_creation"] = "✅ OK" if os.path.exists(test_path) else "❌ Failed"
451
+ except Exception as e:
452
+ tests["results"]["vault_creation"] = f"❌ Error: {e}"
453
+
454
+ # Test OpenAI
455
+ if OPENAI_AVAILABLE and OPENAI_API_KEY:
456
+ try:
457
+ response = openai.chat.completions.create(
458
+ model="gpt-4o-mini",
459
+ messages=[{"role": "user", "content": "Test"}],
460
+ max_tokens=5
461
+ )
462
+ tests["results"]["openai"] = "✅ OK"
463
+ except Exception as e:
464
+ tests["results"]["openai"] = f"❌ Error: {e}"
465
+ else:
466
+ tests["results"]["openai"] = "❌ Not configured"
467
+
468
+ # Test Supabase
469
+ if supabase_client:
470
+ try:
471
+ # Test simple de connexion
472
+ result = supabase_client.table("vault_files").select("id").limit(1).execute()
473
+ tests["results"]["supabase"] = "✅ OK"
474
+ except Exception as e:
475
+ tests["results"]["supabase"] = f"❌ Error: {e}"
476
+ else:
477
+ tests["results"]["supabase"] = "❌ Not configured"
478
+
479
+ return tests
480
 
481
+ # === GESTION GLOBALE DES ERREURS ===
482
  @app.exception_handler(Exception)
483
  async def global_exception_handler(request, exc):
 
484
  print(f"❌ Erreur globale: {exc}")
485
  return JSONResponse(
486
  status_code=500,
487
  content={
488
  "error": "Erreur interne du serveur",
489
  "message": str(exc),
490
+ "timestamp": datetime.now().isoformat(),
491
+ "path": str(request.url)
492
  }
493
  )
494
 
495
  # === DÉMARRAGE ===
496
  if __name__ == "__main__":
497
+ print("🚀 Démarrage Cocoon AI Assistant pour Hugging Face Spaces...")
 
 
 
498
  import uvicorn
499
+ uvicorn.run(app, host="0.0.0.0", port=7860) # Port 7860 pour HF Spaces
requirements.txt CHANGED
@@ -1,43 +1,28 @@
 
 
 
1
  # === API Framework ===
2
  fastapi==0.104.1
3
  uvicorn[standard]==0.24.0
4
  python-multipart==0.0.6
5
 
6
- # === Base de données et storage ===
7
  supabase==1.2.0
8
- psycopg2-binary==2.9.7
9
 
10
- # === IA et Machine Learning ===
11
  openai>=1.3.0
12
- sentence-transformers==2.2.2
 
 
13
  chromadb>=0.4.22
14
- numpy==1.26.4
15
- torch==2.1.0
16
- transformers==4.35.2
17
 
18
- # === Utilitaires ===
19
  python-dotenv==1.0.0
20
- pydantic==2.5.0
21
- typing-extensions==4.8.0
22
-
23
- # === Date et temps ===
24
- python-dateutil==2.8.2
25
-
26
- # === Manipulation de fichiers ===
27
- pathlib2==2.3.7
28
- aiofiles==23.2.0
29
-
30
- # === Logs et monitoring ===
31
- loguru==0.7.2
32
-
33
- # === Sécurité ===
34
- python-jose[cryptography]==3.3.0
35
- passlib[bcrypt]==1.7.4
36
 
37
- # === Tests (optionnel) ===
38
- pytest==7.4.3
39
- pytest-asyncio==0.21.1
40
- httpx==0.25.2
41
 
42
  # === Production ===
43
- gunicorn==21.2.0
 
1
+ # === REQUIREMENTS MINIMAUX POUR HUGGING FACE SPACES ===
2
+ # Version allégée pour éviter les timeouts de build
3
+
4
  # === API Framework ===
5
  fastapi==0.104.1
6
  uvicorn[standard]==0.24.0
7
  python-multipart==0.0.6
8
 
9
+ # === Base de données ===
10
  supabase==1.2.0
 
11
 
12
+ # === IA (versions compatibles) ===
13
  openai>=1.3.0
14
+ # Version plus légère de sentence-transformers
15
+ sentence-transformers>=2.2.0,<3.0.0
16
+ # ChromaDB sans dépendances lourdes
17
  chromadb>=0.4.22
 
 
 
18
 
19
+ # === Utilitaires de base ===
20
  python-dotenv==1.0.0
21
+ pydantic>=2.0.0,<3.0.0
22
+ numpy>=1.24.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # === Fichiers ===
25
+ aiofiles>=23.0.0
 
 
26
 
27
  # === Production ===
28
+ gunicorn>=21.0.0