DEV Community

Cover image for Building a Daily Productivity App with Pieces - Part 2: Adding AI Intelligence with Gemini
Pieces 🌟
Pieces 🌟

Posted on

Building a Daily Productivity App with Pieces - Part 2: Adding AI Intelligence with Gemini

Welcome back! In Part 1, we built a complete PiecesOS service that:

  1. Connects to PiecesOS and maintains a WebSocket connection
  2. Fetches and caches workstream summaries grouped by day
  3. Extracts the summary content from annotations

Now we have all of the infrastructure in place. But here's the thing: having raw summaries with their content is cool, but it's not exactly... useful. I mean, I have all this text about what I did, but what did I actually accomplish today?

That's where Part 2 comes in. We're going to use Google's Gemini AI to transform those raw summaries into actual insights. Think:

  • What did I work on?
  • Which projects did I touch?
  • Who did I collaborate with?
  • What should I remember for tomorrow?

The Challenge

Thanks to Part 1, we now have access to rich summary data using SummaryWithContent:

// lib/models/daily_recap_models.dart

// Don't forget the imports!
import 'dart:convert';
import 'package:google_generative_ai/google_generative_ai.dart';
import '../models/daily_recap_models.dart';

SummaryWithContent {
  id: "4f302bfd-f3c2-4f85-aa79-e7cb314e111d",
  title: "Implemented WebSocket sync",
  content: "# Project XYZ\nFixed critical authentication bug in OAuth token refresh.
            Implemented real-time WebSocket synchronization with automatic 
            reconnection. Pair programmed with Bob on the WebSocket integration...",
  timestamp: 2025-11-04 14:32:15
}

Enter fullscreen mode Exit fullscreen mode

This is great! But it's still just raw text. What we want is structured and actionable insights:

SUMMARY:
   Successfully fixed critical authentication bug and implemented 
   real-time WebSocket synchronization for better data flow.

PROJECTS:
   βœ… Authentication Service [completed]
      Fixed OAuth token refresh logic

   πŸ”„ WebSocket Integration [in_progress]
      Implemented real-time sync with automatic reconnection

PEOPLE WORKED WITH:
   β€’ Alice (code review)
   β€’ Bob (pair programming)

REMINDERS:
   ⚠️  Test WebSocket with production load
   ⚠️  Update documentation for new auth flow
NOTES:
  - Send a message in Google Chat about the progress!

Enter fullscreen mode Exit fullscreen mode

See the difference? One is data, the other is information.

Enter Gemini

Google's Gemini API is perfect for this. It can:

  • Understand natural language
  • Extract structured information
  • Return JSON (which is exactly what we need!)
  • Process multiple summaries at once

Setting Up

First, add the Gemini SDK to pubspec.yaml below the β€˜git:’dependency:

dependencies:
  google_generative_ai: ^0.4.6

Enter fullscreen mode Exit fullscreen mode

You'll also need an API key. Get one from Google AI Studio – it's free for reasonable usage!

Building the Daily Recap Service

Let's create a new service: lib/services/daily_recap_service.dart.

class DailyRecapService {
  final GenerativeModel _model;

  DailyRecapService({required String apiKey})
      : _model = GenerativeModel(
          model: 'gemini-2.5-flash-lite',  // Fast, efficient, and cost-effective!
          apiKey: apiKey,
          generationConfig: GenerationConfig(
            temperature: 0.7,  // Balanced creativity
            topK: 40,
            topP: 0.95,
            maxOutputTokens: 2048,
            responseMimeType: 'application/json',
          ),
        );

  Future<DailyRecapData> generateDailyRecap({
    required DateTime date,
    required List<SummaryWithContent> summaries,
  }) async {
    // We'll build this step by step!
  }
}

Enter fullscreen mode Exit fullscreen mode

-** gemini-2.5-flash-lite:** faster and more cost-effective
-** temperature: 0.7: **Not too creative, not too rigid

Now, let's build the generateDailyRecap function piece by piece.

The Prompt Engineering

Crafting the right prompt is an art. Here are some important tips:

Avoid Vague prompts

Analyze these summaries and tell me what I did today.

Result: Always try to be specific. AI does not read your mind… yet! (Pieces does read your mind, but whatever πŸ˜‰)

Better Structure

Return JSON with: summary, projects, people.

Result: Always say what do you expect the AI to return to be able to correctly parse it

Show Some Examples

Extract and organize into this EXACT JSON format:
{
  "summary": "Brief 1-2 sentence overview",
  "people": ["Person1", "Person2"],
  "projects": [
    {
      "name": "Project Name",
      "description": "What was done",
      "status": "in_progress"  // or "completed" or "not_started"
    }
  ],
  "reminders": ["Reminder 1"],
  "notes": ["Important insight"]
}
Enter fullscreen mode Exit fullscreen mode

Result: AI is similar to human best way to understand by examples

Our Beautiful Prompt

Here's the final prompt we’ll use:

// lib/services/daily_recap_service.dart

  // Add below DailyRecapService() and above Future<>


  /// Build the prompt for Gemini
  String _buildPrompt(DateTime date, String context) {
    final dateStr =
        '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';

    return '''You are an AI assistant analyzing a developer's workstream summaries for the day: $dateStr.

Based on the following workstream summaries, extract and organize the information into a structured daily recap.

WORKSTREAM SUMMARIES:
$context

Your task is to analyze these summaries and create a comprehensive daily recap with the following information:

1. **summary** (string, 1-2 sentences): A brief overview of what was accomplished today. Focus on the main achievements and work done.

2. **people** (array of strings): List of people mentioned or collaborated with. Look for names, @mentions, or collaboration indicators. Can be empty if no one is mentioned.

3. **projects** (array of objects): Projects worked on today. Each project should have:
   - **name** (string): Project or feature name
   - **description** (string): Brief description of what was done
   - **status** (string): One of: "completed", "in_progress", or "not_started"

4. **reminders** (array of strings): Action items, TODOs, or things to remember for later. Look for phrases like "need to", "should", "TODO", "remember to", etc. Can be empty.

5. **notes** (array of strings): Important observations, learnings, or technical notes from the day. Look for insights, discoveries, or important information. Can be empty.

IMPORTANT GUIDELINES:
- Be concise but informative
- Extract actual information from the summaries, don't make things up
- If a category has no relevant information, use an empty array [] or empty string ""
- For project status: use "completed" if the work is done, "in_progress" if actively working on it, "not_started" if mentioned but not begun
- People names should be just the name (e.g., "Alice", "Bob")
- Keep descriptions clear and specific

Return ONLY valid JSON in this exact format:
{
  "summary": "Brief 1-2 sentence overview",
  "people": ["Person1", "Person2"],
  "projects": [
    {
      "name": "Project Name",
      "description": "What was done",
      "status": "in_progress"
    }
  ],
  "reminders": ["Reminder 1", "Reminder 2"],
  "notes": ["Note 1", "Note 2"]
}
''';
  }

Enter fullscreen mode Exit fullscreen mode

Why this works:

1.Clear role: "You are an AI assistant..."
2.Specific format: Exact JSON structure
3*. Examples:* Shows what we want
4.** Constraints:** "Don't make things up", "Empty arrays if no data"
5.** Enum values:** Explicit status options

These two helper methods,_buildPrompt and _buildSummariesContext(we'll see next), are what power our analysis. But where do they fit in the actual application?

Sending Rich Context to Gemini

Now that we have the actual summary content, we can build a rich prompt:

 // lib/services/daily_recap_service.dart

 // Add below _buildPrompt() and above generateDailyRecap()

 String _buildSummariesContext(List<SummaryWithContent> summaries) {
    final buffer = StringBuffer();

    for (int i = 0; i < summaries.length; i++) {
      final summary = summaries[i];
      buffer.writeln('Summary ${i + 1}:');
      buffer.writeln('  ID: ${summary.id}');
      buffer.writeln('  Title: ${summary.title}');
      buffer.writeln(
        '  Time: ${summary.timestamp.hour.toString().padLeft(2, '0')}:${summary.timestamp.minute.toString().padLeft(2, '0')}',
      );
      buffer.writeln('  Content: ${summary.content}');
      buffer.writeln();
    }

    return buffer.toString();
  }

Enter fullscreen mode Exit fullscreen mode

This formats each summary with structured labels and metadata, giving Gemini way more context to work with

Let’s add an empty factory method to create an empty DailyRecapData

// lib/services/daily_recap_service.dart

// Add below _buildPrompt() and above generateDailyRecap() 
 factory DailyRecapData.empty(DateTime date) {
    return DailyRecapData(
      date: date,
      summary: '',
      people: [],
      projects: [],
      reminders: [],
      notes: [],
    );
  }

Enter fullscreen mode Exit fullscreen mode

Handling the Response

Gemini returns JSON (because we set responseMimeType), so parsing is straightforward:

// lib/services/daily_recap_service.dart

// Replace the comment in generateDailyRecap() with this:

try {
  final response = await _model.generateContent([Content.text(prompt)]);
  print("Gemini response received. ${response.text}");  // Debug output
  final rawText = response.text ?? '{}';

  // Extract JSON from markdown code blocks
  final jsonText = _extractJsonFromMarkdown(rawText);
  final data = jsonDecode(jsonText) as Map<String, dynamic>;

  return DailyRecapData.fromJson(date, data); // ... generate recap
} catch (e) {
  print('Error generating recap: $e');
  return DailyRecapData.empty(date);  // Safe fallback
}

Enter fullscreen mode Exit fullscreen mode

Putting It All Together: The Complete generateDailyRecap Function

Now that we've seen all the pieces, here's how they fit together in the actual generateDailyRecap function (how yours should look πŸ˜‰):

Future<DailyRecapData> generateDailyRecap({
  required DateTime date,
  required List<SummaryWithContent> summaries,
}) async {
  // Step 1: Handle edge case - no summaries
  if (summaries.isEmpty) {
    return DailyRecapData.empty(date);
  }

  // Step 2: Build context from summaries using our helper method
  final context = _buildSummariesContext(summaries);

  // Step 3: Craft the prompt using our prompt builder
  final prompt = _buildPrompt(date, context);

  try {
    // Step 4: Send to Gemini and get response
    final response = await _model.generateContent([Content.text(prompt)]);
    print("Gemini response received. ${response.text}");

    // Step 5: Parse the JSON response
    final jsonText = response.text ?? '{}';
    final data = jsonDecode(jsonText) as Map<String, dynamic>;

    // Step 6: Convert to our data model and return
    return DailyRecapData.fromJson(date, data);
  } catch (e) {
    print('Error generating daily recap: $e');
    rethrow; // Let the UI handle the error
  }
}

Enter fullscreen mode Exit fullscreen mode

See how it flows?

  • Check for empty summaries
  • Build the context string from all summaries
  • Create the prompt with instructions
  • Send to Gemini
  • Parse the JSON response
  • Return structured data (or throw error)

The Data Models

I created clean data classes to work with:

// lib/models/daily_recap_models.dart

// Add before SummaryWithContent class and after ProjectStatus {}

class ProjectData {
  final String name;
  final String description;
  final ProjectStatus status;  // enum: completed, inProgress, notStarted

  ProjectData({
    required this.name,
    required this.description,
    required this.status,
  });

  factory ProjectData.fromJson(Map<String, dynamic> json) {
    return ProjectData(
      name: json['name'] as String,
      description: json['description'] as String,
      status: _statusFromString(json['status'] as String),
    );
  }

  static ProjectStatus _statusFromString(String status) {
    switch (status) {
      case 'completed':
        return ProjectStatus.completed;
      case 'in_progress':
        return ProjectStatus.inProgress;
      case 'not_started':
        return ProjectStatus.notStarted;
      default:
        return ProjectStatus.notStarted;
    }
  }
}

class DailyRecapData {
  final DateTime date;
  final String summary;
  final List<String> people;
  final List<ProjectData> projects;
  final List<String> reminders;
  final List<String> notes;

  DailyRecapData({
    required this.date,
    required this.summary,
    required this.people,
    required this.projects,
    required this.reminders,
    required this.notes,
  });

  factory DailyRecapData.fromJson(DateTime date, Map<String, dynamic> json) {
    return DailyRecapData(
      date: date,
      summary: json['summary'] as String? ?? '',
      people: (json['people'] as List<dynamic>?)
              ?.map((e) => e as String)
              .toList() ??
          [],
      projects: (json['projects'] as List<dynamic>?)
              ?.map((e) => ProjectData.fromJson(e as Map<String, dynamic>))
              .toList() ??
          [],
      reminders: (json['reminders'] as List<dynamic>?)
              ?.map((e) => e as String)
              .toList() ??
          [],
      notes: (json['notes'] as List<dynamic>?)
              ?.map((e) => e as String)
              .toList() ??
          [],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This gives us type safety and makes it easy to work with the data later.

Look at what Gemini did:

βœ… Understood that I was working on "Pieces OS Integration"
βœ… Correctly identified it as "completed"
βœ… Extracted actual reminders from my work
βœ… Pulled out technical notes I discovered
βœ… Wrote a coherent summary of the day

And it did all this from just timestamps and titles!

Real-World Example: What Gemini Generated

Here's an actual JSON response that Gemini generated from my workstream summaries:

{
  "summary": "Today's work focused on enhancing user experience for video content, developing a tag generator, testing Flutter capabilities, and reviewing code and infrastructure. Significant progress was made on persona generation for AI training data.",
  "people": [],
  "projects": [
    {
      "name": "Video Analytics & User Experience",
      "description": "Analyzed YouTube video analytics for 'Pieces' content and discussed strategies for improving new user experience by leveraging context and memory,
 considering phased UI exposure and temporary access keys.",
      "status": "in_progress"
    },
    {
      "name": "Tag Generator",
      "description": "Generated a Python script for thematic tagging.",
      "status": "completed"
    },
    {
      "name": "Flutter macOS Dynamic Library Loading",
      "description": "Demonstrated Flutter's macOS dynamic library loading and confirmed clipboard monitoring functionality and its integration with long-term memory.",
      "status": "in_progress"
    },
    {
      "name": "AI Persona Generation",
      "description": "Discussed UI for AI persona generation and initiated content compilation for a partner. Presented the 'persona-query-tag-dataset-gen' project, det
ailing the creation of realistic user personas for the Pieces AI assistant, showcasing comprehensive attributes and providing an example of 'Anja Vestergaard'.",
      "status": "in_progress"
    },
    {
      "name": "ML Training & Django API",
      "description": "Addressed an `UnboundLocalError` in ML training and validated nested task management for a Django API.",
      "status": "completed"
    },
    {
      "name": "Infrastructure Upgrades & Containerization",
      "description": "Reviewed infrastructure upgrades, containerization strategies, cost optimizations, and bug fixes across multiple services, including timezone and 
user invitation flows.",
      "status": "in_progress"
    }
  ],
  "reminders": [],
  "notes": [
    "Leveraging context and memory for new user experience improvements.",
    "Clipboard monitoring functionality confirmed and integrated with long-term memory.",
    "Objective for persona generation project is to generate authentic training data for the AI assistant."
  ]
}

Enter fullscreen mode Exit fullscreen mode

Complete Reference Implementation

For your reference, here's the complete lib/services/daily_recap_service.dart file with everything we've built:

  • The service initialization with Gemini configuration
  • The generateDailyRecap function that orchestrates everything
  • The _buildSummariesContext helper for formatting summaries
  • The _buildPrompt helper for crafting the AI prompt

Continue reading here.

Top comments (0)