Spaces:
Runtime error
Runtime error
save
Browse files- app.py +8 -4
- prompts.yaml +36 -10
- tools/country_info_tool.py +729 -0
- tools/mood_to_need.py +19 -4
- tools/need_to_destination.py +38 -60
- tools/test_mood_to_destination.py +21 -0
- tools/weather_example.py +0 -42
- tools/weather_tool.py +95 -6
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()
|
|
|
|
| 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 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 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 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|