Hugo Bui commited on
Commit
5ad533d
·
unverified ·
1 Parent(s): 76f2e90
tools/country_info_tool.py ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+ from smolagents.tools import Tool
3
+ import requests
4
+ from datetime import datetime, timedelta
5
+ import json
6
+ import os
7
+ from dotenv import load_dotenv
8
+ import re
9
+ import anthropic
10
+
11
+ class CountryInfoTool(Tool):
12
+ name = "country_info"
13
+ description = "Récupère des informations contextuelles importantes sur un pays en temps réel : sécurité, événements actuels, fêtes nationales, climat politique, conseils de voyage."
14
+ inputs = {
15
+ 'country': {'type': 'string', 'description': 'Le nom du pays en français ou en anglais (ex: "France", "États-Unis", "Japon")'},
16
+ 'info_type': {'type': 'string', 'description': 'Type d\'information recherchée: "all", "security", "events", "holidays", "travel", "politics"', 'nullable': True}
17
+ }
18
+ output_type = "string"
19
+
20
+ def __init__(self):
21
+ super().__init__()
22
+ load_dotenv()
23
+
24
+ # Initialiser le client Claude (Anthropic)
25
+ self.claude_client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY'))
26
+
27
+ # Mapping étendu des pays français vers anglais pour les APIs
28
+ self.country_mapping = {
29
+ # Europe
30
+ 'france': 'France', 'allemagne': 'Germany', 'italie': 'Italy', 'espagne': 'Spain',
31
+ 'royaume-uni': 'United Kingdom', 'angleterre': 'United Kingdom', 'écosse': 'United Kingdom',
32
+ 'pays-bas': 'Netherlands', 'hollande': 'Netherlands', 'belgique': 'Belgium',
33
+ 'suisse': 'Switzerland', 'autriche': 'Austria', 'portugal': 'Portugal',
34
+ 'suède': 'Sweden', 'norvège': 'Norway', 'danemark': 'Denmark', 'finlande': 'Finland',
35
+ 'pologne': 'Poland', 'république tchèque': 'Czech Republic', 'tchéquie': 'Czech Republic',
36
+ 'hongrie': 'Hungary', 'roumanie': 'Romania', 'bulgarie': 'Bulgaria',
37
+ 'grèce': 'Greece', 'croatie': 'Croatia', 'slovénie': 'Slovenia', 'slovaquie': 'Slovakia',
38
+ 'estonie': 'Estonia', 'lettonie': 'Latvia', 'lituanie': 'Lithuania',
39
+ 'irlande': 'Ireland', 'islande': 'Iceland', 'malte': 'Malta', 'chypre': 'Cyprus',
40
+ 'serbie': 'Serbia', 'bosnie': 'Bosnia and Herzegovina', 'monténégro': 'Montenegro',
41
+ 'macédoine': 'North Macedonia', 'albanie': 'Albania', 'moldavie': 'Moldova',
42
+ 'ukraine': 'Ukraine', 'biélorussie': 'Belarus', 'russie': 'Russia',
43
+
44
+ # Amériques
45
+ 'états-unis': 'United States', 'usa': 'United States', 'amérique': 'United States',
46
+ 'canada': 'Canada', 'mexique': 'Mexico',
47
+ 'brésil': 'Brazil', 'argentine': 'Argentina', 'chili': 'Chile', 'pérou': 'Peru',
48
+ 'colombie': 'Colombia', 'venezuela': 'Venezuela', 'équateur': 'Ecuador',
49
+ 'bolivie': 'Bolivia', 'paraguay': 'Paraguay', 'uruguay': 'Uruguay',
50
+ 'guatemala': 'Guatemala', 'costa rica': 'Costa Rica', 'panama': 'Panama',
51
+ 'cuba': 'Cuba', 'jamaïque': 'Jamaica', 'haïti': 'Haiti', 'république dominicaine': 'Dominican Republic',
52
+
53
+ # Asie
54
+ 'chine': 'China', 'japon': 'Japan', 'corée du sud': 'South Korea', 'corée du nord': 'North Korea',
55
+ 'inde': 'India', 'pakistan': 'Pakistan', 'bangladesh': 'Bangladesh', 'sri lanka': 'Sri Lanka',
56
+ 'thaïlande': 'Thailand', 'vietnam': 'Vietnam', 'cambodge': 'Cambodia', 'laos': 'Laos',
57
+ 'myanmar': 'Myanmar', 'birmanie': 'Myanmar', 'malaisie': 'Malaysia', 'singapour': 'Singapore',
58
+ 'indonésie': 'Indonesia', 'philippines': 'Philippines', 'brunei': 'Brunei',
59
+ 'mongolie': 'Mongolia', 'kazakhstan': 'Kazakhstan', 'ouzbékistan': 'Uzbekistan',
60
+ 'kirghizistan': 'Kyrgyzstan', 'tadjikistan': 'Tajikistan', 'turkménistan': 'Turkmenistan',
61
+ 'afghanistan': 'Afghanistan', 'iran': 'Iran', 'irak': 'Iraq', 'syrie': 'Syria',
62
+ 'turquie': 'Turkey', 'israël': 'Israel', 'palestine': 'Palestine', 'liban': 'Lebanon',
63
+ 'jordanie': 'Jordan', 'arabie saoudite': 'Saudi Arabia', 'émirats arabes unis': 'United Arab Emirates',
64
+ 'qatar': 'Qatar', 'koweït': 'Kuwait', 'bahreïn': 'Bahrain', 'oman': 'Oman', 'yémen': 'Yemen',
65
+
66
+ # Afrique
67
+ 'maroc': 'Morocco', 'algérie': 'Algeria', 'tunisie': 'Tunisia', 'libye': 'Libya', 'égypte': 'Egypt',
68
+ 'soudan': 'Sudan', 'éthiopie': 'Ethiopia', 'kenya': 'Kenya', 'tanzanie': 'Tanzania',
69
+ 'ouganda': 'Uganda', 'rwanda': 'Rwanda', 'burundi': 'Burundi', 'congo': 'Democratic Republic of the Congo',
70
+ 'république démocratique du congo': 'Democratic Republic of the Congo', 'rdc': 'Democratic Republic of the Congo',
71
+ 'république du congo': 'Republic of the Congo', 'cameroun': 'Cameroon', 'nigeria': 'Nigeria',
72
+ 'ghana': 'Ghana', 'côte d\'ivoire': 'Ivory Coast', 'sénégal': 'Senegal', 'mali': 'Mali',
73
+ 'burkina faso': 'Burkina Faso', 'niger': 'Niger', 'tchad': 'Chad', 'centrafrique': 'Central African Republic',
74
+ 'gabon': 'Gabon', 'guinée équatoriale': 'Equatorial Guinea', 'sao tomé': 'Sao Tome and Principe',
75
+ 'cap-vert': 'Cape Verde', 'guinée-bissau': 'Guinea-Bissau', 'guinée': 'Guinea',
76
+ 'sierra leone': 'Sierra Leone', 'liberia': 'Liberia', 'togo': 'Togo', 'bénin': 'Benin',
77
+ 'mauritanie': 'Mauritania', 'gambie': 'Gambia', 'afrique du sud': 'South Africa',
78
+ 'namibie': 'Namibia', 'botswana': 'Botswana', 'zimbabwe': 'Zimbabwe', 'zambie': 'Zambia',
79
+ 'malawi': 'Malawi', 'mozambique': 'Mozambique', 'madagascar': 'Madagascar', 'maurice': 'Mauritius',
80
+ 'seychelles': 'Seychelles', 'comores': 'Comoros', 'djibouti': 'Djibouti', 'érythrée': 'Eritrea',
81
+ 'somalie': 'Somalia', 'lesotho': 'Lesotho', 'eswatini': 'Eswatini', 'swaziland': 'Eswatini',
82
+
83
+ # Océanie
84
+ 'australie': 'Australia', 'nouvelle-zélande': 'New Zealand', 'fidji': 'Fiji',
85
+ 'papouasie-nouvelle-guinée': 'Papua New Guinea', 'vanuatu': 'Vanuatu', 'samoa': 'Samoa',
86
+ 'tonga': 'Tonga', 'îles salomon': 'Solomon Islands', 'micronésie': 'Micronesia',
87
+ 'palau': 'Palau', 'nauru': 'Nauru', 'kiribati': 'Kiribati', 'tuvalu': 'Tuvalu'
88
+ }
89
+
90
+ # Codes ISO pour certaines APIs
91
+ self.country_codes = {
92
+ 'France': 'FR', 'United States': 'US', 'United Kingdom': 'GB',
93
+ 'Germany': 'DE', 'Italy': 'IT', 'Spain': 'ES', 'Japan': 'JP',
94
+ 'China': 'CN', 'India': 'IN', 'Brazil': 'BR', 'Canada': 'CA',
95
+ 'Australia': 'AU', 'Russia': 'RU', 'Mexico': 'MX', 'South Korea': 'KR',
96
+ 'Netherlands': 'NL', 'Belgium': 'BE', 'Switzerland': 'CH',
97
+ 'Sweden': 'SE', 'Norway': 'NO', 'Denmark': 'DK', 'Turkey': 'TR',
98
+ 'Egypt': 'EG', 'Thailand': 'TH', 'Iran': 'IR'
99
+ }
100
+
101
+ def forward(self, country: str, info_type: Optional[str] = "all") -> str:
102
+ try:
103
+ # Normaliser le nom du pays
104
+ country_normalized = self._normalize_country_name(country)
105
+
106
+ if not country_normalized:
107
+ return f"❌ Pays non reconnu: '{country}'. Essayez avec le nom complet (ex: 'France', 'États-Unis', 'Royaume-Uni')"
108
+
109
+ # Collecter les informations selon le type demandé
110
+ info_sections = []
111
+
112
+ if info_type in ["all", "security"]:
113
+ security_info = self._get_security_info(country_normalized)
114
+ if security_info:
115
+ info_sections.append(security_info)
116
+
117
+ if info_type in ["all", "events"]:
118
+ events_info = self._get_current_events_info(country_normalized)
119
+ if events_info:
120
+ info_sections.append(events_info)
121
+
122
+ if info_type in ["all", "holidays"]:
123
+ holidays_info = self._get_holidays_info(country_normalized)
124
+ if holidays_info:
125
+ info_sections.append(holidays_info)
126
+
127
+ if info_type in ["all", "travel"]:
128
+ travel_info = self._get_travel_info(country_normalized)
129
+ if travel_info:
130
+ info_sections.append(travel_info)
131
+
132
+ if info_type in ["all", "politics"]:
133
+ politics_info = self._get_political_info(country_normalized)
134
+ if politics_info:
135
+ info_sections.append(politics_info)
136
+
137
+ if not info_sections:
138
+ return f"❌ Aucune information disponible pour {country_normalized} actuellement."
139
+
140
+ # Assembler le rapport final
141
+ result = f"🌍 **Informations contextuelles pour {country_normalized}**\n"
142
+ result += f"*Mise à jour: {datetime.now().strftime('%d/%m/%Y %H:%M')}*\n\n"
143
+ result += "\n\n".join(info_sections)
144
+
145
+ # Ajouter une recommandation finale intelligente si Claude est disponible et qu'on demande toutes les infos
146
+ if info_type == "all" and self.claude_client:
147
+ final_recommendation = self._get_llm_final_recommendation(country_normalized, "\n\n".join(info_sections))
148
+ if final_recommendation:
149
+ result += f"\n\n{final_recommendation}"
150
+
151
+ return result
152
+
153
+ except Exception as e:
154
+ return f"❌ Erreur lors de la récupération des informations: {str(e)}"
155
+
156
+ def _normalize_country_name(self, country: str) -> Optional[str]:
157
+ """Normalise le nom du pays"""
158
+ country_lower = country.lower().strip()
159
+
160
+ # Vérifier dans le mapping français -> anglais
161
+ if country_lower in self.country_mapping:
162
+ return self.country_mapping[country_lower]
163
+
164
+ # Vérifier si c'est déjà un nom anglais valide
165
+ for french, english in self.country_mapping.items():
166
+ if country_lower == english.lower():
167
+ return english
168
+
169
+ # Essayer une correspondance partielle
170
+ for french, english in self.country_mapping.items():
171
+ if country_lower in french or french in country_lower:
172
+ return english
173
+
174
+ # Si pas trouvé dans le mapping, essayer de valider via l'API REST Countries
175
+ validated_country = self._validate_country_via_api(country)
176
+ if validated_country:
177
+ return validated_country
178
+
179
+ return None
180
+
181
+ def _validate_country_via_api(self, country: str) -> Optional[str]:
182
+ """Valide et normalise le nom du pays via l'API REST Countries"""
183
+ try:
184
+ # Essayer d'abord avec le nom exact
185
+ url = f"https://restcountries.com/v3.1/name/{country}"
186
+ response = requests.get(url, timeout=5)
187
+
188
+ if response.status_code == 200:
189
+ data = response.json()
190
+ if data:
191
+ # Retourner le nom officiel en anglais
192
+ return data[0].get('name', {}).get('common', country.title())
193
+
194
+ # Si échec, essayer avec une recherche partielle
195
+ url = f"https://restcountries.com/v3.1/name/{country}?fullText=false"
196
+ response = requests.get(url, timeout=5)
197
+
198
+ if response.status_code == 200:
199
+ data = response.json()
200
+ if data:
201
+ # Prendre le premier résultat
202
+ return data[0].get('name', {}).get('common', country.title())
203
+
204
+ return None
205
+
206
+ except Exception:
207
+ # En cas d'erreur, retourner le nom avec la première lettre en majuscule
208
+ return country.title() if len(country) > 2 else None
209
+
210
+ def _get_country_code_from_api(self, country: str) -> Optional[str]:
211
+ """Récupère le code ISO du pays via l'API REST Countries"""
212
+ try:
213
+ url = f"https://restcountries.com/v3.1/name/{country}"
214
+ response = requests.get(url, timeout=5)
215
+
216
+ if response.status_code == 200:
217
+ data = response.json()
218
+ if data:
219
+ # Retourner le code ISO alpha-2
220
+ return data[0].get('cca2', '')
221
+
222
+ return None
223
+
224
+ except Exception:
225
+ return None
226
+
227
+ def _get_security_info(self, country: str) -> str:
228
+ """Récupère les informations de sécurité avec recherche exhaustive"""
229
+ try:
230
+ # Vérifier d'abord si c'est un pays à risque connu
231
+ risk_level = self._check_known_risk_countries(country)
232
+
233
+ # Recherches multiples avec différents mots-clés
234
+ all_news_data = []
235
+
236
+ # Recherche 1: Sécurité générale
237
+ security_keywords = f"{country} travel advisory security warning conflict war"
238
+ news_data1 = self._search_security_news(security_keywords)
239
+ all_news_data.extend(news_data1)
240
+
241
+ # Recherche 2: Conflits spécifiques
242
+ conflict_keywords = f"{country} war conflict violence terrorism attack bombing"
243
+ news_data2 = self._search_security_news(conflict_keywords)
244
+ all_news_data.extend(news_data2)
245
+
246
+ # Recherche 3: Instabilité politique
247
+ political_keywords = f"{country} coup government crisis instability sanctions"
248
+ news_data3 = self._search_security_news(political_keywords)
249
+ all_news_data.extend(news_data3)
250
+
251
+ # Recherche 4: Alertes de voyage
252
+ travel_keywords = f"{country} 'travel ban' 'do not travel' 'avoid travel' embassy"
253
+ news_data4 = self._search_security_news(travel_keywords)
254
+ all_news_data.extend(news_data4)
255
+
256
+ # Supprimer les doublons
257
+ unique_news = []
258
+ seen_titles = set()
259
+ for article in all_news_data:
260
+ title = article.get('title', '')
261
+ if title and title not in seen_titles:
262
+ unique_news.append(article)
263
+ seen_titles.add(title)
264
+
265
+ # Analyser les résultats pour déterminer le niveau de sécurité
266
+ security_level, description, recommendation = self._analyze_security_data(country, unique_news, risk_level)
267
+
268
+ result = f"🛡️ **Sécurité et Conseils de Voyage**\n"
269
+ result += f"{security_level} **Niveau déterminé par analyse en temps réel**\n"
270
+ result += f"📋 {description}\n"
271
+ result += f"🎯 **Recommandation: {recommendation}**"
272
+
273
+ return result
274
+
275
+ except Exception as e:
276
+ return f"🛡️ **Sécurité**: Erreur lors de la récupération - {str(e)}"
277
+
278
+ def _check_known_risk_countries(self, country: str) -> str:
279
+ """Vérifie si le pays est dans la liste des pays à risque connus"""
280
+
281
+ # Pays à très haut risque (guerre active, conflit majeur)
282
+ high_risk_countries = [
283
+ 'Ukraine', 'Afghanistan', 'Syria', 'Yemen', 'Somalia', 'South Sudan',
284
+ 'Central African Republic', 'Mali', 'Burkina Faso', 'Niger',
285
+ 'Democratic Republic of the Congo', 'Myanmar', 'Palestine', 'Gaza',
286
+ 'West Bank', 'Iraq', 'Libya', 'Sudan'
287
+ ]
288
+
289
+ # Pays à risque modéré (instabilité, tensions)
290
+ moderate_risk_countries = [
291
+ 'Iran', 'North Korea', 'Venezuela', 'Belarus', 'Ethiopia',
292
+ 'Chad', 'Cameroon', 'Nigeria', 'Pakistan', 'Bangladesh',
293
+ 'Haiti', 'Lebanon', 'Turkey', 'Egypt', 'Algeria'
294
+ ]
295
+
296
+ # Pays avec tensions spécifiques
297
+ tension_countries = [
298
+ 'Russia', 'China', 'Israel', 'India', 'Kashmir', 'Taiwan',
299
+ 'Hong Kong', 'Thailand', 'Philippines', 'Colombia'
300
+ ]
301
+
302
+ country_lower = country.lower()
303
+
304
+ for risk_country in high_risk_countries:
305
+ if risk_country.lower() in country_lower or country_lower in risk_country.lower():
306
+ return "HIGH_RISK"
307
+
308
+ for risk_country in moderate_risk_countries:
309
+ if risk_country.lower() in country_lower or country_lower in risk_country.lower():
310
+ return "MODERATE_RISK"
311
+
312
+ for risk_country in tension_countries:
313
+ if risk_country.lower() in country_lower or country_lower in risk_country.lower():
314
+ return "TENSION"
315
+
316
+ return "UNKNOWN"
317
+
318
+ def _search_security_news(self, keywords: str) -> list:
319
+ """Recherche d'actualités de sécurité avec période étendue"""
320
+ try:
321
+ # Utiliser NewsAPI si disponible
322
+ api_key = os.getenv('NEWSAPI_KEY')
323
+ if api_key:
324
+ url = "https://newsapi.org/v2/everything"
325
+ params = {
326
+ 'q': keywords,
327
+ 'sortBy': 'publishedAt',
328
+ 'pageSize': 20, # Plus d'articles
329
+ 'language': 'en',
330
+ 'from': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), # 30 jours au lieu de 7
331
+ 'apiKey': api_key
332
+ }
333
+
334
+ response = requests.get(url, params=params, timeout=10)
335
+ if response.status_code == 200:
336
+ data = response.json()
337
+ articles = data.get('articles', [])
338
+
339
+ # Filtrer les articles pertinents
340
+ relevant_articles = []
341
+ for article in articles:
342
+ title = article.get('title', '').lower()
343
+ description = article.get('description', '').lower()
344
+
345
+ # Mots-clés critiques pour filtrer
346
+ critical_keywords = ['war', 'conflict', 'attack', 'bombing', 'terrorism',
347
+ 'violence', 'crisis', 'coup', 'sanctions', 'advisory',
348
+ 'warning', 'danger', 'risk', 'threat', 'security']
349
+
350
+ if any(keyword in title or keyword in description for keyword in critical_keywords):
351
+ relevant_articles.append(article)
352
+
353
+ return relevant_articles
354
+
355
+ # Fallback: recherche via une API publique alternative
356
+ return self._search_alternative_news(keywords)
357
+
358
+ except Exception:
359
+ return []
360
+
361
+ def _search_alternative_news(self, keywords: str) -> list:
362
+ """Recherche alternative sans API key"""
363
+ try:
364
+ # Utiliser une API publique comme Guardian ou BBC
365
+ # Pour l'exemple, on simule une recherche basique
366
+ dangerous_keywords = ['war', 'conflict', 'terrorism', 'violence', 'crisis', 'coup', 'sanctions']
367
+ warning_keywords = ['protest', 'unrest', 'advisory', 'caution', 'alert']
368
+
369
+ # Simulation basée sur les mots-clés (à remplacer par vraie API)
370
+ if any(word in keywords.lower() for word in dangerous_keywords):
371
+ return [{'title': f'Security concerns in {keywords.split()[0]}', 'description': 'Recent security developments'}]
372
+ elif any(word in keywords.lower() for word in warning_keywords):
373
+ return [{'title': f'Travel advisory for {keywords.split()[0]}', 'description': 'Caution advised'}]
374
+
375
+ return []
376
+
377
+ except Exception:
378
+ return []
379
+
380
+ def _analyze_security_data(self, country: str, news_data: list, risk_level: str = "UNKNOWN") -> tuple:
381
+ """Analyse les données de sécurité avec Claude uniquement"""
382
+ try:
383
+ if not self.claude_client:
384
+ return ("⚪",
385
+ "Claude non disponible",
386
+ "❓ Clé API Anthropic requise pour l'analyse de sécurité")
387
+
388
+ # Préparer le contenu des actualités pour l'analyse
389
+ news_content = ""
390
+ for i, article in enumerate(news_data[:10], 1): # Limiter à 10 articles
391
+ title = article.get('title', '')
392
+ description = article.get('description', '')
393
+ if title or description:
394
+ news_content += f"{i}. {title}\n{description}\n\n"
395
+
396
+ # Si pas d'actualités mais pays à haut risque connu, forcer l'analyse
397
+ if not news_content.strip():
398
+ if risk_level == "HIGH_RISK":
399
+ return ("🔴",
400
+ f"Pays à très haut risque - conflit actif ou guerre",
401
+ "🚫 CHANGEZ DE DESTINATION - Zone de conflit active")
402
+ elif risk_level == "MODERATE_RISK":
403
+ return ("🟡",
404
+ f"Pays à risque modéré - instabilité politique",
405
+ "⚠️ Voyage possible avec précautions renforcées")
406
+ else:
407
+ return ("🟢",
408
+ f"Aucune actualité de sécurité récente trouvée",
409
+ "✅ Destination considérée comme sûre")
410
+
411
+ # Utiliser Claude pour analyser
412
+ analysis = self._llm_security_analysis(country, news_content, risk_level)
413
+
414
+ if analysis:
415
+ return analysis
416
+ else:
417
+ return ("⚪",
418
+ "Erreur d'analyse Claude",
419
+ "❓ Impossible d'analyser la sécurité actuellement")
420
+
421
+ except Exception:
422
+ return ("⚪", "Analyse impossible", "❓ Consultez les sources officielles")
423
+
424
+ def _llm_security_analysis(self, country: str, news_content: str, risk_level: str = "UNKNOWN") -> Optional[tuple]:
425
+ """Utilise Claude pour analyser la sécurité du pays"""
426
+ try:
427
+ if not self.claude_client:
428
+ return None
429
+
430
+ prompt = f"""Analysez les actualités récentes suivantes concernant {country} et déterminez le niveau de sécurité pour un voyageur :
431
+
432
+ NIVEAU DE RISQUE CONNU : {risk_level}
433
+ - HIGH_RISK = Pays en guerre active ou conflit majeur
434
+ - MODERATE_RISK = Pays avec instabilité politique significative
435
+ - TENSION = Pays avec tensions géopolitiques
436
+ - UNKNOWN = Pas de classification spéciale
437
+
438
+ ACTUALITÉS RÉCENTES :
439
+ {news_content}
440
+
441
+ INSTRUCTIONS CRITIQUES :
442
+ 1. Si NIVEAU DE RISQUE = HIGH_RISK, vous DEVEZ recommander CHANGEZ_DE_DESTINATION sauf preuve claire d'amélioration
443
+ 2. Pour l'Ukraine, Palestine, Afghanistan, Syrie, Yémen : TOUJOURS ROUGE/CHANGEZ_DE_DESTINATION
444
+ 3. Analysez le niveau de risque pour un touriste/voyageur civil
445
+ 4. Soyez TRÈS STRICT - la sécurité des voyageurs est prioritaire
446
+
447
+ Répondez UNIQUEMENT au format JSON suivant :
448
+
449
+ {{
450
+ "niveau": "ROUGE|JAUNE|VERT",
451
+ "description": "Description courte de la situation (max 100 caractères)",
452
+ "recommandation": "CHANGEZ_DE_DESTINATION|PRECAUTIONS_RENFORCEES|DESTINATION_SURE",
453
+ "justification": "Explication de votre décision (max 200 caractères)"
454
+ }}
455
+
456
+ Critères STRICTS :
457
+ - ROUGE/CHANGEZ_DE_DESTINATION : guerre active, conflit armé, terrorisme actif, coup d'état, violence généralisée, zones de combat
458
+ - JAUNE/PRECAUTIONS_RENFORCEES : manifestations violentes, criminalité très élevée, instabilité politique, tensions ethniques
459
+ - VERT/DESTINATION_SURE : pas de risques majeurs pour les civils
460
+
461
+ PRIORITÉ ABSOLUE : Protéger les voyageurs - en cas de doute, choisissez le niveau de sécurité le plus strict."""
462
+
463
+ response = self.claude_client.messages.create(
464
+ model="claude-3-opus-20240229",
465
+ max_tokens=300,
466
+ temperature=0.1,
467
+ system="Vous êtes un expert en sécurité des voyages. Analysez objectivement les risques.",
468
+ messages=[
469
+ {"role": "user", "content": prompt}
470
+ ]
471
+ )
472
+
473
+ result_text = response.content[0].text.strip()
474
+ # Parser la réponse JSON
475
+ try:
476
+ result = json.loads(result_text)
477
+ niveau = result.get('niveau', 'VERT')
478
+ description = result.get('description', 'Analyse effectuée')
479
+ recommandation = result.get('recommandation', 'DESTINATION_SURE')
480
+ justification = result.get('justification', '')
481
+
482
+ # Convertir en format attendu
483
+ if niveau == 'ROUGE':
484
+ emoji = "🔴"
485
+ advice = "🚫 CHANGEZ DE DESTINATION - " + justification
486
+ elif niveau == 'JAUNE':
487
+ emoji = "🟡"
488
+ advice = "⚠️ Voyage possible avec précautions renforcées - " + justification
489
+ else:
490
+ emoji = "🟢"
491
+ advice = "✅ Destination considérée comme sûre - " + justification
492
+
493
+ return (emoji, description, advice)
494
+
495
+ except json.JSONDecodeError:
496
+ return None
497
+
498
+ except Exception:
499
+ return None
500
+
501
+
502
+
503
+ def _get_current_events_info(self, country: str) -> str:
504
+ """Récupère les événements actuels via recherche web"""
505
+ try:
506
+ # Rechercher les événements récents
507
+ events_keywords = f"{country} current events news today recent"
508
+ events_data = self._search_current_events(events_keywords)
509
+
510
+ if not events_data:
511
+ return f"📅 **Événements**: Aucun événement majeur détecté pour {country}"
512
+
513
+ result = f"📅 **Événements et Contexte Actuel**\n"
514
+ for i, event in enumerate(events_data[:5], 1):
515
+ title = event.get('title', 'Événement non spécifié')
516
+ result += f"• {title}\n"
517
+
518
+ return result.rstrip()
519
+
520
+ except Exception:
521
+ return "📅 **Événements**: Erreur lors de la récupération"
522
+
523
+ def _search_current_events(self, keywords: str) -> list:
524
+ """Recherche d'événements actuels"""
525
+ try:
526
+ # Utiliser NewsAPI si disponible
527
+ api_key = os.getenv('NEWSAPI_KEY')
528
+ if api_key:
529
+ url = "https://newsapi.org/v2/everything"
530
+ params = {
531
+ 'q': keywords,
532
+ 'sortBy': 'publishedAt',
533
+ 'pageSize': 5,
534
+ 'from': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
535
+ 'language': 'en',
536
+ 'apiKey': api_key
537
+ }
538
+
539
+ response = requests.get(url, params=params, timeout=10)
540
+ if response.status_code == 200:
541
+ data = response.json()
542
+ return data.get('articles', [])
543
+
544
+ return []
545
+
546
+ except Exception:
547
+ return []
548
+
549
+ def _get_holidays_info(self, country: str) -> str:
550
+ """Récupère les fêtes nationales via API"""
551
+ try:
552
+ country_code = self.country_codes.get(country, '')
553
+
554
+ # Si pas de code dans notre mapping, essayer de le récupérer via API
555
+ if not country_code:
556
+ country_code = self._get_country_code_from_api(country)
557
+
558
+ if not country_code:
559
+ return f"🎉 **Fêtes**: Code pays non trouvé pour {country}"
560
+
561
+ # Utiliser l'API Calendarific ou similaire
562
+ holidays_data = self._fetch_holidays_api(country_code)
563
+
564
+ if not holidays_data:
565
+ return f"🎉 **Fêtes**: Informations non disponibles pour {country}"
566
+
567
+ current_month = datetime.now().month
568
+ current_year = datetime.now().year
569
+
570
+ result = f"🎉 **Fêtes et Événements Saisonniers**\n"
571
+
572
+ # Filtrer les fêtes du mois actuel et des prochains mois
573
+ upcoming_holidays = []
574
+ for holiday in holidays_data:
575
+ try:
576
+ holiday_date = datetime.strptime(holiday.get('date', ''), '%Y-%m-%d')
577
+ if holiday_date.month >= current_month and holiday_date.year == current_year:
578
+ upcoming_holidays.append(holiday)
579
+ except:
580
+ continue
581
+
582
+ if upcoming_holidays:
583
+ result += f"**Prochaines fêtes:**\n"
584
+ for holiday in upcoming_holidays[:5]:
585
+ name = holiday.get('name', 'Fête inconnue')
586
+ date = holiday.get('date', '')
587
+ result += f"• {name} ({date})\n"
588
+ else:
589
+ result += f"**Aucune fête majeure prévue dans les prochains mois**\n"
590
+
591
+ return result.rstrip()
592
+
593
+ except Exception:
594
+ return "🎉 **Fêtes**: Erreur lors de la récupération"
595
+
596
+ def _fetch_holidays_api(self, country_code: str) -> list:
597
+ """Récupère les fêtes via API publique"""
598
+ try:
599
+ # Utiliser une API publique de fêtes (exemple: Calendarific, Nager.Date)
600
+ year = datetime.now().year
601
+ url = f"https://date.nager.at/api/v3/PublicHolidays/{year}/{country_code}"
602
+
603
+ response = requests.get(url, timeout=10)
604
+ if response.status_code == 200:
605
+ return response.json()
606
+
607
+ return []
608
+
609
+ except Exception:
610
+ return []
611
+
612
+ def _get_travel_info(self, country: str) -> str:
613
+ """Récupère les informations de voyage via API REST Countries"""
614
+ try:
615
+ # Utiliser l'API REST Countries
616
+ url = f"https://restcountries.com/v3.1/name/{country}"
617
+ response = requests.get(url, timeout=10)
618
+
619
+ if response.status_code == 200:
620
+ data = response.json()
621
+ if data:
622
+ country_data = data[0]
623
+
624
+ # Extraire les informations
625
+ currencies = country_data.get('currencies', {})
626
+ languages = country_data.get('languages', {})
627
+ region = country_data.get('region', 'Inconnu')
628
+
629
+ currency_name = list(currencies.keys())[0] if currencies else 'Inconnue'
630
+ language_list = list(languages.values()) if languages else ['Inconnue']
631
+
632
+ result = f"✈️ **Informations Pratiques de Voyage**\n"
633
+ result += f"💰 Monnaie: {currency_name}\n"
634
+ result += f"🗣️ Langues: {', '.join(language_list[:3])}\n"
635
+ result += f"🌍 Région: {region}\n"
636
+ result += f"📋 Vérifiez les exigences visa sur le site officiel du pays"
637
+
638
+ return result
639
+
640
+ return f"✈️ **Voyage**: Informations non disponibles pour {country}"
641
+
642
+ except Exception:
643
+ return "✈️ **Voyage**: Erreur lors de la récupération"
644
+
645
+ def _get_political_info(self, country: str) -> str:
646
+ """Récupère le contexte politique via recherche d'actualités"""
647
+ try:
648
+ # Rechercher des actualités politiques récentes
649
+ political_keywords = f"{country} politics government election democracy"
650
+ political_data = self._search_political_news(political_keywords)
651
+
652
+ if not political_data:
653
+ return f"🏛️ **Politique**: Situation stable pour {country}"
654
+
655
+ result = f"🏛️ **Contexte Politique**\n"
656
+
657
+ # Analyser les actualités politiques
658
+ for article in political_data[:3]:
659
+ title = article.get('title', '')
660
+ if title:
661
+ result += f"• {title}\n"
662
+
663
+ return result.rstrip()
664
+
665
+ except Exception:
666
+ return "🏛️ **Politique**: Erreur lors de la récupération"
667
+
668
+ def _search_political_news(self, keywords: str) -> list:
669
+ """Recherche d'actualités politiques"""
670
+ try:
671
+ api_key = os.getenv('NEWSAPI_KEY')
672
+ if api_key:
673
+ url = "https://newsapi.org/v2/everything"
674
+ params = {
675
+ 'q': keywords,
676
+ 'sortBy': 'publishedAt',
677
+ 'pageSize': 5,
678
+ 'from': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'),
679
+ 'language': 'en',
680
+ 'apiKey': api_key
681
+ }
682
+
683
+ response = requests.get(url, params=params, timeout=10)
684
+ if response.status_code == 200:
685
+ data = response.json()
686
+ return data.get('articles', [])
687
+
688
+ return []
689
+
690
+ except Exception:
691
+ return []
692
+
693
+ def _get_llm_final_recommendation(self, country: str, full_report: str) -> Optional[str]:
694
+ """Utilise Claude pour générer une recommandation finale intelligente"""
695
+ try:
696
+ if not self.claude_client:
697
+ return None
698
+
699
+ prompt = f"""Analysez ce rapport complet sur {country} et donnez une recommandation finale concise pour un voyageur français :
700
+
701
+ RAPPORT COMPLET :
702
+ {full_report}
703
+
704
+ Votre tâche :
705
+ 1. Synthétisez les informations les plus importantes
706
+ 2. Donnez une recommandation claire et actionnable
707
+ 3. Répondez en français, maximum 200 mots
708
+ 4. Utilisez un ton professionnel mais accessible
709
+ 5. Si des risques existent, soyez explicite sur les précautions
710
+
711
+ Format de réponse souhaité :
712
+ 🎯 **RECOMMANDATION FINALE**
713
+ [Votre analyse synthétique et recommandation]
714
+
715
+ Si la destination est dangereuse, utilisez clairement "CHANGEZ DE DESTINATION" dans votre réponse."""
716
+
717
+ response = self.claude_client.messages.create(
718
+ model="claude-3-opus-20240229",
719
+ max_tokens=250,
720
+ temperature=0.2,
721
+ system="Vous êtes un conseiller en voyage expert. Donnez des recommandations claires et pratiques.",
722
+ messages=[
723
+ {"role": "user", "content": prompt}
724
+ ]
725
+ )
726
+ return response.content[0].text.strip()
727
+
728
+ except Exception:
729
+ return None
tools/weather_example.py CHANGED
@@ -1,42 +1,104 @@
1
  #!/usr/bin/env python3
2
  """
3
- Exemple d'utilisation de l'outil météo (API gratuite OpenWeatherMap)
4
  """
5
 
 
 
6
  from weather_tool import WeatherTool
7
- from datetime import datetime, timedelta
8
 
9
  def main():
10
- # Initialiser l'outil météo (charge automatiquement la clé API depuis .env)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  weather_tool = WeatherTool()
12
 
13
- print("=== Exemples d'utilisation de l'outil météo ===\n")
14
-
15
- # Exemple 1: Météo actuelle
16
- print("1. Météo actuelle à Paris:")
17
- result = weather_tool.forward("Paris")
18
- print(result)
19
- print("\n" + "="*50 + "\n")
20
-
21
- # Exemple 2: Météo pour une date spécifique
22
- print("2. Météo à Londres pour demain:")
23
- tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
24
- result = weather_tool.forward("London", date=tomorrow)
25
- print(result)
26
- print("\n" + "="*50 + "\n")
27
-
28
- # Exemple 3: Météo pour un pays
29
- print("3. Météo à Tokyo dans 3 jours:")
30
- future_date = (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d")
31
- result = weather_tool.forward("Tokyo", date=future_date)
32
- print(result)
33
- print("\n" + "="*50 + "\n")
34
-
35
- # Exemple 4: Météo avec une clé API spécifique
36
- print("4. Météo à Berlin (avec clé API personnalisée):")
37
- # result = weather_tool.forward("Berlin", api_key="votre_cle_api_ici")
38
- result = weather_tool.forward("Berlin") # Utilise la clé du .env
39
- print(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  if __name__ == "__main__":
42
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
+ Exemple d'utilisation de WeatherTool avec Claude pour des recommandations intelligentes
4
  """
5
 
6
+ import os
7
+ from dotenv import load_dotenv
8
  from weather_tool import WeatherTool
 
9
 
10
  def main():
11
+ # Charger les variables d'environnement
12
+ load_dotenv()
13
+
14
+ # Vérifier que les clés API sont configurées
15
+ required_keys = ['OPENWEATHER_API_KEY', 'ANTHROPIC_API_KEY']
16
+ missing_keys = [key for key in required_keys if not os.getenv(key)]
17
+
18
+ if missing_keys:
19
+ print("❌ Clés API manquantes dans le fichier .env:")
20
+ for key in missing_keys:
21
+ print(f" - {key}")
22
+ print("\nVeuillez configurer ces clés dans votre fichier .env")
23
+ return
24
+
25
+ # Créer l'outil météo
26
  weather_tool = WeatherTool()
27
 
28
+ print("🌤️ Test de WeatherTool avec recommandations intelligentes Claude")
29
+ print("=" * 70)
30
+
31
+ # Exemples de tests avec différents types d'activités
32
+ test_cases = [
33
+ {
34
+ "location": "Nice",
35
+ "activity": "plage",
36
+ "description": "Station balnéaire avec activité plage"
37
+ },
38
+ {
39
+ "location": "Chamonix",
40
+ "activity": "ski",
41
+ "description": "Station de ski"
42
+ },
43
+ {
44
+ "location": "Paris",
45
+ "activity": "ville",
46
+ "description": "Visite de ville"
47
+ },
48
+ {
49
+ "location": "Biarritz",
50
+ "activity": None, # Test de détection automatique
51
+ "description": "Détection automatique d'activité"
52
+ },
53
+ {
54
+ "location": "Courchevel",
55
+ "activity": None, # Test de détection automatique
56
+ "description": "Détection automatique d'activité"
57
+ }
58
+ ]
59
+
60
+ for i, test in enumerate(test_cases, 1):
61
+ print(f"\n🧪 Test {i}: {test['description']}")
62
+ print(f"📍 Lieu: {test['location']}")
63
+ if test['activity']:
64
+ print(f"🎯 Activité: {test['activity']}")
65
+ print("-" * 50)
66
+
67
+ try:
68
+ if test['activity']:
69
+ result = weather_tool.forward(
70
+ location=test['location'],
71
+ activity_type=test['activity']
72
+ )
73
+ else:
74
+ result = weather_tool.forward(location=test['location'])
75
+
76
+ print(result)
77
+
78
+ except Exception as e:
79
+ print(f"❌ Erreur: {e}")
80
+
81
+ print("\n" + "="*70)
82
+
83
+ # Test avec une date future
84
+ print(f"\n🔮 Test avec date future")
85
+ print(f"📍 Lieu: Cannes (plage)")
86
+ print(f"📅 Date: Dans 3 jours")
87
+ print("-" * 50)
88
+
89
+ try:
90
+ from datetime import datetime, timedelta
91
+ future_date = (datetime.now() + timedelta(days=3)).strftime('%Y-%m-%d')
92
+
93
+ result = weather_tool.forward(
94
+ location="Cannes",
95
+ date=future_date,
96
+ activity_type="plage"
97
+ )
98
+ print(result)
99
+
100
+ except Exception as e:
101
+ print(f"❌ Erreur: {e}")
102
 
103
  if __name__ == "__main__":
104
  main()
tools/weather_tool.py CHANGED
@@ -5,13 +5,15 @@ from datetime import datetime, timedelta
5
  import json
6
  import os
7
  from dotenv import load_dotenv
 
8
 
9
  class WeatherTool(Tool):
10
  name = "weather_forecast"
11
- description = "Obtient les prévisions météorologiques pour un pays/ville spécifique à une date donnée. Utilise l'API gratuite OpenWeatherMap 2.5."
12
  inputs = {
13
  'location': {'type': 'string', 'description': 'Le nom de la ville ou du pays pour lequel obtenir la météo (ex: "Paris", "London", "Tokyo")'},
14
  'date': {'type': 'string', 'description': 'La date pour laquelle obtenir la météo au format YYYY-MM-DD (optionnel, par défaut aujourd\'hui)', 'nullable': True},
 
15
  'api_key': {'type': 'string', 'description': 'Clé API OpenWeatherMap (optionnel si définie dans les variables d\'environnement)', 'nullable': True}
16
  }
17
  output_type = "string"
@@ -24,8 +26,14 @@ class WeatherTool(Tool):
24
  # Utiliser la clé API fournie, sinon celle du .env
25
  self.api_key = api_key or os.getenv('OPENWEATHER_API_KEY')
26
  self.base_url = "http://api.openweathermap.org/data/2.5"
 
 
 
 
 
 
27
 
28
- def forward(self, location: str, date: Optional[str] = None, api_key: Optional[str] = None) -> str:
29
  try:
30
  # Utiliser la clé API fournie ou celle par défaut
31
  used_api_key = api_key or self.api_key
@@ -62,7 +70,21 @@ class WeatherTool(Tool):
62
  city_name = geo_data[0]['name']
63
 
64
  # Utiliser l'API gratuite
65
- return self._get_weather(lat, lon, city_name, country, target_date, used_api_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  except requests.exceptions.Timeout:
68
  return "Erreur: Délai d'attente dépassé. Veuillez réessayer."
@@ -186,11 +208,78 @@ class WeatherTool(Tool):
186
  except KeyError as e:
187
  return f"Erreur lors du formatage des prévisions: {str(e)}"
188
 
189
-
190
-
191
  def _wind_direction(self, degrees: float) -> str:
192
  """Convertit les degrés en direction du vent"""
193
  directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
194
  "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO"]
195
  index = round(degrees / 22.5) % 16
196
- return directions[index]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import json
6
  import os
7
  from dotenv import load_dotenv
8
+ import anthropic
9
 
10
  class WeatherTool(Tool):
11
  name = "weather_forecast"
12
+ description = "Obtient les prévisions météorologiques intelligentes pour un pays/ville avec recommandations basées sur le type de destination et les activités prévues."
13
  inputs = {
14
  'location': {'type': 'string', 'description': 'Le nom de la ville ou du pays pour lequel obtenir la météo (ex: "Paris", "London", "Tokyo")'},
15
  'date': {'type': 'string', 'description': 'La date pour laquelle obtenir la météo au format YYYY-MM-DD (optionnel, par défaut aujourd\'hui)', 'nullable': True},
16
+ 'activity_type': {'type': 'string', 'description': 'Type d\'activité/destination: "plage", "ski", "ville", "randonnee", "camping", "festival" (optionnel)', 'nullable': True},
17
  'api_key': {'type': 'string', 'description': 'Clé API OpenWeatherMap (optionnel si définie dans les variables d\'environnement)', 'nullable': True}
18
  }
19
  output_type = "string"
 
26
  # Utiliser la clé API fournie, sinon celle du .env
27
  self.api_key = api_key or os.getenv('OPENWEATHER_API_KEY')
28
  self.base_url = "http://api.openweathermap.org/data/2.5"
29
+
30
+ # Initialiser le client Claude pour les recommandations intelligentes
31
+ try:
32
+ self.claude_client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY'))
33
+ except:
34
+ self.claude_client = None
35
 
36
+ def forward(self, location: str, date: Optional[str] = None, activity_type: Optional[str] = None, api_key: Optional[str] = None) -> str:
37
  try:
38
  # Utiliser la clé API fournie ou celle par défaut
39
  used_api_key = api_key or self.api_key
 
70
  city_name = geo_data[0]['name']
71
 
72
  # Utiliser l'API gratuite
73
+ weather_data = self._get_weather(lat, lon, city_name, country, target_date, used_api_key)
74
+
75
+ # Ajouter des recommandations intelligentes si Claude est disponible
76
+ if self.claude_client:
77
+ # Utiliser le type d'activité fourni ou essayer de le détecter automatiquement
78
+ detected_activity = activity_type or self._detect_activity_from_location(location)
79
+
80
+ if detected_activity:
81
+ recommendation = self._get_intelligent_recommendation(weather_data, detected_activity, location, target_date)
82
+ if recommendation:
83
+ if not activity_type: # Si détecté automatiquement, l'indiquer
84
+ weather_data += f"\n\n💡 *Activité détectée: {detected_activity}*"
85
+ weather_data += f"\n\n{recommendation}"
86
+
87
+ return weather_data
88
 
89
  except requests.exceptions.Timeout:
90
  return "Erreur: Délai d'attente dépassé. Veuillez réessayer."
 
208
  except KeyError as e:
209
  return f"Erreur lors du formatage des prévisions: {str(e)}"
210
 
 
 
211
  def _wind_direction(self, degrees: float) -> str:
212
  """Convertit les degrés en direction du vent"""
213
  directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
214
  "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO"]
215
  index = round(degrees / 22.5) % 16
216
+ return directions[index]
217
+
218
+ def _get_intelligent_recommendation(self, weather_data: str, activity_type: str, location: str, target_date: Optional[datetime]) -> Optional[str]:
219
+ """Utilise Claude pour générer des recommandations intelligentes basées sur la météo et l'activité"""
220
+ try:
221
+ if not self.claude_client:
222
+ return None
223
+
224
+ date_str = target_date.strftime('%d/%m/%Y') if target_date else "aujourd'hui"
225
+
226
+ prompt = f"""Analysez ces données météorologiques pour {location} le {date_str} et donnez une recommandation pour une activité de type "{activity_type}" :
227
+
228
+ DONNÉES MÉTÉO :
229
+ {weather_data}
230
+
231
+ TYPE D'ACTIVITÉ : {activity_type}
232
+
233
+ Votre tâche :
234
+ 1. Analysez si les conditions météo sont adaptées à cette activité
235
+ 2. Donnez une recommandation claire : IDÉAL / ACCEPTABLE / DÉCONSEILLÉ / CHANGEZ DE DESTINATION
236
+ 3. Proposez des alternatives si nécessaire
237
+ 4. Répondez en français, maximum 150 mots
238
+ 5. Utilisez un ton pratique et bienveillant
239
+
240
+ Exemples de logique :
241
+ - Plage + pluie = CHANGEZ DE DESTINATION ou reportez
242
+ - Ski + température > 5°C = DÉCONSEILLÉ
243
+ - Randonnée + orage = CHANGEZ DE DESTINATION
244
+ - Ville + pluie légère = ACCEPTABLE avec parapluie
245
+ - Festival en extérieur + pluie forte = DÉCONSEILLÉ
246
+
247
+ Format de réponse :
248
+ 🎯 **RECOMMANDATION VOYAGE**
249
+ [Votre analyse et conseil]"""
250
+
251
+ response = self.claude_client.messages.create(
252
+ model="claude-3-opus-20240229",
253
+ max_tokens=200,
254
+ temperature=0.2,
255
+ system="Vous êtes un conseiller météo expert. Donnez des recommandations pratiques et claires pour les activités de voyage.",
256
+ messages=[
257
+ {"role": "user", "content": prompt}
258
+ ]
259
+ )
260
+
261
+ return response.content[0].text.strip()
262
+
263
+ except Exception:
264
+ return None
265
+
266
+ def _detect_activity_from_location(self, location: str) -> Optional[str]:
267
+ """Détecte automatiquement le type d'activité probable basé sur la localisation"""
268
+ location_lower = location.lower()
269
+
270
+ # Stations balnéaires et plages
271
+ beach_keywords = ['nice', 'cannes', 'saint-tropez', 'biarritz', 'deauville', 'miami', 'maldives', 'ibiza', 'mykonos', 'cancun', 'phuket', 'bali']
272
+ if any(keyword in location_lower for keyword in beach_keywords):
273
+ return "plage"
274
+
275
+ # Stations de ski
276
+ ski_keywords = ['chamonix', 'val d\'isère', 'courchevel', 'méribel', 'aspen', 'zermatt', 'st moritz', 'verbier']
277
+ if any(keyword in location_lower for keyword in ski_keywords):
278
+ return "ski"
279
+
280
+ # Destinations de randonnée
281
+ hiking_keywords = ['mont blanc', 'everest', 'kilimanjaro', 'patagonie', 'himalaya', 'alpes', 'pyrénées']
282
+ if any(keyword in location_lower for keyword in hiking_keywords):
283
+ return "randonnee"
284
+
285
+ return None