ltcai 6.1.0 → 6.3.0
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.
- package/README.md +36 -38
- package/docs/CHANGELOG.md +75 -1
- package/frontend/src/App.tsx +3 -1286
- package/frontend/src/components/LanguageSwitcher.tsx +23 -0
- package/frontend/src/components/ProductFlow.tsx +32 -669
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
- package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
- package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
- package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -0
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
- package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
- package/frontend/src/features/brain/BrainComposer.tsx +66 -0
- package/frontend/src/features/brain/BrainConversation.tsx +131 -0
- package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
- package/frontend/src/features/brain/BrainHome.tsx +232 -0
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
- package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
- package/frontend/src/features/brain/brainData.ts +98 -0
- package/frontend/src/features/brain/graphLayout.ts +26 -0
- package/frontend/src/features/brain/types.ts +44 -0
- package/frontend/src/features/review/ReviewCard.tsx +3 -1
- package/frontend/src/features/review/ReviewInbox.tsx +11 -1
- package/frontend/src/i18n.ts +290 -0
- package/frontend/src/pages/Brain.tsx +63 -5
- package/frontend/src/pages/Capture.tsx +102 -13
- package/frontend/src/pages/Library.tsx +72 -6
- package/frontend/src/styles.css +220 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/tools.py +50 -23
- package/latticeai/app_factory.py +59 -76
- package/latticeai/cli/entrypoint.py +283 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot.py +1009 -0
- package/latticeai/runtime/chat_wiring.py +119 -0
- package/latticeai/runtime/lifespan_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +40 -0
- package/latticeai/runtime/platform_runtime_wiring.py +86 -0
- package/latticeai/runtime/review_wiring.py +37 -0
- package/latticeai/runtime/router_registration.py +49 -30
- package/latticeai/runtime/tail_wiring.py +21 -0
- package/latticeai/services/p_reinforce.py +258 -0
- package/latticeai/services/router_context.py +52 -0
- package/ltcai_cli.py +7 -279
- package/p_reinforce.py +4 -255
- package/package.json +3 -2
- package/scripts/check_i18n_literals.mjs +52 -0
- package/scripts/release_smoke.py +133 -0
- package/scripts/wheel_smoke.py +4 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-D76dWuQk.js +16 -0
- package/static/app/assets/index-D76dWuQk.js.map +1 -0
- package/static/app/assets/index-Div5vMlq.css +2 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1002
- package/static/app/assets/index-B744yblP.css +0 -2
- package/static/app/assets/index-DYaUKNfl.js +0 -16
- package/static/app/assets/index-DYaUKNfl.js.map +0 -1
package/latticeai/app_factory.py
CHANGED
|
@@ -17,10 +17,14 @@ from __future__ import annotations
|
|
|
17
17
|
import threading
|
|
18
18
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
19
19
|
|
|
20
|
-
from latticeai.runtime.automation_runtime import build_automation_runtime
|
|
21
20
|
from latticeai.runtime.app_context_runtime import build_app_context
|
|
22
21
|
from latticeai.runtime.bootstrap import build_session_runtime
|
|
23
22
|
from latticeai.runtime.brain_runtime import build_brain_runtime
|
|
23
|
+
from latticeai.runtime.chat_wiring import (
|
|
24
|
+
build_chat_agent_runtime_from_context,
|
|
25
|
+
build_interaction_contexts,
|
|
26
|
+
maybe_build_telegram_chat_mirror,
|
|
27
|
+
)
|
|
24
28
|
from latticeai.runtime.config_runtime import build_config_runtime
|
|
25
29
|
from latticeai.runtime.context_runtime import build_context_runtime
|
|
26
30
|
from latticeai.runtime.hooks_runtime import (
|
|
@@ -29,11 +33,16 @@ from latticeai.runtime.hooks_runtime import (
|
|
|
29
33
|
build_hooks_runtime,
|
|
30
34
|
)
|
|
31
35
|
from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
|
|
36
|
+
from latticeai.runtime.model_wiring import (
|
|
37
|
+
configure_model_runtime_from_context,
|
|
38
|
+
register_model_runtime_routers,
|
|
39
|
+
)
|
|
32
40
|
from latticeai.runtime.platform_services_runtime import (
|
|
33
41
|
build_brain_network,
|
|
34
|
-
build_model_service,
|
|
35
42
|
)
|
|
43
|
+
from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
|
|
36
44
|
from latticeai.runtime.persistence_runtime import build_persistence_runtime
|
|
45
|
+
from latticeai.runtime.review_wiring import build_review_run_now_runner
|
|
37
46
|
from latticeai.runtime.router_registration import (
|
|
38
47
|
build_auth_admin_security_router_bundle,
|
|
39
48
|
build_static_routes_bundle,
|
|
@@ -44,6 +53,7 @@ from latticeai.runtime.router_registration import (
|
|
|
44
53
|
register_review_and_brain_tail_routers,
|
|
45
54
|
)
|
|
46
55
|
from latticeai.runtime.security_runtime import build_security_runtime
|
|
56
|
+
from latticeai.runtime.tail_wiring import register_tail_runtime_routers
|
|
47
57
|
from latticeai.runtime.web_runtime import build_web_runtime
|
|
48
58
|
|
|
49
59
|
if TYPE_CHECKING: # imports for annotations only — keep module import light
|
|
@@ -145,7 +155,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
145
155
|
from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
|
|
146
156
|
from latticeai.api.health import create_health_router
|
|
147
157
|
# ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
|
|
148
|
-
from latticeai.services.platform_runtime import PlatformRuntime
|
|
149
158
|
from latticeai.api.plugins import create_plugins_router
|
|
150
159
|
from latticeai.api.workflow_designer import create_workflow_designer_router
|
|
151
160
|
from latticeai.api.agents import create_agents_router
|
|
@@ -189,7 +198,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
189
198
|
_get_combined_registry,
|
|
190
199
|
_fetch_skills_marketplace, install_skill, SKILLS_DIR,
|
|
191
200
|
)
|
|
192
|
-
from p_reinforce import PReinforceGardener
|
|
201
|
+
from latticeai.services.p_reinforce import PReinforceGardener
|
|
193
202
|
from setup_wizard import get_recommendations, scan_environment
|
|
194
203
|
from tools import ensure_agent_root, execute_tool, knowledge_save
|
|
195
204
|
|
|
@@ -1089,7 +1098,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1089
1098
|
OPEN_REGISTRATION = CONFIG.open_registration
|
|
1090
1099
|
INVITE_CODE = CONFIG.invite_code
|
|
1091
1100
|
INVITE_GATE_ENABLED = CONFIG.invite_gate_enabled
|
|
1092
|
-
|
|
1101
|
+
configure_model_runtime_from_context(
|
|
1102
|
+
configure_model_runtime=configure_model_runtime,
|
|
1093
1103
|
router=router,
|
|
1094
1104
|
APP_MODE=APP_MODE,
|
|
1095
1105
|
DEFAULT_HOST=DEFAULT_HOST,
|
|
@@ -1236,12 +1246,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1236
1246
|
# ── Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy.
|
|
1237
1247
|
# latticeai.api.chat no longer imports telegram_bot (a 45KB module that
|
|
1238
1248
|
# mutates os.environ at import); it calls this injected callback instead.
|
|
1239
|
-
on_chat_message =
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
_spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
|
|
1244
|
-
on_chat_message = _telegram_chat_mirror
|
|
1249
|
+
on_chat_message = maybe_build_telegram_chat_mirror(
|
|
1250
|
+
enable_telegram=ENABLE_TELEGRAM,
|
|
1251
|
+
spawn=_spawn,
|
|
1252
|
+
)
|
|
1245
1253
|
|
|
1246
1254
|
def _recent_chat_context(
|
|
1247
1255
|
limit: int = 10,
|
|
@@ -1257,7 +1265,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1257
1265
|
conversation_id=conversation_id,
|
|
1258
1266
|
)
|
|
1259
1267
|
|
|
1260
|
-
CHAT_AGENT_RUNTIME =
|
|
1268
|
+
CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
|
|
1269
|
+
build_agent_runtime=build_agent_runtime,
|
|
1261
1270
|
model_router=router,
|
|
1262
1271
|
execute_tool=execute_tool,
|
|
1263
1272
|
recent_chat_context=_recent_chat_context,
|
|
@@ -1338,20 +1347,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1338
1347
|
|
|
1339
1348
|
|
|
1340
1349
|
# ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
# Synchronous model bridge for the orchestrator's role runner. Safe
|
|
1345
|
-
# because the agents run endpoint executes start() in a worker thread
|
|
1346
|
-
# (asyncio.to_thread), where no event loop is running.
|
|
1347
|
-
import asyncio as _asyncio
|
|
1348
|
-
|
|
1349
|
-
return str(_asyncio.run(router.generate(
|
|
1350
|
-
message, context=context, max_tokens=max_tokens, temperature=temperature,
|
|
1351
|
-
)))
|
|
1352
|
-
|
|
1353
|
-
PLATFORM = PlatformRuntime(
|
|
1354
|
-
store=WORKSPACE_OS,
|
|
1350
|
+
_platform_automation_runtime = build_platform_automation_runtime(
|
|
1351
|
+
model_router=router,
|
|
1352
|
+
workspace_store=WORKSPACE_OS,
|
|
1355
1353
|
workspace_service=WORKSPACE_SERVICE,
|
|
1356
1354
|
plugin_registry=PLUGIN_REGISTRY,
|
|
1357
1355
|
get_current_user=get_current_user,
|
|
@@ -1359,23 +1357,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1359
1357
|
workspace_scope_from_request=_workspace_scope_from_request,
|
|
1360
1358
|
get_tool_permission=get_tool_permission,
|
|
1361
1359
|
hooks=HOOKS_REGISTRY,
|
|
1362
|
-
llm_generate=_llm_generate_sync,
|
|
1363
|
-
llm_available=lambda: bool(getattr(router, "current_model_id", None)),
|
|
1364
1360
|
agent_registry=AGENT_REGISTRY,
|
|
1365
|
-
)
|
|
1366
|
-
|
|
1367
|
-
_automation_runtime = build_automation_runtime(
|
|
1368
|
-
store=WORKSPACE_OS,
|
|
1369
|
-
platform=PLATFORM,
|
|
1370
1361
|
data_dir=DATA_DIR,
|
|
1371
|
-
workspace_graph=_workspace_graph,
|
|
1372
1362
|
append_audit_event=append_audit_event,
|
|
1373
|
-
hooks=HOOKS_REGISTRY,
|
|
1374
1363
|
)
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1364
|
+
_llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
|
|
1365
|
+
PLATFORM = _platform_automation_runtime["PLATFORM"]
|
|
1366
|
+
_automation_runtime = _platform_automation_runtime["_automation_runtime"]
|
|
1367
|
+
REVIEW_QUEUE = _platform_automation_runtime["REVIEW_QUEUE"]
|
|
1368
|
+
TRIGGER_SERVICE = _platform_automation_runtime["TRIGGER_SERVICE"]
|
|
1369
|
+
AGENT_RUNTIME = _platform_automation_runtime["AGENT_RUNTIME"]
|
|
1370
|
+
RUN_EXECUTOR = _platform_automation_runtime["RUN_EXECUTOR"]
|
|
1379
1371
|
bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
|
|
1380
1372
|
app.state.run_executor = RUN_EXECUTOR
|
|
1381
1373
|
app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
|
|
@@ -1421,22 +1413,19 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1421
1413
|
# ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
|
|
1422
1414
|
# /health, /mode, /runtime_features, /engines(GET) now live in the health router.
|
|
1423
1415
|
# Heavier engine mutation endpoints remain below in server_app.
|
|
1424
|
-
MODEL_SERVICE =
|
|
1416
|
+
MODEL_SERVICE = register_model_runtime_routers(
|
|
1417
|
+
app=app,
|
|
1418
|
+
create_health_router=create_health_router,
|
|
1419
|
+
create_models_router=create_models_router,
|
|
1420
|
+
register_health_and_model_routers=register_health_and_model_routers,
|
|
1425
1421
|
model_router=router,
|
|
1426
1422
|
runtime_features=runtime_features,
|
|
1427
|
-
|
|
1428
|
-
)
|
|
1429
|
-
register_health_and_model_routers(
|
|
1430
|
-
app,
|
|
1431
|
-
create_health_router=create_health_router,
|
|
1432
|
-
model_service=MODEL_SERVICE,
|
|
1423
|
+
is_public_mode=IS_PUBLIC_MODE,
|
|
1433
1424
|
engine_status=engine_status,
|
|
1434
1425
|
get_current_user=get_current_user,
|
|
1435
1426
|
require_auth=REQUIRE_AUTH,
|
|
1436
1427
|
app_version=APP_VERSION,
|
|
1437
1428
|
app_mode=APP_MODE,
|
|
1438
|
-
create_models_router=create_models_router,
|
|
1439
|
-
model_router=router,
|
|
1440
1429
|
require_user=require_user,
|
|
1441
1430
|
load_users=load_users,
|
|
1442
1431
|
get_user_role=get_user_role,
|
|
@@ -1455,7 +1444,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1455
1444
|
engine_model_catalog=ENGINE_MODEL_CATALOG,
|
|
1456
1445
|
model_engine_aliases=MODEL_ENGINE_ALIASES,
|
|
1457
1446
|
cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
|
|
1458
|
-
is_public_mode=IS_PUBLIC_MODE,
|
|
1459
1447
|
allow_local_models=ALLOW_LOCAL_MODELS,
|
|
1460
1448
|
)
|
|
1461
1449
|
|
|
@@ -1478,21 +1466,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1478
1466
|
return None
|
|
1479
1467
|
return PLATFORM.allowed_scopes(user)
|
|
1480
1468
|
|
|
1481
|
-
|
|
1482
|
-
app,
|
|
1483
|
-
create_chat_router=create_chat_router,
|
|
1484
|
-
context=context,
|
|
1485
|
-
create_search_router=create_search_router,
|
|
1486
|
-
search_service=SEARCH_SERVICE,
|
|
1487
|
-
allowed_workspaces_for=_allowed_workspaces_for,
|
|
1488
|
-
require_user=require_user,
|
|
1489
|
-
embedding_info=_embedding_info,
|
|
1490
|
-
create_tools_router=create_tools_router,
|
|
1491
|
-
ingestion_pipeline=INGESTION_PIPELINE,
|
|
1469
|
+
tool_router_context, interaction_router_context = build_interaction_contexts(
|
|
1492
1470
|
config=CONFIG,
|
|
1471
|
+
ingestion_pipeline=INGESTION_PIPELINE,
|
|
1493
1472
|
data_dir=DATA_DIR,
|
|
1494
1473
|
static_dir=STATIC_DIR,
|
|
1495
1474
|
model_router=router,
|
|
1475
|
+
require_user=require_user,
|
|
1496
1476
|
require_admin=require_admin,
|
|
1497
1477
|
get_current_user=get_current_user,
|
|
1498
1478
|
clear_history=clear_history,
|
|
@@ -1510,34 +1490,37 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1510
1490
|
install_mcp=install_mcp,
|
|
1511
1491
|
mcp_public_item=mcp_public_item,
|
|
1512
1492
|
hooks=HOOKS_REGISTRY,
|
|
1513
|
-
|
|
1514
|
-
|
|
1493
|
+
chat_context=context,
|
|
1494
|
+
search_service=SEARCH_SERVICE,
|
|
1495
|
+
allowed_workspaces_for=_allowed_workspaces_for,
|
|
1496
|
+
embedding_info=_embedding_info,
|
|
1515
1497
|
agent_registry=AGENT_REGISTRY,
|
|
1516
|
-
create_memory_router=create_memory_router,
|
|
1517
1498
|
memory_service=MEMORY_SERVICE,
|
|
1518
1499
|
platform=PLATFORM,
|
|
1519
1500
|
)
|
|
1501
|
+
register_interaction_routers(
|
|
1502
|
+
app,
|
|
1503
|
+
interaction_context=interaction_router_context,
|
|
1504
|
+
create_chat_router=create_chat_router,
|
|
1505
|
+
create_search_router=create_search_router,
|
|
1506
|
+
create_tools_router=create_tools_router,
|
|
1507
|
+
create_hooks_router=create_hooks_router,
|
|
1508
|
+
create_agent_registry_router=create_agent_registry_router,
|
|
1509
|
+
create_memory_router=create_memory_router,
|
|
1510
|
+
)
|
|
1520
1511
|
|
|
1521
1512
|
from latticeai.api.review_queue import create_review_queue_router
|
|
1513
|
+
run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
|
|
1522
1514
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
wf_id = (item.get("payload") or {}).get("workflow_id") or (item.get("provenance") or {}).get("workflow_id")
|
|
1526
|
-
if not wf_id:
|
|
1527
|
-
raise HTTPException(status_code=409, detail="review item has no workflow to run")
|
|
1528
|
-
return PLATFORM.run_workflow_by_id(
|
|
1529
|
-
wf_id, user_email, scope, with_agent=False,
|
|
1530
|
-
inputs={"__review_item__": item.get("id")},
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
BRAIN_NETWORK = register_review_and_brain_tail_routers(
|
|
1534
|
-
app,
|
|
1515
|
+
BRAIN_NETWORK = register_tail_runtime_routers(
|
|
1516
|
+
app=app,
|
|
1535
1517
|
create_review_queue_router=create_review_queue_router,
|
|
1518
|
+
register_review_and_brain_tail_routers=register_review_and_brain_tail_routers,
|
|
1536
1519
|
review_queue=REVIEW_QUEUE,
|
|
1537
1520
|
require_user=require_user,
|
|
1538
1521
|
gate_read=PLATFORM.gate_read,
|
|
1539
1522
|
gate_write=PLATFORM.gate_write,
|
|
1540
|
-
run_review_item=
|
|
1523
|
+
run_review_item=run_review_item,
|
|
1541
1524
|
append_audit_event=append_audit_event,
|
|
1542
1525
|
create_browser_router=create_browser_router,
|
|
1543
1526
|
ingestion_pipeline=INGESTION_PIPELINE,
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Command line entrypoint for Lattice AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import socket
|
|
11
|
+
import stat
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from latticeai.cli.runtime import (
|
|
20
|
+
_apply_extra_path,
|
|
21
|
+
_has_module,
|
|
22
|
+
_load_env_file,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _local_ips() -> list[str]:
|
|
26
|
+
ips: list[str] = []
|
|
27
|
+
try:
|
|
28
|
+
hostname = socket.gethostname()
|
|
29
|
+
for info in socket.getaddrinfo(hostname, None):
|
|
30
|
+
addr = info[4][0]
|
|
31
|
+
if ":" not in addr and not addr.startswith("127."):
|
|
32
|
+
if addr not in ips:
|
|
33
|
+
ips.append(addr)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
if not ips:
|
|
37
|
+
try:
|
|
38
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
39
|
+
s.connect(("8.8.8.8", 80))
|
|
40
|
+
ips.append(s.getsockname()[0])
|
|
41
|
+
s.close()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return ips
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_banner(host: str, port: int, tunnel_url: str | None = None) -> None:
|
|
48
|
+
local_url = f"http://localhost:{port}"
|
|
49
|
+
print()
|
|
50
|
+
print("=" * 56)
|
|
51
|
+
print(" Lattice AI is running")
|
|
52
|
+
print(f" Local: {local_url}")
|
|
53
|
+
if host == "0.0.0.0":
|
|
54
|
+
for ip in _local_ips():
|
|
55
|
+
print(f" Network: http://{ip}:{port}")
|
|
56
|
+
print()
|
|
57
|
+
print(" Other devices on the same Wi-Fi can open the")
|
|
58
|
+
print(" Network URL above in their browser.")
|
|
59
|
+
print(" On iPad/Android: browser menu → 'Add to Home Screen'")
|
|
60
|
+
if tunnel_url:
|
|
61
|
+
print()
|
|
62
|
+
print(f" Tunnel: {tunnel_url}")
|
|
63
|
+
print(" Anyone on the internet can access via the Tunnel URL.")
|
|
64
|
+
print("=" * 56)
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def doctor() -> int:
|
|
69
|
+
checks = [
|
|
70
|
+
("Python 3.11+", sys.version_info >= (3, 11), sys.version.split()[0], True),
|
|
71
|
+
("FastAPI", _has_module("fastapi"), "required server dependency", True),
|
|
72
|
+
("Uvicorn", _has_module("uvicorn"), "required server dependency", True),
|
|
73
|
+
("OpenAI SDK", _has_module("openai"), "required for cloud providers", False),
|
|
74
|
+
("MLX", _has_module("mlx"), "required for Apple Silicon multimodal models", False),
|
|
75
|
+
("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma-4/VLM models", False),
|
|
76
|
+
("Ollama binary", shutil.which("ollama") is not None, "optional local-server engine", False),
|
|
77
|
+
]
|
|
78
|
+
data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
|
|
79
|
+
static_dir = Path(os.getenv("LATTICEAI_STATIC_DIR") or Path(__file__).resolve().parent / "static")
|
|
80
|
+
checks.extend([
|
|
81
|
+
("Data dir", data_dir.exists() or data_dir.parent.exists(), str(data_dir), True),
|
|
82
|
+
("Static UI", static_dir.exists(), str(static_dir), True),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
ok = True
|
|
86
|
+
for label, passed, detail, required in checks:
|
|
87
|
+
icon = "OK" if passed else ("MISS" if required else "OPTIONAL")
|
|
88
|
+
print(f"[{icon}] {label}: {detail}")
|
|
89
|
+
ok = ok and (passed or not required)
|
|
90
|
+
|
|
91
|
+
cloud_keys = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY"]
|
|
92
|
+
configured = [key for key in cloud_keys if os.getenv(key)]
|
|
93
|
+
print(f"[INFO] Cloud keys configured: {', '.join(configured) if configured else 'none'}")
|
|
94
|
+
return 0 if ok else 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def _cloudflared_url() -> str:
|
|
100
|
+
system = sys.platform
|
|
101
|
+
machine = platform.machine().lower()
|
|
102
|
+
base = "https://github.com/cloudflare/cloudflared/releases/latest/download"
|
|
103
|
+
if system == "darwin":
|
|
104
|
+
arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
|
|
105
|
+
return f"{base}/cloudflared-darwin-{arch}"
|
|
106
|
+
if system == "win32":
|
|
107
|
+
return f"{base}/cloudflared-windows-amd64.exe"
|
|
108
|
+
arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
|
|
109
|
+
return f"{base}/cloudflared-linux-{arch}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _cloudflared_bin() -> Path:
|
|
113
|
+
suffix = ".exe" if sys.platform == "win32" else ""
|
|
114
|
+
return Path.home() / ".latticeai" / "bin" / f"cloudflared{suffix}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ensure_cloudflared() -> str:
|
|
118
|
+
found = shutil.which("cloudflared")
|
|
119
|
+
if found:
|
|
120
|
+
return found
|
|
121
|
+
dest = _cloudflared_bin()
|
|
122
|
+
if dest.exists():
|
|
123
|
+
return str(dest)
|
|
124
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
url = _cloudflared_url()
|
|
126
|
+
print(" cloudflared not found — downloading from GitHub...")
|
|
127
|
+
try:
|
|
128
|
+
urllib.request.urlretrieve(url, dest)
|
|
129
|
+
if sys.platform != "win32":
|
|
130
|
+
dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
131
|
+
print(f" cloudflared installed at {dest}")
|
|
132
|
+
return str(dest)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f" cloudflared download failed: {e}")
|
|
135
|
+
print(" Install manually: https://developers.cloudflare.com/cloudflared/install")
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _send_telegram(token: str, chat_id: str, text: str) -> None:
|
|
140
|
+
try:
|
|
141
|
+
data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
|
|
142
|
+
req = urllib.request.Request(
|
|
143
|
+
f"https://api.telegram.org/bot{token}/sendMessage",
|
|
144
|
+
data=data,
|
|
145
|
+
)
|
|
146
|
+
urllib.request.urlopen(req, timeout=10)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _start_tunnel(port: int) -> str | None:
|
|
152
|
+
|
|
153
|
+
bin_path = _ensure_cloudflared()
|
|
154
|
+
if not bin_path:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
log_path = Path.home() / ".latticeai" / "tunnel.log"
|
|
158
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
proc = subprocess.Popen(
|
|
161
|
+
[bin_path, "tunnel", "--url", f"http://localhost:{port}"],
|
|
162
|
+
stdout=open(log_path, "w"),
|
|
163
|
+
stderr=subprocess.STDOUT,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Wait for the public URL (up to 30s)
|
|
167
|
+
pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
|
168
|
+
deadline = time.time() + 30
|
|
169
|
+
url: str | None = None
|
|
170
|
+
while time.time() < deadline:
|
|
171
|
+
time.sleep(0.5)
|
|
172
|
+
try:
|
|
173
|
+
text = log_path.read_text(errors="replace")
|
|
174
|
+
m = pattern.search(text)
|
|
175
|
+
if m:
|
|
176
|
+
url = m.group(0)
|
|
177
|
+
break
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
if not url:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
# Telegram notification if configured
|
|
185
|
+
token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
|
|
186
|
+
chat_id = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
|
|
187
|
+
if token and chat_id:
|
|
188
|
+
msg = (
|
|
189
|
+
f"✅ Lattice AI 시작됨\n\n"
|
|
190
|
+
f"🌐 외부 URL: {url}\n"
|
|
191
|
+
f"🏠 로컬: http://localhost:{port}"
|
|
192
|
+
)
|
|
193
|
+
threading.Thread(target=_send_telegram, args=(token, chat_id, msg), daemon=True).start()
|
|
194
|
+
|
|
195
|
+
return url
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def main() -> None:
|
|
201
|
+
app_dir = Path(__file__).resolve().parent
|
|
202
|
+
_load_env_file(app_dir / ".env")
|
|
203
|
+
_apply_extra_path()
|
|
204
|
+
|
|
205
|
+
parser = argparse.ArgumentParser(prog="LTCAI", description="Run the Lattice AI local server.")
|
|
206
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
207
|
+
subparsers.add_parser("doctor", help="Check local runtime dependencies and configuration.")
|
|
208
|
+
parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "127.0.0.1")
|
|
209
|
+
parser.add_argument("--port", type=int, default=int(os.getenv("LATTICEAI_PORT") or "4825"))
|
|
210
|
+
parser.add_argument("--reload", action="store_true", help="Enable uvicorn reload for local development.")
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"--tunnel",
|
|
213
|
+
action="store_true",
|
|
214
|
+
help="Open a public Cloudflare tunnel so anyone can access this server from the internet.",
|
|
215
|
+
)
|
|
216
|
+
args = parser.parse_args()
|
|
217
|
+
|
|
218
|
+
if args.command == "doctor":
|
|
219
|
+
raise SystemExit(doctor())
|
|
220
|
+
|
|
221
|
+
os.chdir(app_dir)
|
|
222
|
+
|
|
223
|
+
if not args.tunnel and os.getenv("LATTICEAI_TUNNEL", "").lower() in (
|
|
224
|
+
"1",
|
|
225
|
+
"true",
|
|
226
|
+
"yes",
|
|
227
|
+
):
|
|
228
|
+
print(
|
|
229
|
+
" LATTICEAI_TUNNEL is ignored during default local startup; "
|
|
230
|
+
"restart with --tunnel to expose this server."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# --tunnel forces 0.0.0.0 so cloudflared can reach the server
|
|
234
|
+
if args.tunnel and args.host == "127.0.0.1":
|
|
235
|
+
args.host = "0.0.0.0"
|
|
236
|
+
os.environ.setdefault("LATTICEAI_HOST", "0.0.0.0")
|
|
237
|
+
os.environ.setdefault("LATTICEAI_CORS_ALLOW_NETWORK", "true")
|
|
238
|
+
os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "true")
|
|
239
|
+
|
|
240
|
+
# Keep the app config in sync with CLI flags. ``Config.from_env`` is the
|
|
241
|
+
# source of truth for /mode, /health.features, SSO defaults, and routers.
|
|
242
|
+
os.environ["LATTICEAI_HOST"] = str(args.host)
|
|
243
|
+
os.environ["LATTICEAI_PORT"] = str(args.port)
|
|
244
|
+
|
|
245
|
+
tunnel_url: str | None = None
|
|
246
|
+
if args.tunnel:
|
|
247
|
+
print()
|
|
248
|
+
print(" Starting Cloudflare tunnel...")
|
|
249
|
+
tunnel_url = _start_tunnel(args.port)
|
|
250
|
+
if not tunnel_url:
|
|
251
|
+
print(" ⚠️ Tunnel URL not obtained — server will start without tunnel.")
|
|
252
|
+
|
|
253
|
+
_print_banner(args.host, args.port, tunnel_url)
|
|
254
|
+
|
|
255
|
+
# Telegram startup notification (local start, tunnel handled separately inside _start_tunnel)
|
|
256
|
+
if not args.tunnel:
|
|
257
|
+
_tg_enabled = os.getenv("LATTICEAI_ENABLE_TELEGRAM", "").strip().lower() in ("1", "true", "yes", "on")
|
|
258
|
+
_tg_token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
|
|
259
|
+
_tg_chat = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
|
|
260
|
+
if _tg_enabled and _tg_token and _tg_chat:
|
|
261
|
+
_local_msg = (
|
|
262
|
+
f"✅ Lattice AI 시작됨\n\n"
|
|
263
|
+
f"🏠 로컬: http://localhost:{args.port}"
|
|
264
|
+
)
|
|
265
|
+
threading.Thread(
|
|
266
|
+
target=_send_telegram,
|
|
267
|
+
args=(_tg_token, _tg_chat, _local_msg),
|
|
268
|
+
daemon=True,
|
|
269
|
+
).start()
|
|
270
|
+
|
|
271
|
+
import uvicorn
|
|
272
|
+
|
|
273
|
+
uvicorn.run(
|
|
274
|
+
"server:app",
|
|
275
|
+
host=args.host,
|
|
276
|
+
port=args.port,
|
|
277
|
+
reload=args.reload,
|
|
278
|
+
log_level="info",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
main()
|
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
WORKSPACE_OS_VERSION = "6.
|
|
22
|
+
WORKSPACE_OS_VERSION = "6.3.0"
|
|
23
23
|
|
|
24
24
|
# Workspace types separate single-user Personal workspaces from shared
|
|
25
25
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
File without changes
|