ltcai 4.3.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +19 -17
  2. package/bin/ltcai.js +6 -2
  3. package/docs/CHANGELOG.md +33 -3
  4. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -22
  5. package/frontend/openapi.json +11 -1
  6. package/frontend/src/App.tsx +15 -1
  7. package/frontend/src/api/client.ts +19 -1
  8. package/frontend/src/api/openapi.ts +10 -0
  9. package/frontend/src/pages/Act.tsx +63 -2
  10. package/frontend/src/pages/Library.tsx +9 -3
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +3 -3
  13. package/lattice_brain/storage/sqlite.py +15 -2
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/agents.py +3 -1
  16. package/latticeai/api/models.py +66 -18
  17. package/latticeai/brain/projection.py +12 -2
  18. package/latticeai/brain/retrieval.py +10 -0
  19. package/latticeai/brain/store.py +6 -1
  20. package/latticeai/core/config.py +3 -1
  21. package/latticeai/core/marketplace.py +1 -1
  22. package/latticeai/core/multi_agent.py +1 -1
  23. package/latticeai/core/product_hardening.py +2 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/services/agent_runtime.py +52 -12
  26. package/latticeai/services/model_runtime.py +83 -2
  27. package/ltcai_cli.py +14 -3
  28. package/package.json +3 -2
  29. package/requirements.txt +17 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/src/main.rs +257 -25
  33. package/src-tauri/tauri.conf.json +20 -1
  34. package/static/app/asset-manifest.json +3 -3
  35. package/static/app/assets/{index-RiJTJliG.js → index-BhPuj8rT.js} +45 -45
  36. package/static/app/assets/index-BhPuj8rT.js.map +1 -0
  37. package/static/app/index.html +1 -1
  38. package/static/app/assets/index-RiJTJliG.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.3.0",
4
- "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
3
+ "version": "4.3.1",
4
+ "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, portable encrypted brain archives)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -68,6 +68,7 @@
68
68
  "files": [
69
69
  "bin/ltcai.js",
70
70
  "LICENSE",
71
+ "requirements.txt",
71
72
  "ltcai_cli.py",
72
73
  "auto_setup.py",
73
74
  "server.py",
@@ -0,0 +1,17 @@
1
+ fastapi>=0.110,<1
2
+ uvicorn>=0.29,<1
3
+ pydantic>=2.7,<3
4
+ httpx>=0.27,<1
5
+ pillow>=10,<13
6
+ openai>=1.30,<3
7
+ python-docx>=1.1,<2
8
+ openpyxl>=3.1,<4
9
+ python-pptx>=0.6.23,<2
10
+ python-multipart>=0.0.9,<0.1
11
+ keyring>=24,<26
12
+ authlib>=1.3,<2
13
+ cryptography>=42,<49
14
+ pdfplumber>=0.11,<0.12
15
+ pypdfium2>=4.30,<6
16
+ watchdog>=4,<7
17
+ psycopg[binary]>=3.2,<4
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "4.3.0"
1657
+ version = "4.3.1"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "4.3.0"
3
+ version = "4.3.1"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,5 +1,7 @@
1
1
  use std::{
2
2
  env,
3
+ fs::OpenOptions,
4
+ path::PathBuf,
3
5
  process::{Child, Command, Stdio},
4
6
  sync::Mutex,
5
7
  };
@@ -10,6 +12,7 @@ use tauri::{Manager, State};
10
12
  struct BackendState {
11
13
  origin: String,
12
14
  command: String,
15
+ cwd: Option<String>,
13
16
  child: Mutex<Option<Child>>,
14
17
  last_error: Mutex<Option<String>>,
15
18
  }
@@ -18,11 +21,19 @@ struct BackendState {
18
21
  struct BackendStatus {
19
22
  origin: String,
20
23
  command: String,
24
+ cwd: Option<String>,
21
25
  running: bool,
22
26
  pid: Option<u32>,
23
27
  last_error: Option<String>,
24
28
  }
25
29
 
30
+ struct BackendLaunch {
31
+ command: String,
32
+ program: String,
33
+ args: Vec<String>,
34
+ cwd: Option<PathBuf>,
35
+ }
36
+
26
37
  #[tauri::command]
27
38
  fn backend_origin(state: State<'_, BackendState>) -> String {
28
39
  state.origin.clone()
@@ -36,7 +47,8 @@ fn backend_status(state: State<'_, BackendState>) -> BackendStatus {
36
47
  #[tauri::command]
37
48
  fn restart_backend(state: State<'_, BackendState>) -> BackendStatus {
38
49
  kill_backend(&state);
39
- match spawn_backend(&state.origin, &state.command) {
50
+ let launch = backend_launch(&state.origin);
51
+ match spawn_backend(&state.origin, &launch) {
40
52
  Ok(child) => {
41
53
  if let Ok(mut slot) = state.child.lock() {
42
54
  *slot = child;
@@ -55,15 +67,174 @@ fn shutdown_backend(state: State<'_, BackendState>) -> BackendStatus {
55
67
  }
56
68
 
57
69
  fn split_command(command: &str) -> Vec<String> {
58
- command
59
- .split_whitespace()
60
- .map(|part| part.to_string())
61
- .collect()
70
+ command.split_whitespace().map(|part| part.to_string()).collect()
71
+ }
72
+
73
+ fn command_in_path(name: &str) -> Option<String> {
74
+ let mut dirs: Vec<PathBuf> = env::var_os("PATH")
75
+ .map(|value| env::split_paths(&value).collect())
76
+ .unwrap_or_default();
77
+ dirs.extend([
78
+ PathBuf::from("/opt/homebrew/bin"),
79
+ PathBuf::from("/usr/local/bin"),
80
+ PathBuf::from("/usr/bin"),
81
+ PathBuf::from("/bin"),
82
+ ]);
83
+ for dir in dirs {
84
+ let candidate = dir.join(name);
85
+ if candidate.is_file() {
86
+ return Some(candidate.to_string_lossy().to_string());
87
+ }
88
+ }
89
+ None
90
+ }
91
+
92
+ fn python_candidates() -> Vec<String> {
93
+ let mut out = Vec::new();
94
+ if let Ok(value) = env::var("LTCAI_PYTHON") {
95
+ out.push(value);
96
+ }
97
+ for name in ["python3", "python"] {
98
+ if let Some(path) = command_in_path(name) {
99
+ out.push(path);
100
+ }
101
+ }
102
+ out.extend([
103
+ "/opt/homebrew/bin/python3".to_string(),
104
+ "/usr/local/bin/python3".to_string(),
105
+ "/usr/bin/python3".to_string(),
106
+ ]);
107
+ out.sort();
108
+ out.dedup();
109
+ out
110
+ }
111
+
112
+ fn module_importable(python: &str, module: &str) -> bool {
113
+ Command::new(python)
114
+ .args(["-c", &format!("import {module}")])
115
+ .stdout(Stdio::null())
116
+ .stderr(Stdio::null())
117
+ .status()
118
+ .map(|status| status.success())
119
+ .unwrap_or(false)
120
+ }
121
+
122
+ fn resource_dir() -> Option<PathBuf> {
123
+ let exe = env::current_exe().ok()?;
124
+ let macos_dir = exe.parent()?;
125
+ let contents_dir = macos_dir.parent()?;
126
+ let resources = contents_dir.join("Resources");
127
+ if resources.exists() {
128
+ Some(resources)
129
+ } else {
130
+ None
131
+ }
132
+ }
133
+
134
+ fn bundled_python_root() -> Option<PathBuf> {
135
+ let resources = resource_dir()?;
136
+ let up = resources.join("_up_");
137
+ if up.join("ltcai_cli.py").is_file() {
138
+ Some(up)
139
+ } else if resources.join("ltcai_cli.py").is_file() {
140
+ Some(resources)
141
+ } else {
142
+ None
143
+ }
144
+ }
145
+
146
+ fn desktop_runtime_dir() -> Option<PathBuf> {
147
+ let home = env::var("HOME").ok()?;
148
+ let dir = PathBuf::from(home).join(".ltcai").join("desktop-runtime");
149
+ let _ = std::fs::create_dir_all(&dir);
150
+ Some(dir)
151
+ }
152
+
153
+ fn python_path_env(launch: &BackendLaunch) -> Option<String> {
154
+ let mut paths: Vec<PathBuf> = Vec::new();
155
+ if let Some(resources) = bundled_python_root() {
156
+ paths.push(resources);
157
+ }
158
+ if let Some(cwd) = &launch.cwd {
159
+ if !paths.iter().any(|path| path == cwd) {
160
+ paths.push(cwd.clone());
161
+ }
162
+ }
163
+ if let Some(existing) = env::var_os("PYTHONPATH") {
164
+ paths.extend(env::split_paths(&existing));
165
+ }
166
+ env::join_paths(paths).ok().map(|value| value.to_string_lossy().to_string())
62
167
  }
63
168
 
64
- fn backend_command() -> String {
65
- env::var("LATTICEAI_DESKTOP_BACKEND_CMD")
66
- .unwrap_or_else(|_| "python3 ltcai_cli.py --host 127.0.0.1 --port 8765".to_string())
169
+ fn backend_launch(origin: &str) -> BackendLaunch {
170
+ let port = origin.rsplit(':').next().unwrap_or("8765").to_string();
171
+ if let Ok(command) = env::var("LATTICEAI_DESKTOP_BACKEND_CMD") {
172
+ let parts = split_command(&command);
173
+ if let Some(program) = parts.first() {
174
+ return BackendLaunch {
175
+ command,
176
+ program: program.clone(),
177
+ args: parts[1..].to_vec(),
178
+ cwd: env::var("LATTICEAI_DESKTOP_BACKEND_CWD").ok().map(PathBuf::from),
179
+ };
180
+ }
181
+ }
182
+
183
+ for name in ["LTCAI", "ltcai"] {
184
+ if let Some(program) = command_in_path(name) {
185
+ return BackendLaunch {
186
+ command: format!("{program} --host 127.0.0.1 --port {port}"),
187
+ program,
188
+ args: vec!["--host".into(), "127.0.0.1".into(), "--port".into(), port],
189
+ cwd: None,
190
+ };
191
+ }
192
+ }
193
+
194
+ for python in python_candidates() {
195
+ if module_importable(&python, "ltcai_cli") {
196
+ return BackendLaunch {
197
+ command: format!("{python} -m ltcai_cli --host 127.0.0.1 --port {port}"),
198
+ program: python,
199
+ args: vec![
200
+ "-m".into(),
201
+ "ltcai_cli".into(),
202
+ "--host".into(),
203
+ "127.0.0.1".into(),
204
+ "--port".into(),
205
+ port,
206
+ ],
207
+ cwd: None,
208
+ };
209
+ }
210
+ }
211
+
212
+ if let Some(resources) = bundled_python_root() {
213
+ let launcher = resources.join("ltcai_cli.py");
214
+ if launcher.is_file() {
215
+ if let Some(python) = python_candidates().into_iter().next() {
216
+ return BackendLaunch {
217
+ command: format!("{python} {} --host 127.0.0.1 --port {port}", launcher.display()),
218
+ program: python,
219
+ args: vec![
220
+ launcher.to_string_lossy().to_string(),
221
+ "--host".into(),
222
+ "127.0.0.1".into(),
223
+ "--port".into(),
224
+ port,
225
+ ],
226
+ cwd: None,
227
+ };
228
+ }
229
+ }
230
+ }
231
+
232
+ BackendLaunch {
233
+ command: "unavailable: LTCAI executable or importable ltcai_cli module not found".to_string(),
234
+ program: String::new(),
235
+ args: Vec::new(),
236
+ cwd: None,
237
+ }
67
238
  }
68
239
 
69
240
  fn set_error(state: &BackendState, err: Option<String>) {
@@ -72,30 +243,64 @@ fn set_error(state: &BackendState, err: Option<String>) {
72
243
  }
73
244
  }
74
245
 
75
- fn spawn_backend(origin: &str, command: &str) -> Result<Option<Child>, String> {
246
+ fn spawn_backend(origin: &str, launch: &BackendLaunch) -> Result<Option<Child>, String> {
76
247
  if env::var("LATTICEAI_DESKTOP_NO_BACKEND").is_ok() {
77
248
  return Ok(None);
78
249
  }
79
- let parts = split_command(&command);
80
- if parts.is_empty() {
81
- return Err("Desktop backend command is empty.".to_string());
250
+ if launch.program.is_empty() {
251
+ return Err("Desktop backend unavailable: LTCAI executable or importable ltcai_cli module not found.".to_string());
82
252
  }
83
- let mut cmd = Command::new(&parts[0]);
84
- cmd.args(&parts[1..])
253
+
254
+ let mut cmd = Command::new(&launch.program);
255
+ cmd.args(&launch.args)
85
256
  .env("LATTICEAI_HOST", "127.0.0.1")
86
257
  .env("LATTICEAI_PORT", origin.rsplit(':').next().unwrap_or("8765"))
87
258
  .env("LATTICEAI_ENABLE_TELEGRAM", "false")
88
259
  .env("LATTICEAI_AUTOLOAD_MODELS", "false")
260
+ .env("LATTICEAI_ALLOW_MODEL_DOWNLOADS", "false")
89
261
  .env("LATTICEAI_CORS_ALLOW_NETWORK", "false")
262
+ .env("LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", "false")
90
263
  .env("LATTICEAI_TUNNEL", "false")
91
- .stdout(Stdio::null())
92
- .stderr(Stdio::null());
93
- if let Ok(cwd) = env::var("LATTICEAI_DESKTOP_BACKEND_CWD") {
264
+ .env(
265
+ "PATH",
266
+ format!(
267
+ "{}:{}",
268
+ "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
269
+ env::var("PATH").unwrap_or_default()
270
+ ),
271
+ );
272
+ if let Some(runtime_dir) = desktop_runtime_dir() {
273
+ cmd.env("LATTICEAI_AGENT_ROOT", runtime_dir.join("agent_workspace"));
274
+ if launch.cwd.is_none() {
275
+ cmd.current_dir(&runtime_dir);
276
+ }
277
+ }
278
+ if let Some(python_path) = python_path_env(launch) {
279
+ cmd.env("PYTHONPATH", python_path);
280
+ }
281
+ if let Some(cwd) = &launch.cwd {
94
282
  cmd.current_dir(cwd);
95
283
  }
284
+ if let Ok(home) = env::var("HOME") {
285
+ let log_dir = PathBuf::from(home).join(".ltcai");
286
+ let _ = std::fs::create_dir_all(&log_dir);
287
+ if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.log")) {
288
+ cmd.stdout(Stdio::from(file));
289
+ } else {
290
+ cmd.stdout(Stdio::null());
291
+ }
292
+ if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.err.log")) {
293
+ cmd.stderr(Stdio::from(file));
294
+ } else {
295
+ cmd.stderr(Stdio::null());
296
+ }
297
+ } else {
298
+ cmd.stdout(Stdio::null()).stderr(Stdio::null());
299
+ }
300
+
96
301
  cmd.spawn()
97
302
  .map(Some)
98
- .map_err(|err| format!("Failed to start desktop backend '{}': {}", parts[0], err))
303
+ .map_err(|err| format!("Failed to start desktop backend '{}': {}", launch.command, err))
99
304
  }
100
305
 
101
306
  fn kill_backend(state: &BackendState) {
@@ -125,25 +330,40 @@ fn status_from_state(state: &BackendState) -> BackendStatus {
125
330
  }
126
331
  }
127
332
  }
128
- let last_error = state
129
- .last_error
130
- .lock()
131
- .ok()
132
- .and_then(|guard| guard.clone());
333
+ let last_error = state.last_error.lock().ok().and_then(|guard| guard.clone());
133
334
  BackendStatus {
134
335
  origin: state.origin.clone(),
135
336
  command: state.command.clone(),
337
+ cwd: state.cwd.clone(),
136
338
  running,
137
339
  pid,
138
340
  last_error,
139
341
  }
140
342
  }
141
343
 
344
+ fn wait_for_backend(origin: &str) {
345
+ let host_port = origin
346
+ .trim_start_matches("http://")
347
+ .trim_start_matches("https://")
348
+ .split('/')
349
+ .next()
350
+ .unwrap_or("127.0.0.1:8765")
351
+ .to_string();
352
+ for _ in 0..45 {
353
+ if std::net::TcpStream::connect(&host_port).is_ok() {
354
+ return;
355
+ }
356
+ std::thread::sleep(std::time::Duration::from_millis(500));
357
+ }
358
+ }
359
+
142
360
  fn main() {
143
361
  let origin = env::var("LATTICEAI_DESKTOP_BACKEND_ORIGIN")
144
362
  .unwrap_or_else(|_| "http://127.0.0.1:8765".to_string());
145
- let command = backend_command();
146
- let (child, last_error) = match spawn_backend(&origin, &command) {
363
+ let launch = backend_launch(&origin);
364
+ let command = launch.command.clone();
365
+ let cwd = launch.cwd.as_ref().map(|path| path.to_string_lossy().to_string());
366
+ let (child, last_error) = match spawn_backend(&origin, &launch) {
147
367
  Ok(child) => (child, None),
148
368
  Err(err) => (None, Some(err)),
149
369
  };
@@ -151,6 +371,7 @@ fn main() {
151
371
  .manage(BackendState {
152
372
  origin,
153
373
  command,
374
+ cwd,
154
375
  child: Mutex::new(child),
155
376
  last_error: Mutex::new(last_error),
156
377
  })
@@ -163,6 +384,17 @@ fn main() {
163
384
  .setup(|app| {
164
385
  if let Some(window) = app.get_webview_window("main") {
165
386
  let _ = window.set_title("Lattice AI");
387
+ let _ = window.show();
388
+ let _ = window.set_focus();
389
+ let origin = app.state::<BackendState>().origin.clone();
390
+ let target = format!("{}/app", origin.trim_end_matches('/'));
391
+ let mut window_for_nav = window.clone();
392
+ std::thread::spawn(move || {
393
+ wait_for_backend(&origin);
394
+ if let Ok(url) = tauri::Url::parse(&target) {
395
+ let _ = window_for_nav.navigate(url);
396
+ }
397
+ });
166
398
  }
167
399
  Ok(())
168
400
  })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "4.3.0",
4
+ "version": "4.3.1",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -31,6 +31,25 @@
31
31
  "dmg",
32
32
  "app"
33
33
  ],
34
+ "resources": [
35
+ "../auto_setup.py",
36
+ "../kg_schema.py",
37
+ "../knowledge_graph.py",
38
+ "../knowledge_graph_api.py",
39
+ "../llm_router.py",
40
+ "../local_knowledge_api.py",
41
+ "../ltcai_cli.py",
42
+ "../mcp_registry.py",
43
+ "../p_reinforce.py",
44
+ "../server.py",
45
+ "../setup_wizard.py",
46
+ "../telegram_bot.py",
47
+ "../requirements.txt",
48
+ "../latticeai",
49
+ "../lattice_brain",
50
+ "../tools",
51
+ "../static"
52
+ ],
34
53
  "icon": [
35
54
  "../static/icons/icon-192.png",
36
55
  "../static/icons/icon-512.png"
@@ -1,12 +1,12 @@
1
1
  {
2
- "version": "4.3.0",
2
+ "version": "4.3.1",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/index.html"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "index.html": "/static/app/assets/index-RiJTJliG.js",
9
+ "index.html": "/static/app/assets/index-BhPuj8rT.js",
10
10
  "assets/index-yZswHE3d.css": "/static/app/assets/index-yZswHE3d.css"
11
11
  },
12
12
  "vite": {
@@ -17,7 +17,7 @@
17
17
  "isDynamicEntry": true
18
18
  },
19
19
  "index.html": {
20
- "file": "assets/index-RiJTJliG.js",
20
+ "file": "assets/index-BhPuj8rT.js",
21
21
  "name": "index",
22
22
  "src": "index.html",
23
23
  "isEntry": true,