A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Added more repl session commands - Added /share <sessionId>, for sharing a particular session other than the current running session. - Added /list-sessions, which will show all the available sessions, pagination is yet to be implemented. - Added /load-session <sessionId>, which will the load the particular session and we can continue working on that session

madclaws f025f969 9a318296

+136 -190
+1 -1
.github/workflows/rust.yml
··· 54 54 key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 55 55 56 56 - name: Install tarpaulin 57 - run: cargo install cargo-tarpaulin 57 + run: cargo install cargo-tarpaulin --locked 58 58 59 59 - name: Run coverage 60 60 run: cargo tarpaulin --out Xml -- --test-threads 1
+3 -20
tiles/src/core/account/atproto.rs
··· 3 3 use anyhow::{Result, anyhow}; 4 4 use atrium_api::{ 5 5 agent::Agent, 6 - types::{ 7 - Unknown, 8 - string::{Datetime, Did}, 9 - }, 6 + types::{Unknown, string::Did}, 10 7 }; 11 8 use atrium_common::store::{Store, memory::MemoryStore}; 12 9 use atrium_identity::{ ··· 25 22 use reqwest::Client; 26 23 use rusqlite::{Connection, OptionalExtension, params}; 27 24 use serde::{Deserialize, Serialize}; 28 - use serde_json::json; 29 25 use std::{fmt::Debug, process::Command, sync::Arc, time::Duration}; 30 26 use tokio::sync::oneshot; 31 27 ··· 290 286 291 287 //TODO: Move the login check to common fn 292 288 pub async fn share_session(conn: &Connection, shared_session: SharedSession) -> Result<()> { 293 - if let Some(auth_data) = fetch_logged_in_data(&conn)? { 289 + if let Some(auth_data) = fetch_logged_in_data(conn)? { 294 290 let (client, mem_session_store) = create_oauth_client()?; 295 291 let session: Session = serde_json::from_str(&auth_data.session)?; 296 292 let did_struct = ··· 301 297 //TODO: Add a user friendly err latta 302 298 let oauth_session = client.restore(&did_struct).await?; 303 299 let agent = Agent::new(oauth_session); 304 - 305 - // let test_record = json!({ 306 - // "$type": "run.tiles.session", 307 - // "session_id": "019dd050-f337-7507-a8bc-b5eaf3547cc5", 308 - // "name": "dummy_session", 309 - // "contents": [ 310 - // { 311 - // "role": "user", 312 - // "content": "dummy content" 313 - // } 314 - // ], 315 - // "created_at": Datetime::now().as_str() 316 - // }); 317 300 318 301 let shared_session_value = serde_json::to_value(shared_session)?; 319 302 let record: Unknown = serde_json::from_value(shared_session_value)?; ··· 361 344 handle: auth_data.handle, 362 345 }; 363 346 364 - upsert_auth_data(&conn, &auth_data)?; 347 + upsert_auth_data(conn, &auth_data)?; 365 348 } else { 366 349 println!("No logged-in user, please login") 367 350 }
+22
tiles/src/core/chats.rs
··· 375 375 )?; 376 376 Ok(sesh) 377 377 } 378 + 379 + pub fn fetch_sessions(conn: &Connection) -> Result<Vec<Session>> { 380 + let query = "select id, name, creator_id, created_at from sessions order by created_at desc"; 381 + 382 + let mut stmt = conn.prepare(query)?; 383 + let session_rows = stmt.query_map([], |row| { 384 + Ok(Session { 385 + id: row.get(0)?, 386 + name: row.get(1)?, 387 + creator_id: row.get(2)?, 388 + created_at: row.get::<usize, f64>(3)? as u64, 389 + }) 390 + })?; 391 + 392 + let mut sessions: Vec<Session> = vec![]; 393 + 394 + for session in session_rows { 395 + sessions.push(session?); 396 + } 397 + Ok(sessions) 398 + } 399 + 378 400 fn encode_delta_to_bytes(delta_chats: &DeltaChat) -> Vec<u8> { 379 401 postcard::to_stdvec(delta_chats).expect("Failed to convert to bytes with postcard") 380 402 }
+110 -169
tiles/src/runtime/mlx.rs
··· 1 1 use crate::core::account::atproto::share_session; 2 2 use crate::core::account::local::get_current_user; 3 3 use crate::core::chats::{ 4 - self, Message, create_session, fetch_chats_by_session_id, fetch_session, save_chat, 4 + Message, create_session, fetch_chats_by_session_id, fetch_sessions, save_chat, 5 5 }; 6 6 use crate::core::storage::db::Dbconn; 7 7 use crate::runtime::RunArgs; ··· 13 13 use atrium_api::types::string::Datetime; 14 14 use log::info; 15 15 use reqwest::{Client, StatusCode}; 16 - use rusqlite::Connection; 17 16 use rustyline::completion::Completer; 18 17 use rustyline::highlight::Highlighter; 19 18 use rustyline::hint::Hinter; ··· 269 268 State, 270 269 #[serde(rename = "share")] 271 270 Share, 271 + #[serde(rename = "list-sessions")] 272 + ListSessions, 273 + #[serde(rename = "load-session")] 274 + LoadSession, 272 275 #[serde(other)] 273 276 Unknown, 274 277 } ··· 289 292 content: String, 290 293 } 291 294 292 - fn handle_input(input: &str, modelname: &str) -> InputType { 295 + fn handle_input(input: &str) -> InputType { 293 296 if let Some(cmd) = input.strip_prefix('/') { 294 297 match cmd { 295 298 "help" | "?" => { 296 - show_help(modelname); 299 + show_help(); 297 300 InputType::Skip 298 301 } 299 302 "bye" => InputType::Exit, ··· 308 311 } 309 312 } 310 313 311 - fn show_help(model_name: &str) { 312 - let _ = model_name; 314 + fn show_help() { 315 + let help_list = vec![ 316 + ("status", "Show the current session state"), 317 + ("list-sessions", "List available sessions"), 318 + ( 319 + "share", 320 + "Create a shareable link for currently running session", 321 + ), 322 + ( 323 + "load-session <sessionId>", 324 + "Loads and resume the given session", 325 + ), 326 + ( 327 + "share", 328 + "Create a shareable link for currently running session", 329 + ), 330 + ( 331 + "share <sessionId>", 332 + "Create a shareable link for given sessionId", 333 + ), 334 + ("help", "Show this help message"), 335 + ("bye", "Exit the REPL"), 336 + ]; 337 + 338 + // finding the length of the longest command, for padding purposes 339 + let max_length = help_list 340 + .iter() 341 + .fold(0, |acc, x| if x.0.len() > acc { x.0.len() } else { acc }); 313 342 314 343 println!("Available Commands:"); 315 - println!(" /state Show the current session state"); 316 - println!(" /help Show this help message"); 317 - println!(" /bye Exit the REPL"); 344 + 345 + for help in help_list { 346 + let final_str = format!( 347 + " /{}{}\t\t{}", 348 + help.0, 349 + " ".repeat(max_length - help.0.len()), 350 + help.1 351 + ); 352 + 353 + println!("{}", final_str); 354 + } 355 + 318 356 println!(); 319 357 320 358 println!("\nDocumentation: https://tiles.run/book"); ··· 376 414 let _ = reader 377 415 .read_line(&mut pi_session_state) 378 416 .context("Failed reading pi session state")?; 379 - println!("{}", pi_session_state); 380 417 let response: PiResponse = serde_json::from_str(&pi_session_state)?; 381 418 if let PiResponse::Response(msg) = response { 382 419 let state: GetStateData = 383 420 serde_json::from_value(msg.data.expect("get state parsing failed"))?; 384 421 session_id = state.session_id; 385 422 } 386 - 423 + let mut session_turn_count = 0; 387 424 loop { 388 425 let readline = editor.readline(">>> "); 389 426 let input = match readline { ··· 410 447 if input.is_empty() { 411 448 continue; 412 449 } 413 - match handle_input(&input, modelname.as_str()) { 450 + match handle_input(&input) { 414 451 InputType::Skip => continue, 415 452 InputType::Exit => { 416 453 let end_payload = json!({ ··· 434 471 } 435 472 InputType::Command(cmd) => { 436 473 let args: Vec<&str> = cmd.split(" ").collect(); 437 - let cmd_json = json!(cmd); 438 - // println!("{}", cmd_json.to_string()); 474 + let main_cmd = args.first().expect("Main command should be there"); 475 + 476 + let cmd_json = json!(main_cmd); 477 + 439 478 let command: CommandType = serde_json::from_value(cmd_json)?; 440 479 match command { 441 480 CommandType::Unknown => { ··· 446 485 continue; 447 486 } 448 487 CommandType::Share => { 449 - process_share_session(&db_conn, &session_id, &args).await?; 488 + process_share_session(db_conn, &session_id, &args).await?; 489 + continue; 490 + } 491 + CommandType::ListSessions => { 492 + show_session_info(db_conn)?; 493 + continue; 494 + } 495 + CommandType::LoadSession => { 496 + match load_session(db_conn, &args) { 497 + Ok((sesh_id, turn_count)) => { 498 + session_id = sesh_id; 499 + session_turn_count = turn_count; 500 + } 501 + Err(err) => { 502 + println!("{}", err) 503 + } 504 + } 450 505 continue; 451 506 } 452 507 cmd_type => { ··· 459 514 } 460 515 461 516 let reader = BufReader::new(&mut stdout); 462 - let mut session_turn_count = 0; 463 517 let mut last_chat_id: String = "".to_owned(); 464 518 for line in reader.lines() { 465 519 //TODO: handle the unwrap ··· 604 658 } 605 659 } 606 660 607 - //TODO: Have 2 separate chat functions for memory and non-memory 608 - // #[allow(clippy::too_many_arguments)] 609 - // async fn chat( 610 - // input: &str, 611 - // modelfile: &Modelfile, 612 - // chat_start: bool, 613 - // python_code: &str, 614 - // g_reply: &str, 615 - // run_args: &RunArgs, 616 - // prev_response_id: &str, 617 - // conn: &Connection, 618 - // user: &User, 619 - // conversations: &[Message], 620 - // ) -> Result<ChatResponse> { 621 - // let client = Client::new(); 622 - // let modelname = modelfile 623 - // .from 624 - // .clone() 625 - // .ok_or_else(|| anyhow!("Failed to get model name"))?; 626 - // let prompt = modelfile.system.clone().unwrap_or("".to_owned()); 627 - // let convo_input = create_chat_input(input, prompt.as_str(), conversations); 628 - // let body = json!({ 629 - // "model": modelname, 630 - // "input": convo_input, 631 - // "reasoning": {"effort": "medium"}, 632 - // "chat_start": chat_start, 633 - // "stream": true, 634 - // "previous_response_id": prev_response_id, 635 - // "python_code": python_code, 636 - // "messages": [{"role": "assistant", "content": g_reply}, {"role": "user", "content": input}] 637 - // }); 638 - 639 - // let memory_body = json!({ 640 - // "model": modelname, 641 - // "input": input, 642 - // "chat_start": chat_start, 643 - // "stream": true, 644 - // "python_code": python_code, 645 - // "messages": [{"role": "assistant", "content": g_reply}, {"role": "user", "content": input}] 646 - 647 - // }); 648 - // let res = if run_args.memory { 649 - // let api_url = "http://127.0.0.1:6969/v1/chat/completions"; 650 - // client.post(api_url).json(&memory_body).send().await? 651 - // } else { 652 - // let api_url = "http://127.0.0.1:6969/v1/responses"; 653 - // client.post(api_url).json(&body).send().await? 654 - // }; 655 - 656 - // let chat = save_chat(conn, user, input, None)?; 657 - // let mut stream = res.bytes_stream(); 658 - // let mut accumulated = String::new(); 659 - // let mut metrics: Option<BenchmarkMetrics> = None; 660 - // let mut is_answer_start = false; 661 - // let mut prev_response_id: String = String::from(""); 662 - // let mut output_completed: bool = false; 663 - // while let Some(chunk) = stream.next().await { 664 - // let chunk = chunk?; 665 - // let s = String::from_utf8_lossy(&chunk); 666 - // for line in s.lines() { 667 - // if !line.starts_with("data: ") { 668 - // continue; 669 - // } 670 - 671 - // let data = line.trim_start_matches("data: "); 672 - 673 - // if data == "[DONE]" { 674 - // let mut chat_resp = convert_to_chat_response( 675 - // &accumulated, 676 - // run_args.memory, 677 - // prev_response_id, 678 - // metrics, 679 - // ); 680 - // chat_resp.parent_chat_id = Some(chat.id); 681 - // return Ok(chat_resp); 682 - // } 683 - 684 - // //TODO: This will break if we ask the model to give an essay and all 685 - // let v: Value = serde_json::from_str(data).unwrap(); 686 - // // Check for metrics in the response 687 - // if let Some(metrics_obj) = v.get("metrics") { 688 - // metrics = serde_json::from_value(metrics_obj.clone()).ok(); 689 - // } 690 - // let model_text: Option<&str> = if run_args.memory { 691 - // v["choices"][0]["delta"]["content"].as_str() 692 - // } else { 693 - // prev_response_id = serde_json::to_string(&v["id"])? 694 - // .trim_matches('\"') 695 - // .to_owned(); 696 - 697 - // if serde_json::to_string(&v["status"])?.contains("completed") { 698 - // output_completed = true; 699 - // } 700 - 701 - // v["output"][0]["content"][0]["text"].as_str() 702 - // }; 703 - 704 - // if let Some(delta) = model_text { 705 - // if !run_args.memory { 706 - // if delta.contains("**[Answer]**") { 707 - // is_answer_start = true 708 - // } 709 - // if !output_completed { 710 - // accumulated.push_str(delta); 711 - // if !is_answer_start { 712 - // print!("{}", delta.dimmed()); 713 - // } else { 714 - // print!("{}", delta); 715 - // }; 716 - // } 717 - // } else { 718 - // accumulated.push_str(delta); 719 - // } 720 - // use std::io::Write; 721 - // std::io::stdout().flush().ok(); 722 - // } 723 - // } 724 - // } 725 - 726 - // Err(anyhow!("Result failed")) 727 - // } 728 - 729 - // fn convert_to_chat_response( 730 - // content: &str, 731 - // memory_mode: bool, 732 - // prev_response_id: String, 733 - // metrics: Option<BenchmarkMetrics>, 734 - // ) -> ChatResponse { 735 - // ChatResponse { 736 - // reply: extract_reply(content, memory_mode), 737 - // code: None, 738 - // prev_response_id, 739 - // metrics, 740 - // parent_chat_id: None, 741 - // } 742 - // } 743 - 744 - // fn extract_reply(content: &str, memory_mode: bool) -> String { 745 - // if !memory_mode && content.contains("**[Answer]**") { 746 - // let list_a = content.split("**[Answer]**").collect::<Vec<&str>>(); 747 - // list_a[1].to_owned() 748 - // } else if !memory_mode { 749 - // content.to_owned() 750 - // } else if content.contains("<reply>") && content.contains("</reply>") { 751 - // let list_a = content.split("<reply>").collect::<Vec<&str>>(); 752 - // let list_b = list_a[1].split("</reply>").collect::<Vec<&str>>(); 753 - // list_b[0].to_owned() 754 - // } else { 755 - // "".to_owned() 756 - // } 757 - // } 758 - 759 661 #[allow(dead_code)] 760 662 fn extract_python(content: &str) -> String { 761 663 if content.contains("<python>") && content.contains("</python>") { ··· 976 878 }; 977 879 978 880 share_session(&conn.common, shared_sessions).await?; 979 - // pass it to the atproto share_session fn 881 + Ok(()) 882 + } 883 + 884 + fn show_session_info(db_conn: &Dbconn) -> Result<()> { 885 + let sessions = fetch_sessions(&db_conn.chat)?; 886 + 887 + let mut count = 0; 888 + for session in sessions { 889 + count += 1; 890 + println!("{}.\t{}\t{}", count, session.id, session.name); 891 + } 980 892 Ok(()) 981 893 } 894 + 895 + //TODO: load the session via prompt into the model too 896 + fn load_session(db_conn: &Dbconn, args: &[&str]) -> Result<(String, usize)> { 897 + let args = if let Some((_main_command, sub_commands)) = args.split_first() { 898 + sub_commands 899 + } else { 900 + return Err(anyhow!("Not a valid command")); 901 + }; 902 + 903 + let session_id = if args.is_empty() { 904 + println!("Please provide sessionId"); 905 + return Err(anyhow!("Please provide sessionId")); 906 + } else { 907 + args[0] 908 + }; 909 + // fetch session and the chats for the session_id 910 + 911 + let delta_chats = fetch_chats_by_session_id(&db_conn.chat, session_id)?; 912 + 913 + if delta_chats.sessions.is_empty() { 914 + println!("Session {} not available", session_id); 915 + } 916 + 917 + for chat in &delta_chats.chats { 918 + println!("{}", chat.content); 919 + } 920 + 921 + Ok((session_id.to_string(), delta_chats.chats.len())) 922 + }