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 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!
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 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"]
}
''';
}
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?
- 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() ??
[],
);
}
}
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 functionthat orchestrates everything - The
_buildSummariesContexthelper for formatting summaries - The
_buildPrompthelper for crafting the AI prompt
Continue reading here.
Top comments (0)