ltcai 6.1.0 → 6.2.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 +33 -38
- package/docs/CHANGELOG.md +33 -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/ProductFlowScreens.tsx +688 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainHome.tsx +999 -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/i18n.ts +198 -0
- 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 +36 -45
- 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/lifespan_runtime.py +1 -1
- package/latticeai/runtime/platform_runtime_wiring.py +87 -0
- package/latticeai/runtime/router_registration.py +49 -30
- 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 +2 -1
- 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-B744yblP.css → index-B2-1Gm0q.css} +1 -1
- package/static/app/assets/index-D91Rz5--.js +16 -0
- package/static/app/assets/index-D91Rz5--.js.map +1 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1002
- package/static/app/assets/index-DYaUKNfl.js +0 -16
- package/static/app/assets/index-DYaUKNfl.js.map +0 -1
package/latticeai/api/tools.py
CHANGED
|
@@ -26,7 +26,8 @@ from latticeai.services.tool_dispatch import (
|
|
|
26
26
|
get_tool_permission,
|
|
27
27
|
list_tool_permissions,
|
|
28
28
|
)
|
|
29
|
-
from
|
|
29
|
+
from latticeai.services.router_context import ToolRouterContext
|
|
30
|
+
from latticeai.services.p_reinforce import BRAIN_DIR
|
|
30
31
|
from tools import (
|
|
31
32
|
AGENT_ROOT,
|
|
32
33
|
ToolError,
|
|
@@ -177,30 +178,56 @@ class ToolGitShowRequest(BaseModel):
|
|
|
177
178
|
|
|
178
179
|
def create_tools_router(
|
|
179
180
|
*,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
181
|
+
tool_context: ToolRouterContext | None = None,
|
|
182
|
+
config=None,
|
|
183
|
+
ingestion_pipeline=None,
|
|
184
|
+
data_dir: Path | None = None,
|
|
185
|
+
static_dir: Path | None = None,
|
|
186
|
+
model_router=None,
|
|
187
|
+
require_user=None,
|
|
188
|
+
require_admin=None,
|
|
189
|
+
get_current_user=None,
|
|
190
|
+
clear_history=None,
|
|
191
|
+
append_audit_event=None,
|
|
192
|
+
enforce_rate_limit=None,
|
|
193
|
+
bytes_match_extension=None,
|
|
194
|
+
classify_sensitive_message=None,
|
|
195
|
+
save_to_history=None,
|
|
196
|
+
enable_graph: bool | None = None,
|
|
197
|
+
knowledge_graph=None,
|
|
198
|
+
require_graph=None,
|
|
199
|
+
local_kg_watcher=None,
|
|
200
|
+
load_mcp_installs=None,
|
|
201
|
+
recommend_mcps=None,
|
|
202
|
+
install_mcp=None,
|
|
203
|
+
mcp_public_item=None,
|
|
202
204
|
hooks=None,
|
|
203
205
|
) -> APIRouter:
|
|
206
|
+
if tool_context is not None:
|
|
207
|
+
config = tool_context.config
|
|
208
|
+
ingestion_pipeline = tool_context.ingestion_pipeline
|
|
209
|
+
data_dir = tool_context.data_dir
|
|
210
|
+
static_dir = tool_context.static_dir
|
|
211
|
+
model_router = tool_context.model_router
|
|
212
|
+
require_user = tool_context.require_user
|
|
213
|
+
require_admin = tool_context.require_admin
|
|
214
|
+
get_current_user = tool_context.get_current_user
|
|
215
|
+
clear_history = tool_context.clear_history
|
|
216
|
+
append_audit_event = tool_context.append_audit_event
|
|
217
|
+
enforce_rate_limit = tool_context.enforce_rate_limit
|
|
218
|
+
bytes_match_extension = tool_context.bytes_match_extension
|
|
219
|
+
classify_sensitive_message = tool_context.classify_sensitive_message
|
|
220
|
+
save_to_history = tool_context.save_to_history
|
|
221
|
+
enable_graph = tool_context.enable_graph
|
|
222
|
+
knowledge_graph = tool_context.knowledge_graph
|
|
223
|
+
require_graph = tool_context.require_graph
|
|
224
|
+
local_kg_watcher = tool_context.local_kg_watcher
|
|
225
|
+
load_mcp_installs = tool_context.load_mcp_installs
|
|
226
|
+
recommend_mcps = tool_context.recommend_mcps
|
|
227
|
+
install_mcp = tool_context.install_mcp
|
|
228
|
+
mcp_public_item = tool_context.mcp_public_item
|
|
229
|
+
hooks = tool_context.hooks
|
|
230
|
+
|
|
204
231
|
api_router = APIRouter()
|
|
205
232
|
HOOKS = hooks
|
|
206
233
|
CONFIG = config
|
package/latticeai/app_factory.py
CHANGED
|
@@ -17,7 +17,6 @@ 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
|
|
@@ -33,6 +32,7 @@ from latticeai.runtime.platform_services_runtime import (
|
|
|
33
32
|
build_brain_network,
|
|
34
33
|
build_model_service,
|
|
35
34
|
)
|
|
35
|
+
from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
|
|
36
36
|
from latticeai.runtime.persistence_runtime import build_persistence_runtime
|
|
37
37
|
from latticeai.runtime.router_registration import (
|
|
38
38
|
build_auth_admin_security_router_bundle,
|
|
@@ -45,6 +45,7 @@ from latticeai.runtime.router_registration import (
|
|
|
45
45
|
)
|
|
46
46
|
from latticeai.runtime.security_runtime import build_security_runtime
|
|
47
47
|
from latticeai.runtime.web_runtime import build_web_runtime
|
|
48
|
+
from latticeai.services.router_context import InteractionRouterContext, ToolRouterContext
|
|
48
49
|
|
|
49
50
|
if TYPE_CHECKING: # imports for annotations only — keep module import light
|
|
50
51
|
from fastapi import FastAPI
|
|
@@ -145,7 +146,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
145
146
|
from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
|
|
146
147
|
from latticeai.api.health import create_health_router
|
|
147
148
|
# ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
|
|
148
|
-
from latticeai.services.platform_runtime import PlatformRuntime
|
|
149
149
|
from latticeai.api.plugins import create_plugins_router
|
|
150
150
|
from latticeai.api.workflow_designer import create_workflow_designer_router
|
|
151
151
|
from latticeai.api.agents import create_agents_router
|
|
@@ -189,7 +189,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
189
189
|
_get_combined_registry,
|
|
190
190
|
_fetch_skills_marketplace, install_skill, SKILLS_DIR,
|
|
191
191
|
)
|
|
192
|
-
from p_reinforce import PReinforceGardener
|
|
192
|
+
from latticeai.services.p_reinforce import PReinforceGardener
|
|
193
193
|
from setup_wizard import get_recommendations, scan_environment
|
|
194
194
|
from tools import ensure_agent_root, execute_tool, knowledge_save
|
|
195
195
|
|
|
@@ -1239,7 +1239,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1239
1239
|
on_chat_message = None
|
|
1240
1240
|
if ENABLE_TELEGRAM:
|
|
1241
1241
|
def _telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
|
|
1242
|
-
from telegram_bot import broadcast_web_chat
|
|
1242
|
+
from latticeai.integrations.telegram_bot import broadcast_web_chat
|
|
1243
1243
|
_spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
|
|
1244
1244
|
on_chat_message = _telegram_chat_mirror
|
|
1245
1245
|
|
|
@@ -1338,20 +1338,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1338
1338
|
|
|
1339
1339
|
|
|
1340
1340
|
# ── 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,
|
|
1341
|
+
_platform_automation_runtime = build_platform_automation_runtime(
|
|
1342
|
+
model_router=router,
|
|
1343
|
+
workspace_store=WORKSPACE_OS,
|
|
1355
1344
|
workspace_service=WORKSPACE_SERVICE,
|
|
1356
1345
|
plugin_registry=PLUGIN_REGISTRY,
|
|
1357
1346
|
get_current_user=get_current_user,
|
|
@@ -1359,23 +1348,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1359
1348
|
workspace_scope_from_request=_workspace_scope_from_request,
|
|
1360
1349
|
get_tool_permission=get_tool_permission,
|
|
1361
1350
|
hooks=HOOKS_REGISTRY,
|
|
1362
|
-
llm_generate=_llm_generate_sync,
|
|
1363
|
-
llm_available=lambda: bool(getattr(router, "current_model_id", None)),
|
|
1364
1351
|
agent_registry=AGENT_REGISTRY,
|
|
1365
|
-
)
|
|
1366
|
-
|
|
1367
|
-
_automation_runtime = build_automation_runtime(
|
|
1368
|
-
store=WORKSPACE_OS,
|
|
1369
|
-
platform=PLATFORM,
|
|
1370
1352
|
data_dir=DATA_DIR,
|
|
1371
|
-
workspace_graph=_workspace_graph,
|
|
1372
1353
|
append_audit_event=append_audit_event,
|
|
1373
|
-
hooks=HOOKS_REGISTRY,
|
|
1374
1354
|
)
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1355
|
+
_llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
|
|
1356
|
+
PLATFORM = _platform_automation_runtime["PLATFORM"]
|
|
1357
|
+
_automation_runtime = _platform_automation_runtime["_automation_runtime"]
|
|
1358
|
+
REVIEW_QUEUE = _platform_automation_runtime["REVIEW_QUEUE"]
|
|
1359
|
+
TRIGGER_SERVICE = _platform_automation_runtime["TRIGGER_SERVICE"]
|
|
1360
|
+
AGENT_RUNTIME = _platform_automation_runtime["AGENT_RUNTIME"]
|
|
1361
|
+
RUN_EXECUTOR = _platform_automation_runtime["RUN_EXECUTOR"]
|
|
1379
1362
|
bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
|
|
1380
1363
|
app.state.run_executor = RUN_EXECUTOR
|
|
1381
1364
|
app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
|
|
@@ -1478,21 +1461,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1478
1461
|
return None
|
|
1479
1462
|
return PLATFORM.allowed_scopes(user)
|
|
1480
1463
|
|
|
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,
|
|
1464
|
+
tool_router_context = ToolRouterContext(
|
|
1492
1465
|
config=CONFIG,
|
|
1466
|
+
ingestion_pipeline=INGESTION_PIPELINE,
|
|
1493
1467
|
data_dir=DATA_DIR,
|
|
1494
1468
|
static_dir=STATIC_DIR,
|
|
1495
1469
|
model_router=router,
|
|
1470
|
+
require_user=require_user,
|
|
1496
1471
|
require_admin=require_admin,
|
|
1497
1472
|
get_current_user=get_current_user,
|
|
1498
1473
|
clear_history=clear_history,
|
|
@@ -1510,13 +1485,29 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1510
1485
|
install_mcp=install_mcp,
|
|
1511
1486
|
mcp_public_item=mcp_public_item,
|
|
1512
1487
|
hooks=HOOKS_REGISTRY,
|
|
1513
|
-
|
|
1514
|
-
|
|
1488
|
+
)
|
|
1489
|
+
interaction_router_context = InteractionRouterContext(
|
|
1490
|
+
chat_context=context,
|
|
1491
|
+
search_service=SEARCH_SERVICE,
|
|
1492
|
+
allowed_workspaces_for=_allowed_workspaces_for,
|
|
1493
|
+
require_user=require_user,
|
|
1494
|
+
embedding_info=_embedding_info,
|
|
1495
|
+
tool_context=tool_router_context,
|
|
1496
|
+
hooks=HOOKS_REGISTRY,
|
|
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
|
|
1522
1513
|
|
|
@@ -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.2.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
|