ltcai 8.8.0 → 9.0.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 -37
- package/auto_setup.py +84 -70
- package/docs/CHANGELOG.md +113 -237
- package/docs/CODE_REVIEW_2026-07-06.md +764 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
- package/docs/DEVELOPMENT.md +10 -10
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -1
- package/docs/WHY_LATTICE.md +4 -3
- package/docs/architecture.md +4 -0
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -9
- package/lattice_brain/conversations.py +156 -21
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +39 -496
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +297 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +972 -0
- package/lattice_brain/graph/json_utils.py +25 -0
- package/lattice_brain/graph/retrieval.py +66 -597
- package/lattice_brain/graph/retrieval_docgen.py +210 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/runtime.py +16 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +68 -0
- package/lattice_brain/portability.py +1 -9
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +166 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/utils.py +28 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +368 -418
- package/latticeai/api/chat_helpers.py +227 -0
- package/latticeai/api/computer_use.py +149 -31
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +3 -2
- package/latticeai/api/models.py +4 -1
- package/latticeai/api/permissions.py +72 -33
- package/latticeai/api/setup.py +17 -2
- package/latticeai/api/tools.py +105 -62
- package/latticeai/app_factory.py +101 -296
- package/latticeai/core/agent.py +25 -7
- package/latticeai/core/io_utils.py +37 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/sessions.py +11 -3
- package/latticeai/core/tool_registry.py +15 -4
- package/latticeai/core/users.py +4 -9
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/core/workspace_os_utils.py +3 -17
- package/latticeai/integrations/telegram_bot.py +7 -2
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +58 -136
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +9 -0
- package/latticeai/runtime/bootstrap.py +1 -1
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/namespace_runtime.py +173 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/memory_service.py +213 -0
- package/latticeai/services/model_engines.py +79 -12
- package/latticeai/services/model_runtime.py +24 -4
- package/latticeai/services/platform_runtime.py +9 -1
- package/latticeai/services/process_audit.py +208 -0
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/review_queue.py +64 -11
- package/latticeai/services/run_executor.py +21 -0
- package/latticeai/services/search_service.py +106 -30
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +66 -0
- package/latticeai/services/workspace_service.py +15 -0
- package/package.json +1 -1
- package/scripts/check_i18n_literals.mjs +20 -8
- package/scripts/i18n_literal_allowlist.json +34 -0
- package/scripts/lint_frontend.mjs +6 -2
- package/setup_wizard.py +196 -74
- 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 +11 -11
- package/static/app/assets/{Act-C7K9wsO9.js → Act-21lIXx2E.js} +1 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
- package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-BA7Z2Q1u.js} +1 -1
- package/static/app/assets/{Library-Cgj-EF50.js → Library-bFMtyni3.js} +1 -1
- package/static/app/assets/System-K6krGCqn.js +1 -0
- package/static/app/assets/index-C4R3ws30.js +17 -0
- package/static/app/assets/index-ChSeOB02.css +2 -0
- package/static/app/assets/primitives-sQU3it5I.js +1 -0
- package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-DK3Fd_lR.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/local_files.py +6 -0
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/static/app/assets/Brain-I1OSzxJu.js +0 -321
- package/static/app/assets/System-D1Lkei3I.js +0 -1
- package/static/app/assets/index--P0ksosz.js +0 -17
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/primitives-BLqaKk5g.js +0 -1
|
@@ -271,6 +271,7 @@ class MemoryService:
|
|
|
271
271
|
memory_count = len(memory_ids) + len(snaps) + len(convs)
|
|
272
272
|
return {
|
|
273
273
|
"sources": sources,
|
|
274
|
+
"recent_memories": self._manager_recent_memories([*ws_mem, *project_mem], limit=8),
|
|
274
275
|
"tiers": list(TIERS),
|
|
275
276
|
"usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
|
|
276
277
|
"brain_readiness": self._brain_readiness(
|
|
@@ -284,6 +285,23 @@ class MemoryService:
|
|
|
284
285
|
"generated_at": _now(),
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _manager_recent_memories(memories: List[Dict[str, Any]], *, limit: int = 8) -> List[Dict[str, Any]]:
|
|
290
|
+
rows: List[Dict[str, Any]] = []
|
|
291
|
+
for item in memories[: max(1, limit)]:
|
|
292
|
+
metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
|
|
293
|
+
rows.append({
|
|
294
|
+
"id": item.get("id") or "",
|
|
295
|
+
"kind": item.get("kind") or "memory",
|
|
296
|
+
"content": str(item.get("content") or "")[:320],
|
|
297
|
+
"tags": item.get("tags") if isinstance(item.get("tags"), list) else [],
|
|
298
|
+
"metadata": metadata,
|
|
299
|
+
"workspace_id": item.get("workspace_id") or "personal",
|
|
300
|
+
"created_at": item.get("created_at"),
|
|
301
|
+
"updated_at": item.get("updated_at"),
|
|
302
|
+
})
|
|
303
|
+
return rows
|
|
304
|
+
|
|
287
305
|
def brain_quality_summary(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
288
306
|
"""Return the backend-owned Brain readiness signal for API consumers."""
|
|
289
307
|
return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
|
|
@@ -338,6 +356,22 @@ class MemoryService:
|
|
|
338
356
|
has_recall=bool(recall_items),
|
|
339
357
|
graph_concepts=graph_concepts,
|
|
340
358
|
)
|
|
359
|
+
suggested_questions = self._brain_brief_suggested_questions(
|
|
360
|
+
focus=focus,
|
|
361
|
+
has_durable_evidence=bool(proofs.get("has_durable_evidence")),
|
|
362
|
+
has_recall=bool(recall_items),
|
|
363
|
+
graph_concepts=graph_concepts,
|
|
364
|
+
conversations=conversations,
|
|
365
|
+
)
|
|
366
|
+
proactive_actions = self._brain_brief_proactive_actions(
|
|
367
|
+
focus=focus,
|
|
368
|
+
state=state,
|
|
369
|
+
has_durable_evidence=bool(proofs.get("has_durable_evidence")),
|
|
370
|
+
has_recall=bool(recall_items),
|
|
371
|
+
graph_concepts=graph_concepts,
|
|
372
|
+
vector_items=vector_items,
|
|
373
|
+
healthy_sources=healthy_sources,
|
|
374
|
+
)
|
|
341
375
|
return {
|
|
342
376
|
"status": state,
|
|
343
377
|
"score": int(readiness.get("score") or 0),
|
|
@@ -345,6 +379,8 @@ class MemoryService:
|
|
|
345
379
|
"body_key": f"brain.brief.body.{state if state in {'quiet', 'forming', 'alive'} else 'quiet'}",
|
|
346
380
|
"focus": focus,
|
|
347
381
|
"next_actions": actions,
|
|
382
|
+
"suggested_questions": suggested_questions,
|
|
383
|
+
"proactive_actions": proactive_actions,
|
|
348
384
|
"evidence": [
|
|
349
385
|
{
|
|
350
386
|
"id": "durable",
|
|
@@ -501,6 +537,183 @@ class MemoryService:
|
|
|
501
537
|
})
|
|
502
538
|
return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
503
539
|
|
|
540
|
+
@staticmethod
|
|
541
|
+
def _brain_brief_proactive_actions(
|
|
542
|
+
*,
|
|
543
|
+
focus: Dict[str, Any],
|
|
544
|
+
state: str,
|
|
545
|
+
has_durable_evidence: bool,
|
|
546
|
+
has_recall: bool,
|
|
547
|
+
graph_concepts: int,
|
|
548
|
+
vector_items: int,
|
|
549
|
+
healthy_sources: int,
|
|
550
|
+
) -> List[Dict[str, Any]]:
|
|
551
|
+
"""Return concrete, one-click actions Brain can proactively suggest."""
|
|
552
|
+
focus_title = str(focus.get("title") or "").strip() or "Brain"
|
|
553
|
+
focus_detail = str(focus.get("detail") or "").strip()
|
|
554
|
+
actions: List[Dict[str, Any]] = []
|
|
555
|
+
if not has_durable_evidence:
|
|
556
|
+
actions.append({
|
|
557
|
+
"id": "proactive_add_source",
|
|
558
|
+
"intent": "route",
|
|
559
|
+
"label_key": "brain.proactive.addSource.label",
|
|
560
|
+
"detail_key": "brain.proactive.addSource.detail",
|
|
561
|
+
"route": "/capture",
|
|
562
|
+
"prompt": "Add a useful source to my Brain and explain what it learned.",
|
|
563
|
+
"priority": 100,
|
|
564
|
+
})
|
|
565
|
+
actions.append({
|
|
566
|
+
"id": "proactive_seed_memory",
|
|
567
|
+
"intent": "ask",
|
|
568
|
+
"label_key": "brain.proactive.seed.label",
|
|
569
|
+
"detail_key": "brain.proactive.seed.detail",
|
|
570
|
+
"prompt": "Help me seed my Brain with the most useful personal context to remember.",
|
|
571
|
+
"priority": 90,
|
|
572
|
+
})
|
|
573
|
+
return actions
|
|
574
|
+
|
|
575
|
+
if has_recall:
|
|
576
|
+
actions.append({
|
|
577
|
+
"id": "proactive_evidence_review",
|
|
578
|
+
"intent": "ask",
|
|
579
|
+
"label_key": "brain.proactive.evidence.label",
|
|
580
|
+
"detail_key": "brain.proactive.evidence.detail",
|
|
581
|
+
"prompt": (
|
|
582
|
+
f"Review the evidence Brain has for {focus_title}. "
|
|
583
|
+
"Separate confirmed facts, weak signals, contradictions, and next checks."
|
|
584
|
+
),
|
|
585
|
+
"priority": 100,
|
|
586
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
587
|
+
})
|
|
588
|
+
actions.append({
|
|
589
|
+
"id": "proactive_delegate",
|
|
590
|
+
"intent": "delegate",
|
|
591
|
+
"label_key": "brain.proactive.delegate.label",
|
|
592
|
+
"detail_key": "brain.proactive.delegate.detail",
|
|
593
|
+
"prompt": (
|
|
594
|
+
f"Turn {focus_title} into an execution plan, verify the known context, "
|
|
595
|
+
"and return concrete next steps with risks."
|
|
596
|
+
),
|
|
597
|
+
"priority": 95,
|
|
598
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
599
|
+
})
|
|
600
|
+
actions.append({
|
|
601
|
+
"id": "proactive_review_draft",
|
|
602
|
+
"intent": "review",
|
|
603
|
+
"label_key": "brain.proactive.review.label",
|
|
604
|
+
"detail_key": "brain.proactive.review.detail",
|
|
605
|
+
"prompt": (
|
|
606
|
+
f"Create a reviewable task from Brain's current focus: {focus_title}. "
|
|
607
|
+
f"{focus_detail[:240]}"
|
|
608
|
+
).strip(),
|
|
609
|
+
"priority": 90,
|
|
610
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
if graph_concepts > 0:
|
|
614
|
+
actions.append({
|
|
615
|
+
"id": "proactive_map_connections",
|
|
616
|
+
"intent": "route",
|
|
617
|
+
"label_key": "brain.proactive.map.label",
|
|
618
|
+
"detail_key": "brain.proactive.map.detail",
|
|
619
|
+
"route": "/knowledge-graph",
|
|
620
|
+
"prompt": f"Map the strongest Knowledge Graph connections around {focus_title}.",
|
|
621
|
+
"priority": 82,
|
|
622
|
+
"context": {"focus": focus_title, "graph_concepts": graph_concepts},
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
if state == "alive" and vector_items > 0 and healthy_sources > 0:
|
|
626
|
+
actions.append({
|
|
627
|
+
"id": "proactive_weekly_brief",
|
|
628
|
+
"intent": "review",
|
|
629
|
+
"label_key": "brain.proactive.weekly.label",
|
|
630
|
+
"detail_key": "brain.proactive.weekly.detail",
|
|
631
|
+
"prompt": (
|
|
632
|
+
"Prepare a weekly Brain review: what changed, what decisions are pending, "
|
|
633
|
+
"what should be delegated, and what evidence is stale."
|
|
634
|
+
),
|
|
635
|
+
"priority": 78,
|
|
636
|
+
"context": {"vector_items": vector_items, "healthy_sources": healthy_sources},
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
640
|
+
|
|
641
|
+
@staticmethod
|
|
642
|
+
def _brain_brief_suggested_questions(
|
|
643
|
+
*,
|
|
644
|
+
focus: Dict[str, Any],
|
|
645
|
+
has_durable_evidence: bool,
|
|
646
|
+
has_recall: bool,
|
|
647
|
+
graph_concepts: int,
|
|
648
|
+
conversations: int,
|
|
649
|
+
) -> List[Dict[str, Any]]:
|
|
650
|
+
"""Return reusable, localized UI prompt descriptors for the Brain home."""
|
|
651
|
+
focus_title = str(focus.get("title") or "").strip()
|
|
652
|
+
focus_kind = str(focus.get("kind") or "empty")
|
|
653
|
+
questions: List[Dict[str, Any]] = []
|
|
654
|
+
|
|
655
|
+
if not has_durable_evidence or focus_kind == "empty":
|
|
656
|
+
questions.extend([
|
|
657
|
+
{
|
|
658
|
+
"id": "start_brain",
|
|
659
|
+
"label_key": "brain.suggestion.start.label",
|
|
660
|
+
"detail_key": "brain.suggestion.start.detail",
|
|
661
|
+
"prompt_key": "brain.suggestion.start.prompt",
|
|
662
|
+
"params": {},
|
|
663
|
+
"priority": 10,
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"id": "add_context",
|
|
667
|
+
"label_key": "brain.suggestion.context.label",
|
|
668
|
+
"detail_key": "brain.suggestion.context.detail",
|
|
669
|
+
"prompt_key": "brain.suggestion.context.prompt",
|
|
670
|
+
"params": {},
|
|
671
|
+
"priority": 9,
|
|
672
|
+
},
|
|
673
|
+
])
|
|
674
|
+
return questions
|
|
675
|
+
|
|
676
|
+
questions.append({
|
|
677
|
+
"id": "focus_next",
|
|
678
|
+
"label_key": "brain.suggestion.focus.label",
|
|
679
|
+
"detail_key": "brain.suggestion.focus.detail",
|
|
680
|
+
"prompt_key": "brain.suggestion.focus.prompt",
|
|
681
|
+
"params": {"focus": focus_title or "Brain"},
|
|
682
|
+
"priority": 10,
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
if has_recall:
|
|
686
|
+
questions.append({
|
|
687
|
+
"id": "evidence_check",
|
|
688
|
+
"label_key": "brain.suggestion.evidence.label",
|
|
689
|
+
"detail_key": "brain.suggestion.evidence.detail",
|
|
690
|
+
"prompt_key": "brain.suggestion.evidence.prompt",
|
|
691
|
+
"params": {"focus": focus_title or "this topic"},
|
|
692
|
+
"priority": 9,
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
if graph_concepts > 0:
|
|
696
|
+
questions.append({
|
|
697
|
+
"id": "graph_connections",
|
|
698
|
+
"label_key": "brain.suggestion.graph.label",
|
|
699
|
+
"detail_key": "brain.suggestion.graph.detail",
|
|
700
|
+
"prompt_key": "brain.suggestion.graph.prompt",
|
|
701
|
+
"params": {"focus": focus_title or "Knowledge Graph"},
|
|
702
|
+
"priority": 8,
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
if conversations > 0:
|
|
706
|
+
questions.append({
|
|
707
|
+
"id": "conversation_followup",
|
|
708
|
+
"label_key": "brain.suggestion.history.label",
|
|
709
|
+
"detail_key": "brain.suggestion.history.detail",
|
|
710
|
+
"prompt_key": "brain.suggestion.history.prompt",
|
|
711
|
+
"params": {"focus": focus_title or "recent conversations"},
|
|
712
|
+
"priority": 7,
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
return sorted(questions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
716
|
+
|
|
504
717
|
def brain_proof(
|
|
505
718
|
self,
|
|
506
719
|
*,
|
|
@@ -23,6 +23,13 @@ from typing import Any, Dict, List, Optional
|
|
|
23
23
|
|
|
24
24
|
from fastapi import HTTPException
|
|
25
25
|
|
|
26
|
+
from latticeai.services.process_audit import (
|
|
27
|
+
CommandConfirmationError,
|
|
28
|
+
append_process_audit_event,
|
|
29
|
+
command_plan,
|
|
30
|
+
require_command_confirmation,
|
|
31
|
+
)
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
def _progress_payload(*args, **kwargs) -> Dict[str, object]:
|
|
28
35
|
try:
|
|
@@ -440,8 +447,8 @@ def engine_support_status(engine: str) -> Dict[str, object]:
|
|
|
440
447
|
return {"supported": True, "reason": None}
|
|
441
448
|
|
|
442
449
|
|
|
443
|
-
def
|
|
444
|
-
from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS
|
|
450
|
+
def _engine_install_command(engine: str) -> tuple[list[str], str, bool]:
|
|
451
|
+
from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS
|
|
445
452
|
|
|
446
453
|
if engine not in ENGINE_INSTALLERS:
|
|
447
454
|
raise HTTPException(status_code=400, detail="지원하지 않는 엔진입니다.")
|
|
@@ -449,14 +456,7 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
449
456
|
required_binary = installer.get("requires_binary")
|
|
450
457
|
if required_binary and shutil.which(required_binary) is None:
|
|
451
458
|
raise HTTPException(status_code=400, detail=f"{required_binary}가 설치되어 있지 않아 자동 설치할 수 없습니다.")
|
|
452
|
-
command = installer["command"]
|
|
453
|
-
run_kwargs = {
|
|
454
|
-
"cwd": str(BASE_DIR),
|
|
455
|
-
"capture_output": True,
|
|
456
|
-
"text": True,
|
|
457
|
-
"timeout": 900,
|
|
458
|
-
"check": False,
|
|
459
|
-
}
|
|
459
|
+
command = list(installer["command"])
|
|
460
460
|
|
|
461
461
|
if engine == "vllm" and sys.platform == "darwin" and platform.machine() == "arm64":
|
|
462
462
|
command = [
|
|
@@ -468,13 +468,69 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
468
468
|
"~/.venv-vllm-metal/bin/pip install -U pip setuptools wheel; "
|
|
469
469
|
"~/.venv-vllm-metal/bin/pip install vllm-metal",
|
|
470
470
|
]
|
|
471
|
+
requires_admin = bool(command and command[0] in {"apt", "apt-get", "dnf", "pacman"})
|
|
472
|
+
return command, str(BASE_DIR), requires_admin
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def engine_install_plan(engine: str) -> Dict[str, Any]:
|
|
476
|
+
command, cwd, requires_admin = _engine_install_command(engine)
|
|
477
|
+
return command_plan(
|
|
478
|
+
command,
|
|
479
|
+
name=f"engine:{engine}",
|
|
480
|
+
purpose="engine_install",
|
|
481
|
+
cwd=cwd,
|
|
482
|
+
requires_admin=requires_admin,
|
|
483
|
+
metadata={"engine": engine},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict[str, Any]:
|
|
488
|
+
from latticeai.services.model_runtime import engine_installed
|
|
489
|
+
|
|
490
|
+
command, cwd, _requires_admin = _engine_install_command(engine)
|
|
491
|
+
plan = engine_install_plan(engine)
|
|
492
|
+
try:
|
|
493
|
+
require_command_confirmation(command, confirmation_token, cwd=cwd, purpose="engine_install")
|
|
494
|
+
except CommandConfirmationError as exc:
|
|
495
|
+
append_process_audit_event("engine_install", plan=plan, status="denied", error=str(exc))
|
|
496
|
+
raise HTTPException(
|
|
497
|
+
status_code=403,
|
|
498
|
+
detail={
|
|
499
|
+
"status": "confirmation_required",
|
|
500
|
+
"reason": str(exc),
|
|
501
|
+
"install_plan": plan,
|
|
502
|
+
},
|
|
503
|
+
) from exc
|
|
504
|
+
|
|
505
|
+
run_kwargs = {
|
|
506
|
+
"cwd": cwd,
|
|
507
|
+
"capture_output": True,
|
|
508
|
+
"text": True,
|
|
509
|
+
"timeout": 900,
|
|
510
|
+
"check": False,
|
|
511
|
+
}
|
|
471
512
|
try:
|
|
513
|
+
append_process_audit_event("engine_install", plan=plan, status="started")
|
|
472
514
|
completed = subprocess.run(command, **run_kwargs)
|
|
473
515
|
except subprocess.TimeoutExpired:
|
|
516
|
+
append_process_audit_event("engine_install", plan=plan, status="timeout")
|
|
474
517
|
raise HTTPException(status_code=408, detail="엔진 설치 시간이 초과되었습니다.")
|
|
518
|
+
except Exception as exc:
|
|
519
|
+
append_process_audit_event("engine_install", plan=plan, status="error", error=str(exc))
|
|
520
|
+
raise
|
|
521
|
+
append_process_audit_event(
|
|
522
|
+
"engine_install",
|
|
523
|
+
plan=plan,
|
|
524
|
+
status="finished",
|
|
525
|
+
returncode=completed.returncode,
|
|
526
|
+
stdout=completed.stdout,
|
|
527
|
+
stderr=completed.stderr,
|
|
528
|
+
)
|
|
475
529
|
result = {
|
|
476
530
|
"engine": engine,
|
|
477
|
-
"
|
|
531
|
+
"command_hash": plan["command_hash"],
|
|
532
|
+
"command_preview": plan["command_preview"],
|
|
533
|
+
"install_plan": plan,
|
|
478
534
|
"returncode": completed.returncode,
|
|
479
535
|
"stdout": completed.stdout[-12000:],
|
|
480
536
|
"stderr": completed.stderr[-12000:],
|
|
@@ -491,15 +547,25 @@ def install_engine(engine: str) -> Dict[str, Any]:
|
|
|
491
547
|
if already_up:
|
|
492
548
|
result["daemon_started"] = "already_running"
|
|
493
549
|
else:
|
|
550
|
+
daemon_command = [ollama, "serve"]
|
|
551
|
+
daemon_plan = command_plan(
|
|
552
|
+
daemon_command,
|
|
553
|
+
name="engine:ollama:serve",
|
|
554
|
+
purpose="engine_daemon_start",
|
|
555
|
+
metadata={"engine": "ollama"},
|
|
556
|
+
)
|
|
494
557
|
try:
|
|
558
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="started")
|
|
495
559
|
subprocess.Popen(
|
|
496
|
-
|
|
560
|
+
daemon_command,
|
|
497
561
|
stdout=subprocess.DEVNULL,
|
|
498
562
|
stderr=subprocess.DEVNULL,
|
|
499
563
|
start_new_session=True,
|
|
500
564
|
)
|
|
565
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="spawned")
|
|
501
566
|
result["daemon_started"] = True
|
|
502
567
|
except Exception as exc:
|
|
568
|
+
append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="error", error=str(exc))
|
|
503
569
|
logging.warning("ollama serve spawn failed: %s", exc)
|
|
504
570
|
result["daemon_started"] = False
|
|
505
571
|
return result
|
|
@@ -589,6 +655,7 @@ __all__ = [
|
|
|
589
655
|
"pull_ollama_model_with_progress",
|
|
590
656
|
"get_ollama_pulled_models",
|
|
591
657
|
"engine_support_status",
|
|
658
|
+
"engine_install_plan",
|
|
592
659
|
"install_engine",
|
|
593
660
|
"_smoke_test_loaded_model",
|
|
594
661
|
]
|
|
@@ -47,6 +47,7 @@ from .model_engines import (
|
|
|
47
47
|
pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
|
|
48
48
|
get_ollama_pulled_models as _get_ollama_pulled_models,
|
|
49
49
|
engine_support_status as _engine_support_status,
|
|
50
|
+
engine_install_plan as _engine_install_plan,
|
|
50
51
|
install_engine as _install_engine,
|
|
51
52
|
local_binary as _local_binary,
|
|
52
53
|
vllm_executable as _vllm_executable,
|
|
@@ -88,6 +89,8 @@ class ModelRuntimeState:
|
|
|
88
89
|
self.ENABLE_GRAPH = True
|
|
89
90
|
self.AUTOLOAD_MODELS = False
|
|
90
91
|
self.MODEL_IDLE_UNLOAD_SECONDS = 0
|
|
92
|
+
self.ALLOW_MODEL_DOWNLOADS = False
|
|
93
|
+
self.MODEL_DOWNLOAD_TIMEOUT = 300
|
|
91
94
|
self.ALLOW_LOCAL_MODELS = True
|
|
92
95
|
self.REQUIRE_AUTH = False
|
|
93
96
|
self.INVITE_GATE_ENABLED = False
|
|
@@ -118,7 +121,8 @@ class ModelRuntimeState:
|
|
|
118
121
|
"""Internal no-warning sync used at init."""
|
|
119
122
|
global router, APP_MODE, DEFAULT_HOST, DEFAULT_PORT, DATA_DIR, BASE_DIR
|
|
120
123
|
global ENABLE_TELEGRAM, ENABLE_GRAPH, AUTOLOAD_MODELS, MODEL_IDLE_UNLOAD_SECONDS
|
|
121
|
-
global
|
|
124
|
+
global ALLOW_MODEL_DOWNLOADS, MODEL_DOWNLOAD_TIMEOUT, ALLOW_LOCAL_MODELS, REQUIRE_AUTH
|
|
125
|
+
global INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
|
|
122
126
|
global CORS_ALLOW_NETWORK, PUBLIC_MODEL, LOCAL_MODEL, IS_PUBLIC_MODE
|
|
123
127
|
global keyring, get_current_user, get_user_api_key
|
|
124
128
|
router = self.router
|
|
@@ -131,6 +135,8 @@ class ModelRuntimeState:
|
|
|
131
135
|
ENABLE_GRAPH = self.ENABLE_GRAPH
|
|
132
136
|
AUTOLOAD_MODELS = self.AUTOLOAD_MODELS
|
|
133
137
|
MODEL_IDLE_UNLOAD_SECONDS = self.MODEL_IDLE_UNLOAD_SECONDS
|
|
138
|
+
ALLOW_MODEL_DOWNLOADS = self.ALLOW_MODEL_DOWNLOADS
|
|
139
|
+
MODEL_DOWNLOAD_TIMEOUT = self.MODEL_DOWNLOAD_TIMEOUT
|
|
134
140
|
ALLOW_LOCAL_MODELS = self.ALLOW_LOCAL_MODELS
|
|
135
141
|
REQUIRE_AUTH = self.REQUIRE_AUTH
|
|
136
142
|
INVITE_GATE_ENABLED = self.INVITE_GATE_ENABLED
|
|
@@ -159,7 +165,8 @@ def _env_bool(key: str, default: bool = False) -> bool:
|
|
|
159
165
|
def _download_allowed(allow_download: bool = False) -> bool:
|
|
160
166
|
# Prefer STATE (the source of truth) over bare module global for internal logic.
|
|
161
167
|
autoload = getattr(STATE, "AUTOLOAD_MODELS", AUTOLOAD_MODELS)
|
|
162
|
-
|
|
168
|
+
configured = getattr(STATE, "ALLOW_MODEL_DOWNLOADS", ALLOW_MODEL_DOWNLOADS)
|
|
169
|
+
return bool(allow_download) or bool(configured) or bool(autoload)
|
|
163
170
|
|
|
164
171
|
|
|
165
172
|
def _download_block(provider: str, model_name: str) -> None:
|
|
@@ -726,6 +733,13 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
726
733
|
return _ensure_llamacpp_server(model_name)
|
|
727
734
|
|
|
728
735
|
|
|
736
|
+
def _safe_engine_install_plan(engine: str) -> Optional[Dict[str, object]]:
|
|
737
|
+
try:
|
|
738
|
+
return _engine_install_plan(engine)
|
|
739
|
+
except Exception:
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
|
|
729
743
|
def engine_installed(engine: str) -> bool:
|
|
730
744
|
if engine == "local_mlx":
|
|
731
745
|
return bool(
|
|
@@ -847,6 +861,7 @@ def engine_status() -> List[Dict]:
|
|
|
847
861
|
"installed": engine_installed("local_mlx"),
|
|
848
862
|
"installable": True,
|
|
849
863
|
"install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
|
|
864
|
+
"install_plan": _safe_engine_install_plan("local_mlx"),
|
|
850
865
|
"models": mlx_models,
|
|
851
866
|
},
|
|
852
867
|
{
|
|
@@ -857,6 +872,7 @@ def engine_status() -> List[Dict]:
|
|
|
857
872
|
"installed": ollama_installed,
|
|
858
873
|
"installable": True,
|
|
859
874
|
"install_label": ENGINE_INSTALLERS["ollama"]["label"],
|
|
875
|
+
"install_plan": _safe_engine_install_plan("ollama"),
|
|
860
876
|
"models": ollama_models,
|
|
861
877
|
},
|
|
862
878
|
]
|
|
@@ -872,6 +888,7 @@ def engine_status() -> List[Dict]:
|
|
|
872
888
|
"support_reason": support["reason"],
|
|
873
889
|
"installable": support["supported"] and spec["id"] in ENGINE_INSTALLERS,
|
|
874
890
|
"install_label": ENGINE_INSTALLERS.get(spec["id"], {}).get("label"),
|
|
891
|
+
"install_plan": _safe_engine_install_plan(spec["id"]) if spec["id"] in ENGINE_INSTALLERS else None,
|
|
875
892
|
"requires": spec["requires"],
|
|
876
893
|
"models": (
|
|
877
894
|
vllm_models if spec["id"] == "vllm"
|
|
@@ -900,6 +917,7 @@ def engine_status() -> List[Dict]:
|
|
|
900
917
|
"installed": engine_installed(provider),
|
|
901
918
|
"installable": True,
|
|
902
919
|
"install_label": ENGINE_INSTALLERS[provider]["label"],
|
|
920
|
+
"install_plan": _safe_engine_install_plan(provider),
|
|
903
921
|
"requires": env_key,
|
|
904
922
|
"models": provider_models,
|
|
905
923
|
})
|
|
@@ -920,6 +938,8 @@ def runtime_features() -> Dict:
|
|
|
920
938
|
"graph_enabled": s.ENABLE_GRAPH,
|
|
921
939
|
"autoload_models": s.AUTOLOAD_MODELS,
|
|
922
940
|
"model_idle_unload_seconds": s.MODEL_IDLE_UNLOAD_SECONDS,
|
|
941
|
+
"allow_model_downloads": s.ALLOW_MODEL_DOWNLOADS,
|
|
942
|
+
"model_download_timeout": s.MODEL_DOWNLOAD_TIMEOUT,
|
|
923
943
|
"model_memory_policy": r.model_memory_policy() if r else None,
|
|
924
944
|
"allow_local_models": s.ALLOW_LOCAL_MODELS,
|
|
925
945
|
"security": {
|
|
@@ -944,8 +964,8 @@ def runtime_features() -> Dict:
|
|
|
944
964
|
},
|
|
945
965
|
}
|
|
946
966
|
|
|
947
|
-
def install_engine(engine: str) -> Dict:
|
|
948
|
-
return _install_engine(engine)
|
|
967
|
+
def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict:
|
|
968
|
+
return _install_engine(engine, confirmation_token=confirmation_token)
|
|
949
969
|
|
|
950
970
|
|
|
951
971
|
def _resolve_model_alias(model_id: str, engine: Optional[str] = None) -> str:
|
|
@@ -144,7 +144,15 @@ class PlatformRuntime:
|
|
|
144
144
|
def provider(goal: str):
|
|
145
145
|
try:
|
|
146
146
|
mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
|
|
147
|
-
|
|
147
|
+
ctx = [str(m.get("content") or "")[:180] for m in mems[:6]]
|
|
148
|
+
# Enrich with agent-synthesis memories preferentially for self-improving Brain feel
|
|
149
|
+
try:
|
|
150
|
+
allm = self.store.list_memories(user_email=user, workspace_id=scope).get("memories", [])
|
|
151
|
+
synth = [str(m.get("content") or "")[:160] for m in allm if "agent-synthesis" in (m.get("tags") or [])][:3]
|
|
152
|
+
ctx = synth + ctx
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
return ctx[:8]
|
|
148
156
|
except Exception:
|
|
149
157
|
return []
|
|
150
158
|
return provider
|