Skip to content

Fix bugs in the InterviewSchedulingBot repository #36

@Nazarii-31

Description

@Nazarii-31

Interview Scheduling Bot Bug Fix Specifications
I need you to help fix the InterviewSchedulingBot's slot generation and formatting bugs. The bot currently produces inconsistent results and lacks proper date formatting. Follow these specific implementation instructions.

Current Problems
When asked for the same slots repeatedly, the bot returns completely different time slots each time
No proper date formatting (missing "Monday [04.08.2025]" format)
Inconsistent response structure between responses
Missing recommendations and explanations for time slots
Implementation Instructions
STEP 1: CREATE ENHANCED TIME SLOT MODEL
Create a new file Models/EnhancedTimeSlot.cs:

C#
using System;
using System.Collections.Generic;

namespace InterviewBot.Models
{
public class EnhancedTimeSlot
{
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public List AvailableParticipants { get; set; } = new List();
public List UnavailableParticipants { get; set; } = new List();
public double Score { get; set; }
public string Reason { get; set; }
public bool IsRecommended { get; set; }

    public string GetFormattedTimeRange() => 
        $"{StartTime:HH:mm} - {EndTime:HH:mm}";

    public string GetFormattedDateWithDay() =>
        $"{StartTime:dddd} [{StartTime:dd.MM.yyyy}]";
}

}
STEP 2: CREATE DATE FORMATTING SERVICE
Create a new file Services/DateFormattingService.cs:

C#
using System;

namespace InterviewBot.Services
{
public static class DateFormattingService
{
public static string FormatDateWithDay(DateTime date)
=> $"{date:dddd} [{date:dd.MM.yyyy}]";

    public static string FormatDateRange(DateTime start, DateTime end)
        => $"[{start:dd.MM.yyyy} - {end:dd.MM.yyyy}]";
        
    public static string FormatTimeRange(DateTime start, DateTime end)
        => $"{start:HH:mm} - {end:HH:mm}";
        
    // Get next business day (skip weekends)
    public static DateTime GetNextBusinessDay(DateTime date)
    {
        var nextDay = date.AddDays(1);
        while (nextDay.DayOfWeek == DayOfWeek.Saturday || nextDay.DayOfWeek == DayOfWeek.Sunday)
        {
            nextDay = nextDay.AddDays(1);
        }
        return nextDay;
    }
    
    public static string GetRelativeDateDescription(DateTime targetDate, DateTime currentDate)
    {
        if (targetDate.Date == currentDate.Date)
            return "today";
        if (targetDate.Date == currentDate.AddDays(1).Date)
            return "tomorrow";
            
        int daysDifference = (targetDate.Date - currentDate.Date).Days;
        if (daysDifference > 1 && daysDifference <= 7)
            return $"in {daysDifference} days";
            
        return $"on {FormatDateWithDay(targetDate)}";
    }
}

}
STEP 3: CREATE DETERMINISTIC SLOT RECOMMENDATION SERVICE
Create a new file Services/DeterministicSlotRecommendationService.cs:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using InterviewBot.Models;

namespace InterviewBot.Services
{
public class DeterministicSlotRecommendationService
{
public List GenerateConsistentTimeSlots(
DateTime startDate,
DateTime endDate,
int durationMinutes,
List participantEmails)
{
// Create a deterministic seed based on inputs
string seedInput = string.Join(",", participantEmails.OrderBy(e => e)) +
startDate.ToString("yyyy-MM-dd") +
endDate.ToString("yyyy-MM-dd") +
durationMinutes.ToString();
int seed = seedInput.GetHashCode();
var random = new Random(seed);

        var result = new List<EnhancedTimeSlot>();
        
        // Generate slots for each day in the range
        for (var day = startDate.Date; day <= endDate.Date; day = day.AddDays(1))
        {
            // Skip weekends
            if (day.DayOfWeek == DayOfWeek.Saturday || day.DayOfWeek == DayOfWeek.Sunday)
                continue;
                
            // Generate slots aligned to 15-minute intervals
            var dayStart = new DateTime(day.Year, day.Month, day.Day, 
                                      startDate.Hour, 0, 0);
            var dayEnd = new DateTime(day.Year, day.Month, day.Day, 
                                     endDate.Hour, 0, 0);
            
            // Start times always at quarter hours (00, 15, 30, 45)
            for (var time = dayStart; time < dayEnd; time = time.AddMinutes(15))
            {
                var slotEnd = time.AddMinutes(durationMinutes);
                
                // Skip if end time exceeds day end
                if (slotEnd > dayEnd)
                    continue;
                
                // Deterministically determine participant availability
                var availableParticipants = new List<string>();
                var unavailableParticipants = new List<string>();
                
                foreach (var email in participantEmails)
                {
                    // Generate deterministic availability based on email and time
                    int participantSeed = (email + time.ToString("HH:mm")).GetHashCode();
                    var participantRandom = new Random(participantSeed);
                    bool isAvailable = participantRandom.Next(100) < 80; // 80% chance available
                    
                    if (isAvailable)
                        availableParticipants.Add(email);
                    else
                        unavailableParticipants.Add(email);
                }
                
                // Skip slots where no one is available
                if (availableParticipants.Count == 0)
                    continue;
                    
                // Calculate slot score
                double availabilityScore = (double)availableParticipants.Count / participantEmails.Count * 100;
                double timeOfDayScore = GetTimeOfDayScore(time);
                double totalScore = (availabilityScore * 0.7) + (timeOfDayScore * 0.3);
                
                var slot = new EnhancedTimeSlot
                {
                    StartTime = time,
                    EndTime = slotEnd,
                    AvailableParticipants = availableParticipants,
                    UnavailableParticipants = unavailableParticipants,
                    Score = totalScore,
                    Reason = GenerateReason(availableParticipants, participantEmails, time)
                };
                
                result.Add(slot);
            }
        }
        
        // Sort and take top N slots
        var sortedSlots = result
            .OrderByDescending(s => s.Score)
            .ThenBy(s => s.StartTime)
            .Take(10)
            .ToList();
            
        // Mark best slot as recommended
        if (sortedSlots.Any())
        {
            sortedSlots[0].IsRecommended = true;
            sortedSlots[0].Reason += " ⭐ RECOMMENDED";
        }
        
        return sortedSlots;
    }
    
    private double GetTimeOfDayScore(DateTime time)
    {
        int hour = time.Hour;
        
        // Morning premium (9-11 AM)
        if (hour >= 9 && hour < 11)
            return 100;
            
        // Mid-morning (11 AM-12 PM)
        if (hour >= 11 && hour < 12)
            return 90;
            
        // After lunch (1-3 PM)
        if (hour >= 13 && hour < 15)
            return 80;
            
        // Late afternoon (3-5 PM)
        if (hour >= 15 && hour < 17)
            return 70;
            
        // Early morning or evening
        return 60;
    }
    
    private string GenerateReason(List<string> availableParticipants, List<string> allParticipants, DateTime time)
    {
        if (availableParticipants.Count == allParticipants.Count)
            return "(All participants available)";
            
        double availabilityPercent = (double)availableParticipants.Count / allParticipants.Count * 100;
        
        // Time-based reasons
        int hour = time.Hour;
        
        if (hour >= 9 && hour < 11)
            return $"({availableParticipants.Count}/{allParticipants.Count} participants - Morning productivity peak)";
            
        if (hour >= 11 && hour < 12)
            return $"({availableParticipants.Count}/{allParticipants.Count} participants - Mid-morning slot)";
            
        if (hour >= 13 && hour < 14)
            return $"({availableParticipants.Count}/{allParticipants.Count} participants - After lunch)";
            
        if (hour >= 14 && hour < 16)
            return $"({availableParticipants.Count}/{allParticipants.Count} participants - Afternoon slot)";
            
        if (hour >= 16)
            return $"({availableParticipants.Count}/{allParticipants.Count} participants - Late day slot)";
            
        return $"({availableParticipants.Count}/{allParticipants.Count} participants available)";
    }
}

}
STEP 4: CREATE RESPONSE FORMATTER
Create a new file Services/TimeSlotResponseFormatter.cs:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using InterviewBot.Models;

namespace InterviewBot.Services
{
public class TimeSlotResponseFormatter
{
public string FormatResponse(
List slots,
int durationMinutes,
DateTime startDate,
DateTime endDate)
{
var sb = new StringBuilder();

        // Handle no slots found
        if (!slots.Any())
        {
            return $"I couldn't find any available {durationMinutes}-minute time slots for " +
                   $"{DateFormattingService.FormatDateWithDay(startDate)}. " +
                   "Would you like me to check a different time range?";
        }
        
        // Determine if single day or date range
        bool isSingleDay = startDate.Date == endDate.Date;
        
        // Format header
        if (isSingleDay)
        {
            sb.AppendLine($"Here are the available {durationMinutes}-minute time slots for {DateFormattingService.FormatDateWithDay(startDate)}:");
            sb.AppendLine();
            sb.AppendLine($"{DateFormattingService.FormatDateWithDay(startDate)}");
        }
        else
        {
            string dateRange = DateFormattingService.FormatDateRange(startDate, endDate);
            sb.AppendLine($"Here are the available {durationMinutes}-minute time slots for " +
                         (startDate.Date.AddDays(7) >= endDate.Date ? "next week" : "the specified period") +
                         $" {dateRange}:");
            sb.AppendLine();
        }
        
        // Group by day and format slots
        var slotsByDay = slots
            .GroupBy(s => s.StartTime.Date)
            .OrderBy(g => g.Key);
            
        foreach (var dayGroup in slotsByDay)
        {
            // Skip repeating day header if single day
            if (!isSingleDay)
            {
                sb.AppendLine();
                sb.AppendLine($"{DateFormattingService.FormatDateWithDay(dayGroup.Key)}");
            }
            
            sb.AppendLine();
            
            // List slots for this day
            foreach (var slot in dayGroup.OrderBy(s => s.StartTime))
            {
                sb.AppendLine($"{slot.GetFormattedTimeRange()} {slot.Reason}");
            }
        }
        
        sb.AppendLine();
        sb.Append("Please let me know which time slot works best for you.");
        
        return sb.ToString();
    }
}

}
STEP 5: MODIFY BOT CLASS
Update your InterviewSchedulingBot class to use these new services:

C#
// Add fields
private readonly DeterministicSlotRecommendationService _slotService;
private readonly TimeSlotResponseFormatter _formatter;

// Update constructor
public InterviewSchedulingBot(/* existing params */,
DeterministicSlotRecommendationService slotService,
TimeSlotResponseFormatter formatter)
{
// Existing code
_slotService = slotService;
_formatter = formatter;
}

// Add method to handle slot requests
private async Task HandleSlotRequestAsync(ITurnContext turnContext, string message, CancellationToken cancellationToken)
{
// Extract emails
var emails = ExtractEmailsFromMessage(message);
if (!emails.Any())
{
await turnContext.SendActivityAsync("I couldn't find any email addresses in your request. Please include participant emails.");
return;
}

// Extract duration
int duration = ExtractDurationFromMessage(message);

// Extract date range
var (startDate, endDate) = ExtractDateRangeFromMessage(message);

// Generate slots
var slots = _slotService.GenerateConsistentTimeSlots(startDate, endDate, duration, emails);

// Format response
string response = _formatter.FormatResponse(slots, duration, startDate, endDate);

await turnContext.SendActivityAsync(MessageFactory.Text(response), cancellationToken);

}

// Helper methods
private List ExtractEmailsFromMessage(string message)
{
// Use regex to extract emails
var emails = new List();
var regex = new Regex(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}");
var matches = regex.Matches(message);
foreach (Match match in matches)
{
emails.Add(match.Value);
}
return emails;
}

private int ExtractDurationFromMessage(string message)
{
// Use regex to extract duration
var regex = new Regex(@"(\d+)\s*(?:min|mins|minutes)");
var match = regex.Match(message);
if (match.Success && int.TryParse(match.Groups[1].Value, out int duration))
{
return duration;
}
return 60; // Default to 60 minutes
}

private (DateTime start, DateTime end) ExtractDateRangeFromMessage(string message)
{
DateTime now = DateTime.Now;
DateTime tomorrow = DateFormattingService.GetNextBusinessDay(now);

// Default to tomorrow
DateTime start = new DateTime(tomorrow.Year, tomorrow.Month, tomorrow.Day, 9, 0, 0);
DateTime end = new DateTime(tomorrow.Year, tomorrow.Month, tomorrow.Day, 17, 0, 0);

// Handle specific time ranges
if (message.Contains("tomorrow"))
{
    // Already set to tomorrow
}
else if (message.Contains("next week"))
{
    // Find next Monday
    DateTime nextMonday = now.Date;
    while (nextMonday.DayOfWeek != DayOfWeek.Monday || nextMonday.Date <= now.Date)
    {
        nextMonday = nextMonday.AddDays(1);
    }
    
    start = new DateTime(nextMonday.Year, nextMonday.Month, nextMonday.Day, 9, 0, 0);
    end = new DateTime(nextMonday.AddDays(4).Year, nextMonday.AddDays(4).Month, nextMonday.AddDays(4).Day, 17, 0, 0);
}

// Handle time of day
if (message.Contains("morning"))
{
    start = new DateTime(start.Year, start.Month, start.Day, 9, 0, 0);
    end = new DateTime(end.Year, end.Month, end.Day, 12, 0, 0);
}
else if (message.Contains("afternoon"))
{
    start = new DateTime(start.Year, start.Month, start.Day, 13, 0, 0);
    end = new DateTime(end.Year, end.Month, end.Day, 17, 0, 0);
}

return (start, end);

}

// Update OnMessageActivityAsync
protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
string message = turnContext.Activity.Text.ToLowerInvariant();

// Check if this is a slot request
if (message.Contains("slot") || message.Contains("schedule") || message.Contains("time") || message.Contains("meeting"))
{
    await HandleSlotRequestAsync(turnContext, turnContext.Activity.Text, cancellationToken);
    return;
}

// Existing code for other message types

}
STEP 6: UPDATE DEPENDENCY REGISTRATION
Update your Program.cs or Startup.cs:

C#
// Register new services
builder.Services.AddSingleton();
builder.Services.AddSingleton();
Testing Instructions
Build and run the application
Send these test messages:
"Give me free slots for tomorrow morning for jane.smith@company.com and alex.wilson@company.com for 66 mins"
(Repeat the exact same message 3 times)
Verify that:
You get the SAME time slots each time (consistency)
The date is properly formatted as "Monday [04.08.2025]" (assuming tomorrow is Monday)
Time slots are aligned to 15-minute intervals
One slot is marked as recommended
Each slot has an explanation

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions