Spaces:
Sleeping
Sleeping
Joar Paganus
commited on
Commit
·
2b605ad
1
Parent(s):
b76a59a
updated agent
Browse files- agent.py +50 -58
- app.py +23 -52
- requirements.txt +4 -1
- weather.py +98 -0
agent.py
CHANGED
|
@@ -28,14 +28,6 @@ INSTRUCTIONS FOR USING TOOL RESULTS:
|
|
| 28 |
- Use those results to answer the user’s latest question.
|
| 29 |
- Summarize the results naturally. Do NOT restate the log format.
|
| 30 |
- NEVER reproduce or invent <tool_results> blocks.
|
| 31 |
-
- NEVER output lines of the form:
|
| 32 |
-
"You have executed the following tools..."
|
| 33 |
-
"- tool_name(args_dict) -> result"
|
| 34 |
-
- NEVER fabricate new tool calls or logs.
|
| 35 |
-
|
| 36 |
-
WHEN THERE ARE NO TOOL RESULTS:
|
| 37 |
-
- Answer based on weather knowledge, common sense, and conversation context.
|
| 38 |
-
- Still avoid any mention of “tools”, “calls”, “executions”, or logs.
|
| 39 |
|
| 40 |
YOUR OUTPUT:
|
| 41 |
- Your entire reply must be ONLY natural language directed to the user.
|
|
@@ -111,25 +103,46 @@ def parse_tool_calls(tool_output: str):
|
|
| 111 |
calls.append((func_name, kwargs))
|
| 112 |
return calls
|
| 113 |
|
| 114 |
-
def
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
max_tokens=max_tokens,
|
| 118 |
temperature=temperature,
|
| 119 |
top_p=top_p,
|
| 120 |
-
|
| 121 |
-
stream=False,
|
| 122 |
)
|
| 123 |
-
return out["choices"][0]["text"]
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
def select_tools_with_llm(llm, user_message: str, tools_schema: list) -> list:
|
| 135 |
tool_selection_system = f"""You are an expert in composing functions. You are given a question and a set of possible functions.
|
|
@@ -137,24 +150,18 @@ Based on the question, you will need to make one or more function/tool calls to
|
|
| 137 |
If none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out. You should only return the function call in tools call sections.
|
| 138 |
|
| 139 |
If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1=params_value1, params_name2=params_value2...), func_name2(params)]
|
| 140 |
-
|
| 141 |
|
| 142 |
Here is a list of functions in JSON format that you can invoke:
|
| 143 |
|
| 144 |
{json.dumps(tools_schema, indent=2)}
|
| 145 |
"""
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
raw =
|
| 152 |
-
llm,
|
| 153 |
-
prompt,
|
| 154 |
-
max_tokens=256,
|
| 155 |
-
temperature=0.2,
|
| 156 |
-
top_p=0.95,
|
| 157 |
-
)
|
| 158 |
return parse_tool_calls(raw), raw
|
| 159 |
|
| 160 |
def call_tools(tool_calls, tool_registry):
|
|
@@ -177,7 +184,7 @@ def call_tools(tool_calls, tool_registry):
|
|
| 177 |
results.append({"name": func_name, "args": kwargs, "result": res})
|
| 178 |
return results
|
| 179 |
|
| 180 |
-
def respond(
|
| 181 |
if tools is None:
|
| 182 |
tools = []
|
| 183 |
|
|
@@ -186,12 +193,8 @@ def respond(message, history, system_message, llm, tools=None):
|
|
| 186 |
tools_schema = [function_to_json(f) for f in tools]
|
| 187 |
|
| 188 |
# 2. Let the LLM select tools based on the message
|
| 189 |
-
tool_calls, initial_message = select_tools_with_llm(llm,
|
| 190 |
|
| 191 |
-
# We wanted to use the inital_message above in the response, for example
|
| 192 |
-
# when parameters are missing but the model was too bad at making such responses
|
| 193 |
-
# that it had to be omitted for now.
|
| 194 |
-
|
| 195 |
# 3. Call tools if needed, otherwise respond
|
| 196 |
if tool_calls and tools:
|
| 197 |
tool_results = call_tools(tool_calls, tool_registry)
|
|
@@ -199,22 +202,11 @@ def respond(message, history, system_message, llm, tools=None):
|
|
| 199 |
for tr in tool_results:
|
| 200 |
tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
|
| 201 |
final_system_message = f"{system_message}{LAST_SYSTEM_MESSAGE} {tool_info_str}</tool_results>\n"
|
| 202 |
-
else:
|
| 203 |
-
final_system_message = system_message
|
| 204 |
|
| 205 |
-
|
| 206 |
-
prompt = build_prompt(final_system_message, history, message)
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
stop=["User:", "System:"],
|
| 214 |
-
stream=True,
|
| 215 |
-
)
|
| 216 |
-
partial = ""
|
| 217 |
-
for out in stream:
|
| 218 |
-
token = out["choices"][0]["text"]
|
| 219 |
-
partial += token
|
| 220 |
-
yield partial
|
|
|
|
| 28 |
- Use those results to answer the user’s latest question.
|
| 29 |
- Summarize the results naturally. Do NOT restate the log format.
|
| 30 |
- NEVER reproduce or invent <tool_results> blocks.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
YOUR OUTPUT:
|
| 33 |
- Your entire reply must be ONLY natural language directed to the user.
|
|
|
|
| 103 |
calls.append((func_name, kwargs))
|
| 104 |
return calls
|
| 105 |
|
| 106 |
+
def add_history(user_message, history, system_message):
|
| 107 |
+
new_history = [{"role": "system", "content": system_message}]
|
| 108 |
+
if history:
|
| 109 |
+
for el in history:
|
| 110 |
+
if el["role"] == "user":
|
| 111 |
+
user = el["content"][0]["text"]
|
| 112 |
+
new_history.append({"role": "user", "content": user})
|
| 113 |
+
elif el["role"] == "assistant":
|
| 114 |
+
user = el["content"][0]["text"]
|
| 115 |
+
new_history.append({"role": "assistant", "content": user})
|
| 116 |
+
new_history.append({"role": "user", "content": user_message})
|
| 117 |
+
return new_history
|
| 118 |
+
|
| 119 |
+
def generate_chat(llm, messages, max_tokens=256, temperature=0.2, top_p=0.95):
|
| 120 |
+
completion_stream = llm.create_chat_completion(
|
| 121 |
+
messages=messages,
|
| 122 |
max_tokens=max_tokens,
|
| 123 |
temperature=temperature,
|
| 124 |
top_p=top_p,
|
| 125 |
+
stream=True,
|
|
|
|
| 126 |
)
|
|
|
|
| 127 |
|
| 128 |
+
answer = ""
|
| 129 |
+
for chunk in completion_stream:
|
| 130 |
+
delta = chunk["choices"][0].get("delta", {})
|
| 131 |
+
token = delta.get("content", None)
|
| 132 |
+
if token:
|
| 133 |
+
answer += token
|
| 134 |
+
yield answer
|
| 135 |
+
|
| 136 |
+
def generate_non_stream_chat(llm, messages, max_tokens=256, temperature=0.2, top_p=0.95):
|
| 137 |
+
res = llm.create_chat_completion(
|
| 138 |
+
messages=messages,
|
| 139 |
+
max_tokens=max_tokens,
|
| 140 |
+
temperature=temperature,
|
| 141 |
+
top_p=top_p,
|
| 142 |
+
stream=False,
|
| 143 |
+
)
|
| 144 |
+
# Return just the final text
|
| 145 |
+
return res["choices"][0]["message"]["content"]
|
| 146 |
|
| 147 |
def select_tools_with_llm(llm, user_message: str, tools_schema: list) -> list:
|
| 148 |
tool_selection_system = f"""You are an expert in composing functions. You are given a question and a set of possible functions.
|
|
|
|
| 150 |
If none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out. You should only return the function call in tools call sections.
|
| 151 |
|
| 152 |
If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1=params_value1, params_name2=params_value2...), func_name2(params)]
|
| 153 |
+
If you call a function, you SHOULD NOT include any other text in the response.
|
| 154 |
|
| 155 |
Here is a list of functions in JSON format that you can invoke:
|
| 156 |
|
| 157 |
{json.dumps(tools_schema, indent=2)}
|
| 158 |
"""
|
| 159 |
+
|
| 160 |
+
messages = [
|
| 161 |
+
{"role": "system", "content": tool_selection_system},
|
| 162 |
+
{"role": "user", "content": user_message},
|
| 163 |
+
]
|
| 164 |
+
raw = generate_non_stream_chat(llm, messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
return parse_tool_calls(raw), raw
|
| 166 |
|
| 167 |
def call_tools(tool_calls, tool_registry):
|
|
|
|
| 184 |
results.append({"name": func_name, "args": kwargs, "result": res})
|
| 185 |
return results
|
| 186 |
|
| 187 |
+
def respond(user_message, history, system_message, llm, tools=None):
|
| 188 |
if tools is None:
|
| 189 |
tools = []
|
| 190 |
|
|
|
|
| 193 |
tools_schema = [function_to_json(f) for f in tools]
|
| 194 |
|
| 195 |
# 2. Let the LLM select tools based on the message
|
| 196 |
+
tool_calls, initial_message = select_tools_with_llm(llm, user_message, tools_schema)
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
# 3. Call tools if needed, otherwise respond
|
| 199 |
if tool_calls and tools:
|
| 200 |
tool_results = call_tools(tool_calls, tool_registry)
|
|
|
|
| 202 |
for tr in tool_results:
|
| 203 |
tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
|
| 204 |
final_system_message = f"{system_message}{LAST_SYSTEM_MESSAGE} {tool_info_str}</tool_results>\n"
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
messages = add_history(user_message, history, final_system_message)
|
|
|
|
| 207 |
|
| 208 |
+
stream = generate_chat(llm, messages, temperature=0.7, top_p=0.95)
|
| 209 |
+
for out in stream:
|
| 210 |
+
yield out
|
| 211 |
+
else:
|
| 212 |
+
return initial_message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -20,10 +20,9 @@ except Exception as e:
|
|
| 20 |
print(f"--- INSTALLATION FAILED: {e} ---")
|
| 21 |
|
| 22 |
import gradio as gr
|
| 23 |
-
import wikipedia
|
| 24 |
-
from agent import respond,
|
| 25 |
from llama_cpp import Llama
|
| 26 |
-
import random
|
| 27 |
|
| 28 |
# ---------------- CONFIG ----------------
|
| 29 |
BASE_REPO_ID = "unsloth/Llama-3.2-3B-Instruct-GGUF"
|
|
@@ -31,6 +30,9 @@ BASE_FILENAME = "Llama-3.2-3B-Instruct-Q4_K_M.gguf"
|
|
| 31 |
|
| 32 |
FT_REPO_ID = "JoarP/Llama-3.2-3B-FineTome5K-gguf"
|
| 33 |
FT_FILENAME = "v1"
|
|
|
|
|
|
|
|
|
|
| 34 |
N_CTX = 2048
|
| 35 |
N_THREADS = 2
|
| 36 |
|
|
@@ -54,15 +56,23 @@ try:
|
|
| 54 |
)
|
| 55 |
AVAILABLE_MODELS["Llama 3.2 3B"] = llm_base
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
except Exception as e:
|
| 58 |
print(f"Error loading model: {e}")
|
| 59 |
raise e
|
| 60 |
|
| 61 |
|
| 62 |
#--------------- TO RUN LOCALLY -----------------
|
| 63 |
-
|
| 64 |
# llm = Llama(
|
| 65 |
-
# model_path="
|
| 66 |
# n_ctx=N_CTX,
|
| 67 |
# n_threads=None,
|
| 68 |
# )
|
|
@@ -72,7 +82,7 @@ except Exception as e:
|
|
| 72 |
# }
|
| 73 |
# ------------- FAST RESPONSE WITHOUT AGENT --------------
|
| 74 |
|
| 75 |
-
def respond_fast(
|
| 76 |
"""
|
| 77 |
Fast path: no tools, no agent. Just a single LLM call with the
|
| 78 |
given system message and chat history.
|
|
@@ -82,52 +92,16 @@ def respond_fast(message, history, model_choice):
|
|
| 82 |
if llm is None:
|
| 83 |
llm = next(iter(AVAILABLE_MODELS.values()))
|
| 84 |
|
| 85 |
-
|
| 86 |
-
formatted_history = format_history(history)
|
| 87 |
-
|
| 88 |
-
# Build a simple chat-style prompt
|
| 89 |
-
prompt = build_prompt("You are a helpful assistant. Just chat with the user.", formatted_history, message)
|
| 90 |
|
| 91 |
# Single streaming generation
|
| 92 |
-
stream = llm
|
| 93 |
-
prompt,
|
| 94 |
-
max_tokens=256,
|
| 95 |
-
temperature=0.7,
|
| 96 |
-
top_p=0.9,
|
| 97 |
-
stop=["User:", "System:"],
|
| 98 |
-
stream=True,
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
partial = ""
|
| 102 |
for out in stream:
|
| 103 |
-
|
| 104 |
-
partial += token
|
| 105 |
-
yield partial
|
| 106 |
-
|
| 107 |
-
# ------------- HELP FUNCTION ------------------
|
| 108 |
-
|
| 109 |
-
def format_history(history):
|
| 110 |
-
formatted_history = []
|
| 111 |
-
for turn in history:
|
| 112 |
-
role = turn.get("role", "user")
|
| 113 |
-
raw_content = turn.get("content", "")
|
| 114 |
-
block = raw_content[0]
|
| 115 |
-
content = block["text"]
|
| 116 |
-
formatted_history.append({
|
| 117 |
-
"role": role,
|
| 118 |
-
"content": content
|
| 119 |
-
})
|
| 120 |
-
return formatted_history
|
| 121 |
|
| 122 |
|
| 123 |
# ------------- TOOLS DEFINITIONS --------------
|
| 124 |
-
|
| 125 |
-
"""Returns weather. Args: location (city)."""
|
| 126 |
-
return random.choice(["cloudy", "rainy", "sunny", "foobar"])
|
| 127 |
-
|
| 128 |
-
def get_temperature(location: str) -> str:
|
| 129 |
-
"""Returns temperature. Args: location (city)."""
|
| 130 |
-
return random.choice(["-10", "0", "20", "30"])
|
| 131 |
|
| 132 |
def multiply(a: int, b: int) -> int:
|
| 133 |
"""Multiplies two integers. Args: a, b."""
|
|
@@ -152,7 +126,7 @@ def search_wikipedia(query: str) -> str:
|
|
| 152 |
AGENTS = {
|
| 153 |
"Weather": {
|
| 154 |
"system_message": "You are a helpful weather assistant",
|
| 155 |
-
"tools": [
|
| 156 |
},
|
| 157 |
"Math": {
|
| 158 |
"system_message": "You are a helpful math assistant",
|
|
@@ -164,7 +138,7 @@ AGENTS = {
|
|
| 164 |
},
|
| 165 |
}
|
| 166 |
|
| 167 |
-
# ------------- WRAPPER FUNCTION ----------------
|
| 168 |
def app_respond(message, history, model_choice, agent_choice):
|
| 169 |
"""Pass the selected agent to our agentic framwork"""
|
| 170 |
llm = AVAILABLE_MODELS.get(model_choice)
|
|
@@ -173,12 +147,9 @@ def app_respond(message, history, model_choice, agent_choice):
|
|
| 173 |
|
| 174 |
agent_config = AGENTS.get(agent_choice)
|
| 175 |
|
| 176 |
-
# Conversion logic
|
| 177 |
-
formatted_history = format_history(history)
|
| 178 |
-
|
| 179 |
for chunk in respond(
|
| 180 |
message,
|
| 181 |
-
|
| 182 |
system_message=agent_config["system_message"],
|
| 183 |
llm=llm,
|
| 184 |
tools=agent_config["tools"]
|
|
|
|
| 20 |
print(f"--- INSTALLATION FAILED: {e} ---")
|
| 21 |
|
| 22 |
import gradio as gr
|
| 23 |
+
import wikipedia
|
| 24 |
+
from agent import respond, generate_chat, add_history
|
| 25 |
from llama_cpp import Llama
|
|
|
|
| 26 |
|
| 27 |
# ---------------- CONFIG ----------------
|
| 28 |
BASE_REPO_ID = "unsloth/Llama-3.2-3B-Instruct-GGUF"
|
|
|
|
| 30 |
|
| 31 |
FT_REPO_ID = "JoarP/Llama-3.2-3B-FineTome5K-gguf"
|
| 32 |
FT_FILENAME = "v1"
|
| 33 |
+
|
| 34 |
+
FT_REPO_ID_2 = "JoarP/Llama-3.2-3B-Finetuning"
|
| 35 |
+
FT_FILENAME_2 = "FuncCall-Synthetic-Small"
|
| 36 |
N_CTX = 2048
|
| 37 |
N_THREADS = 2
|
| 38 |
|
|
|
|
| 56 |
)
|
| 57 |
AVAILABLE_MODELS["Llama 3.2 3B"] = llm_base
|
| 58 |
|
| 59 |
+
print("Loading model...")
|
| 60 |
+
llm_ft_2 = Llama.from_pretrained(
|
| 61 |
+
repo_id=FT_REPO_ID_2,
|
| 62 |
+
filename=FT_FILENAME_2,
|
| 63 |
+
n_ctx=N_CTX,
|
| 64 |
+
n_threads=N_THREADS,
|
| 65 |
+
)
|
| 66 |
+
AVAILABLE_MODELS["FuncCall FT - Llama 3.2 3B"] = llm_ft_2
|
| 67 |
+
|
| 68 |
except Exception as e:
|
| 69 |
print(f"Error loading model: {e}")
|
| 70 |
raise e
|
| 71 |
|
| 72 |
|
| 73 |
#--------------- TO RUN LOCALLY -----------------
|
|
|
|
| 74 |
# llm = Llama(
|
| 75 |
+
# model_path="works2.gguf",
|
| 76 |
# n_ctx=N_CTX,
|
| 77 |
# n_threads=None,
|
| 78 |
# )
|
|
|
|
| 82 |
# }
|
| 83 |
# ------------- FAST RESPONSE WITHOUT AGENT --------------
|
| 84 |
|
| 85 |
+
def respond_fast(user_message, history, model_choice):
|
| 86 |
"""
|
| 87 |
Fast path: no tools, no agent. Just a single LLM call with the
|
| 88 |
given system message and chat history.
|
|
|
|
| 92 |
if llm is None:
|
| 93 |
llm = next(iter(AVAILABLE_MODELS.values()))
|
| 94 |
|
| 95 |
+
history = add_history(user_message, history, "You are a helpful assistant. Just chat with the user.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# Single streaming generation
|
| 98 |
+
stream = generate_chat(llm, history, max_tokens=256, temperature=0.2, top_p=0.95)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
for out in stream:
|
| 100 |
+
yield out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
# ------------- TOOLS DEFINITIONS --------------
|
| 104 |
+
from weather import get_current_weather, get_current_temperature
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
def multiply(a: int, b: int) -> int:
|
| 107 |
"""Multiplies two integers. Args: a, b."""
|
|
|
|
| 126 |
AGENTS = {
|
| 127 |
"Weather": {
|
| 128 |
"system_message": "You are a helpful weather assistant",
|
| 129 |
+
"tools": [get_current_weather, get_current_temperature]
|
| 130 |
},
|
| 131 |
"Math": {
|
| 132 |
"system_message": "You are a helpful math assistant",
|
|
|
|
| 138 |
},
|
| 139 |
}
|
| 140 |
|
| 141 |
+
# ------------- WRAPPER FUNCTION FOR AGENTS ----------------
|
| 142 |
def app_respond(message, history, model_choice, agent_choice):
|
| 143 |
"""Pass the selected agent to our agentic framwork"""
|
| 144 |
llm = AVAILABLE_MODELS.get(model_choice)
|
|
|
|
| 147 |
|
| 148 |
agent_config = AGENTS.get(agent_choice)
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
for chunk in respond(
|
| 151 |
message,
|
| 152 |
+
history,
|
| 153 |
system_message=agent_config["system_message"],
|
| 154 |
llm=llm,
|
| 155 |
tools=agent_config["tools"]
|
requirements.txt
CHANGED
|
@@ -1,3 +1,6 @@
|
|
| 1 |
gradio
|
| 2 |
huggingface_hub
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
gradio
|
| 2 |
huggingface_hub
|
| 3 |
+
openmeteo-requests
|
| 4 |
+
requests-cache
|
| 5 |
+
retry-requests
|
| 6 |
+
geopy
|
weather.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import openmeteo_requests
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import requests_cache
|
| 4 |
+
from retry_requests import retry
|
| 5 |
+
from geopy.geocoders import Nominatim
|
| 6 |
+
|
| 7 |
+
def call_openmeteo_current(city: str, variable: str) -> str:
|
| 8 |
+
# Setup the Open-Meteo API client with cache and retry on error
|
| 9 |
+
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
| 10 |
+
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
| 11 |
+
openmeteo = openmeteo_requests.Client(session = retry_session)
|
| 12 |
+
|
| 13 |
+
url = "https://api.open-meteo.com/v1/forecast"
|
| 14 |
+
|
| 15 |
+
lat, lon = get_coordinates(city)
|
| 16 |
+
params = {
|
| 17 |
+
"latitude": lat,
|
| 18 |
+
"longitude": lon,
|
| 19 |
+
"current": variable,
|
| 20 |
+
}
|
| 21 |
+
responses = openmeteo.weather_api(url, params=params)
|
| 22 |
+
response = responses[0]
|
| 23 |
+
current = response.Current()
|
| 24 |
+
variable = current.Variables(0).Value()
|
| 25 |
+
return variable
|
| 26 |
+
|
| 27 |
+
def get_current_weather(city: str) -> str:
|
| 28 |
+
"""Get the summary of the current weather in a city."""
|
| 29 |
+
weather = call_openmeteo_current(city, "weather_code")
|
| 30 |
+
weather_str = weather_code_to_description(weather)
|
| 31 |
+
return weather_str
|
| 32 |
+
|
| 33 |
+
def get_current_temperature(city: str) -> str:
|
| 34 |
+
"""Get the current temperature in a city."""
|
| 35 |
+
temperature = call_openmeteo_current(city, "temperature_2m")
|
| 36 |
+
return str(round(temperature))
|
| 37 |
+
|
| 38 |
+
def get_current_wind_speed(city: str) -> str:
|
| 39 |
+
"""Get the current wind speed (10m) in a city."""
|
| 40 |
+
wind = call_openmeteo_current(city, "wind_speed_10m")
|
| 41 |
+
return str(round(wind)) + " m/s"
|
| 42 |
+
|
| 43 |
+
def get_coordinates(city: str):
|
| 44 |
+
geolocator = Nominatim(user_agent="NewApp")
|
| 45 |
+
location = geolocator.geocode(city)
|
| 46 |
+
return location.latitude, location.longitude
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def weather_code_to_description(code: float | int) -> str:
|
| 50 |
+
"""
|
| 51 |
+
Convert Open-Meteo (WMO) weather codes to human-readable descriptions.
|
| 52 |
+
Accepts int or float values (e.g., 1.0 -> 1).
|
| 53 |
+
"""
|
| 54 |
+
code = int(code)
|
| 55 |
+
|
| 56 |
+
WEATHER_CODE_MAP = {
|
| 57 |
+
0: "Clear sky",
|
| 58 |
+
|
| 59 |
+
1: "Mainly clear",
|
| 60 |
+
2: "Partly cloudy",
|
| 61 |
+
3: "Overcast",
|
| 62 |
+
|
| 63 |
+
45: "Fog",
|
| 64 |
+
48: "Depositing rime fog",
|
| 65 |
+
|
| 66 |
+
51: "Light drizzle",
|
| 67 |
+
53: "Moderate drizzle",
|
| 68 |
+
55: "Dense drizzle",
|
| 69 |
+
|
| 70 |
+
56: "Light freezing drizzle",
|
| 71 |
+
57: "Dense freezing drizzle",
|
| 72 |
+
|
| 73 |
+
61: "Slight rain",
|
| 74 |
+
63: "Moderate rain",
|
| 75 |
+
65: "Heavy rain",
|
| 76 |
+
|
| 77 |
+
66: "Light freezing rain",
|
| 78 |
+
67: "Heavy freezing rain",
|
| 79 |
+
|
| 80 |
+
71: "Slight snowfall",
|
| 81 |
+
73: "Moderate snowfall",
|
| 82 |
+
75: "Heavy snowfall",
|
| 83 |
+
|
| 84 |
+
77: "Snow grains",
|
| 85 |
+
|
| 86 |
+
80: "Slight rain showers",
|
| 87 |
+
81: "Moderate rain showers",
|
| 88 |
+
82: "Violent rain showers",
|
| 89 |
+
|
| 90 |
+
85: "Slight snow showers",
|
| 91 |
+
86: "Heavy snow showers",
|
| 92 |
+
|
| 93 |
+
95: "Thunderstorm",
|
| 94 |
+
96: "Thunderstorm with slight hail",
|
| 95 |
+
99: "Thunderstorm with heavy hail",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
return WEATHER_CODE_MAP.get(code, f"Unknown weather code: {code}")
|