Skip to main content

cognee_database/ops/
tutorial_seeder.rs

1//! Tutorial notebook seeder — called on first `list_notebooks` for a fresh user.
2//!
3//! Cell assets are bundled at compile time via `include_dir!`.  The seeder
4//! inserts two notebooks with deterministic UUID5 ids that match the Python SDK.
5
6use include_dir::{Dir, include_dir};
7use serde_json::{Value, json};
8use tracing::instrument;
9use uuid::{Uuid, uuid};
10
11use crate::traits::NotebookDb;
12use crate::types::DatabaseError;
13
14// ─── Bundled assets ───────────────────────────────────────────────────────────
15
16static TUTORIALS_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/assets/notebooks/tutorials");
17
18// ─── Deterministic notebook IDs ──────────────────────────────────────────────
19
20/// UUID5(NAMESPACE_OID, "Cognee Basics - tutorial 🧠")
21pub const TUTORIAL_BASICS_ID: Uuid = uuid!("c29dfdef-70d8-5c6d-8968-ed7f019ab20b");
22
23/// UUID5(NAMESPACE_OID, "Python Development with Cognee - tutorial 🧠")
24pub const TUTORIAL_PYTHON_DEV_ID: Uuid = uuid!("057cf04b-ab12-5052-84d9-492203097a56");
25
26// ─── Helpers ─────────────────────────────────────────────────────────────────
27
28fn extract_markdown_heading(content: &str) -> Option<&str> {
29    for line in content.lines() {
30        let trimmed = line.trim();
31        if let Some(rest) = trimmed.strip_prefix("### ") {
32            return Some(rest.trim());
33        }
34        if let Some(rest) = trimmed.strip_prefix("## ") {
35            return Some(rest.trim());
36        }
37        if let Some(rest) = trimmed.strip_prefix("# ") {
38            return Some(rest.trim());
39        }
40    }
41    None
42}
43
44fn parse_cell_index(name: &str) -> i64 {
45    let stem = name
46        .strip_suffix(".md")
47        .or_else(|| name.strip_suffix(".py"))
48        .unwrap_or(name);
49    stem.strip_prefix("cell-")
50        .and_then(|s| s.parse().ok())
51        .unwrap_or(-1)
52}
53
54fn build_cells(tutorial_dir: &include_dir::Dir<'_>) -> Value {
55    let mut entries: Vec<(i64, Value)> = tutorial_dir
56        .files()
57        .filter_map(|f| {
58            let name = f.path().file_name()?.to_str()?;
59            if !name.starts_with("cell-") {
60                return None;
61            }
62            let content = f.contents_utf8()?;
63            let idx = parse_cell_index(name);
64            let (cell_type, cell_name) = if name.ends_with(".md") {
65                let heading = extract_markdown_heading(content).unwrap_or(name);
66                ("markdown", heading.to_owned())
67            } else if name.ends_with(".py") {
68                ("code", "Code Cell".to_owned())
69            } else {
70                return None;
71            };
72
73            let cell = json!({
74                "id": Uuid::new_v4().to_string(),
75                "type": cell_type,
76                "name": cell_name,
77                "content": content,
78            });
79            Some((idx, cell))
80        })
81        .collect();
82
83    entries.sort_by_key(|(idx, _)| *idx);
84    Value::Array(entries.into_iter().map(|(_, v)| v).collect())
85}
86
87// ─── Tutorial spec ─────────────────────────────────────────────────────────────
88
89struct TutorialSpec {
90    id: Uuid,
91    name: &'static str,
92    dir_name: &'static str,
93}
94
95const TUTORIALS: &[TutorialSpec] = &[
96    TutorialSpec {
97        id: TUTORIAL_BASICS_ID,
98        name: "Cognee Basics - tutorial 🧠",
99        dir_name: "cognee-basics",
100    },
101    TutorialSpec {
102        id: TUTORIAL_PYTHON_DEV_ID,
103        name: "Python Development with Cognee - tutorial 🧠",
104        dir_name: "python-development-with-cognee",
105    },
106];
107
108// ─── Public API ───────────────────────────────────────────────────────────────
109
110/// Seed the two tutorial notebooks for `user_id` if not already present.
111///
112/// Idempotent: if both tutorial ids already exist for this owner, this is
113/// a no-op.  If either is missing it is inserted with the deterministic UUID5
114/// id and `deletable=false`.
115#[instrument(
116    name = "cognee.db.relational.tutorial_seeder.seed_tutorials_if_first_call",
117    level = "info",
118    skip_all,
119    err
120)]
121pub async fn seed_tutorials_if_first_call(
122    db: &dyn NotebookDb,
123    user_id: Uuid,
124) -> Result<(), DatabaseError> {
125    for spec in TUTORIALS {
126        if db.get_by_id_and_owner(spec.id, user_id).await?.is_some() {
127            continue;
128        }
129
130        let cells = match TUTORIALS_DIR.get_dir(spec.dir_name) {
131            Some(dir) => build_cells(dir),
132            None => {
133                tracing::warn!(
134                    "Tutorial directory '{}' not found in bundled assets",
135                    spec.dir_name
136                );
137                json!([])
138            }
139        };
140
141        db.create_seeded(spec.id, user_id, spec.name.to_owned(), cells, false)
142            .await?;
143    }
144    Ok(())
145}