Skip to main content

cognee_models/
temporal_event.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// A point in time extracted from text during temporal cognify.
6/// Mirrors Python: cognee.modules.engine.models.Timestamp
7/// time_at stores milliseconds since Unix epoch (UTC) — same unit as Python.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9pub struct CognifyTimestamp {
10    pub year: u16,
11    pub month: u8,  // 1-12; unknown → 1
12    pub day: u8,    // 1-31; unknown → 1
13    pub hour: u8,   // 0-23; unknown → 0
14    pub minute: u8, // 0-59; unknown → 0
15    pub second: u8, // 0-59; unknown → 0
16    /// Milliseconds since Unix epoch (UTC). Computed from the date/time fields.
17    pub time_at: i64,
18    /// Formatted string "YYYY-MM-DD HH:MM:SS" for human readability.
19    pub timestamp_str: String,
20}
21
22/// A time range stored as a graph node of type "Interval".
23/// Mirrors Python: cognee.modules.engine.models.Interval
24/// Field names time_from / time_to match Python exactly.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
26pub struct CognifyInterval {
27    pub time_from: CognifyTimestamp,
28    pub time_to: CognifyTimestamp,
29}
30
31/// An event extracted from text, optionally anchored to a point or range in time.
32/// Mirrors Python: cognee.modules.engine.models.Event
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
34pub struct TemporalEvent {
35    pub name: String,
36    pub description: Option<String>,
37    pub location: Option<String>,
38    /// Single point-in-time: creates edge Event -[at]-> Timestamp.
39    /// Mutually exclusive with `during`.
40    pub at: Option<CognifyTimestamp>,
41    /// Time range: creates edge Event -[during]-> Interval.
42    /// The Interval node then carries edges to its two Timestamps.
43    /// Mutually exclusive with `at`.
44    pub during: Option<CognifyInterval>,
45    /// Entity attributes attached by the second LLM pass.
46    #[serde(default)]
47    pub attributes: Vec<EventAttribute>,
48}
49
50/// An entity related to an event, extracted during temporal entity enrichment.
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
52pub struct EventAttribute {
53    pub entity: String,
54    pub entity_type: String,
55    /// Snake_case relationship name, 1-2 words, e.g. "subject", "participant", "source_cause".
56    pub relationship: String,
57}
58
59fn default_month() -> u8 {
60    1
61}
62
63fn default_day() -> u8 {
64    1
65}
66
67/// LLM output schema for a timestamp. Mirrors Python task model Timestamp.
68/// All fields except year default to 1/0; the extractor computes time_at.
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
70pub struct RawExtractedTimestamp {
71    pub year: u16,
72    #[serde(default = "default_month")]
73    pub month: u8, // default 1
74    #[serde(default = "default_day")]
75    pub day: u8, // default 1
76    #[serde(default)]
77    pub hour: u8, // default 0
78    #[serde(default)]
79    pub minute: u8, // default 0
80    #[serde(default)]
81    pub second: u8, // default 0
82}
83
84/// Convert a raw LLM-extracted timestamp to a CognifyTimestamp with computed time_at.
85/// Returns None if the date is invalid (e.g. month=13).
86pub fn to_cognify_timestamp(raw: RawExtractedTimestamp) -> Option<CognifyTimestamp> {
87    let naive = NaiveDate::from_ymd_opt(raw.year as i32, raw.month as u32, raw.day as u32)?
88        .and_hms_opt(raw.hour as u32, raw.minute as u32, raw.second as u32)?;
89    let time_at = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc).timestamp_millis(); // milliseconds, matching Python
90    let timestamp_str = format!(
91        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
92        raw.year, raw.month, raw.day, raw.hour, raw.minute, raw.second
93    );
94    Some(CognifyTimestamp {
95        year: raw.year,
96        month: raw.month,
97        day: raw.day,
98        hour: raw.hour,
99        minute: raw.minute,
100        second: raw.second,
101        time_at,
102        timestamp_str,
103    })
104}
105
106#[cfg(test)]
107#[allow(
108    clippy::unwrap_used,
109    clippy::expect_used,
110    reason = "test code — panics are acceptable failures"
111)]
112mod tests {
113    use super::*;
114    use chrono::{TimeZone, Utc};
115
116    #[test]
117    fn to_cognify_timestamp_happy_path() {
118        let raw = RawExtractedTimestamp {
119            year: 2024,
120            month: 3,
121            day: 15,
122            hour: 0,
123            minute: 0,
124            second: 0,
125        };
126        let result = to_cognify_timestamp(raw).unwrap();
127
128        let expected_dt = Utc.with_ymd_and_hms(2024, 3, 15, 0, 0, 0).unwrap();
129        assert_eq!(result.time_at, expected_dt.timestamp_millis());
130        assert_eq!(result.timestamp_str, "2024-03-15 00:00:00");
131        assert_eq!(result.year, 2024);
132        assert_eq!(result.month, 3);
133        assert_eq!(result.day, 15);
134    }
135
136    #[test]
137    fn to_cognify_timestamp_with_time_components() {
138        let raw = RawExtractedTimestamp {
139            year: 2024,
140            month: 7,
141            day: 4,
142            hour: 14,
143            minute: 30,
144            second: 45,
145        };
146        let result = to_cognify_timestamp(raw).unwrap();
147
148        let expected_dt = Utc.with_ymd_and_hms(2024, 7, 4, 14, 30, 45).unwrap();
149        assert_eq!(result.time_at, expected_dt.timestamp_millis());
150        assert_eq!(result.timestamp_str, "2024-07-04 14:30:45");
151        assert_eq!(result.hour, 14);
152        assert_eq!(result.minute, 30);
153        assert_eq!(result.second, 45);
154    }
155
156    #[test]
157    fn to_cognify_timestamp_invalid_dates_return_none() {
158        // Month 13 is invalid
159        let raw = RawExtractedTimestamp {
160            year: 2024,
161            month: 13,
162            day: 1,
163            hour: 0,
164            minute: 0,
165            second: 0,
166        };
167        assert!(to_cognify_timestamp(raw).is_none());
168
169        // Feb 30 is invalid
170        let raw = RawExtractedTimestamp {
171            year: 2024,
172            month: 2,
173            day: 30,
174            hour: 0,
175            minute: 0,
176            second: 0,
177        };
178        assert!(to_cognify_timestamp(raw).is_none());
179    }
180
181    #[test]
182    fn to_cognify_timestamp_serde_defaults() {
183        // Simulates serde defaults: year from LLM, month=1, day=1, h/m/s=0
184        let raw = RawExtractedTimestamp {
185            year: 1889,
186            month: 1,
187            day: 1,
188            hour: 0,
189            minute: 0,
190            second: 0,
191        };
192        let result = to_cognify_timestamp(raw).unwrap();
193
194        let expected_dt = Utc.with_ymd_and_hms(1889, 1, 1, 0, 0, 0).unwrap();
195        assert_eq!(result.time_at, expected_dt.timestamp_millis());
196        assert_eq!(result.timestamp_str, "1889-01-01 00:00:00");
197        assert_eq!(result.year, 1889);
198        assert_eq!(result.month, 1);
199        assert_eq!(result.day, 1);
200    }
201}