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:
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',
apiKey: apiKey,
generationConfig: GenerationConfig(
temperature: 0.7,
topK: 40,
topP: 0.95,
maxOutputTokens: 2048,
responseMimeType: 'application/json',
),
);
Future<DailyRecapData> generateDailyRecap({
required DateTime date,
required List<SummaryWithContent> summaries,
}) async {
}
}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"
}
],
"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:
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:
Clear role: "You are an AI assistant..."
Specific format: Exact JSON structure
Examples: Shows what we want
Constraints: "Don't make things up", "Empty arrays if no data"
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:
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
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:
try {
final response = await _model.generateContent([Content.text(prompt)]);
print("Gemini response received. ${response.text}");
final rawText = response.text ?? '{}';
final jsonText = _extractJsonFromMarkdown(rawText);
final data = jsonDecode(jsonText) as Map<String, dynamic>;
return DailyRecapData.fromJson(date, data);
} catch (e) {
print('Error generating recap: $e');
return DailyRecapData.empty(date);
}
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 {
if (summaries.isEmpty) {
return DailyRecapData.empty(date);
}
final context = _buildSummariesContext(summaries);
final prompt = _buildPrompt(date, context);
try {
final response = await _model.generateContent([Content.text(prompt)]);
print("Gemini response received. ${response.text}");
final jsonText = response.text ?? '{}';
final data = jsonDecode(jsonText) as Map<String, dynamic>;
return DailyRecapData.fromJson(date, data);
} catch (e) {
print('Error generating daily recap: $e');
rethrow;
}
}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:
class ProjectData {
final String name;
final String description;
final ProjectStatus status;
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',
),
);
Future<DailyRecapData> generateDailyRecap({
required DateTime date,
required List<SummaryWithContent> summaries,
}) async {
if (summaries.isEmpty) {
return DailyRecapData.empty(date);
}
final context = _buildSummariesContext(summaries);
final prompt = _buildPrompt(date, context);
try {
final response = await _model.generateContent([Content.text(prompt)]);
print("Gemini response received. ${response.text}");
final jsonText = response.text ?? '{}';
final data = jsonDecode(jsonText) as Map<String, dynamic>;
final recap = DailyRecapData.fromJson(date, data);
return recap;
} catch (e) {
print('Error generating daily recap: $e');
rethrow;
}
}
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();
}
String _extractJsonFromMarkdown(String text) {
String cleaned = text.trim();
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.