/

AI & LLM

Dec 4, 2025

Dec 4, 2025

Building a daily productivity app with Pieces — Part 2: Adding AI Intelligence with Gemini

Build a daily productivity app with Pieces (Part 2) by adding AI intelligence with Google Gemini, covering architecture, prompts, integrations, and practical tips to ship smarter workflows.

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

  • Connects to PiecesOS and maintains a WebSocket connection

  • Fetches and caches workstream summaries grouped by day

  • 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
}

This is great! But it's still just raw text. What we want are 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

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

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!
  }
}
  • 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"]
}

Result: AI is similar to humans; the best way to understand is 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"]
}
''';
  }

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();
  }

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: [],
    );
  }


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
}


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
  }
}

See how it flows?

  1. Check for empty summaries

  2. Build the context string from all summaries

  3. Create the prompt with instructions

  4. Send to Gemini

  5. Parse the JSON response

  6. 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() ??
          [],
    );
  }
}

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."
  ]
}


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

class DailyRecapService {
  final GenerativeModel _model;
  late final Box<DailyRecapData> _cacheBox;

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


  /// Generate a daily recap from workstream summaries with their content
  /// Set forceRegenerate to true to bypass cache
  Future<DailyRecapData> generateDailyRecap({
    required DateTime date,
    required List<SummaryWithContent> summaries,
  }) async {
    // Check cache first (unless forcing regeneration)
    if (summaries.isEmpty) {
      return DailyRecapData.empty(date);
    }

    // Build context from summaries
    final context = _buildSummariesContext(summaries);

    // Craft the prompt
    final prompt = _buildPrompt(date, context);

    try {
      final response = await _model.generateContent([Content.text(prompt)]);
      // ignore: avoid_print
      print("Gemini response received. ${response.text}");
      final jsonText = response.text ?? '{}';

      // Parse the JSON response
      final data = jsonDecode(jsonText) as Map<String, dynamic>;

      final recap = DailyRecapData.fromJson(date, data);

      return recap;
    } catch (e) {
      // ignore: avoid_print
      print('Error generating daily recap: $e');
      rethrow; // Throw error so UI can handle it
    }
  }

  /// Build context string from summaries with their content
  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();
  }

/// Extract JSON from markdown code blocks
  String _extractJsonFromMarkdown(String text) {
    // Remove markdown code block formatting
    String cleaned = text.trim();
    
    // Remove leading ```json or ```
    if (cleaned.startsWith('```')) {
      cleaned = cleaned.replaceFirst(RegExp(r'^```(?:json)?\s*'), '');
    }
    
    // Remove trailing ```
    if (cleaned.endsWith('```')) {
      cleaned = cleaned.replaceFirst(RegExp(r'\s*```$'), '');
    }
    
    return cleaned.trim();
  }

  /// 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"]
}
''';
  }
}

Reference GitHub to view the full project.

Written by

Written by

SHARE

Building a daily productivity app with Pieces — Part 2: Adding AI Intelligence with Gemini

Recent

Dec 4, 2025

Dec 4, 2025

Building a daily productivity app with Pieces — Part 2: Adding AI Intelligence with Gemini

Build a daily productivity app with Pieces (Part 2) by adding AI intelligence with Google Gemini, covering architecture, prompts, integrations, and practical tips to ship smarter workflows.

Build a daily productivity app with Pieces (Part 2) by adding AI intelligence with Google Gemini, covering architecture, prompts, integrations, and practical tips to ship smarter workflows.

Dec 1, 2025

Dec 1, 2025

Building daily stand-up generator using Pieces API — Part 1: The SDK overview

Learn how to build a daily stand-up generator using the Pieces API. This first part of the series covers the SDK overview, key capabilities, and how developers can streamline workflow automation with Pieces.

Learn how to build a daily stand-up generator using the Pieces API. This first part of the series covers the SDK overview, key capabilities, and how developers can streamline workflow automation with Pieces.

Nov 27, 2025

Nov 27, 2025

How we stopped watching our engineers struggle through stand-ups

Tired of awkward standup meetings where great engineers sound like they did nothing? I automated our team's standups with AI and got 3 people promoted. Here's exactly how we changed standups and made real work visible to managers.

Tired of awkward standup meetings where great engineers sound like they did nothing? I automated our team's standups with AI and got 3 people promoted. Here's exactly how we changed standups and made real work visible to managers.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.