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: models json auto creation

madclaws 8bffe490 b054e051

+233 -67
+1
.gitignore
··· 10 10 *.pkg 11 11 models/ 12 12 pkgroot_models/ 13 + pi-darwin-arm64.tar.gz
+13 -43
HACKING.md
··· 50 50 uv sync 51 51 cd .. 52 52 ``` 53 + ### Embedding Pi 54 + 55 + [Pi](https://github.com/badlogic/pi-mono) is a minimal coding agent for agentic harness. We embed Pi in Tiles so that it can sit in between the CLI and inference layer to provide more powerful features for the regular knowledge work, agent harness and whatever that comes in future is just an extension away thus making Tiles flexible and can ride the wave of standards. 56 + 57 + Current approach on how we integrate Pi is, we pack the pi bun binary with the tiles installer and use Pi in rpc mode from Tiles. So Pi interacts with the Tiles model inference and communicates with the Tiles Pi via stdin, stdout json. 58 + 59 + 60 + #### Setting up PI 61 + 62 + For development, Tiles expect a `pi` folder under `.tiles_dev/tiles/` (This folder is created first time when we run tiles from the root directory with `cargo run`). We can run `just build_w_pi` which will handle downloading and extracting the relevant Pi binary. 63 + 64 + 53 65 54 66 ## Running Tiles (Development) 55 67 ··· 74 86 75 87 > **Tip:** Refer to the `justfile` for additional common commands and automation. For troubleshooting, see [CONTRIBUTING.md](CONTRIBUTING.md) and open an issue if you need help. 76 88 77 - ### Building Tiles installer (Development) 89 + ## Building Tiles installer (Development) 78 90 79 91 Install [venvstacks](https://github.com/lmstudio-ai/venvstacks?tab=readme-ov-file#installing) for portable py runtime 80 92 ··· 91 103 ``` 92 104 93 105 Now `tiles` should be available in PATH 94 - 95 - 96 - ### Development with PI 97 - 98 - [Pi](https://github.com/badlogic/pi-mono) is a minimal coding agent for agentic harness. Instead of providing harness by ourselves we will be leveraging Pi. 99 - 100 - 101 - Current approach on how we integrate Pi is, we pack the pi bun binary with the tiles installer and we switch to Pi repl from tiles cli, if harness is required. There are two ways we can do communicate with Pi, either via rpc mode or directly use the pi binary and get into the whole Pi ecosystem. 102 - 103 - For better maintainability and to be update with Pi, using rpc mode is the way. But as we are in experimental mode with Pi, for now we wont use rpc instead completely use Pi's repl and UI. So at this stage we use our own fork for tighter integration with Tiles system. But this can change later. So Pi will be available under a flag `tiles -p` or `tiles run -p <MODELFILE_PATH>` 104 - 105 - 106 - #### Setting up PI 107 - 108 - `git clone https://github.com/tilesprivacy/tiles-pi/tree/feat/integrate-w-tiles` 109 - 110 - `npm install` - for installing the deps 111 - 112 - ``` 113 - export TILES_PI_BUILD_ENV=debug # (other values: release) 114 - export TILES_PI_DEV_CONFIG_PATH=<TILES_REPO_PATH>/.tiles_dev/tiles 115 - ``` 116 - 117 - Set these env vars. `TILES_PI_BUILD_ENV` is used to find the correct config.toml file for Pi to read. tiles-pi rely on config.toml for user data directory, current model etc. At this point config.toml act as a shared memory for tiles-pi and tiles. For development we use `debug` value. If debug mode then it uses `TILES_PI_DEV_CONFIG_PATH`. So internally all the app-files, user-data etc are in a .tiles_dev folder at the root of project. Pi also creates it agent directory here under `.tiles_dev/tiles/data/pi/agent`. 118 - 119 - If mode is anything other than debug, then its release mode and the config.toml path is fixed, so need to worry about. Important thing to note is pi/agent directory will be in the tiles user data directory. 120 - 121 - To work with Pi we need to run Pi on a terminal and tiles inference py server on another, and tiles daemon shld also be running background. 122 - 123 - - Running Pi 124 - - From root of `tiles-pi` run `npm run build && ./pi-test.sh` 125 - - Running py server 126 - - From root of `tiles` run `just serve` 127 - - Running tiles daemon 128 - - First check if daemon is already running by `curl -X GET http://127.0.0.1:1729/`, if its returning tiles version, then daemon is running and its fine. 129 - 130 - - If above curl failed, then do `cargo run -- -x`. This will run tiles in non-repl mode, simultaneously running a deamon in background. 131 - 132 - Now these are running, u can jump into pi repl and do stuff with the model 133 - 134 - Later if we need to test the e2e integration in development, we need to build the tiles-pi binary and extract the artificats into `.tiles_dev/tiles/pi`. 135 - For that we can run `just build_w_pi`. 136 106 137 107 138 108 ## Additional Resources
+6 -6
scripts/build_with_pi_dev.sh
··· 1 1 #!/usr/bin/env bash 2 2 set -euo pipefail 3 3 4 - PI_TAR_DIR="/Users/tiles/Downloads" 5 - # cargo build 4 + rm -rf .tiles_dev/tiles/pi 6 5 7 - # Build the pi binary 6 + VERSION=$(grep '^pi' toolchain.toml | head -1 | awk -F'"' '{print $2}') 8 7 9 - # sh "${TILES_PI_DIR}/scripts/build-binaries.sh" --skip-deps --platform darwin-arm64 8 + TAR_URL="https://github.com/badlogic/pi-mono/releases/download/${VERSION}/pi-darwin-arm64.tar.gz" 10 9 11 - rm -rf .tiles_dev/tiles/pi 10 + curl -fL -o "pi-darwin-arm64.tar.gz" "$TAR_URL" 12 11 13 - cp "${PI_TAR_DIR}/pi-darwin-arm64.tar.gz" ".tiles_dev/tiles/" 12 + cp "pi-darwin-arm64.tar.gz" ".tiles_dev/tiles/" 14 13 15 14 cd .tiles_dev/tiles 16 15 17 16 tar -xvf pi-darwin-arm64.tar.gz 18 17 18 + rm pi-darwin-arm64.tar.gz
+1 -6
tiles/src/main.rs
··· 9 9 network::{link, sync}, 10 10 }, 11 11 daemon::{start_cmd, start_server, stop_cmd}, 12 - runtime::{RunArgs, build_runtime, mlx::start_pi_rpc}, 12 + runtime::{RunArgs, build_runtime}, 13 13 utils::installer, 14 14 }; 15 15 ··· 77 77 /// The DID of the peer you want to sync 78 78 did: Option<String>, 79 79 }, 80 - Pi, 81 80 } 82 81 83 82 #[derive(Debug, Args)] ··· 295 294 } 296 295 }, 297 296 Some(Commands::Sync { did }) => sync(did).await?, 298 - Some(Commands::Pi) => { 299 - // blah 300 - start_pi_rpc()?; 301 - } 302 297 } 303 298 Ok(()) 304 299 }
+24 -12
tiles/src/runtime/mlx.rs
··· 2 2 use crate::core::chats::{Message, create_session, save_chat}; 3 3 use crate::core::storage::db::Dbconn; 4 4 use crate::runtime::RunArgs; 5 - use crate::utils::config::{ConfigProvider, DefaultProvider, get_memory_path, get_model_cache}; 5 + use crate::utils::config::{ 6 + ConfigProvider, DefaultProvider, create_pi_provider_config, get_memory_path, get_model_cache, 7 + }; 6 8 use crate::utils::hf_model_downloader::*; 7 9 use anyhow::{Context, Result, anyhow}; 8 10 use log::info; ··· 15 17 use rustyline::{Config, Editor, Helper}; 16 18 use serde::{Deserialize, Serialize}; 17 19 use serde_json::{Value, json}; 18 - use std::fs::OpenOptions; 20 + use std::fs::{self, OpenOptions}; 19 21 use std::io::{BufRead, BufReader, Write}; 20 22 use std::path::PathBuf; 21 23 use std::process::{Child, Command}; ··· 132 134 Self::new() 133 135 } 134 136 } 137 + 138 + const PY_PORT: u32 = 6969; 135 139 136 140 impl MLXRuntime { 137 141 pub fn new() -> Self { ··· 338 342 339 343 let mut conversations: Vec<Message> = vec![]; 340 344 341 - let mut pi_process = start_pi_rpc()?; 345 + let mut pi_process = start_pi_rpc(&modelname)?; 342 346 let mut session_id = String::new(); 343 347 let pi_stdin = pi_process.stdin.as_mut().unwrap(); 344 348 let mut stdout = pi_process.stdout.take().expect("stdout"); ··· 613 617 614 618 pub async fn ping() -> Result<()> { 615 619 let client = Client::new(); 616 - let res = client.get("http://127.0.0.1:6969/ping").send().await; 620 + let url = format!("http://127.0.0.1:{}/ping", PY_PORT); 621 + let res = client.get(url).send().await; 617 622 618 623 match res { 619 624 Err(err) => Err(anyhow!("Server down due to {:?}", err)), ··· 923 928 } 924 929 } 925 930 926 - pub fn start_pi_rpc() -> Result<Child> { 927 - let mut pi_dir = DefaultProvider.get_lib_dir()?; 931 + // Need to create models.json for the provider 932 + fn start_pi_rpc(model_name: &str) -> Result<Child> { 933 + let tiles_lib_dir = DefaultProvider.get_lib_dir()?; 928 934 let user_data_dir = DefaultProvider.get_user_data_dir()?; 929 - let pi_agent_dir = user_data_dir.join("pi/agent"); 930 - std::fs::create_dir_all(&pi_agent_dir).context("Failed to create pi_agent_dir")?; 931 - pi_dir = pi_dir.join("pi"); 932 - let pi_exec_path = pi_dir.join("pi"); 935 + let pi_agent_dir = user_data_dir.join("pi/agent/"); 936 + std::fs::create_dir_all(&pi_agent_dir).context("Failed to create Pi agent directory")?; 937 + 938 + let provider_config_file_path = pi_agent_dir.join("models.json"); 939 + let endpoint_url = format!("http://127.0.0.1:{}/v1", PY_PORT); 940 + let model_config = create_pi_provider_config(model_name, &endpoint_url)?; 941 + 942 + fs::write(provider_config_file_path, model_config)?; 943 + let pi_exec_path = tiles_lib_dir.join("pi/pi"); 944 + 933 945 let pi_process = Command::new(pi_exec_path) 934 946 .arg("--mode") 935 947 .arg("rpc") 936 - // .arg("--no-session") 948 + .arg("--no-session") 937 949 .env("PI_CODING_AGENT_DIR", pi_agent_dir) 938 950 .env("PI_OFFLINE", "true") 939 951 .stdin(Stdio::piped()) 940 952 .stdout(Stdio::piped()) 941 953 .spawn() 942 - .expect("failed to PI"); 954 + .expect("failed to run Pi"); 943 955 944 956 Ok(pi_process) 945 957 }
+176
tiles/src/utils/config.rs
··· 15 15 /// - /models - Where the pre-downloaded models. 16 16 use anyhow::{Context, Result, anyhow}; 17 17 use serde::{Deserialize, Serialize}; 18 + use serde_json::json; 19 + use std::collections::HashMap; 18 20 use std::fs::File; 19 21 use std::path::PathBuf; 20 22 use std::str::FromStr; ··· 45 47 pub model: Option<ModelConfig>, 46 48 } 47 49 50 + #[derive(Serialize, Deserialize, Clone)] 51 + pub struct PiModelConfig { 52 + pub providers: HashMap<String, PiProviderConfig>, 53 + } 54 + 55 + #[derive(Serialize, Deserialize, Clone)] 56 + pub struct PiProviderConfig { 57 + api: String, 58 + #[serde(rename = "apiKey")] 59 + api_key: String, 60 + #[serde(rename = "baseUrl")] 61 + base_url: String, 62 + pub models: Vec<PiProviderModelConfig>, 63 + } 64 + 65 + #[derive(Serialize, Deserialize, Clone)] 66 + pub struct PiProviderModelConfig { 67 + pub id: String, 68 + } 48 69 const MODEL_SUB_PATH: &str = "models/huggingface/hub"; 49 70 pub trait ConfigProvider { 50 71 fn get_config_dir(&self) -> Result<PathBuf>; ··· 399 420 Ok(()) 400 421 } 401 422 423 + pub fn create_pi_provider_config(model_name: &str, enpoint_base_url: &str) -> Result<String> { 424 + let provider_config = PiProviderConfig { 425 + api: String::from("openai-responses"), 426 + api_key: String::from("tiles"), 427 + base_url: enpoint_base_url.to_string(), 428 + models: vec![PiProviderModelConfig { 429 + id: model_name.to_string(), 430 + }], 431 + }; 432 + 433 + let mut provider: HashMap<String, PiProviderConfig> = HashMap::new(); 434 + 435 + provider.insert("tiles".to_owned(), provider_config); 436 + let pi_model = PiModelConfig { 437 + providers: provider, 438 + }; 439 + let config = json!(pi_model); 440 + 441 + serde_json::to_string(&config).map_err(Into::<anyhow::Error>::into) 442 + } 443 + 444 + #[allow(dead_code)] 445 + fn try_update_pi_provider_model(config: &str, model_name: &str) -> Result<String> { 446 + let mut pi_model_config: PiModelConfig = serde_json::from_str(&config)?; 447 + let mut tiles_provider_config: PiProviderConfig = pi_model_config 448 + .providers 449 + .get("tiles") 450 + .expect("Expected tiles key in under provider in models.json") 451 + .clone(); 452 + 453 + if tiles_provider_config.models[0].id != model_name { 454 + tiles_provider_config.models = vec![PiProviderModelConfig { 455 + id: model_name.to_owned(), 456 + }]; 457 + let mut provider: HashMap<String, PiProviderConfig> = HashMap::new(); 458 + provider.insert("tiles".to_owned(), tiles_provider_config); 459 + pi_model_config.providers = provider; 460 + serde_json::to_string(&pi_model_config).map_err(Into::<anyhow::Error>::into) 461 + } else { 462 + Ok(config.to_owned()) 463 + } 464 + } 465 + 402 466 //TODO: Add more tests for config.toml 403 467 #[cfg(test)] 404 468 mod tests { ··· 438 502 do_update_current_model(&mut config, "model_name").unwrap(); 439 503 440 504 assert_eq!("model_name", config.model.unwrap().current); 505 + } 506 + 507 + #[test] 508 + fn test_valid_create_pi_provider_config() { 509 + let config_str = create_pi_provider_config( 510 + "mlx-community/Qwen3.5-4B-MLX-4bit", 511 + "http://127.0.0.1:0000/v1", 512 + ) 513 + .unwrap(); 514 + 515 + let config: PiModelConfig = serde_json::from_str(&config_str).unwrap(); 516 + 517 + let expected_json = json!({ 518 + "providers": { 519 + "tiles": { 520 + "api": "openai-responses", 521 + "apiKey": "tiles", 522 + "baseUrl": "http://127.0.0.1:0000/v1", 523 + "models": [ 524 + { 525 + "id": "mlx-community/Qwen3.5-4B-MLX-4bit" 526 + } 527 + ] 528 + } 529 + } 530 + }); 531 + 532 + assert_eq!(expected_json, serde_json::to_value(&config).unwrap()) 533 + } 534 + 535 + #[test] 536 + fn test_valid_model_config_update() { 537 + let config_str = create_pi_provider_config( 538 + "mlx-community/Qwen3.5-4B-MLX-4bit", 539 + "http://127.0.0.1:0000/v1", 540 + ) 541 + .unwrap(); 542 + 543 + let config: PiModelConfig = serde_json::from_str(&config_str).unwrap(); 544 + 545 + let expected_json = json!({ 546 + "providers": { 547 + "tiles": { 548 + "api": "openai-responses", 549 + "apiKey": "tiles", 550 + "baseUrl": "http://127.0.0.1:0000/v1", 551 + "models": [ 552 + { 553 + "id": "mlx-community/Qwen3.5-4B-MLX-4bit" 554 + } 555 + ] 556 + } 557 + } 558 + }); 559 + 560 + assert_eq!(expected_json, serde_json::to_value(&config).unwrap()); 561 + 562 + let new_config_str = try_update_pi_provider_model(&config_str, "new_model").unwrap(); 563 + 564 + let new_config: PiModelConfig = serde_json::from_str(&new_config_str).unwrap(); 565 + 566 + let expected_json = json!({ 567 + "providers": { 568 + "tiles": { 569 + "api": "openai-responses", 570 + "apiKey": "tiles", 571 + "baseUrl": "http://127.0.0.1:0000/v1", 572 + "models": [ 573 + { 574 + "id": "new_model" 575 + } 576 + 577 + ] 578 + } 579 + } 580 + }); 581 + 582 + assert_eq!(expected_json, serde_json::to_value(&new_config).unwrap()); 583 + assert_ne!(config_str, new_config_str); 584 + } 585 + 586 + #[test] 587 + fn test_no_model_config_update() { 588 + let config_str = create_pi_provider_config( 589 + "mlx-community/Qwen3.5-4B-MLX-4bit", 590 + "http://127.0.0.1:0000/v1", 591 + ) 592 + .unwrap(); 593 + 594 + let config: PiModelConfig = serde_json::from_str(&config_str).unwrap(); 595 + 596 + let expected_json = json!({ 597 + "providers": { 598 + "tiles": { 599 + "api": "openai-responses", 600 + "apiKey": "tiles", 601 + "baseUrl": "http://127.0.0.1:0000/v1", 602 + "models": [ 603 + { 604 + "id": "mlx-community/Qwen3.5-4B-MLX-4bit" 605 + } 606 + ] 607 + } 608 + } 609 + }); 610 + 611 + assert_eq!(expected_json, serde_json::to_value(&config).unwrap()); 612 + 613 + let new_config_str = 614 + try_update_pi_provider_model(&config_str, "mlx-community/Qwen3.5-4B-MLX-4bit").unwrap(); 615 + 616 + assert_eq!(config_str, new_config_str); 441 617 } 442 618 }
+12
toolchain.toml
··· 1 + # Tiles toolchain 2 + # Here we can find all the info related to libs,tools that either Tiles embed with it or Tiles rely on development. This is used for tracking the versions etc, which then can be used by other programs or scripts to derive values runtime. 3 + 4 + 5 + [embedded-tools] 6 + pi = "v0.67.68" 7 + sqlcipher = "4.10.0" 8 + 9 + [dev-tools] 10 + venvstacks = "0.8.0" 11 + 12 + # grep '^pi' toolchain.toml | head -1 | awk -F'"' '{print $2}'