···8686@app.post("/v1/responses")
8787async def create_chat_response(request: ResponsesRequest):
8888 """
8989- Create a response with openResponse format
8989+ Create a response with openResponses format
9090 """
91919292 if request.stream:
···4455use anyhow::{Result, anyhow};
66use owo_colors::OwoColorize;
77-use tiles::runtime::Runtime;
88-use tiles::utils::accounts::{
77+use tiles::core;
88+use tiles::core::accounts::{
99 RootUser, create_root_account, get_root_user_details, save_root_account, set_nickname,
1010};
1111+use tiles::runtime::Runtime;
1112use tiles::utils::config::{
1213 ConfigProvider, DefaultProvider, get_or_create_config, set_user_data_path,
1314};
···146147}
147148148149fn setup_default_user_data_dir<T: ConfigProvider>(config_provider: &T) -> Result<()> {
149149- // gets default data dir -> ~/.local/share/tiles/data
150150- // shows this is the data dir
151151- // asks if they want to change, if y, asks for new loc, else keep current one
152152- // writes the default/new path to in config.toml data->path
153153- //
154150 let user_data_dir = config_provider.get_user_data_dir()?;
155151 println!("{}", FTUE_DATA_DIR_PROMPT);
156152 println!(" {}", user_data_dir.display());
···228224}
229225230226pub async fn try_app_update() -> Result<()> {
227227+ // no need to check updates in dev mode
228228+ if cfg!(debug_assertions) {
229229+ return Ok(());
230230+ }
231231 let update_info: UpdateInfo = get_update_info().await?;
232232 if update_info.can_update {
233233 let update_str = format!(
···252252 Ok(())
253253}
254254255255-pub async fn run(runtime: &Runtime, run_args: RunArgs) {
256256- let _ = runtime.run(run_args).await;
255255+pub async fn run(runtime: &Runtime, run_args: RunArgs) -> Result<()> {
256256+ core::init().inspect_err(|e| eprintln!("Tiles core init failed due to {:?}", e))?;
257257+ runtime.run(run_args).await
257258}
258259259260pub fn set_data(path: &str) {
+621
tiles/src/core/accounts.rs
···11+//! Accounts
22+// Stuff related to account and identity system
33+use anyhow::{Result, anyhow};
44+use rusqlite::{Connection, types::FromSqlError};
55+use std::{
66+ fmt::Display,
77+ time::{SystemTime, UNIX_EPOCH},
88+};
99+use tilekit::accounts::create_identity;
1010+use toml::Table;
1111+use uuid::Uuid;
1212+1313+use crate::{
1414+ core::storage::db::{DBTYPE, get_db_conn},
1515+ utils::config::{get_or_create_config, save_config},
1616+};
1717+const ROOT_USER_CONFIG_KEY: &str = "root-user";
1818+1919+const ROOT_PARSE_ERROR: &str = "Failed to parse root user config";
2020+#[allow(dead_code)]
2121+pub struct RootUser {
2222+ pub id: String,
2323+ pub nickname: String,
2424+}
2525+2626+#[derive(Debug)]
2727+pub enum ACCOUNT {
2828+ LOCAL,
2929+}
3030+3131+#[derive(Debug)]
3232+pub struct AccountError {
3333+ pub error: String,
3434+}
3535+3636+impl Display for AccountError {
3737+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3838+ write!(f, "{}", self.error)
3939+ }
4040+}
4141+impl std::error::Error for AccountError {}
4242+impl TryFrom<String> for ACCOUNT {
4343+ type Error = AccountError;
4444+ fn try_from(value: String) -> Result<Self, Self::Error> {
4545+ let value_lower = value.to_lowercase();
4646+ match value_lower.as_str() {
4747+ "local" => Ok(ACCOUNT::LOCAL),
4848+ _ => Err(AccountError {
4949+ error: "Invalid account type".to_owned(),
5050+ }),
5151+ }
5252+ }
5353+}
5454+impl Display for ACCOUNT {
5555+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5656+ match self {
5757+ Self::LOCAL => write!(f, "{}", String::from("local")),
5858+ }
5959+ }
6060+}
6161+6262+//TODO: add doc, mirrors user table schema
6363+#[allow(dead_code)]
6464+#[derive(Debug)]
6565+pub struct User {
6666+ pub id: uuid::Uuid,
6767+ pub user_id: String,
6868+ pub username: String,
6969+ pub active_profile: bool,
7070+ pub account_type: ACCOUNT,
7171+ pub root: bool,
7272+ pub created_at: u64,
7373+ pub updated_at: u64,
7474+}
7575+7676+impl Display for RootUser {
7777+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7878+ write!(f, "id: {}\nnickname: {}\n", self.id, self.nickname)
7979+ }
8080+}
8181+8282+impl RootUser {
8383+ pub fn new(config: &Table) -> Result<Self> {
8484+ let id = config
8585+ .get("id")
8686+ .ok_or_else(|| anyhow!("Missing ID"))?
8787+ .as_str()
8888+ .ok_or_else(|| anyhow!("ID not a string"))?;
8989+ let nickname = config
9090+ .get("nickname")
9191+ .ok_or_else(|| anyhow!("Missing Nickname"))?
9292+ .as_str()
9393+ .ok_or_else(|| anyhow!("Nickname not a string"))?;
9494+ Ok(RootUser {
9595+ id: id.to_owned(),
9696+ nickname: nickname.to_owned(),
9797+ })
9898+ }
9999+100100+ pub fn to_table(&self) -> Table {
101101+ let mut root_user_table = Table::new();
102102+ root_user_table.insert(String::from("id"), toml::Value::String(self.id.clone()));
103103+ root_user_table.insert(
104104+ String::from("nickname"),
105105+ toml::Value::String(self.nickname.clone()),
106106+ );
107107+ root_user_table
108108+ }
109109+}
110110+111111+/// Returns a `RootUser`, which represents a root user
112112+///
113113+/// # Params
114114+///
115115+/// - config: A `Table` type of entire config.toml file
116116+pub fn get_root_user_details(config: &Table) -> Result<RootUser> {
117117+ let root_user = config
118118+ .get(ROOT_USER_CONFIG_KEY)
119119+ .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?;
120120+ let root_user_table = root_user
121121+ .as_table()
122122+ .ok_or_else(|| anyhow!("root user not a table"))?;
123123+ RootUser::new(root_user_table)
124124+}
125125+126126+/// Create a root account
127127+/// Stores the private credentials in OS secure password manager
128128+///
129129+/// # Params
130130+///
131131+/// - config: A `Table` type of entire config.toml file
132132+/// - nickname: Nickname for the identity (Optional)
133133+///
134134+/// Returns the root_user_config as a `Table` type
135135+pub fn create_root_account(config: &Table, nickname: Option<String>) -> Result<Table> {
136136+ let root_user = config
137137+ .get(ROOT_USER_CONFIG_KEY)
138138+ .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?;
139139+ let root_user_table = root_user
140140+ .as_table()
141141+ .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?;
142142+ let root_user_data = RootUser::new(root_user_table)?;
143143+ let did = root_user_data.id;
144144+ if did.is_empty() {
145145+ Ok(create_root_user(root_user_table, nickname)?)
146146+ } else {
147147+ Ok(root_user_table.clone())
148148+ }
149149+}
150150+151151+/// Save the root config in `Table` type to config.toml
152152+///
153153+/// # Params
154154+///
155155+/// - config: A `Table` type of entire config.toml file
156156+/// - root_user_config: A `Table` type of root user
157157+pub fn save_root_account(mut config: Table, root_user_config: &Table) -> Result<()> {
158158+ config.insert(
159159+ String::from(ROOT_USER_CONFIG_KEY),
160160+ toml::Value::Table(root_user_config.clone()),
161161+ );
162162+ save_config(&config)
163163+}
164164+165165+/// Sets nickname for the root account
166166+///
167167+/// # Params
168168+///
169169+/// - config: A `Table` type of entire config.toml file
170170+/// - nickname: Nickname for the identity
171171+///
172172+/// Returns the root_user_config as a `Table` type
173173+pub fn set_nickname(config: &Table, nickname: &str) -> Result<Table> {
174174+ let root_user = config
175175+ .get(ROOT_USER_CONFIG_KEY)
176176+ .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?;
177177+178178+ let mut root_user_table = root_user
179179+ .as_table()
180180+ .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?
181181+ .clone();
182182+ let did = root_user_table
183183+ .get("id")
184184+ .and_then(|v| v.as_str())
185185+ .ok_or(anyhow!("Failed to get id from config"))?;
186186+ if did.is_empty() {
187187+ Err(anyhow::anyhow!("No Root user available"))
188188+ } else {
189189+ root_user_table.insert("id".to_owned(), toml::Value::String(did.to_owned()));
190190+ root_user_table.insert(
191191+ "nickname".to_owned(),
192192+ toml::Value::String(nickname.to_owned()),
193193+ );
194194+ Ok(root_user_table)
195195+ }
196196+}
197197+198198+pub fn get_current_user(conn: &Connection) -> Result<User> {
199199+ let mut fetch_current_user = conn.prepare("select id, user_id, username, account_type, active_profile, root, created_at, updated_at from users where active_profile= true")?;
200200+201201+ fetch_current_user
202202+ .query_one([], |row| {
203203+ let id: String = row.get(0)?;
204204+ let account_type: String = row.get(3)?;
205205+ let created_at: f64 = row.get(6)?;
206206+ let updated_at: f64 = row.get(7)?;
207207+ Ok(User {
208208+ id: Uuid::try_parse(&id).map_err(FromSqlError::other)?,
209209+ user_id: row.get(1)?,
210210+ username: row.get(2)?,
211211+ account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?,
212212+ active_profile: row.get(4)?,
213213+ root: row.get(5)?,
214214+215215+ created_at: created_at as u64,
216216+ updated_at: updated_at as u64,
217217+ })
218218+ })
219219+ .map_err(<rusqlite::Error as Into<anyhow::Error>>::into)
220220+}
221221+// TODO: when we support multiple accounts
222222+// make sure that there can't be multiple rows with
223223+// root true.
224224+pub fn save_root_account_db() -> Result<()> {
225225+ let conn = get_db_conn(DBTYPE::COMMON)?;
226226+ let config = get_or_create_config()?;
227227+ let root_user = get_root_user_details(&config)?;
228228+ let user = User {
229229+ id: Uuid::now_v7(),
230230+ user_id: root_user.id,
231231+ username: root_user.nickname,
232232+ account_type: ACCOUNT::LOCAL,
233233+ active_profile: true,
234234+ root: true,
235235+ created_at: SystemTime::now()
236236+ .duration_since(UNIX_EPOCH)
237237+ .expect("time went backwards")
238238+ .as_secs(),
239239+ updated_at: SystemTime::now()
240240+ .duration_since(UNIX_EPOCH)
241241+ .expect("time went backwards")
242242+ .as_secs(),
243243+ };
244244+245245+ let mut fetch_root_user = conn.prepare("select id from users where root = true")?;
246246+247247+ match fetch_root_user.query_one([], |_row| Ok(())) {
248248+ Err(rusqlite::Error::QueryReturnedNoRows) => {
249249+ conn.execute("insert into users (id, user_id, username, active_profile, account_type, root) values
250250+ (?1, ?2, ?3,?4, ?5, ?6)", (&user.id.to_string(), &user.user_id, &user.username, &user.active_profile,
251251+ user.account_type.to_string(), &user.root))?;
252252+ Ok(())
253253+ }
254254+ Err(_err) => Err(anyhow!("Fetching user from db failed")),
255255+ _ => Ok(()),
256256+ }
257257+}
258258+259259+fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> {
260260+ let mut root_user_table = root_user_config.clone();
261261+ match create_identity("tiles") {
262262+ Ok(did) => {
263263+ root_user_table.insert("id".to_owned(), toml::Value::String(did));
264264+ if let Some(nickname) = nickname {
265265+ root_user_table.insert("nickname".to_owned(), toml::Value::String(nickname));
266266+ }
267267+ Ok(root_user_table)
268268+ }
269269+ Err(err) => Err(err),
270270+ }
271271+}
272272+273273+#[cfg(test)]
274274+mod tests {
275275+ use super::*;
276276+ use crate::core::accounts::{
277277+ RootUser, create_root_account, get_current_user, get_root_user_details, set_nickname,
278278+ };
279279+ use anyhow::Result;
280280+ use keyring::{mock, set_default_credential_builder};
281281+ use rusqlite::Connection;
282282+ use toml::Table;
283283+284284+ #[test]
285285+ fn test_get_root_user_details_empty_id() -> Result<()> {
286286+ let config: Table = toml::from_str(
287287+ r#"
288288+ [root-user]
289289+ id = ''
290290+ nickname = ''
291291+ "#,
292292+ )
293293+ .unwrap();
294294+ let acc_details = get_root_user_details(&config)?;
295295+ assert!(acc_details.id.is_empty());
296296+ Ok(())
297297+ }
298298+299299+ #[test]
300300+ fn test_get_root_user_details_valid_id() -> Result<()> {
301301+ let config: Table = toml::from_str(
302302+ r#"
303303+ [root-user]
304304+ id = 'did:key:xyz'
305305+ nickname = ''
306306+ "#,
307307+ )
308308+ .unwrap();
309309+ let acc_details = get_root_user_details(&config)?;
310310+ assert!(acc_details.id.contains("did:key"));
311311+ Ok(())
312312+ }
313313+314314+ #[test]
315315+ fn test_create_root_account_but_exists() {
316316+ let config: Table = toml::from_str(
317317+ r#"
318318+ [root-user]
319319+ id = 'did:key:xyz'
320320+ nickname = ''
321321+ "#,
322322+ )
323323+ .unwrap();
324324+ let root_user = create_root_account(&config, None).unwrap();
325325+326326+ assert_eq!(
327327+ root_user.get("id").unwrap().as_str().unwrap(),
328328+ "did:key:xyz"
329329+ );
330330+ }
331331+332332+ #[test]
333333+ fn test_create_root_account_new() {
334334+ set_default_credential_builder(mock::default_credential_builder());
335335+ let config: Table = toml::from_str(
336336+ r#"
337337+ [root-user]
338338+ id = ''
339339+ nickname = ''
340340+ "#,
341341+ )
342342+ .unwrap();
343343+ let root_user = create_root_account(&config, None).unwrap();
344344+345345+ assert_ne!(
346346+ root_user.get("id").unwrap().as_str().unwrap(),
347347+ "did:key:xyz"
348348+ );
349349+350350+ assert!(
351351+ root_user
352352+ .get("id")
353353+ .unwrap()
354354+ .as_str()
355355+ .unwrap()
356356+ .starts_with("did:key")
357357+ );
358358+ }
359359+360360+ #[test]
361361+ fn test_create_root_account_new_w_nickname() {
362362+ set_default_credential_builder(mock::default_credential_builder());
363363+ let config: Table = toml::from_str(
364364+ r#"
365365+ [root-user]
366366+ id = ''
367367+ nickname = ''
368368+ "#,
369369+ )
370370+ .unwrap();
371371+ let root_user = create_root_account(&config, Some(String::from("madclaws"))).unwrap();
372372+373373+ assert_ne!(
374374+ root_user.get("id").unwrap().as_str().unwrap(),
375375+ "did:key:xyz"
376376+ );
377377+378378+ assert!(
379379+ root_user
380380+ .get("id")
381381+ .unwrap()
382382+ .as_str()
383383+ .unwrap()
384384+ .starts_with("did:key")
385385+ );
386386+387387+ assert_eq!(
388388+ root_user.get("nickname").unwrap().as_str().unwrap(),
389389+ "madclaws"
390390+ );
391391+ }
392392+393393+ #[test]
394394+ fn test_get_root_user_details_missing_key() {
395395+ let config: Table = toml::from_str(
396396+ r#"
397397+ # no root-user table
398398+ [other]
399399+ foo = "bar"
400400+ "#,
401401+ )
402402+ .unwrap();
403403+404404+ let res = get_root_user_details(&config);
405405+ assert!(res.is_err(), "Expected error when root-user key is missing");
406406+ }
407407+408408+ #[test]
409409+ fn test_root_user_new_wrong_types() {
410410+ // id is integer, nickname is table
411411+ let config: Table = toml::from_str(
412412+ r#"
413413+ [root-user]
414414+ id = 123
415415+ nickname = { nested = "value" }
416416+ "#,
417417+ )
418418+ .unwrap();
419419+420420+ let root_tbl = config.get("root-user").unwrap().as_table().unwrap().clone();
421421+ assert!(
422422+ RootUser::new(&root_tbl).is_err(),
423423+ "Expected error for wrong types"
424424+ );
425425+ }
426426+427427+ #[test]
428428+ fn test_root_user_roundtrip_table() -> Result<()> {
429429+ let user = RootUser {
430430+ id: "did:key:abc".into(),
431431+ nickname: "nick".into(),
432432+ };
433433+ let tbl = user.to_table();
434434+ let parsed = RootUser::new(&tbl)?;
435435+ assert_eq!(parsed.id, user.id);
436436+ assert_eq!(parsed.nickname, user.nickname);
437437+ Ok(())
438438+ }
439439+440440+ #[test]
441441+ fn test_set_nickname_but_invalid_config() {
442442+ let config: Table = toml::from_str(
443443+ r#"
444444+ [ruser]
445445+ id = ''
446446+ "#,
447447+ )
448448+ .unwrap();
449449+450450+ assert!(set_nickname(&config, "madclaws").is_err())
451451+ }
452452+453453+ #[test]
454454+ fn test_set_nickname_success() {
455455+ let config: Table = toml::from_str(
456456+ r#"
457457+ [root-user]
458458+ id = 'did:key:xyz'
459459+ nickname = ''
460460+ "#,
461461+ )
462462+ .unwrap();
463463+464464+ let updated = set_nickname(&config, "madclaws").expect("nickname update should succeed");
465465+ assert_eq!(
466466+ updated.get("id").and_then(|v| v.as_str()),
467467+ Some("did:key:xyz")
468468+ );
469469+ assert_eq!(
470470+ updated.get("nickname").and_then(|v| v.as_str()),
471471+ Some("madclaws")
472472+ );
473473+ }
474474+475475+ #[test]
476476+ fn test_set_nickname_with_empty_id_fails() {
477477+ let config: Table = toml::from_str(
478478+ r#"
479479+ [root-user]
480480+ id = ''
481481+ nickname = ''
482482+ "#,
483483+ )
484484+ .unwrap();
485485+486486+ let err = set_nickname(&config, "madclaws").expect_err("empty id should fail");
487487+ assert!(err.to_string().contains("No Root user available"));
488488+ }
489489+490490+ fn setup_db_schema() -> Connection {
491491+ let conn = Connection::open_in_memory().unwrap();
492492+ conn.execute(
493493+ "
494494+ CREATE TABLE IF NOT EXISTS users (
495495+ id TEXT PRIMARY KEY,
496496+ user_id TEXT NOT NULL,
497497+ username TEXT NOT NULL,
498498+ active_profile INTEGER NOT NULL DEFAULT 0 CHECK (active_profile IN (0,1)),
499499+ account_type TEXT NOT NULL,
500500+ root INTEGER NOT NULL DEFAULT 0 CHECK (root IN (0,1)),
501501+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
502502+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
503503+ UNIQUE(account_type, user_id)
504504+ );
505505+ ",
506506+ [],
507507+ )
508508+ .unwrap();
509509+510510+ conn
511511+ }
512512+513513+ #[test]
514514+ fn test_get_user_when_no_user() {
515515+ let conn = setup_db_schema();
516516+ assert!(get_current_user(&conn).is_err())
517517+ }
518518+519519+ #[test]
520520+ fn test_get_current_user_valid() {
521521+ let conn = setup_db_schema();
522522+ let user = User {
523523+ id: Uuid::now_v7(),
524524+ user_id: String::from("did"),
525525+ username: String::from("nickname"),
526526+ account_type: ACCOUNT::LOCAL,
527527+ active_profile: true,
528528+ root: true,
529529+ created_at: SystemTime::now()
530530+ .duration_since(UNIX_EPOCH)
531531+ .expect("time went backwards")
532532+ .as_secs(),
533533+ updated_at: SystemTime::now()
534534+ .duration_since(UNIX_EPOCH)
535535+ .expect("time went backwards")
536536+ .as_secs(),
537537+ };
538538+539539+ let mut fetch_root_user = conn
540540+ .prepare("select id from users where root = true")
541541+ .unwrap();
542542+543543+ match fetch_root_user.query_one([], |_row| Ok(())) {
544544+ Err(rusqlite::Error::QueryReturnedNoRows) => {
545545+ conn.execute("insert into users (id, user_id, username, active_profile, account_type, root) values
546546+ (?1, ?2, ?3,?4, ?5, ?6)", (&user.id.to_string(), &user.user_id, &user.username, &user.active_profile,
547547+ user.account_type.to_string(), &user.root)).unwrap();
548548+ }
549549+ Err(_err) => (),
550550+ _ => (),
551551+ }
552552+553553+ assert!(get_current_user(&conn).is_ok())
554554+ }
555555+556556+ #[test]
557557+ fn test_get_current_user_invalid_uuid_fails() {
558558+ let conn = setup_db_schema();
559559+ conn.execute(
560560+ "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at)
561561+ values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
562562+ (
563563+ "not-a-uuid",
564564+ "did:key:test",
565565+ "nickname",
566566+ true,
567567+ "local",
568568+ true,
569569+ 1_i64,
570570+ 1_i64,
571571+ ),
572572+ )
573573+ .unwrap();
574574+575575+ assert!(get_current_user(&conn).is_err());
576576+ }
577577+578578+ #[test]
579579+ fn test_get_current_user_invalid_account_type_fails() {
580580+ let conn = setup_db_schema();
581581+ conn.execute(
582582+ "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at)
583583+ values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
584584+ (
585585+ Uuid::now_v7().to_string(),
586586+ "did:key:test",
587587+ "nickname",
588588+ true,
589589+ "unknown",
590590+ true,
591591+ 1_i64,
592592+ 1_i64,
593593+ ),
594594+ )
595595+ .unwrap();
596596+597597+ assert!(get_current_user(&conn).is_err());
598598+ }
599599+600600+ #[test]
601601+ fn test_get_current_user_inactive_only_rows_fails() {
602602+ let conn = setup_db_schema();
603603+ conn.execute(
604604+ "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at)
605605+ values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
606606+ (
607607+ Uuid::now_v7().to_string(),
608608+ "did:key:test",
609609+ "nickname",
610610+ false,
611611+ "local",
612612+ true,
613613+ 1_i64,
614614+ 1_i64,
615615+ ),
616616+ )
617617+ .unwrap();
618618+619619+ assert!(get_current_user(&conn).is_err());
620620+ }
621621+}
+243
tiles/src/core/chats.rs
···11+//! Chats.rs
22+//!
33+//! Stuff related to chats with the models
44+//!
55+66+use crate::core::accounts::User;
77+use crate::runtime::mlx::ChatResponse;
88+use crate::utils::get_unix_time_now;
99+use anyhow::Result;
1010+use rusqlite::Connection;
1111+use tilekit::modelfile::Role;
1212+use uuid::Uuid;
1313+// model the chats table
1414+1515+#[derive(serde::Serialize, Clone, Debug)]
1616+pub struct Message {
1717+ pub r#type: String,
1818+ pub role: Role,
1919+ pub content: String,
2020+}
2121+2222+pub struct Chats {
2323+ pub id: Uuid,
2424+ content: String,
2525+ // The id of the responses api obj
2626+ response_id: Option<String>,
2727+ // The Model chat user role
2828+ role: Role,
2929+ user_id: String,
3030+ // The parent Id of a model's reply
3131+ context_id: Option<Uuid>,
3232+ created_at: u64,
3333+ updated_at: u64,
3434+}
3535+3636+pub fn save_chat(
3737+ conn: &Connection,
3838+ user: &User,
3939+ input: &str,
4040+ chat_resp: Option<&ChatResponse>,
4141+) -> Result<Chats> {
4242+ if let Some(chat_response) = chat_resp {
4343+ let chat_resp_cloned = chat_response.clone();
4444+ let chat = Chats {
4545+ id: Uuid::now_v7(),
4646+ user_id: user.user_id.clone(),
4747+ content: input.to_owned(),
4848+ response_id: Some(chat_resp_cloned.prev_response_id),
4949+ role: Role::Assistant,
5050+ context_id: chat_resp_cloned.parent_chat_id,
5151+ created_at: get_unix_time_now(),
5252+ updated_at: get_unix_time_now(),
5353+ };
5454+5555+ conn.execute("insert into chats(id, user_id, content, resp_id, role, context_id, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", (&chat.id.to_string(), &chat.user_id, &chat.content, &chat.response_id, Into::<String>::into(chat.role), &chat.context_id.unwrap_or(Uuid::nil()).to_string(), &chat.created_at.to_string(), &chat.updated_at.to_string()))?;
5656+5757+ Ok(chat)
5858+ } else {
5959+ let chat = Chats {
6060+ id: Uuid::now_v7(),
6161+ user_id: user.user_id.clone(),
6262+ content: input.to_owned(),
6363+ response_id: None,
6464+ role: Role::User,
6565+ context_id: None,
6666+ created_at: get_unix_time_now(),
6767+ updated_at: get_unix_time_now(),
6868+ };
6969+7070+ conn.execute("insert into chats(id, user_id, content, resp_id, role, context_id, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", (&chat.id.to_string(), &chat.user_id, &chat.content, &chat.response_id, Into::<String>::into(chat.role), &chat.context_id.unwrap_or(Uuid::nil()).to_string(), &chat.created_at.to_string(), &chat.updated_at.to_string()))?;
7171+7272+ Ok(chat)
7373+ }
7474+}
7575+7676+#[cfg(test)]
7777+mod tests {
7878+ use std::time::{SystemTime, UNIX_EPOCH};
7979+8080+ use rusqlite::Connection;
8181+ use tilekit::modelfile::Role;
8282+ use uuid::Uuid;
8383+8484+ use crate::{
8585+ core::{
8686+ accounts::{ACCOUNT, User},
8787+ chats::save_chat,
8888+ },
8989+ runtime::mlx::ChatResponse,
9090+ };
9191+9292+ #[test]
9393+ fn test_valid_input_save_chat() {
9494+ let conn = setup_db_schema();
9595+ let user = create_user();
9696+ let input = "2+2";
9797+ let chat = save_chat(&conn, &user, input, None).expect("chat should be saved");
9898+9999+ assert_eq!(chat.user_id, user.user_id);
100100+ assert!(chat.response_id.is_none());
101101+ assert!(chat.context_id.is_none());
102102+103103+ let saved = fetch_saved_chat_row(&conn, &chat.id);
104104+ assert_eq!(saved.content, input);
105105+ assert_eq!(saved.resp_id, None);
106106+ assert_eq!(saved.role, Into::<String>::into(Role::User));
107107+ assert_eq!(saved.user_id, user.user_id);
108108+ assert_eq!(saved.context_id, Uuid::nil().to_string());
109109+ }
110110+111111+ #[test]
112112+ fn test_valid_response_save_chat() {
113113+ let conn = setup_db_schema();
114114+ let user = create_user();
115115+ let parent_chat_id = Uuid::now_v7();
116116+ let chat_resp = ChatResponse {
117117+ reply: "reply".to_owned(),
118118+ code: "code".to_owned(),
119119+ prev_response_id: String::from("resp_prev"),
120120+ parent_chat_id: Some(parent_chat_id),
121121+ metrics: None,
122122+ };
123123+ let input = "2+2";
124124+ let chat = save_chat(&conn, &user, input, Some(&chat_resp)).expect("chat should be saved");
125125+126126+ assert_eq!(chat.user_id, user.user_id);
127127+ assert_eq!(chat.response_id.as_deref(), Some("resp_prev"));
128128+ assert_eq!(chat.context_id, Some(parent_chat_id));
129129+130130+ let saved = fetch_saved_chat_row(&conn, &chat.id);
131131+ assert_eq!(saved.content, input);
132132+ assert_eq!(saved.resp_id, Some(String::from("resp_prev")));
133133+ assert_eq!(saved.role, Into::<String>::into(Role::Assistant));
134134+ assert_eq!(saved.user_id, user.user_id);
135135+ assert_eq!(saved.context_id, parent_chat_id.to_string());
136136+ }
137137+138138+ #[test]
139139+ fn test_response_without_parent_chat_id_saves_nil_context() {
140140+ let conn = setup_db_schema();
141141+ let user = create_user();
142142+ let chat_resp = ChatResponse {
143143+ reply: "reply".to_owned(),
144144+ code: "code".to_owned(),
145145+ prev_response_id: String::from("resp_prev"),
146146+ parent_chat_id: None,
147147+ metrics: None,
148148+ };
149149+150150+ let chat =
151151+ save_chat(&conn, &user, "hello", Some(&chat_resp)).expect("chat should be saved");
152152+153153+ assert_eq!(chat.context_id, None);
154154+ let saved = fetch_saved_chat_row(&conn, &chat.id);
155155+ assert_eq!(saved.role, Into::<String>::into(Role::Assistant));
156156+ assert_eq!(saved.context_id, Uuid::nil().to_string());
157157+ }
158158+159159+ #[test]
160160+ fn test_empty_input_is_saved() {
161161+ let conn = setup_db_schema();
162162+ let user = create_user();
163163+164164+ let chat = save_chat(&conn, &user, "", None).expect("empty content should still be saved");
165165+166166+ let saved = fetch_saved_chat_row(&conn, &chat.id);
167167+ assert_eq!(saved.content, "");
168168+ assert_eq!(saved.role, Into::<String>::into(Role::User));
169169+ }
170170+171171+ #[test]
172172+ fn test_save_chat_errors_when_table_missing() {
173173+ let conn = Connection::open_in_memory().expect("in-memory db should open");
174174+ let user = create_user();
175175+176176+ let result = save_chat(&conn, &user, "2+2", None);
177177+178178+ assert!(result.is_err());
179179+ }
180180+181181+ struct SavedChatRow {
182182+ content: String,
183183+ resp_id: Option<String>,
184184+ role: String,
185185+ user_id: String,
186186+ context_id: String,
187187+ }
188188+189189+ fn fetch_saved_chat_row(conn: &Connection, chat_id: &Uuid) -> SavedChatRow {
190190+ conn.query_row(
191191+ "SELECT content, resp_id, role, user_id, context_id FROM chats WHERE id = ?1",
192192+ [chat_id.to_string()],
193193+ |row| {
194194+ Ok(SavedChatRow {
195195+ content: row.get(0)?,
196196+ resp_id: row.get(1)?,
197197+ role: row.get(2)?,
198198+ user_id: row.get(3)?,
199199+ context_id: row.get(4)?,
200200+ })
201201+ },
202202+ )
203203+ .expect("saved chat row should exist")
204204+ }
205205+206206+ fn create_user() -> User {
207207+ User {
208208+ id: Uuid::now_v7(),
209209+ user_id: String::from("did"),
210210+ username: String::from("nickname"),
211211+ account_type: ACCOUNT::LOCAL,
212212+ active_profile: true,
213213+ root: true,
214214+ created_at: SystemTime::now()
215215+ .duration_since(UNIX_EPOCH)
216216+ .expect("time went backwards")
217217+ .as_secs(),
218218+ updated_at: SystemTime::now()
219219+ .duration_since(UNIX_EPOCH)
220220+ .expect("time went backwards")
221221+ .as_secs(),
222222+ }
223223+ }
224224+ fn setup_db_schema() -> Connection {
225225+ let conn = Connection::open_in_memory().unwrap();
226226+ conn.execute(
227227+ "CREATE TABLE IF NOT EXISTS chats (
228228+ id TEXT PRIMARY KEY,
229229+ content TEXT NOT NULL,
230230+ resp_id TEXT,
231231+ role TEXT NOT NULL,
232232+ user_id TEXT NOT NULL,
233233+ context_id TEXT ,
234234+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
235235+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
236236+ );",
237237+ [],
238238+ )
239239+ .unwrap();
240240+241241+ conn
242242+ }
243243+}
+16-1
tiles/src/core/mod.rs
···11-// to be deprecated and removed, the core stuff will be moved to tilekit sdk
11+//! tiles-core
22+//!
33+//! The core runtime which different UI apps can leverage
44+//! Generally the core will be run as daemon and interact with other sub components
55+66+use anyhow::Result;
77+88+use crate::core::{accounts::save_root_account_db, storage::db::init_db};
291010+pub mod accounts;
1111+pub mod chats;
312pub mod health;
1313+pub mod storage;
1414+// Entrypoint of the core
1515+pub fn init() -> Result<()> {
1616+ init_db()?;
1717+ save_root_account_db()
1818+}
+95
tiles/src/core/storage/db.rs
···11+//! Core Database Handling
22+//!
33+//! Uses sqlite as the underlying database
44+//!
55+66+use std::path::PathBuf;
77+88+use anyhow::{Result, anyhow};
99+use rusqlite::Connection;
1010+1111+use crate::utils::config::{ConfigProvider, DefaultProvider};
1212+use rusqlite_migration::{M, Migrations};
1313+pub enum DBTYPE {
1414+ COMMON,
1515+ CHAT,
1616+}
1717+1818+// DEFINE MIGRATIONS
1919+2020+// TODO: add the schema doc
2121+const COMMON_MIGRATION_ARRAY: &[M] = &[M::up(
2222+ "
2323+ CREATE TABLE IF NOT EXISTS users (
2424+ id TEXT PRIMARY KEY,
2525+ user_id TEXT NOT NULL,
2626+ username TEXT NOT NULL,
2727+ active_profile INTEGER NOT NULL DEFAULT 0 CHECK (active_profile IN (0,1)),
2828+ account_type TEXT NOT NULL,
2929+ root INTEGER NOT NULL DEFAULT 0 CHECK (root IN (0,1)),
3030+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
3131+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
3232+ UNIQUE(account_type, user_id)
3333+ );
3434+ ",
3535+)];
3636+3737+const COMMON_MIGRATIONS: Migrations = Migrations::from_slice(COMMON_MIGRATION_ARRAY);
3838+3939+// TODO: add the schema doc
4040+const CHATS_MIGRATION_ARRAY: &[M] = &[M::up(
4141+ "CREATE TABLE IF NOT EXISTS chats (
4242+ id TEXT PRIMARY KEY,
4343+ content TEXT NOT NULL,
4444+ resp_id TEXT,
4545+ role TEXT NOT NULL,
4646+ user_id TEXT NOT NULL,
4747+ context_id TEXT,
4848+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
4949+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5050+ )",
5151+)];
5252+5353+const CHATS_MIGRATIONS: Migrations = Migrations::from_slice(CHATS_MIGRATION_ARRAY);
5454+5555+pub fn init_db() -> Result<()> {
5656+ let mut chat_conn = get_db_conn(DBTYPE::CHAT)?;
5757+ let mut common_conn = get_db_conn(DBTYPE::COMMON)?;
5858+5959+ apply_migrations(&mut common_conn, &mut chat_conn)
6060+}
6161+6262+pub fn get_db_conn(db_type: DBTYPE) -> Result<Connection> {
6363+ let db_path = get_db_path(db_type)?;
6464+ let conn = Connection::open(db_path)
6565+ .map_err(|e| anyhow!("Failed to create db connection due to {:?}", e))?;
6666+6767+ conn.pragma_update(None, "journal_mode", "WAL")?;
6868+ Ok(conn)
6969+}
7070+7171+fn apply_migrations(common_conn: &mut Connection, chat_conn: &mut Connection) -> Result<()> {
7272+ COMMON_MIGRATIONS
7373+ .to_latest(common_conn)
7474+ .map_err(<rusqlite_migration::Error as Into<anyhow::Error>>::into)?;
7575+ CHATS_MIGRATIONS.to_latest(chat_conn).map_err(|e| e.into())
7676+}
7777+fn get_db_path(db_type: DBTYPE) -> Result<PathBuf> {
7878+ let user_data_dir = DefaultProvider.get_user_data_dir()?;
7979+ match db_type {
8080+ DBTYPE::COMMON => Ok(user_data_dir.join("common.db")),
8181+ DBTYPE::CHAT => Ok(user_data_dir.join("chats.db")),
8282+ }
8383+}
8484+8585+#[cfg(test)]
8686+mod tests {
8787+8888+ use super::*;
8989+9090+ #[test]
9191+ fn migrations_test() {
9292+ assert!(COMMON_MIGRATIONS.validate().is_ok());
9393+ assert!(CHATS_MIGRATIONS.validate().is_ok());
9494+ }
9595+}