Skip to content

Commit

Permalink
Merge pull request #10 from SaM-92/dev_b
Browse files Browse the repository at this point in the history
Enhance User Interaction: Interactive Feature, Query Limit Implementation, and Docstrings
  • Loading branch information
SaM-92 authored Mar 3, 2024
2 parents b44fe07 + f55a817 commit 138dfb0
Show file tree
Hide file tree
Showing 6 changed files with 4,600 additions and 11 deletions.
Binary file modified .DS_Store
Binary file not shown.
4,110 changes: 4,110 additions & 0 deletions eirgrid_api.ipynb

Large diffs are not rendered by default.

140 changes: 134 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from subs.telegram_func import (
telegram_carbon_intensity,
telegram_fuel_mix,
telegram_personalised_handler,
)
from dotenv import load_dotenv
from datetime import datetime, timedelta

# add vars to azure
# Load environment variables from .env file
Expand All @@ -33,10 +35,23 @@
# SELECT_OPTION = 0
TIME_COLUMN_SELECTED = 1
# FOLLOW_UP = 0
SELECT_OPTION, FOLLOW_UP, FEEDBACK = range(3)
SELECT_OPTION, FOLLOW_UP, FEEDBACK, ASK_PLAN, FOLLOW_UP_CONVERSATION = range(5)


async def energy_api_func(update: Update, context: CallbackContext):
"""
Processes user requests for energy-related information via a Telegram bot and replies with relevant data or status updates.
This asynchronous function handles user queries for carbon intensity or fuel mix information. Upon receiving a request, it acknowledges receipt and processes the request based on the user's selected option. It supports dynamic user interactions by maintaining state and offering follow-up actions.
Args:
update (Update): Contains the incoming update data, including the user's message and chat information.
context (CallbackContext): Holds context-specific data like user data for state management between interactions.
Returns:
This function sends messages directly to the Telegram chat.
Returns a status code indicating the next step in the conversation flow, such as FOLLOW_UP for continuing interaction.
"""

user_first_name = update.message.from_user.first_name

Expand All @@ -63,7 +78,14 @@ async def energy_api_func(update: Update, context: CallbackContext):
f"Sorry {user_first_name}! 🤖 We are still working on this feature. Please try again later."
)

options_keyboard = [["Start Over", "End Conversation", "Provide Feedback"]]
options_keyboard = [
[
"✨ Personalised Recommendations",
"🔄 Start Over",
"🔚 End Conversation",
"💬 Provide Feedback",
]
]
reply_markup = ReplyKeyboardMarkup(options_keyboard, one_time_keyboard=True)

await update.message.reply_text(
Expand Down Expand Up @@ -202,7 +224,7 @@ async def follow_up(update: Update, context: CallbackContext) -> int:

async def feedback_command(update: Update, context: CallbackContext) -> int:
logger.info("Entered feedback_command")
await update.message.reply_text("Please type your feedback.")
await update.message.reply_text("💬 Please type your feedback.")
return FEEDBACK


Expand All @@ -220,6 +242,93 @@ async def feedback_text(update: Update, context: CallbackContext) -> int:
return ConversationHandler.END


async def personalised_recommendations_handler(
update: Update, context: CallbackContext
) -> None:
# Prompt the user to specify their plans or devices they intend to use
await update.message.reply_text(
"🔌💡 Wondering about the best time for laundry to save energy? Just mention the device or ask me—like when to do laundry? I'm here to guide you! 🌿👕"
)
return ASK_PLAN


async def planning_response_handler(update: Update, context: CallbackContext) -> int:
user_id = update.message.from_user.id
now = datetime.now()
# Initialize or update user query data
if "query_data" not in context.user_data:
context.user_data["query_data"] = {}
if user_id not in context.user_data["query_data"]:
context.user_data["query_data"][user_id] = {"count": 0, "last_query_time": now}

user_query_data = context.user_data["query_data"][user_id]
time_since_last_query = now - user_query_data["last_query_time"]

# Check if cooldown period has passed (3 hours)
if time_since_last_query > timedelta(hours=3):
# Reset query count after cooldown
user_query_data["count"] = 0
user_query_data["last_query_time"] = now
elif user_query_data["count"] >= 3:
# Calculate remaining cooldown time
remaining_cooldown = timedelta(hours=3) - time_since_last_query
remaining_minutes = int(remaining_cooldown.total_seconds() / 60)
# Inform user of cooldown and remaining time
await update.message.reply_text(
f"⌛️🚫 You have reached your query limit. Please wait for {remaining_minutes} minutes before trying again. ⏰🔒"
)
return ConversationHandler.END # or your designated state for handling this

# Increment query count and update last query time
user_query_data["count"] += 1
user_query_data["last_query_time"] = now

# User's response to the planning question
user_query = update.message.text

# Check if there's an existing conversation context
if "conversation_context" not in context.user_data:
context.user_data["conversation_context"] = user_query

else:
# Append new question to existing context
context.user_data["conversation_context"] += f"\n{user_query}"

user_first_name = update.message.from_user.first_name
# Logic to process the user's response and provide recommendations
# Your recommendation logic here
AI_response_to_query = await telegram_personalised_handler(
update, context, user_first_name, context.user_data["conversation_context"]
)
if AI_response_to_query:
await update.message.reply_text(AI_response_to_query)
else:
# Provide a default message or handle the case as needed
await update.message.reply_text(
"I'm sorry, but I couldn't process your request. Please try again."
)

# Ask if they have any further questions
await update.message.reply_text("Any further questions (Y/N)?")

# Transition to another state or end the conversation
return FOLLOW_UP_CONVERSATION


async def follow_up_handler(update: Update, context: CallbackContext) -> int:
user_response = update.message.text.lower()

if user_response in ["yes", "y"]:
# Prompt for the next question
await update.message.reply_text("🤔 What would you like to know next?")
return ASK_PLAN
else:
await update.message.reply_text(
"Thank you for using our service. Have a great day! 💚 🌎"
)
return ConversationHandler.END


def main() -> None:
"""
Entry point of the program.
Expand All @@ -239,6 +348,10 @@ def main() -> None:
entry_points=[
CommandHandler("start", start),
CommandHandler("energy_status", energy_status),
CommandHandler(
"personal_advice",
personalised_recommendations_handler,
),
CommandHandler("feedback", feedback_command),
# MessageHandler(filters.Document.ALL, doc_handler),
],
Expand All @@ -247,14 +360,26 @@ def main() -> None:
MessageHandler(filters.TEXT & ~filters.COMMAND, energy_api_func)
],
FOLLOW_UP: [
MessageHandler(filters.Regex("^(Start Over)$"), start_over_handler),
MessageHandler(
filters.Regex("^(End Conversation)$"), end_conversation_handler
filters.Regex("^✨ Personalised Recommendations$"),
personalised_recommendations_handler,
),
MessageHandler(filters.Regex("^(Provide Feedback)$"), follow_up),
MessageHandler(filters.Regex("^🔄 Start Over$"), start_over_handler),
MessageHandler(
filters.Regex("^🔚 End Conversation$"), end_conversation_handler
),
MessageHandler(filters.Regex("^💬 Provide Feedback$"), follow_up),
# Add a fallback handler within FOLLOW_UP for unexpected inputs
MessageHandler(filters.ALL, unexpected_input_handler),
],
ASK_PLAN: [
MessageHandler(
filters.TEXT & ~filters.COMMAND, planning_response_handler
)
],
FOLLOW_UP_CONVERSATION: [
MessageHandler(filters.TEXT & ~filters.COMMAND, follow_up_handler)
],
FEEDBACK: [MessageHandler(filters.TEXT & ~filters.COMMAND, feedback_text)],
},
fallbacks=[
Expand All @@ -276,6 +401,9 @@ def main() -> None:
application.add_handler(
CommandHandler("cancel", cancel)
) # Directly handle cancel command
application.add_handler(
CommandHandler("personal_advice", personalised_recommendations_handler)
)

application.run_polling()

Expand Down
87 changes: 87 additions & 0 deletions subs/energy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@


def eirgrid_api(area, region, start_time, end_time):
"""Fetches data from the EirGrid API for a specified area and region within a given time range.
Args:
area (str): The data area of interest. Valid values include "CO2Stats", "generationactual",
"co2emission", "co2intensity", "interconnection", "SnspAll", "frequency",
"demandactual", "windactual", "fuelMix".
region (str): The region for which the data is requested. Options are "ROI" (Republic of Ireland),
"NI" (Northern Ireland), or "ALL" for both.
start_time (str): The start time for the data request in 'YYYY-MM-DD' format.
end_time (str): The end time for the data request in 'YYYY-MM-DD' format.
Returns:
pd.DataFrame: A DataFrame containing the requested data.
"""
# area = [
# "CO2Stats",
# "generationactual",
Expand All @@ -35,17 +49,42 @@ def eirgrid_api(area, region, start_time, end_time):

# Function to round time to the nearest 15 minutes
def round_time(dt):
"""Rounds a datetime object's minutes to the nearest quarter hour.
Args:
dt (datetime): The datetime object to round.
Returns:
datetime: A new datetime object rounded to the nearest 15 minutes, with seconds and microseconds set to 0.
"""
# Round minutes to the nearest 15
new_minute = (dt.minute // 15) * 15
return dt.replace(minute=new_minute, second=0, microsecond=0)


# Function to format date in a specific format
def format_date(dt):
"""Formats a datetime object into a specific string representation.
Args:
dt (datetime): The datetime object to format.
Returns:
str: The formatted date string in 'dd-mmm-yyyy+HH%3AMM' format
"""
return dt.strftime("%d-%b-%Y").lower() + "+" + dt.strftime("%H%%3A%M")


def carbon_api_forecast():
"""
Fetches CO2 emission forecast data for a specified region within a 24-hour period starting from the nearest half-hour mark.
The function rounds down the current time to the nearest half-hour to align with data availability, sets the end time to the end of the current day, and then constructs and sends a request to the CO2 forecast API for the specified region. The response is processed into a pandas DataFrame, indexed by the effective time of each forecast.
Returns:
pd.DataFrame: A DataFrame containing CO2 emission forecast data, indexed by effective time, or None if an error occurs.
"""

# data is availble every 30 minutes, so we need to start at the nearest half-hour
def round_down_time(dt):
# Round down to the nearest half-ho/ur
Expand Down Expand Up @@ -105,6 +144,14 @@ def create_url(start_datetime, end_datetime, region):


def carbon_api_intensity():
"""
Fetches and analyzes CO2 intensity data from the previous day, rounded to the nearest 15 minutes.
This function retrieves CO2 intensity data for the last 24 hours, processes the data to fill any gaps with interpolated values, and then calculates the mean, minimum, and maximum CO2 intensity values for the period.
Returns:
tuple: A tuple containing a dictionary with 'mean', 'min', and 'max' CO2 intensity values, and a pandas DataFrame with the recent CO2 intensity data indexed by effective time. Returns (None, None) in case of an error.
"""
try:
# Current date and time, rounded to the nearest 15 minutes
now = round_time(datetime.datetime.now())
Expand Down Expand Up @@ -158,6 +205,14 @@ def carbon_api_intensity():


def fuel_mix():
"""
Retrieves and processes the fuel mix data for the current time, rounded to the nearest 15 minutes, compared to the same time yesterday.
This function fetches the fuel mix data, maps raw field names to more descriptive names, calculates the percentage share of each fuel type in the total energy mix, and determines whether the region is net importing or exporting energy based on the fuel mix data.
Returns:
tuple: A tuple containing a pandas DataFrame with the fuel mix data, including the percentage share of each fuel type, and a string indicating if the region is 'importing' or 'exporting' energy. Returns (None, None) in case of an error.
"""
try:
# Current date and time, rounded to the nearest 15 minutes
now = round_time(datetime.datetime.now())
Expand Down Expand Up @@ -207,6 +262,17 @@ def fuel_mix():


def classify_status(value, min_val, max_val):
"""
Categorizes a numeric value as 'low', 'medium', or 'high' based on its comparison with provided minimum and maximum values.
Args:
value (float): The numeric value to categorize.
min_val (float): The minimum value of the range.
max_val (float): The maximum value of the range.
Returns:
str: A string indicating the category of the value: 'low' if the value is less than the min_val, 'high' if it is greater than the max_val, and 'medium' if it falls within the range inclusive of the min_val and max_val.
"""
if value < min_val:
return "low"
elif value > max_val:
Expand All @@ -216,6 +282,18 @@ def classify_status(value, min_val, max_val):


def status_classification(df, co2_stats_prior_day):
"""
Classifies each CO2 emission value in the dataframe based on its comparison to the prior day's statistics and predefined EU standards.
For each emission value, this function assigns a 'status_compared_to_yesterday' based on whether the value falls below, within, or above the range defined by the previous day's minimum and maximum CO2 values. It also assigns a 'status_compared_to_EU' based on a comparison to predefined minimum and maximum values representing EU standards.
Args:
df (pd.DataFrame): A DataFrame containing CO2 emission data with a column named 'Value'.
co2_stats_prior_day (dict): A dictionary containing 'min' and 'max' keys with float values representing the previous day's CO2 emission range.
Returns:
pd.DataFrame: The modified DataFrame with two new columns: 'status_compared_to_yesterday' and 'status_compared_to_EU', each containing classification results ('low', 'medium', 'high') for the CO2 values.
"""
df["status_compared_to_yesterday"] = df["Value"].apply(
classify_status, args=(co2_stats_prior_day["min"], co2_stats_prior_day["max"])
)
Expand All @@ -225,6 +303,15 @@ def status_classification(df, co2_stats_prior_day):


def co2_int_plot(df_):
"""
Plots CO2 intensity data with comparisons to the previous day, EU standards, and value intensity using a color-coded bar chart.
Args:
df_ (pd.DataFrame): A DataFrame containing CO2 intensity data, with columns for 'EffectiveTime', 'Value', 'status_compared_to_yesterday', and 'status_compared_to_EU'.
Returns:
matplotlib.pyplot: The plotted figure.
"""
# Assuming df_carbon_forecast_indexed is your DataFrame
df = df_.reset_index(inplace=False)
df["EffectiveTime"] = pd.to_datetime(df["EffectiveTime"])
Expand Down
Loading

0 comments on commit 138dfb0

Please sign in to comment.