.env DELETED
@@ -1,3 +0,0 @@
1
- SERPAPI_API_KEY="5f1ba2f2179fafad997e2514ccaa634be88cc9d7c5d0382e9e5b528ba0e434c0O"
2
- OPENWEATHER_API_KEY="e6945bba120fdad7ef7421c5cdbe731c"
3
- NEWSAPI_KEY="c104341b18f34979b0b117549f5a76c2"
 
 
 
 
Gradio_UI.py DELETED
@@ -1,121 +0,0 @@
1
- # gradio_UI.py
2
-
3
- import os
4
- import re
5
- import mimetypes
6
- import shutil
7
- from typing import Optional
8
-
9
- import gradio as gr
10
-
11
- from smolagents.agent_types import AgentText, AgentImage, AgentAudio, handle_agent_output_types
12
- from smolagents.agents import ActionStep, MultiStepAgent
13
- from smolagents.memory import MemoryStep
14
- from smolagents.utils import _is_package_available
15
-
16
- def pull_messages_from_step(step_log: MemoryStep):
17
- if isinstance(step_log, ActionStep):
18
- step_number = f"Step {step_log.step_number}" if step_log.step_number is not None else ""
19
- yield gr.ChatMessage(role="assistant", content=f"**{step_number}**")
20
-
21
- if hasattr(step_log, "model_output") and step_log.model_output is not None:
22
- model_output = re.sub(r"```.?\s*<end_code>", "```", step_log.model_output.strip())
23
- yield gr.ChatMessage(role="assistant", content=model_output)
24
-
25
- if hasattr(step_log, "tool_calls") and step_log.tool_calls:
26
- tool_call = step_log.tool_calls[0]
27
- content = str(tool_call.arguments.get("answer", tool_call.arguments)) if isinstance(tool_call.arguments, dict) else str(tool_call.arguments)
28
- if tool_call.name == "python_interpreter":
29
- content = f"```python\n{re.sub(r'<end_code>', '', content).strip()}\n```"
30
- tool_msg = gr.ChatMessage(
31
- role="assistant",
32
- content=content,
33
- metadata={"title": f"🛠️ Used tool {tool_call.name}", "id": "tool_call", "status": "pending"},
34
- )
35
- yield tool_msg
36
- if step_log.observations:
37
- yield gr.ChatMessage(role="assistant", content=step_log.observations.strip(), metadata={"title": "📝 Execution Logs", "parent_id": "tool_call", "status": "done"})
38
- if step_log.error:
39
- yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error", "parent_id": "tool_call", "status": "done"})
40
- tool_msg.metadata["status"] = "done"
41
-
42
- elif step_log.error:
43
- yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error"})
44
-
45
- meta = f"<span style='color:#bbb;font-size:12px;'>Input tokens: {getattr(step_log, 'input_token_count', 0)} | Output tokens: {getattr(step_log, 'output_token_count', 0)} | Duration: {round(getattr(step_log, 'duration', 0), 2)}</span>"
46
- yield gr.ChatMessage(role="assistant", content=meta)
47
- yield gr.ChatMessage(role="assistant", content="-----")
48
-
49
- def stream_to_gradio(agent, task: str, reset_agent_memory: bool = False, additional_args: Optional[dict] = None):
50
- total_input_tokens = 0
51
- total_output_tokens = 0
52
- for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
53
- if hasattr(agent.model, "last_input_token_count"):
54
- total_input_tokens += agent.model.last_input_token_count
55
- total_output_tokens += agent.model.last_output_token_count
56
- if isinstance(step_log, ActionStep):
57
- step_log.input_token_count = agent.model.last_input_token_count
58
- step_log.output_token_count = agent.model.last_output_token_count
59
- for message in pull_messages_from_step(step_log):
60
- yield message
61
- final_answer = handle_agent_output_types(step_log)
62
- if isinstance(final_answer, AgentText):
63
- yield gr.ChatMessage(role="assistant", content=f"**Final answer:**\n{final_answer.to_string()}")
64
- elif isinstance(final_answer, AgentImage):
65
- yield gr.ChatMessage(role="assistant", content={"path": final_answer.to_string(), "mime_type": "image/png"})
66
- elif isinstance(final_answer, AgentAudio):
67
- yield gr.ChatMessage(role="assistant", content={"path": final_answer.to_string(), "mime_type": "audio/wav"})
68
- else:
69
- yield gr.ChatMessage(role="assistant", content=f"**Final answer:** {str(final_answer)}")
70
-
71
- class GradioUI:
72
- def __init__(self, agent: MultiStepAgent):
73
- if not _is_package_available("gradio"):
74
- raise ModuleNotFoundError("Please install 'gradio' with: pip install 'smolagents[gradio]'")
75
- self.agent = agent
76
-
77
- def interact_with_agent(self, prompt, messages):
78
- messages.append(gr.ChatMessage(role="user", content=prompt))
79
- yield messages
80
- for msg in stream_to_gradio(self.agent, task=prompt):
81
- messages.append(msg)
82
- yield messages
83
- yield messages
84
-
85
- # def launch(self):
86
- # with gr.Blocks(fill_height=True) as demo:
87
- # stored_messages = gr.State([])
88
- # chatbot = gr.Chatbot(label="🌍 Mood-Based Travel Agent", type="messages")
89
- # user_input = gr.Textbox(lines=1, label="Describe your mood")
90
- # user_input.submit(
91
- # lambda text, hist: (hist + [gr.ChatMessage(role="user", content=text)], ""),
92
- # [user_input, stored_messages],
93
- # [stored_messages, user_input],
94
- # ).then(self.interact_with_agent, [user_input, stored_messages], [chatbot])
95
- # demo.launch(debug=True, share=True)
96
-
97
- def launch(self):
98
- def run_agent_interface(prompt):
99
- messages = []
100
- for msg in stream_to_gradio(self.agent, task=prompt):
101
- messages.append(msg)
102
- return "\n".join([m.content if isinstance(m.content, str) else str(m.content) for m in messages])
103
-
104
-
105
- demo = gr.Interface(
106
- fn=run_agent_interface,
107
- inputs=gr.Textbox(
108
- label="Describe your mood",
109
- placeholder="e.g., I need a lemon-scented reset by the sea...",
110
- lines=1
111
- ),
112
- outputs=gr.Textbox(label="Response"),
113
- title="Mood-Based Travel Agent",
114
- description="Plan your perfect Mediterranean escape, one mood at a time.",
115
- theme="default",
116
- api_name="predict"
117
- )
118
-
119
- demo.launch(debug=True, share=True)
120
-
121
- __all__ = ["GradioUI", "stream_to_gradio"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
__pycache__/Gradio_UI.cpython-313.pyc DELETED
Binary file (7.7 kB)
 
app.py CHANGED
@@ -1,18 +1,15 @@
1
- from smolagents import CodeAgent, LiteLLMModel
2
- from tools.mood_to_need import MoodToNeedTool, claude_mood_to_need_model
3
- from tools.need_to_destination import NeedToDestinationTool, claude_need_to_destination_model
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 tools.mock_tools import MoodToNeedTool, NeedToDestinationTool, WeatherTool, FlightsFinderTool, FinalAnswerTool
13
 
14
  # Initialize Claude model via Hugging Face
15
- model = LiteLLMModel(
16
  model_id="claude-3-opus-20240229",
17
  temperature=0.7,
18
  max_tokens=2048
@@ -23,31 +20,17 @@ with open("prompts.yaml", "r") as f:
23
  prompt_templates = yaml.safe_load(f)
24
 
25
  # Define the agent with all tools
26
- # agent = CodeAgent(
27
- # model=model,
28
- # tools=[
29
- # MoodToNeedTool(), # Step 1: Mood → Need
30
- # NeedToDestinationTool(), # Step 2: Need → Destination
31
- # WeatherTool(), # Step 3: Weather for destination
32
- # FlightsFinderTool(), # Step 4: Destination → Flights # Step 5: Claude wrap
33
- # FinalAnswerTool() # Required final output
34
- # ],
35
- # max_steps=6,
36
- # verbosity_level=1,
37
- # prompt_templates=prompt_templates
38
- # )
39
-
40
  agent = CodeAgent(
41
  model=model,
42
  tools=[
43
- MoodToNeedTool(model=claude_mood_to_need_model), # Step 1: Mood → Need
44
- NeedToDestinationTool(model=claude_need_to_destination_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=10,
51
  verbosity_level=1,
52
  prompt_templates=prompt_templates
53
  )
 
1
+ from smolagents import CodeAgent, HfApiModel
2
+ from tools.mood_to_need import mood_to_need
3
+ from tools.need_to_destination import need_to_destination
4
+ from tools.get_weather import get_weather
5
+ from tools.get_flights import get_flights
6
+ from tools.final_wrap import final_wrap
7
  from tools.final_answer import FinalAnswerTool
 
 
 
8
  from Gradio_UI import GradioUI
9
  import yaml
 
10
 
11
  # Initialize Claude model via Hugging Face
12
+ model = HfApiModel(
13
  model_id="claude-3-opus-20240229",
14
  temperature=0.7,
15
  max_tokens=2048
 
20
  prompt_templates = yaml.safe_load(f)
21
 
22
  # Define the agent with all tools
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  agent = CodeAgent(
24
  model=model,
25
  tools=[
26
+ mood_to_need, # Step 1: Mood → Need
27
+ need_to_destination, # Step 2: Need → Destination
28
+ get_weather, # Step 3: Weather for destination
29
+ get_flights, # Step 4: Destination → Flights
30
+ final_wrap, # Step 5: Claude wrap
31
+ FinalAnswerTool() # Required final output
32
  ],
33
+ max_steps=6,
34
  verbosity_level=1,
35
  prompt_templates=prompt_templates
36
  )
prompts.yaml DELETED
@@ -1,137 +0,0 @@
1
- system_prompt: |-
2
- You are a world-class travel assistant agent named WanderMind, built to help users find the perfect destination based on their mood.
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
-
19
- Each step should follow this format:
20
-
21
- Thought: explain what you will do and why.
22
- Code:
23
- ⁠ py
24
- # your code here
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
- - MoodToNeed(mood: str) → str: Extracts the emotional need behind a mood (e.g., "to reconnect").
57
- - NeedToDestination(need: str) → list: Suggests destinations and flight info for that need. Returns list of destinations with flight details.
58
- - weather_forecast(location: str, date: str, activity_type: str) → str: Gets weather forecast with intelligent recommendations.
59
- - flights_finder(departure_airport: str, arrival_airport: str, outbound_date: str, return_date: str) → str: Lists flights between airports.
60
- - country_info(country: str, info_type: str) → str: Gets security, events, holidays and travel info for a country.
61
- - final_answer(answer: Any): Ends the task and returns the final result.
62
-
63
- IMPORTANT: You MUST use these tools instead of writing Python code to simulate their functionality. Call the tools directly with their exact names.
64
-
65
- DO NOT use a tool unless needed. Plan your steps clearly. You can retry with different inputs if the weather is bad.
66
-
67
- Now begin!
68
-
69
- planning:
70
- initial_facts: |-
71
- ### 1. Facts given in the task
72
- - The user provides their mood.
73
-
74
- ### 2. Facts to look up
75
- - Emotional need based on mood.
76
- - Destination and activity based on need.
77
- - Current weather at destination.
78
- - Flights from user origin to destination.
79
- - Quote for mood.
80
-
81
- ### 3. Facts to derive
82
- - Whether the weather fits the emotional need.
83
- - If not, re-iterate destination choice.
84
- - Final wrap-up message to user.
85
-
86
- initial_plan: |-
87
- 1. Check if user provided mood, origin, and travel dates. If missing, ask for them.
88
- 2. Extract emotional need from user mood using MoodToNeed().
89
- 3. Suggest destinations using NeedToDestination().
90
- 4. For each suggested destination, get weather forecast using weather_forecast().
91
- 5. Get country information using country_info() to check safety and context.
92
- 6. Assess if weather and country conditions suit the need. If not, try another destination.
93
- 7. Get flights using flights_finder() with proper airport codes.
94
- 8. Compose final inspirational message and call final_answer().
95
- <end_plan>
96
-
97
- update_facts_pre_messages: |-
98
- ### 1. Facts given in the task
99
- ### 2. Facts that we have learned
100
- ### 3. Facts still to look up
101
- ### 4. Facts still to derive
102
-
103
- update_facts_post_messages: |-
104
- Please update your facts:
105
- ### 1. Facts given in the task
106
- ### 2. Facts that we have learned
107
- ### 3. Facts still to look up
108
- ### 4. Facts still to derive
109
-
110
- update_plan_pre_messages: |-
111
- Below is your current task and history. Please write a new plan based on the updated facts.
112
-
113
- update_plan_post_messages: |-
114
- Write a clean new plan with the latest facts. You must respect tool usage rules.
115
-
116
- managed_agent:
117
- task: |-
118
- You are a helpful sub-agent named '{{name}}'.
119
- Your manager gives you this task:
120
- {{task}}
121
-
122
- You MUST return:
123
- ### 1. Task outcome (short version):
124
- ### 2. Task outcome (extremely detailed version):
125
- ### 3. Additional context (if any)
126
-
127
- Wrap everything in a final_answer().
128
-
129
- report: |-
130
- Final answer from agent '{{name}}':
131
- {{final_answer}}
132
-
133
- final_answer:
134
- pre_messages: |-
135
- Let's summarize everything before presenting the final answer:
136
- post_messages: |-
137
- Here's your final result. Enjoy your journey!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,8 +1 @@
1
- huggingface_hub==0.31.2
2
- requests>=2.25.0
3
- smolagents>=1.18.0
4
- python-dotenv>=0.19.0
5
- markdownify>=0.11.0
6
- serpapi==0.1.5
7
- anthropic>=0.24.0
8
- smolagents[litellm]
 
1
+ huggingface_hub==0.25.2
 
 
 
 
 
 
 
test_tools.py DELETED
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from tools.mood_to_need import MoodToNeedTool, claude_mood_to_need_model
4
- from tools.need_to_destination import NeedToDestinationTool, claude_need_to_destination_model
5
- from tools.weather_tool import WeatherTool
6
- from tools.country_info_tool import CountryInfoTool
7
- from tools.find_flight import FlightsFinderTool
8
-
9
- def test_tools():
10
- print("🧪 Testing individual tools...\n")
11
-
12
- # Test 1: MoodToNeed
13
- print("1️⃣ Testing MoodToNeed...")
14
- mood_tool = MoodToNeedTool(model=claude_mood_to_need_model)
15
- need = mood_tool.forward(mood="happy")
16
- print(f" Input: 'happy' → Output: '{need}'\n")
17
-
18
- # Test 2: NeedToDestination
19
- print("2️⃣ Testing NeedToDestination...")
20
- destination_tool = NeedToDestinationTool(model=claude_need_to_destination_model)
21
- destinations = destination_tool.forward(need=need)
22
- print(f" Input: '{need}' → Output: {destinations}\n")
23
-
24
- # Test 3: WeatherTool
25
- print("3️⃣ Testing WeatherTool...")
26
- weather_tool = WeatherTool()
27
- if destinations and len(destinations) > 0:
28
- first_dest = destinations[0]["destination"]
29
- weather = weather_tool.forward(location=first_dest)
30
- print(f" Input: '{first_dest}' → Output: {weather[:200]}...\n")
31
-
32
- # Test 4: CountryInfoTool
33
- print("4️⃣ Testing CountryInfoTool...")
34
- country_tool = CountryInfoTool()
35
- if destinations and len(destinations) > 0:
36
- # Extract country from destination
37
- dest_parts = destinations[0]["destination"].split(", ")
38
- if len(dest_parts) > 1:
39
- country = dest_parts[1]
40
- safety = country_tool.forward(country=country, info_type="security")
41
- print(f" Input: '{country}' → Output: {safety[:200]}...\n")
42
-
43
- # Test 5: FlightsFinderTool
44
- print("5️⃣ Testing FlightsFinderTool...")
45
- flights_tool = FlightsFinderTool()
46
- if destinations and len(destinations) > 0:
47
- dest_airport = destinations[0]["departure"]["to_airport"]
48
- flights = flights_tool.forward(
49
- departure_airport="CDG",
50
- arrival_airport=dest_airport,
51
- outbound_date="2025-06-15",
52
- return_date="2025-06-22"
53
- )
54
- print(f" Input: CDG → {dest_airport} → Output: {flights[:200]}...\n")
55
-
56
- print("✅ All tools tested!")
57
-
58
- if __name__ == "__main__":
59
- test_tools()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/__pycache__/country_info_tool.cpython-310.pyc DELETED
Binary file (23.6 kB)
 
tools/__pycache__/final_answer.cpython-310.pyc DELETED
Binary file (918 Bytes)
 
tools/__pycache__/final_answer.cpython-313.pyc DELETED
Binary file (1.07 kB)
 
tools/__pycache__/find_flight.cpython-310.pyc DELETED
Binary file (3.58 kB)
 
tools/__pycache__/find_flight.cpython-313.pyc DELETED
Binary file (4.53 kB)
 
tools/__pycache__/mood_to_need.cpython-310.pyc DELETED
Binary file (2.25 kB)
 
tools/__pycache__/mood_to_need.cpython-313.pyc DELETED
Binary file (1.88 kB)
 
tools/__pycache__/need_to_destination.cpython-310.pyc DELETED
Binary file (2.56 kB)
 
tools/__pycache__/need_to_destination.cpython-313.pyc DELETED
Binary file (3.4 kB)
 
tools/__pycache__/weather_tool.cpython-310.pyc DELETED
Binary file (10 kB)
 
tools/__pycache__/weather_tool.cpython-313.pyc DELETED
Binary file (10.4 kB)
 
tools/__pycache__/weather_tool_v2.cpython-310.pyc DELETED
Binary file (10.2 kB)
 
tools/country_info_tool.py DELETED
@@ -1,729 +0,0 @@
1
- from typing import Any
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 = "Retrieves important contextual information about a country in real-time: security, current events, national holidays, political climate, travel advice."
14
- inputs = {
15
- 'country': {'type': 'string', 'description': 'Country name in French or English (e.g., "France", "United States", "Japan")'},
16
- 'info_type': {'type': 'string', 'description': 'Type of information requested: "all" (recommended), "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('ANTROPIC_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: 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"❌ Country not recognized: '{country}'. Try with the full name (e.g., 'France', 'United States', 'United Kingdom')"
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"❌ No information available for {country_normalized} currently."
139
-
140
- # Assembler le rapport final
141
- result = f"🌍 **Contextual Information for {country_normalized}**\n"
142
- result += f"*Updated: {datetime.now().strftime('%m/%d/%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"❌ Error retrieving information: {str(e)}"
155
-
156
- def _normalize_country_name(self, country: 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):
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):
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"🛡️ **Security and Travel Advice**\n"
269
- result += f"{security_level} **Level determined by real-time analysis**\n"
270
- result += f"📋 {description}\n"
271
- result += f"🎯 **Recommendation: {recommendation}**"
272
-
273
- return result
274
-
275
- except Exception as e:
276
- return f"🛡️ **Security**: Error during retrieval - {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 not available",
386
- "❓ Anthropic API key required for security analysis")
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"Very high risk country - active conflict or war",
401
- "🚫 CHANGE DESTINATION - Active conflict zone")
402
- elif risk_level == "MODERATE_RISK":
403
- return ("🟡",
404
- f"Moderate risk country - political instability",
405
- "⚠️ Travel possible with enhanced precautions")
406
- else:
407
- return ("🟢",
408
- f"No recent security news found",
409
- "✅ Destination considered safe")
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
- "Claude analysis error",
419
- "❓ Unable to analyze security currently")
420
-
421
- except Exception:
422
- return ("⚪", "Analysis impossible", "❓ Consult official sources")
423
-
424
- def _llm_security_analysis(self, country: str, news_content: str, risk_level: str = "UNKNOWN"):
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"""Analyze the following recent news about {country} and determine the security level for a traveler:
431
-
432
- KNOWN RISK LEVEL: {risk_level}
433
- - HIGH_RISK = Country in active war or major conflict
434
- - MODERATE_RISK = Country with significant political instability
435
- - TENSION = Country with geopolitical tensions
436
- - UNKNOWN = No special classification
437
-
438
- RECENT NEWS:
439
- {news_content}
440
-
441
- CRITICAL INSTRUCTIONS:
442
- 1. If RISK LEVEL = HIGH_RISK, you MUST recommend CHANGE_DESTINATION unless clear evidence of improvement
443
- 2. For Ukraine, Palestine, Afghanistan, Syria, Yemen: ALWAYS RED/CHANGE_DESTINATION
444
- 3. Analyze risk level for a civilian tourist/traveler
445
- 4. Be VERY STRICT - traveler safety is priority
446
-
447
- Respond ONLY in the following JSON format:
448
-
449
- {{
450
- "level": "RED|YELLOW|GREEN",
451
- "description": "Short situation description (max 100 characters)",
452
- "recommendation": "CHANGE_DESTINATION|ENHANCED_PRECAUTIONS|SAFE_DESTINATION",
453
- "justification": "Explanation of your decision (max 200 characters)"
454
- }}
455
-
456
- STRICT Criteria:
457
- - RED/CHANGE_DESTINATION: active war, armed conflict, active terrorism, coup, widespread violence, combat zones
458
- - YELLOW/ENHANCED_PRECAUTIONS: violent protests, very high crime, political instability, ethnic tensions
459
- - GREEN/SAFE_DESTINATION: no major risks for civilians
460
-
461
- ABSOLUTE PRIORITY: Protect travelers - when in doubt, choose the strictest security level."""
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
- level = result.get('level', 'GREEN')
478
- description = result.get('description', 'Analysis completed')
479
- recommendation = result.get('recommendation', 'SAFE_DESTINATION')
480
- justification = result.get('justification', '')
481
-
482
- # Convertir en format attendu
483
- if level == 'RED':
484
- emoji = "🔴"
485
- advice = "🚫 CHANGE DESTINATION - " + justification
486
- elif level == 'YELLOW':
487
- emoji = "🟡"
488
- advice = "⚠️ Travel possible with enhanced precautions - " + justification
489
- else:
490
- emoji = "🟢"
491
- advice = "✅ Destination considered safe - " + 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
- """Retrieves current events via web search"""
505
- try:
506
- # Search for recent events
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"📅 **Events**: No major events detected for {country}"
512
-
513
- result = f"📅 **Current Events and Context**\n"
514
- for i, event in enumerate(events_data[:5], 1):
515
- title = event.get('title', 'Event not specified')
516
- result += f"• {title}\n"
517
-
518
- return result.rstrip()
519
-
520
- except Exception:
521
- return "📅 **Events**: Error during retrieval"
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
- """Retrieves national holidays via API"""
551
- try:
552
- country_code = self.country_codes.get(country, '')
553
-
554
- # If no code in our mapping, try to get it 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"🎉 **Holidays**: Country code not found for {country}"
560
-
561
- # Use Calendarific API or similar
562
- holidays_data = self._fetch_holidays_api(country_code)
563
-
564
- if not holidays_data:
565
- return f"🎉 **Holidays**: Information not available for {country}"
566
-
567
- current_month = datetime.now().month
568
- current_year = datetime.now().year
569
-
570
- result = f"🎉 **Holidays and Seasonal Events**\n"
571
-
572
- # Filter holidays from current month and upcoming months
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"**Upcoming holidays:**\n"
584
- for holiday in upcoming_holidays[:5]:
585
- name = holiday.get('name', 'Unknown holiday')
586
- date = holiday.get('date', '')
587
- result += f"• {name} ({date})\n"
588
- else:
589
- result += f"**No major holidays scheduled in the coming months**\n"
590
-
591
- return result.rstrip()
592
-
593
- except Exception:
594
- return "🎉 **Holidays**: Error during retrieval"
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
- """Retrieves travel information via REST Countries API"""
614
- try:
615
- # Use REST Countries API
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
- # Extract information
625
- currencies = country_data.get('currencies', {})
626
- languages = country_data.get('languages', {})
627
- region = country_data.get('region', 'Unknown')
628
-
629
- currency_name = list(currencies.keys())[0] if currencies else 'Unknown'
630
- language_list = list(languages.values()) if languages else ['Unknown']
631
-
632
- result = f"✈️ **Practical Travel Information**\n"
633
- result += f"💰 Currency: {currency_name}\n"
634
- result += f"🗣️ Languages: {', '.join(language_list[:3])}\n"
635
- result += f"🌍 Region: {region}\n"
636
- result += f"📋 Check visa requirements on the country's official website"
637
-
638
- return result
639
-
640
- return f"✈️ **Travel**: Information not available for {country}"
641
-
642
- except Exception:
643
- return "✈️ **Travel**: Error during retrieval"
644
-
645
- def _get_political_info(self, country: str) -> str:
646
- """Retrieves political context via news search"""
647
- try:
648
- # Search for recent political news
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"🏛️ **Politics**: Stable situation for {country}"
654
-
655
- result = f"🏛️ **Political Context**\n"
656
-
657
- # Analyze political news
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 "🏛️ **Politics**: Error during retrieval"
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):
694
- """Uses Claude to generate an intelligent final recommendation"""
695
- try:
696
- if not self.claude_client:
697
- return None
698
-
699
- prompt = f"""Analyze this complete report about {country} and provide a concise final recommendation for a traveler:
700
-
701
- COMPLETE REPORT:
702
- {full_report}
703
-
704
- Your task:
705
- 1. Synthesize the most important information
706
- 2. Give a clear and actionable recommendation
707
- 3. Respond in English, maximum 200 words
708
- 4. Use a professional but accessible tone
709
- 5. If risks exist, be explicit about precautions
710
-
711
- Desired response format:
712
- 🎯 **FINAL RECOMMENDATION**
713
- [Your synthetic analysis and recommendation]
714
-
715
- If the destination is dangerous, clearly use "CHANGE DESTINATION" in your response."""
716
-
717
- response = self.claude_client.messages.create(
718
- model="claude-3-opus-20240229",
719
- max_tokens=250,
720
- temperature=0.2,
721
- system="You are an expert travel advisor. Provide clear and practical recommendations.",
722
- messages=[
723
- {"role": "user", "content": prompt}
724
- ]
725
- )
726
- return response.content[0].text.strip()
727
-
728
- except Exception:
729
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/find_flight.py DELETED
@@ -1,123 +0,0 @@
1
- import os
2
- from typing import Optional
3
- from smolagents.tools import Tool
4
- import serpapi
5
- from dotenv import load_dotenv
6
- load_dotenv() # Loads variables from .env into environment
7
-
8
-
9
-
10
- class FlightsFinderTool(Tool):
11
- name = "flights_finder"
12
- description = "Find flights using the Google Flights engine."
13
- inputs = {
14
- 'departure_airport': {'type': 'string', 'description': 'Departure airport code (IATA)'},
15
- 'arrival_airport': {'type': 'string', 'description': 'Arrival airport code (IATA)'},
16
- 'outbound_date': {'type': 'string', 'description': 'Outbound date in YYYY-MM-DD format'},
17
- 'return_date': {'type': 'string', 'description': 'Return date in YYYY-MM-DD format'},
18
- 'adults': {'type': 'integer', 'default': 1, 'nullable': True, 'description': 'Number of adults'},
19
- 'children': {'type': 'integer', 'default': 0, 'nullable': True, 'description': 'Number of children'},
20
- }
21
- output_type = "string"
22
-
23
- @staticmethod
24
- def find_flight(
25
- departure_airport: Optional[str] = None,
26
- arrival_airport: Optional[str] = None,
27
- date: Optional[str] = None,
28
- adults: Optional[int] = 1,
29
- children: Optional[int] = 0,
30
- ) -> str:
31
- """
32
- Finds the cheapest one-way flight for a given route and date.
33
-
34
- Args:
35
- departure_airport (str): Departure airport code (IATA)
36
- arrival_airport (str): Arrival airport code (IATA)
37
- date (str): Flight date in YYYY-MM-DD format
38
- adults (int): Number of adults
39
- children (int): Number of children
40
- infants_in_seat (int): Number of infants in seat
41
- infants_on_lap (int): Number of lap infants
42
-
43
- Returns:
44
- str: Formatted string with cheapest flight details
45
- """
46
- params = {
47
- 'api_key': os.getenv("SERPAPI_API_KEY"),
48
- 'engine': 'google_flights',
49
- 'hl': 'en',
50
- 'gl': 'us',
51
- 'departure_id': departure_airport,
52
- 'arrival_id': arrival_airport,
53
- 'outbound_date': date,
54
- 'currency': 'USD',
55
- 'adults': adults,
56
- 'children': children,
57
- 'type': 2,
58
- }
59
-
60
- try:
61
- search = serpapi.search(params)
62
- flights = search.data.get("best_flights", [])
63
- if not flights:
64
- return "No flights found."
65
-
66
- # Find the flight with the lowest price
67
- cheapest = min(flights, key=lambda f: f.get("price", float("inf")))
68
-
69
- if not cheapest.get("flights"):
70
- return "No flight segments found."
71
-
72
- flight = cheapest["flights"][0]
73
- dep = flight["departure_airport"]
74
- arr = flight["arrival_airport"]
75
- dep_time = dep["time"]
76
- arr_time = arr["time"]
77
- duration = flight["duration"]
78
- airline = flight.get("airline", "Unknown")
79
- price = cheapest["price"]
80
-
81
- hours = duration // 60
82
- minutes = duration % 60
83
- duration_str = f"{hours}h {minutes}m"
84
-
85
- return (
86
- f"From {dep['id']} at {dep_time} → {arr['id']} at {arr_time} | "
87
- f"Duration: {duration_str}\nAirline: {airline} | Price: ${price}"
88
- )
89
-
90
- except Exception as e:
91
- return f"Error occurred: {e}"
92
-
93
- def forward(
94
- self,
95
- departure_airport: str,
96
- arrival_airport: str,
97
- outbound_date: str,
98
- return_date: str,
99
- adults: int = 1,
100
- children: int = 0,
101
- ) -> str:
102
- outbound = self.find_flight(
103
- departure_airport=departure_airport,
104
- arrival_airport=arrival_airport,
105
- date=outbound_date,
106
- adults=adults,
107
- children=children,
108
- )
109
-
110
- inbound = self.find_flight(
111
- departure_airport=arrival_airport,
112
- arrival_airport=departure_airport,
113
- date=return_date,
114
- adults=adults,
115
- children=children,
116
- )
117
-
118
- return f"✈️ Outbound Flight:\n{outbound}\n\n🛬 Inbound Flight:\n{inbound}"
119
-
120
-
121
- def __init__(self, *args, **kwargs):
122
- self.is_initialized = False
123
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/mock_tools.py DELETED
@@ -1,81 +0,0 @@
1
- from smolagents.tools import tool
2
-
3
- @tool
4
- def mood_to_need(mood: str) -> str:
5
- """
6
- Map a mood to a need.
7
-
8
- Args:
9
- mood (str): The current emotional state of the user.
10
-
11
- Returns:
12
- str: A description of what the user needs based on the mood.
13
- """
14
- return "You need relaxation and nature."
15
-
16
- @tool
17
- def need_to_destination(need: str) -> str:
18
- """
19
- Map a need to a destination.
20
-
21
- Args:
22
- need (str): The user's identified need (e.g., relaxation, adventure).
23
-
24
- Returns:
25
- str: A suggested destination that fulfills the need.
26
- """
27
- return "Bali, Indonesia"
28
-
29
- @tool
30
- def get_weather(dest: str) -> str:
31
- """
32
- Get weather forecast for a destination.
33
-
34
- Args:
35
- dest (str): The destination location.
36
-
37
- Returns:
38
- str: A weather forecast for the given destination.
39
- """
40
- return "Sunny and 28°C"
41
-
42
- @tool
43
- def get_flights(dest: str) -> str:
44
- """
45
- Get flight options for a destination.
46
-
47
- Args:
48
- dest (str): The destination location.
49
-
50
- Returns:
51
- str: A list of flight options for the destination.
52
- """
53
- return "Flight from Paris to Bali: €600 roundtrip"
54
-
55
- @tool
56
- def final_wrap(info: str) -> str:
57
- """
58
- Create a final wrap-up message.
59
-
60
- Args:
61
- info (str): Summary information about the destination and travel.
62
-
63
- Returns:
64
- str: A personalized wrap-up message.
65
- """
66
- return f"Bali sounds like a perfect place for relaxation with great weather and affordable flights!"
67
-
68
- @tool
69
- def final_answer_tool(answer: str) -> str:
70
- """
71
- Provides a final answer to the user.
72
-
73
- Args:
74
- answer (str): The final recommendation or conclusion.
75
-
76
- Returns:
77
- str: The same final answer.
78
- """
79
- return answer
80
-
81
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/mood_to_need.py DELETED
@@ -1,62 +0,0 @@
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.
9
-
10
- Attributes:
11
- model: A callable language model used to generate the output.
12
- """
13
- name = "MoodToNeed"
14
- inputs = {
15
- "mood": {"type": "string", "description": "User's mood as text"},
16
- }
17
- output_type = "string"
18
-
19
- description = "Converts user mood into a travel-related need."
20
-
21
- def __init__(self, model: callable) -> None:
22
- """
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:
30
- """
31
- Generates a vacation need from a user mood string.
32
-
33
- Args:
34
- mood: A string describing the user's emotional state.
35
-
36
- Returns:
37
- A short string describing the travel-related need.
38
- """
39
- prompt = (
40
- f"Given the user's mood, suggest a travel need.\n"
41
- f'Mood: "{mood}"\n'
42
- f'Return only the need, no explanation.\n'
43
- f'Example:\n'
44
- f'Mood: "I am exhausted" → Need: "A calm wellness retreat"\n'
45
- f'Mood: "{mood}"\n'
46
- f'Need:'
47
- )
48
- response = self.model(prompt)
49
- return response.strip()
50
-
51
- client = Anthropic(api_key=os.getenv("ANTROPIC_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 DELETED
@@ -1,69 +0,0 @@
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"
10
- inputs = {
11
- "need": {"type": "string", "description": "User's travel need as text"},
12
- }
13
- output_type = "array"
14
- description = "Suggests destinations and flight info based on user need."
15
-
16
- def __init__(self, model: callable, departure_airport: str = "CDG") -> None:
17
- super().__init__()
18
- self.model = model
19
- self.departure_airport = departure_airport
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())
52
- except json.JSONDecodeError:
53
- raise ValueError("Could not parse LLM output to JSON.")
54
-
55
- return destinations
56
-
57
-
58
- client = Anthropic(api_key=os.getenv("ANTROPIC_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 DELETED
@@ -1,21 +0,0 @@
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/visit_webpage.py CHANGED
@@ -3,7 +3,6 @@ from smolagents.tools import Tool
3
  import requests
4
  import markdownify
5
  import smolagents
6
- import re
7
 
8
  class VisitWebpageTool(Tool):
9
  name = "visit_webpage"
 
3
  import requests
4
  import markdownify
5
  import smolagents
 
6
 
7
  class VisitWebpageTool(Tool):
8
  name = "visit_webpage"
tools/weather_tool.py DELETED
@@ -1,285 +0,0 @@
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 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"
20
-
21
- def __init__(self, api_key: Optional[str] = None):
22
- super().__init__()
23
- # Charger les variables d'environnement depuis le fichier .env
24
- load_dotenv()
25
-
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('ANTROPIC_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
40
-
41
- if not used_api_key:
42
- return "Erreur: Clé API OpenWeatherMap requise. Ajoutez OPENWEATHER_API_KEY dans votre fichier .env ou obtenez une clé gratuite sur https://openweathermap.org/api"
43
-
44
- # Parser la date si fournie
45
- target_date = None
46
- if date:
47
- try:
48
- target_date = datetime.strptime(date, "%Y-%m-%d")
49
- except ValueError:
50
- return f"Erreur: Format de date invalide. Utilisez YYYY-MM-DD (ex: 2024-01-15)"
51
-
52
- # Obtenir les coordonnées de la localisation
53
- geo_url = f"http://api.openweathermap.org/geo/1.0/direct"
54
- geo_params = {
55
- 'q': location,
56
- 'limit': 1,
57
- 'appid': used_api_key
58
- }
59
-
60
- geo_response = requests.get(geo_url, params=geo_params, timeout=10)
61
- geo_response.raise_for_status()
62
- geo_data = geo_response.json()
63
-
64
- if not geo_data:
65
- return f"Erreur: Localisation '{location}' non trouvée. Essayez avec le nom d'une ville ou d'un pays plus précis."
66
-
67
- lat = geo_data[0]['lat']
68
- lon = geo_data[0]['lon']
69
- country = geo_data[0].get('country', '')
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."
91
- except requests.exceptions.HTTPError as e:
92
- if e.response.status_code == 401:
93
- return "Erreur 401: Clé API invalide ou non activée. Vérifiez votre clé API OpenWeatherMap et assurez-vous qu'elle est activée (peut prendre quelques heures après création)."
94
- elif e.response.status_code == 429:
95
- return "Erreur 429: Limite de requêtes dépassée. Attendez avant de refaire une requête."
96
- else:
97
- return f"Erreur HTTP {e.response.status_code}: {str(e)}"
98
- except requests.exceptions.RequestException as e:
99
- return f"Erreur de requête: {str(e)}"
100
- except Exception as e:
101
- return f"Erreur inattendue: {str(e)}"
102
-
103
- def _get_weather(self, lat: float, lon: float, city_name: str, country: str, target_date: Optional[datetime], api_key: str) -> str:
104
- """Utilise l'API gratuite 2.5"""
105
-
106
- if not target_date or target_date.date() == datetime.now().date():
107
- # Météo actuelle
108
- weather_url = f"{self.base_url}/weather"
109
- params = {
110
- 'lat': lat,
111
- 'lon': lon,
112
- 'appid': api_key,
113
- 'units': 'metric',
114
- 'lang': 'fr'
115
- }
116
-
117
- response = requests.get(weather_url, params=params, timeout=10)
118
- response.raise_for_status()
119
- data = response.json()
120
-
121
- return self._format_current_weather(data, city_name, country)
122
-
123
- elif target_date and target_date <= datetime.now() + timedelta(days=5):
124
- # Prévisions sur 5 jours
125
- forecast_url = f"{self.base_url}/forecast"
126
- params = {
127
- 'lat': lat,
128
- 'lon': lon,
129
- 'appid': api_key,
130
- 'units': 'metric',
131
- 'lang': 'fr'
132
- }
133
-
134
- response = requests.get(forecast_url, params=params, timeout=10)
135
- response.raise_for_status()
136
- data = response.json()
137
-
138
- return self._format_forecast_weather(data, city_name, country, target_date)
139
- else:
140
- return "Erreur: Les prévisions ne sont disponibles que pour les 5 prochains jours maximum."
141
-
142
- def _format_current_weather(self, data: dict, city_name: str, country: str) -> str:
143
- """Formate les données météo actuelles"""
144
- try:
145
- weather = data['weather'][0]
146
- main = data['main']
147
- wind = data.get('wind', {})
148
-
149
- result = f"🌤️ **Météo actuelle pour {city_name}, {country}**\n\n"
150
- result += f"**Conditions:** {weather['description'].title()}\n"
151
- result += f"**Température:** {main['temp']:.1f}°C (ressenti: {main['feels_like']:.1f}°C)\n"
152
- result += f"**Humidité:** {main['humidity']}%\n"
153
- result += f"**Pression:** {main['pressure']} hPa\n"
154
-
155
- if 'speed' in wind:
156
- result += f"**Vent:** {wind['speed']} m/s"
157
- if 'deg' in wind:
158
- result += f" ({self._wind_direction(wind['deg'])})"
159
- result += "\n"
160
-
161
- if 'visibility' in data:
162
- result += f"**Visibilité:** {data['visibility']/1000:.1f} km\n"
163
-
164
- return result
165
-
166
- except KeyError as e:
167
- return f"Erreur lors du formatage des données météo: {str(e)}"
168
-
169
- def _format_forecast_weather(self, data: dict, city_name: str, country: str, target_date: datetime) -> str:
170
- """Formate les prévisions météo pour une date spécifique"""
171
- try:
172
- target_date_str = target_date.strftime("%Y-%m-%d")
173
-
174
- # Trouver les prévisions pour la date cible
175
- forecasts_for_date = []
176
- for forecast in data['list']:
177
- forecast_date = datetime.fromtimestamp(forecast['dt']).strftime("%Y-%m-%d")
178
- if forecast_date == target_date_str:
179
- forecasts_for_date.append(forecast)
180
-
181
- if not forecasts_for_date:
182
- return f"Aucune prévision disponible pour le {target_date_str}"
183
-
184
- result = f"🌤️ **Prévisions météo pour {city_name}, {country} - {target_date.strftime('%d/%m/%Y')}**\n\n"
185
-
186
- for i, forecast in enumerate(forecasts_for_date):
187
- time = datetime.fromtimestamp(forecast['dt']).strftime("%H:%M")
188
- weather = forecast['weather'][0]
189
- main = forecast['main']
190
- wind = forecast.get('wind', {})
191
-
192
- result += f"**{time}:**\n"
193
- result += f" • Conditions: {weather['description'].title()}\n"
194
- result += f" • Température: {main['temp']:.1f}°C (ressenti: {main['feels_like']:.1f}°C)\n"
195
- result += f" • Humidité: {main['humidity']}%\n"
196
-
197
- if 'speed' in wind:
198
- result += f" • Vent: {wind['speed']} m/s"
199
- if 'deg' in wind:
200
- result += f" ({self._wind_direction(wind['deg'])})"
201
- result += "\n"
202
-
203
- if i < len(forecasts_for_date) - 1:
204
- result += "\n"
205
-
206
- return result
207
-
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