Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions prompts/daily_review_signal_extraction_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
You analyze a completed daily review, supported by the journal entries and structured extractions from the same day.

Your task is to extract normalized day-level signals from the daily review.
Do not give advice.
Do not diagnose.
Do not write a reflection.
Do not invent claims that are not supported by the daily review or the source entries.
Do not infer cross-day trends. Each signal must represent that day only.

Return a JSON object with a "signals" array. Each element is a signal with these fields:
- signal_type: one of "theme", "emotion", "behavior", "need", "tension", "pattern", "tomorrow_attention"
- label: short normalized label for the signal (must not be empty)
- status: need status (use only for need signals), null otherwise — one of: "activated", "unmet", "fulfilled", "unclear"
- valence: behavior valence (use only for behavior signals), null otherwise — one of: "positive", "negative", "ambiguous", "neutral", "unclear"
- strength: number 0.0–1.0, how strongly this signal appeared that day
- confidence: number 0.0–1.0, how certain you are that the signal is supported by the source material
- evidence: short, specific sentence grounded in the daily review or entries

Signal type rules:
- theme: a recurring topic or domain that shaped the day
- emotion: a felt emotional state with clear presence in the review or entries
- behavior: something the user did or consistently did not do
- need: a psychological need or value that was salient, unmet, or fulfilled
- tension: an internal conflict or competing pull
- pattern: a day-level repeated pattern — only if the review or entries explicitly support it; never infer cross-day patterns
- tomorrow_attention: a specific point of attention or intention suggested for the next day

Field constraints:
- behavior signals must have a valence; all other types must have valence null
- need signals must have a status; all other types must have status null
- strength and confidence must be between 0.0 and 1.0
- evidence must be short and directly grounded in the provided material

Label reuse:
- You may receive a list of known labels from previous days, grouped by signal type.
- When a signal you are about to emit has the same meaning as a known label of the same type, reuse that exact label instead of minting a new variant (e.g., reuse "plan switching" instead of writing "switching plans").
- Only create a new label when no known label fits the meaning.
- Never force a known label onto a signal with a different meaning.

Quality rules:
- Do not create a signal from a weak hint; omit it instead, or use very low confidence
- Do not create diagnosis-like signals (e.g., "anxiety disorder", "depression")
- Do not make identity-level claims about the user (e.g., "user is a perfectionist")
- Do not create a pattern signal unless the daily review text explicitly supports a within-day repetition
- Keep labels short and normalized (2–5 words)
- Keep evidence to one sentence
- Prefer omitting uncertain signals over generating them with low confidence labels
28 changes: 28 additions & 0 deletions prompts/daily_review_with_entry_extractions_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Daily Review Prompt (with Entry Extractions) v2

You generate concise daily journal reviews.

## Context

You receive raw journal notes and, when available, structured extractions for those notes.
- Raw journal notes are the **source of truth**.
- Structured extractions are **analytical aids** provided by another process to help you notice patterns.

You may also receive points of attention carried over from yesterday's review.
- Carried-over points are **context**, not facts about today.

## Rules

- Use only the data provided in the prompt.
- **Trust the raw journal notes if they conflict with the structured extractions.**
- Extractions may be imperfect; use them to notice repeated emotions, behaviors, needs, domains, and signals, but do not blindly repeat them.
- Summarize emotional and practical themes from today only.
- Identify notable patterns or tensions from today only.
- Suggest one or two practical points of attention for tomorrow.
- If today's entries clearly address or repeat a carried-over point of attention, note the follow-through or recurrence briefly; otherwise ignore the carried-over point. Do not invent progress or guilt the user about it.
- Keep the review concise, readable, and grounded.
- Match the main language used in the journal entries.
- If there are too few entries to identify meaningful themes, say so briefly.
- Avoid clinical diagnosis or therapy-style overreach.
- Do not overstate patterns or claim long-term patterns from one day of evidence.
- Do not include a top-level "Today's review" heading; the application adds it.
173 changes: 133 additions & 40 deletions src/journal/review/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ use rig::{
use thiserror::Error;

use crate::{
journal::review::{DailyReviewPrompt, DailyReviewPromptError, JournalEntryWithExtraction},
journal::review::{
DailyReviewPrompt, DailyReviewPromptError, JournalEntryWithExtraction,
signals::types::DailyReviewSignal,
},
prompts::{PromptSource, ResolvedPrompt},
};

Expand Down Expand Up @@ -61,6 +64,7 @@ pub trait ReviewGenerator: Send + Sync {
async fn generate_daily_review(
&self,
entries: &[JournalEntryWithExtraction],
carried_attention: &[DailyReviewSignal],
) -> Result<String, ReviewGenerationError>;
}

Expand Down Expand Up @@ -197,9 +201,10 @@ impl ReviewGenerator for RigOpenAiReviewGenerator {
async fn generate_daily_review(
&self,
entries: &[JournalEntryWithExtraction],
carried_attention: &[DailyReviewSignal],
) -> Result<String, ReviewGenerationError> {
self.refresh_prompt().await?;
let prompt = build_daily_review_prompt(entries);
let prompt = build_daily_review_prompt(entries, carried_attention);
let instructions = self.prompt.read().unwrap().text.clone();
self.provider
.complete_daily_review(&self.config.model, &instructions, &prompt)
Expand All @@ -208,7 +213,10 @@ impl ReviewGenerator for RigOpenAiReviewGenerator {
}
}

fn build_daily_review_prompt(entries: &[JournalEntryWithExtraction]) -> String {
fn build_daily_review_prompt(
entries: &[JournalEntryWithExtraction],
carried_attention: &[DailyReviewSignal],
) -> String {
let formatted_entries = entries
.iter()
.map(|entry_with_ext| {
Expand All @@ -232,6 +240,24 @@ fn build_daily_review_prompt(entries: &[JournalEntryWithExtraction]) -> String {
.collect::<Vec<_>>()
.join("\n");

let carried_section = if carried_attention.is_empty() {
String::new()
} else {
let points = carried_attention
.iter()
.map(|signal| {
format!(
"- {} (flagged on {}): \"{}\"",
signal.label, signal.review_date, signal.evidence
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
"\n\nCarried over from yesterday's review — points of attention previously flagged for today:\n{points}"
)
};

format!(
r#"Write a daily review using only these journal entries.

Expand All @@ -244,7 +270,7 @@ Themes:
- ...

Pay attention tomorrow:
- ...
- ...{carried_section}

Journal entries:
{formatted_entries}"#
Expand All @@ -264,7 +290,7 @@ pub mod fake {
use async_trait::async_trait;

use super::{ReviewGenerationError, ReviewGenerator};
use crate::journal::review::JournalEntryWithExtraction;
use crate::journal::review::{JournalEntryWithExtraction, signals::types::DailyReviewSignal};

#[derive(Debug, Clone)]
pub struct FakeReviewGenerator {
Expand All @@ -273,6 +299,7 @@ pub mod fake {
results: Arc<Mutex<VecDeque<Result<String, ReviewGenerationError>>>>,
calls: Arc<AtomicUsize>,
entries_seen: Arc<Mutex<Vec<Vec<JournalEntryWithExtraction>>>>,
carried_seen: Arc<Mutex<Vec<Vec<DailyReviewSignal>>>>,
}

impl FakeReviewGenerator {
Expand All @@ -291,6 +318,7 @@ pub mod fake {
results: Arc::new(Mutex::new(VecDeque::from(results))),
calls: Arc::new(AtomicUsize::new(0)),
entries_seen: Arc::new(Mutex::new(Vec::new())),
carried_seen: Arc::new(Mutex::new(Vec::new())),
}
}

Expand All @@ -301,6 +329,10 @@ pub mod fake {
pub fn entries_seen(&self) -> Vec<Vec<JournalEntryWithExtraction>> {
self.entries_seen.lock().unwrap().clone()
}

pub fn carried_seen(&self) -> Vec<Vec<DailyReviewSignal>> {
self.carried_seen.lock().unwrap().clone()
}
}

#[async_trait]
Expand All @@ -316,9 +348,14 @@ pub mod fake {
async fn generate_daily_review(
&self,
entries: &[JournalEntryWithExtraction],
carried_attention: &[DailyReviewSignal],
) -> Result<String, ReviewGenerationError> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.entries_seen.lock().unwrap().push(entries.to_vec());
self.carried_seen
.lock()
.unwrap()
.push(carried_attention.to_vec());

self.results
.lock()
Expand Down Expand Up @@ -454,11 +491,14 @@ mod tests {
assert_eq!(generator.prompt_version(), "custom-prompt");
assert_eq!(
generator
.generate_daily_review(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "wrote a test"),
extraction: None,
}])
.generate_daily_review(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "wrote a test"),
extraction: None,
}],
&[]
)
.await
.unwrap(),
"review text"
Expand All @@ -480,11 +520,14 @@ mod tests {
);

generator
.generate_daily_review(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "requested date entry"),
extraction: None,
}])
.generate_daily_review(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "requested date entry"),
extraction: None,
}],
&[],
)
.await
.unwrap();

Expand All @@ -504,11 +547,14 @@ mod tests {
);

let error = generator
.generate_daily_review(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "wrote a test"),
extraction: None,
}])
.generate_daily_review(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "wrote a test"),
extraction: None,
}],
&[],
)
.await
.unwrap_err();

Expand All @@ -517,16 +563,54 @@ mod tests {

#[test]
fn generated_prompt_requests_review_format() {
let prompt_text = build_daily_review_prompt(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "finished the feature"),
extraction: None,
}]);
let prompt_text = build_daily_review_prompt(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "finished the feature"),
extraction: None,
}],
&[],
);

assert!(prompt_text.contains("Summary:"));
assert!(prompt_text.contains("Themes:"));
assert!(prompt_text.contains("Pay attention tomorrow:"));
assert!(prompt_text.contains("finished the feature"));
assert!(!prompt_text.contains("Carried over from yesterday"));
}

#[test]
fn generated_prompt_includes_carried_over_attention_points() {
let carried = DailyReviewSignal {
id: 1,
daily_review_id: 1,
review_date: chrono::NaiveDate::from_ymd_opt(2026, 4, 27).unwrap(),
signal_type: crate::journal::review::signals::types::SignalType::TomorrowAttention,
label: "protect the morning focus block".to_string(),
status: None,
valence: None,
strength: 0.8,
confidence: 0.9,
evidence: "Planned to keep mornings meeting-free.".to_string(),
model: "m".to_string(),
prompt_version: "v1".to_string(),
created_at: Utc.with_ymd_and_hms(2026, 4, 27, 22, 0, 0).unwrap(),
updated_at: Utc.with_ymd_and_hms(2026, 4, 27, 22, 0, 0).unwrap(),
};

let prompt_text = build_daily_review_prompt(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "finished the feature"),
extraction: None,
}],
&[carried],
);

assert!(prompt_text.contains("Carried over from yesterday's review"));
assert!(prompt_text.contains("protect the morning focus block"));
assert!(prompt_text.contains("flagged on 2026-04-27"));
assert!(prompt_text.contains("Planned to keep mornings meeting-free."));
}

#[tokio::test]
Expand Down Expand Up @@ -561,11 +645,14 @@ mod tests {
.with_prompt_source(source);

generator
.generate_daily_review(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "first"),
extraction: None,
}])
.generate_daily_review(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "first"),
extraction: None,
}],
&[],
)
.await
.unwrap();

Expand All @@ -586,11 +673,14 @@ mod tests {
.unwrap();

generator
.generate_daily_review(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "second"),
extraction: None,
}])
.generate_daily_review(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "second"),
extraction: None,
}],
&[],
)
.await
.unwrap();

Expand All @@ -613,11 +703,14 @@ mod tests {
needs: vec![],
possible_patterns: vec![],
};
let prompt_text = build_daily_review_prompt(&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "entry with extraction"),
extraction: Some(extraction),
}]);
let prompt_text = build_daily_review_prompt(
&[JournalEntryWithExtraction {
id: "1".to_string(),
entry: entry(28, "entry with extraction"),
extraction: Some(extraction),
}],
&[],
);

assert!(prompt_text.contains("entry with extraction"));
assert!(prompt_text.contains("Entry #1"));
Expand Down
2 changes: 1 addition & 1 deletion src/journal/review/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use crate::prompts::file::{self, PromptFile, PromptFileError};

pub const DEFAULT_REVIEW_PROMPT_PATH: &str = "prompts/daily_review_with_entry_extractions_v1.md";
pub const DEFAULT_REVIEW_PROMPT_PATH: &str = "prompts/daily_review_with_entry_extractions_v2.md";

const PROMPT_KIND: &str = "daily review";

Expand Down
Loading
Loading