Spaces:
Runtime error
Runtime error
test
#1
by
hugit0
- opened
- .env +0 -3
- Gradio_UI.py +0 -121
- __pycache__/Gradio_UI.cpython-313.pyc +0 -0
- app.py +14 -31
- prompts.yaml +0 -137
- requirements.txt +1 -8
- test_tools.py +0 -59
- tools/__pycache__/country_info_tool.cpython-310.pyc +0 -0
- tools/__pycache__/final_answer.cpython-310.pyc +0 -0
- tools/__pycache__/final_answer.cpython-313.pyc +0 -0
- tools/__pycache__/find_flight.cpython-310.pyc +0 -0
- tools/__pycache__/find_flight.cpython-313.pyc +0 -0
- tools/__pycache__/mood_to_need.cpython-310.pyc +0 -0
- tools/__pycache__/mood_to_need.cpython-313.pyc +0 -0
- tools/__pycache__/need_to_destination.cpython-310.pyc +0 -0
- tools/__pycache__/need_to_destination.cpython-313.pyc +0 -0
- tools/__pycache__/weather_tool.cpython-310.pyc +0 -0
- tools/__pycache__/weather_tool.cpython-313.pyc +0 -0
- tools/__pycache__/weather_tool_v2.cpython-310.pyc +0 -0
- tools/country_info_tool.py +0 -729
- tools/find_flight.py +0 -123
- tools/mock_tools.py +0 -81
- tools/mood_to_need.py +0 -62
- tools/need_to_destination.py +0 -69
- tools/test_mood_to_destination.py +0 -21
- tools/visit_webpage.py +0 -1
- tools/weather_tool.py +0 -285
.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,
|
2 |
-
from tools.mood_to_need import
|
3 |
-
from tools.need_to_destination import
|
4 |
-
from tools.
|
5 |
-
from tools.
|
|
|
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 =
|
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 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
],
|
50 |
-
max_steps=
|
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.
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|