<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Precious Adeoye</title>
    <description>The latest articles on Forem by Precious Adeoye (@precious_adeoye_24027ce73).</description>
    <link>https://forem.com/precious_adeoye_24027ce73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3597040%2F95c5200f-adee-474b-8c00-d0cd6cc0dd52.jpg</url>
      <title>Forem: Precious Adeoye</title>
      <link>https://forem.com/precious_adeoye_24027ce73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/precious_adeoye_24027ce73"/>
    <language>en</language>
    <item>
      <title>Building a Smart Task Tracker Agent for Telex.im with ASP.NET Core</title>
      <dc:creator>Precious Adeoye</dc:creator>
      <pubDate>Wed, 05 Nov 2025 11:05:08 +0000</pubDate>
      <link>https://forem.com/precious_adeoye_24027ce73/building-a-smart-task-tracker-agent-for-telexim-with-aspnet-core-27dd</link>
      <guid>https://forem.com/precious_adeoye_24027ce73/building-a-smart-task-tracker-agent-for-telexim-with-aspnet-core-27dd</guid>
      <description>&lt;p&gt;Introduction&lt;br&gt;
As part of the HNG Internship Stage 3 Backend Task, I built an AI-powered task management agent that integrates seamlessly with Telex.im. This wasn't just another CRUD application—it required implementing natural language processing, background job scheduling, and the A2A (Agent-to-Agent) protocol for real-time chat integration.&lt;br&gt;
In this post, I'll walk you through the entire development process, from architecture decisions to deployment challenges, and share the lessons I learned along the way.&lt;br&gt;
Why a Task Tracker Agent?&lt;br&gt;
When presented with several options (code helper, task tracker, or data summarizer), I chose the task tracker for three key reasons:&lt;/p&gt;

&lt;p&gt;Practical Value: Every team struggles with task management. Having an AI agent that understands natural language makes task tracking effortless.&lt;br&gt;
Technical Depth: This project allowed me to showcase multiple backend skills—API design, database management, background processing, natural language parsing, and cloud deployment.&lt;br&gt;
User Experience: The difference between typing create task "Deploy to production" due Friday 5pm versus filling out a form is significant. Natural language makes the agent feel intelligent and intuitive.&lt;/p&gt;

&lt;p&gt;Tech Stack &amp;amp; Architecture&lt;br&gt;
Core Technologies&lt;/p&gt;

&lt;p&gt;ASP.NET Core 8.0: For building a robust, high-performance Web API&lt;br&gt;
Entity Framework Core: ORM for database operations with SQLite&lt;br&gt;
Hangfire: Background job processing for automated reminders&lt;br&gt;
Railway: Cloud deployment platform with Docker support&lt;/p&gt;

&lt;p&gt;Project Architecture&lt;br&gt;
I structured the project following clean architecture principles:&lt;br&gt;
TaskTrackerAgent/&lt;br&gt;
├── Controllers/        # API endpoints (A2A integration)&lt;br&gt;
├── Models/            # Domain entities and DTOs&lt;br&gt;
├── Services/          # Business logic layer&lt;br&gt;
├── Data/             # Database context and configurations&lt;br&gt;
└── Program.cs        # Application startup and configuration&lt;br&gt;
This separation of concerns makes the codebase maintainable and testable.&lt;br&gt;
Key Features Implemented&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Natural Language Processing
The most challenging—and rewarding—part was building the NLP service. Users shouldn't have to remember exact command syntax; the agent should understand them.
I implemented regex-based parsing that handles:
Date Recognition:
csharp// Understands: today, tomorrow, Monday, Friday, Nov 5, December 25
if (lowerMessage.Contains("tomorrow"))
{
return now.Date.AddDays(1).AddHours(17); // Default to 5 PM
}&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// Day of week parsing&lt;br&gt;
var daysOfWeek = new[] { "monday", "tuesday", "wednesday", ... };&lt;br&gt;
foreach (var day in daysOfWeek)&lt;br&gt;
{&lt;br&gt;
    if (lowerMessage.Contains(day))&lt;br&gt;
    {&lt;br&gt;
        var targetDay = (DayOfWeek)Array.IndexOf(daysOfWeek, day);&lt;br&gt;
        var daysUntil = ((int)targetDay - (int)now.DayOfWeek + 7) % 7;&lt;br&gt;
        // ... calculate target date&lt;br&gt;
    }&lt;br&gt;
}&lt;br&gt;
Time Parsing:&lt;br&gt;
csharp// Handles: 3pm, 5:30pm, 10am&lt;br&gt;
var timeMatch = Regex.Match(message, @"(\d{1,2})\s*(am|pm)", RegexOptions.IgnoreCase);&lt;br&gt;
if (timeMatch.Success)&lt;br&gt;
{&lt;br&gt;
    var hour = int.Parse(timeMatch.Groups[1].Value);&lt;br&gt;
    if (timeMatch.Groups[2].Value.ToLower() == "pm" &amp;amp;&amp;amp; hour != 12) &lt;br&gt;
        hour += 12;&lt;br&gt;
    return targetDate.AddHours(hour);&lt;br&gt;
}&lt;br&gt;
Command Recognition:&lt;br&gt;
The NLP service parses user intent and extracts parameters:&lt;br&gt;
csharppublic TaskCommand ParseCommand(string message)&lt;br&gt;
{&lt;br&gt;
    var lowerMessage = message.ToLower().Trim();&lt;br&gt;
    var command = new TaskCommand();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (lowerMessage.Contains("create task") || 
    lowerMessage.Contains("add task"))
{
    command.Type = CommandType.CreateTask;
    command.Title = ExtractTaskTitle(message);
    command.DueDate = ExtractDueDate(message);
    return command;
}
// ... more command types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A2A Protocol Integration&lt;br&gt;
Telex.im uses the A2A (Agent-to-Agent) protocol for chat integration. I implemented this in the A2AController:&lt;br&gt;
csharp[HttpPost("taskAgent")]&lt;br&gt;
public async Task ProcessMessage([FromBody] A2ARequest request)&lt;br&gt;
{&lt;br&gt;
try&lt;br&gt;
{&lt;br&gt;
    var userId = request.User?.Id ?? "anonymous";&lt;br&gt;
    var channelId = request.ChannelId;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Parse the natural language command
var command = _nlpService.ParseCommand(request.Message);

// Execute the command
var response = await ExecuteCommand(command, userId, channelId);

return Ok(new A2AResponse { Text = response });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
catch (Exception ex)&lt;br&gt;
{&lt;br&gt;
    _logger.LogError(ex, "Error processing message");&lt;br&gt;
    return Ok(new A2AResponse &lt;br&gt;
    { &lt;br&gt;
        Text = $"Sorry, I encountered an error. Please try again." &lt;br&gt;
    });&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
The key insight here: always return 200 OK. Even on errors, you return a friendly message to the user. This prevents breaking the chat experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database Design&lt;br&gt;
I used Entity Framework Core with SQLite for simplicity and portability:&lt;br&gt;
csharppublic class TaskItem&lt;br&gt;
{&lt;br&gt;
public int Id { get; set; }&lt;br&gt;
public string Title { get; set; }&lt;br&gt;
public DateTime? DueDate { get; set; }&lt;br&gt;
public bool IsCompleted { get; set; }&lt;br&gt;
public string CreatedBy { get; set; }  // User ID from Telex&lt;br&gt;
public string? TelexChannelId { get; set; }  // Channel context&lt;br&gt;
}&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;public class Reminder&lt;br&gt;
{&lt;br&gt;
    public int Id { get; set; }&lt;br&gt;
    public int TaskId { get; set; }&lt;br&gt;
    public DateTime ReminderTime { get; set; }&lt;br&gt;
    public bool IsSent { get; set; }&lt;br&gt;
    public string UserId { get; set; }&lt;br&gt;
}&lt;br&gt;
The database schema is simple but effective. Each task is tied to a user and optionally to a Telex channel, allowing for both personal and team task management.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Background Job Processing
For reminders, I used Hangfire to run background jobs:
csharp// In Program.cs - Configure Hangfire
builder.Services.AddHangfire(configuration =&amp;gt; configuration
.UseSQLiteStorage(connectionString));&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;builder.Services.AddHangfireServer();&lt;/p&gt;

&lt;p&gt;// Schedule recurring job&lt;br&gt;
RecurringJob.AddOrUpdate("check-reminders", &lt;br&gt;
    () =&amp;gt; reminderService.CheckAndSendReminders(), &lt;br&gt;
    Cron.Minutely);&lt;br&gt;
The ReminderService checks every minute for due reminders:&lt;br&gt;
csharppublic async Task CheckAndSendReminders()&lt;br&gt;
{&lt;br&gt;
    var pendingReminders = await _taskService.GetPendingRemindersAsync();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;foreach (var reminder in pendingReminders)
{
    // Send notification back to Telex
    _logger.LogInformation(
        $"Reminder: {reminder.Task?.Title} for user {reminder.UserId}");

    await _taskService.MarkReminderAsSentAsync(reminder.Id);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Challenges &amp;amp; Solutions&lt;br&gt;
Challenge 1: Natural Language Ambiguity&lt;br&gt;
Problem: Users phrase things differently. "tomorrow at 3" vs "3pm tomorrow" vs "tomorrow 3pm".&lt;br&gt;
Solution: I created multiple regex patterns and prioritized them. The parser tries specific patterns first (like time extraction), then falls back to defaults:&lt;br&gt;
csharp// Extract time if present, otherwise default to 5 PM&lt;br&gt;
var timeMatch = Regex.Match(message, @"(\d{1,2})\s*(am|pm)");&lt;br&gt;
var targetDate = /* calculated date */;&lt;br&gt;
return timeMatch.Success &lt;br&gt;
    ? targetDate.AddHours(extractedHour) &lt;br&gt;
    : targetDate.AddHours(17);  // Default 5 PM&lt;br&gt;
Challenge 2: Docker Deployment Configuration&lt;br&gt;
Problem: Railway uses dynamic ports, and ASP.NET Core needs to bind to the correct one.&lt;br&gt;
Solution: Configure the app to read from Railway's PORT environment variable:&lt;br&gt;
dockerfile# In Dockerfile&lt;br&gt;
ENV ASPNETCORE_URLS=http://+:8080&lt;/p&gt;

&lt;h1&gt;
  
  
  In Program.cs
&lt;/h1&gt;

&lt;p&gt;var port = Environment.GetEnvironmentVariable("PORT") ?? "5000";&lt;br&gt;
app.Run($"&lt;a href="http://0.0.0.0:%7Bport%7D%22" rel="noopener noreferrer"&gt;http://0.0.0.0:{port}"&lt;/a&gt;);&lt;br&gt;
Challenge 3: SQLite in Docker&lt;br&gt;
Problem: SQLite database needs write permissions in the container.&lt;br&gt;
Solution: Ensure the database file is created in a writable directory and configure EF Core to create it automatically:&lt;br&gt;
csharpusing (var scope = app.Services.CreateScope())&lt;br&gt;
{&lt;br&gt;
    var dbContext = scope.ServiceProvider.GetRequiredService();&lt;br&gt;
    dbContext.Database.EnsureCreated();&lt;br&gt;
}&lt;br&gt;
Challenge 4: Error Handling in Chat Context&lt;br&gt;
Problem: Traditional API error responses (404, 500) break the chat experience.&lt;br&gt;
Solution: Always return 200 OK with user-friendly error messages:&lt;br&gt;
csharpcatch (Exception ex)&lt;br&gt;
{&lt;br&gt;
    _logger.LogError(ex, "Error processing message");&lt;br&gt;
    return Ok(new A2AResponse &lt;br&gt;
    { &lt;br&gt;
        Text = "Sorry, I encountered an error. Please try again." &lt;br&gt;
    });&lt;br&gt;
}&lt;br&gt;
This maintains conversational flow even when things go wrong.&lt;br&gt;
Deployment Process&lt;br&gt;
Step 1: Docker Configuration&lt;br&gt;
I created a multi-stage Dockerfile for optimized builds:&lt;br&gt;
dockerfile# Build stage&lt;br&gt;
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build&lt;br&gt;
WORKDIR /src&lt;br&gt;
COPY *.csproj ./&lt;br&gt;
RUN dotnet restore&lt;br&gt;
COPY . ./&lt;br&gt;
RUN dotnet publish -c Release -o /app/publish&lt;/p&gt;

&lt;h1&gt;
  
  
  Runtime stage
&lt;/h1&gt;

&lt;p&gt;FROM mcr.microsoft.com/dotnet/aspnet:8.0&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY --from=build /app/publish .&lt;br&gt;
EXPOSE 8080&lt;br&gt;
ENTRYPOINT ["dotnet", "TaskTrackerAgent.dll"]&lt;br&gt;
This reduces the final image size by 60% compared to a single-stage build.&lt;br&gt;
Step 2: Railway Deployment&lt;br&gt;
Railway made deployment surprisingly simple:&lt;/p&gt;

&lt;p&gt;Push code to GitHub&lt;br&gt;
Connect repository to Railway&lt;br&gt;
Railway auto-detects Dockerfile&lt;br&gt;
Click deploy&lt;/p&gt;

&lt;p&gt;Railway automatically:&lt;/p&gt;

&lt;p&gt;Builds the Docker image&lt;br&gt;
Deploys to their infrastructure&lt;br&gt;
Provides a public URL&lt;br&gt;
Sets up SSL/&lt;/p&gt;

</description>
      <category>agents</category>
      <category>showdev</category>
      <category>ai</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
