mi720 commited on
Commit
4412731
·
2 Parent(s): e141c38 b50765b
app.py CHANGED
@@ -4,6 +4,9 @@ from tools.need_to_destination import NeedToDestinationTool
4
  from tools.weather_tool import WeatherTool
5
  from tools.find_flight import FlightsFinderTool
6
  from tools.final_answer import FinalAnswerTool
 
 
 
7
  from Gradio_UI import GradioUI
8
  import yaml
9
  from tool.mock_tools import MoodToNeedTool, NeedToDestinationTool, WeatherTool, FlightsFinderTool, FinalAnswerTool
@@ -37,11 +40,12 @@ with open("prompts.yaml", "r") as f:
37
  agent = CodeAgent(
38
  model=model,
39
  tools=[
40
- MoodToNeedTool(), # Step 1: Mood → Need
41
- NeedToDestinationTool(), # Step 2: Need → Destination
42
  WeatherTool(), # Step 3: Weather for destination
43
- FlightsFinderTool(), # Step 4: Destination → Flights
44
- FinalAnswerTool() # Step 5: Claude wrap
 
45
  ],
46
  max_steps=6,
47
  verbosity_level=1,
 
4
  from tools.weather_tool import WeatherTool
5
  from tools.find_flight import FlightsFinderTool
6
  from tools.final_answer import FinalAnswerTool
7
+ from tools.country_info_tool import CountryInfoTool
8
+ from smolagents import CodeAgent,DuckDuckGoSearchTool, HfApiModel,load_tool,tool
9
+ from smolagents import MultiStepAgent, ActionStep, AgentText, AgentImage, AgentAudio, handle_agent_output_types
10
  from Gradio_UI import GradioUI
11
  import yaml
12
  from tool.mock_tools import MoodToNeedTool, NeedToDestinationTool, WeatherTool, FlightsFinderTool, FinalAnswerTool
 
40
  agent = CodeAgent(
41
  model=model,
42
  tools=[
43
+ MoodToNeedTool(model=model), # Step 1: Mood → Need
44
+ NeedToDestinationTool(model=model), # Step 2: Need → Destination
45
  WeatherTool(), # Step 3: Weather for destination
46
+ FlightsFinderTool(), # Step 4: Destination → Flights # Step 5: Claude wrap
47
+ FinalAnswerTool(), # Required final output
48
+ CountryInfoTool() # Step 6: Country info
49
  ],
50
  max_steps=6,
51
  verbosity_level=1,
prompts.yaml CHANGED
@@ -3,15 +3,16 @@ system_prompt: |-
3
  You operate in an autonomous, multi-step thinking loop using Thought → Code → Observation.
4
 
5
  Your job is to:
6
- - Reflect on the user’s mood
7
- - Infer their emotional need
8
- - Suggest a travel destination with a matching activity
9
- - Check the weather
10
- - Decide whether the weather suits the emotional need
11
- - If not, suggest another destination
12
- - Once happy, find flights from the origin
13
- - Wrap everything into an inspirational message
14
- - Optionally, add a quote based on the mood
 
15
 
16
  You must solve the full task by reasoning, calling tools or managed agents if needed, and writing Python code in the Code block.
17
 
@@ -24,7 +25,32 @@ system_prompt: |-
24
   ⁠<end_code>
25
  Observation: result of previous code
26
 
27
- You must output a final result using ⁠ final_answer() ⁠.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  Your available tools are:
30
  - mood_to_need(mood: str) → str: Extracts the emotional need behind a mood (e.g., "to reconnect").
 
3
  You operate in an autonomous, multi-step thinking loop using Thought → Code → Observation.
4
 
5
  Your job is to:
6
+ - Reflect on the user’s mood
7
+ - Infer their emotional need
8
+ - Suggest a travel destination with a matching activity
9
+ - Check the weather
10
+ - Retrieve relevant country information
11
+ - Decide whether the weather and country conditions suit the emotional need
12
+ - If not, suggest another destination
13
+ - Once happy, find flights from the origin
14
+ - Wrap everything into an inspirational message
15
+ - Optionally, add a quote based on the mood
16
 
17
  You must solve the full task by reasoning, calling tools or managed agents if needed, and writing Python code in the Code block.
18
 
 
25
   ⁠<end_code>
26
  Observation: result of previous code
27
 
28
+ You must output a final result using ⁠ final_answer() ⁠.
29
+
30
+
31
+ 🛫 Your very first job is to ensure the user has provided the following inputs:
32
+ - `mood`: how the user is feeling (e.g., "stressed", "adventurous")
33
+ - `origin`: the city or airport they’re departing from
34
+ - `week`: approximate travel week or date range (e.g., "mid July" or "2025-07-15")
35
+
36
+ ❗If one or more are missing, ask for them clearly and stop the plan until you receive all of them.
37
+
38
+ Example check code:
39
+ ```py
40
+ if not mood or not origin or not week:
41
+ missing = []
42
+ if not mood:
43
+ missing.append("your mood")
44
+ if not origin:
45
+ missing.append("your departure city or airport")
46
+ if not week:
47
+ missing.append("your travel week or dates")
48
+
49
+ print(f"Before we start planning, could you tell me {', '.join(missing)}? 😊")
50
+ else:
51
+ print("All required inputs provided. Let's begin planning.")
52
+ ```<end_code>
53
+
54
 
55
  Your available tools are:
56
  - mood_to_need(mood: str) → str: Extracts the emotional need behind a mood (e.g., "to reconnect").
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/mood_to_need.py CHANGED
@@ -1,7 +1,8 @@
1
- """Tool to map a user's mood to a vacation need."""
2
-
3
  from smolagents.tools import Tool
4
-
 
 
 
5
  class MoodToNeedTool(Tool):
6
  """
7
  A tool that converts user mood descriptions into vacation needs using an LLM.
@@ -22,6 +23,7 @@ class MoodToNeedTool(Tool):
22
  Args:
23
  model: A callable language model with a __call__(str) -> str interface.
24
  """
 
25
  self.model = model
26
 
27
  def forward(self, mood: str) -> str:
@@ -44,4 +46,17 @@ class MoodToNeedTool(Tool):
44
  f'Need:'
45
  )
46
  response = self.model(prompt)
47
- return response.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from smolagents.tools import Tool
2
+ from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
3
+ import os
4
+ from dotenv import load_dotenv
5
+ load_dotenv() # Loads variables from .env into environment
6
  class MoodToNeedTool(Tool):
7
  """
8
  A tool that converts user mood descriptions into vacation needs using an LLM.
 
23
  Args:
24
  model: A callable language model with a __call__(str) -> str interface.
25
  """
26
+ super().__init__()
27
  self.model = model
28
 
29
  def forward(self, mood: str) -> str:
 
46
  f'Need:'
47
  )
48
  response = self.model(prompt)
49
+ return response.strip()
50
+
51
+ client = Anthropic(api_key=os.getenv("ANTHROPIC_KEY"))
52
+
53
+ def claude_mood_to_need_model(prompt: str) -> str:
54
+ message = client.messages.create(
55
+ model="claude-3-opus-20240229",
56
+ max_tokens=1024,
57
+ temperature=0.7,
58
+ messages=[
59
+ {"role": "user", "content": prompt}
60
+ ]
61
+ )
62
+ return message.content[0].text
tools/need_to_destination.py CHANGED
@@ -1,5 +1,9 @@
1
  from smolagents.tools import Tool
 
 
2
  import json
 
 
3
 
4
  class NeedToDestinationTool(Tool):
5
  name = "NeedToDestination"
@@ -16,32 +20,32 @@ class NeedToDestinationTool(Tool):
16
 
17
  def forward(self, need: str) -> list[dict]:
18
  prompt = f"""
19
- You are a travel agent AI.
20
 
21
- Based on the user's need: "{need}",
22
- suggest 2-3 travel destinations with round-trip flight information.
23
 
24
- Return the output as valid JSON in the following format:
25
 
26
- [
27
- {{
28
- "destination": "DestinationName",
29
- "departure": {{
30
- "date": "YYYY-MM-DD",
31
- "from_airport": "{self.departure_airport}",
32
- "to_airport": "XXX"
33
- }},
34
- "return": {{
35
- "date": "YYYY-MM-DD",
36
- "from_airport": "XXX",
37
- "to_airport": "{self.departure_airport}"
38
- }}
39
- }},
40
- ...
41
- ]
42
 
43
- DO NOT add explanations, only return raw JSON.
44
- """
45
  result = self.model(prompt)
46
  try:
47
  destinations = json.loads(result.strip())
@@ -50,42 +54,16 @@ DO NOT add explanations, only return raw JSON.
50
 
51
  return destinations
52
 
53
- def mock_model(prompt: str) -> str:
54
- return """
55
- [
56
- {
57
- "destination": "Nice",
58
- "departure": {
59
- "date": "2025-07-01",
60
- "from_airport": "CDG",
61
- "to_airport": "NCE"
62
- },
63
- "return": {
64
- "date": "2025-07-07",
65
- "from_airport": "NCE",
66
- "to_airport": "CDG"
67
- }
68
- },
69
- {
70
- "destination": "Barcelona",
71
- "departure": {
72
- "date": "2025-07-02",
73
- "from_airport": "CDG",
74
- "to_airport": "BCN"
75
- },
76
- "return": {
77
- "date": "2025-07-08",
78
- "from_airport": "BCN",
79
- "to_airport": "CDG"
80
- }
81
- }
82
- ]
83
- """
84
 
85
- if __name__ == "__main__":
86
- tool = NeedToDestinationTool(model=mock_model)
87
- test_need = "A relaxing beach holiday"
88
- destinations = tool(need=test_need)
89
- print("Destinations returned by the tool:")
90
- for dest in destinations:
91
- print(dest)
 
 
 
 
 
 
1
  from smolagents.tools import Tool
2
+ from anthropic import Anthropic
3
+ import os
4
  import json
5
+ from dotenv import load_dotenv
6
+ load_dotenv() # Loads variables from .env into environment
7
 
8
  class NeedToDestinationTool(Tool):
9
  name = "NeedToDestination"
 
20
 
21
  def forward(self, need: str) -> list[dict]:
22
  prompt = f"""
23
+ You are a travel agent AI.
24
 
25
+ Based on the user's need: "{need}",
26
+ suggest 2-3 travel destinations with round-trip flight information.
27
 
28
+ Return the output as valid JSON in the following format:
29
 
30
+ [
31
+ {{
32
+ "destination": "DestinationName",
33
+ "departure": {{
34
+ "date": "YYYY-MM-DD",
35
+ "from_airport": "{self.departure_airport}",
36
+ "to_airport": "XXX"
37
+ }},
38
+ "return": {{
39
+ "date": "YYYY-MM-DD",
40
+ "from_airport": "XXX",
41
+ "to_airport": "{self.departure_airport}"
42
+ }}
43
+ }},
44
+ ...
45
+ ]
46
 
47
+ DO NOT add explanations, only return raw JSON.
48
+ """
49
  result = self.model(prompt)
50
  try:
51
  destinations = json.loads(result.strip())
 
54
 
55
  return destinations
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ client = Anthropic(api_key=os.getenv("ANTHROPIC_KEY"))
59
+
60
+ def claude_need_to_destination_model(prompt: str) -> str:
61
+ message = client.messages.create(
62
+ model="claude-3-opus-20240229",
63
+ max_tokens=1024,
64
+ temperature=0.7,
65
+ messages=[
66
+ {"role": "user", "content": prompt}
67
+ ]
68
+ )
69
+ return message.content[0].text
tools/test_mood_to_destination.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tools.mood_to_need import MoodToNeedTool, claude_mood_to_need_model
2
+ from tools.need_to_destination import NeedToDestinationTool, claude_need_to_destination_model
3
+ import json
4
+
5
+ mood_tool = MoodToNeedTool(model=claude_mood_to_need_model)
6
+ mood = "I'm feeling stuck in a routine and need change"
7
+ # mood = "I feel overwhelmed and burned out."
8
+ # mood = "I just got out of a break up"
9
+
10
+ need = mood_tool(mood=mood)
11
+ print(f"Mood: {mood}")
12
+ print("Need:", need)
13
+
14
+ destination_tool = NeedToDestinationTool(model=claude_need_to_destination_model, departure_airport="CDG")
15
+ try:
16
+ destinations = destination_tool(need=need)
17
+ print("\n→ Suggested Destinations:")
18
+ for dest in destinations:
19
+ print(json.dumps(dest, indent=2))
20
+ except ValueError as e:
21
+ print(f"Error parsing Claude output: {e}")
tools/weather_example.py DELETED
@@ -1,42 +0,0 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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