ltcai 5.2.0 → 5.4.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 +144 -164
- package/docs/CHANGELOG.md +63 -0
- package/docs/DEVELOPMENT.md +99 -0
- package/docs/LEGACY_COMPATIBILITY.md +55 -0
- package/docs/WHY_LATTICE.md +4 -3
- package/frontend/src/App.tsx +8 -2
- package/frontend/src/api/client.ts +2 -0
- package/frontend/src/components/FirstRunGuide.tsx +5 -5
- package/frontend/src/components/ProductFlow.tsx +1 -1
- package/frontend/src/i18n.ts +40 -40
- package/frontend/src/pages/Act.tsx +82 -1
- package/frontend/src/pages/Library.tsx +18 -6
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +12 -0
- package/lattice_brain/portability.py +14 -0
- package/lattice_brain/runtime/__init__.py +53 -0
- package/lattice_brain/runtime/agent_runtime.py +7 -0
- package/lattice_brain/runtime/hooks.py +6 -0
- package/lattice_brain/runtime/multi_agent.py +7 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/workflow_designer.py +60 -0
- package/latticeai/app_factory.py +5 -78
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/__init__.py +2 -0
- package/latticeai/runtime/brain_runtime.py +41 -0
- package/latticeai/runtime/config_runtime.py +36 -0
- package/latticeai/runtime/security_runtime.py +27 -0
- package/latticeai/services/brain_automation.py +214 -0
- package/latticeai/services/model_capability_registry.py +2 -3
- package/latticeai/services/triggers.py +61 -4
- package/package.json +2 -2
- package/scripts/verify_hf_model_registry.py +1 -3
- 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-C7vzwUjU.js +16 -0
- package/static/app/assets/index-C7vzwUjU.js.map +1 -0
- package/static/app/assets/index-HN4f2wbe.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-CQmHhk8Q.css +0 -2
- package/static/app/assets/index-DsnfomFs.js +0 -16
- package/static/app/assets/index-DsnfomFs.js.map +0 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
"""AgentRuntime — the single boundary for agent execution and observability.
|
|
2
2
|
|
|
3
|
+
(lattice_brain/runtime/agent_runtime.py)
|
|
4
|
+
책임: 퍼사드. store/orchestrator/hooks/audit 주입 받아 start/reserve/complete,
|
|
5
|
+
status/health/config/events/replay/stop, pre/post_run hook firing.
|
|
6
|
+
RunExecutor와 /agents 라우터의 유일한 의존 대상.
|
|
7
|
+
의존: .multi_agent (orchestrator), .hooks, store (WORKSPACE_OS).
|
|
8
|
+
진입점: app_factory.py:AGENT_RUNTIME (wiring root), api/agents.py, RunExecutor.
|
|
9
|
+
|
|
3
10
|
Before this module the agent concern was spread across three places: the
|
|
4
11
|
:class:`~latticeai.core.multi_agent.MultiAgentOrchestrator` (role pipeline),
|
|
5
12
|
the :class:`~latticeai.services.platform_runtime.PlatformRuntime` (cross-system
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Hooks platform — a persisted registry of lifecycle extension points.
|
|
2
2
|
|
|
3
|
+
(lattice_brain/runtime/hooks.py)
|
|
4
|
+
책임: HookContext/Result, HooksRegistry (persist+order+enable+builtin), dispatch_tool,
|
|
5
|
+
fire_hook (pre/post_run/tool/workflow), BUILTIN_HOOKS 등록.
|
|
6
|
+
의존성: threading, subprocess (user hooks). dispatch_tool은 tool path 단일화.
|
|
7
|
+
상위: agent_runtime.py (pre/post_run), tool_dispatch.py, api/tools, core/agent (shared).
|
|
8
|
+
|
|
3
9
|
Lattice AI runs several behaviours at well-defined points in the agent / tool /
|
|
4
10
|
workflow lifecycle (audit logging, secret redaction, sensitive-data
|
|
5
11
|
classification, tool-permission gating, memory snapshots, workflow replay
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"""Multi-Agent Runtime 2.1.
|
|
1
|
+
"""Multi-Agent Runtime 2.1. (lattice_brain/runtime/multi_agent.py)
|
|
2
|
+
|
|
3
|
+
책임: 역할 파이프라인 실행, handoff/context_packet/timeline/review/retry 기록 생성,
|
|
4
|
+
OrchestrationContext, default/llm_role_runner, MultiAgentOrchestrator.
|
|
5
|
+
의존성: 없음 (순수). runner 주입으로 simulation vs llm 분리.
|
|
6
|
+
상위 호출자: agent_runtime.py (orchestrator_factory), platform_runtime.
|
|
2
7
|
|
|
3
8
|
The runtime remains a small, dependency-injected orchestrator, but v2.1 makes
|
|
4
9
|
the operational objects first-class: handoffs, context packets, review/retry
|
|
@@ -14,7 +19,7 @@ from datetime import datetime
|
|
|
14
19
|
from typing import Any, Callable, Dict, List, Optional
|
|
15
20
|
|
|
16
21
|
|
|
17
|
-
MULTI_AGENT_VERSION = "5.
|
|
22
|
+
MULTI_AGENT_VERSION = "5.4.0"
|
|
18
23
|
|
|
19
24
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
25
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
|
@@ -51,6 +51,10 @@ class WorkflowImportRequest(BaseModel):
|
|
|
51
51
|
data: Dict[str, Any] = {}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class WorkflowRecipeInstallRequest(BaseModel):
|
|
55
|
+
enabled: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
54
58
|
def create_workflow_designer_router(
|
|
55
59
|
*,
|
|
56
60
|
store,
|
|
@@ -254,6 +258,62 @@ def create_workflow_designer_router(
|
|
|
254
258
|
return {"running": False, "tick_seconds": None, "armed": []}
|
|
255
259
|
return trigger_service.describe()
|
|
256
260
|
|
|
261
|
+
@router.get("/workflows/api/automation/recipes")
|
|
262
|
+
async def automation_recipes(request: Request):
|
|
263
|
+
require_user(request)
|
|
264
|
+
from latticeai.services.brain_automation import list_brain_automation_recipes
|
|
265
|
+
|
|
266
|
+
return list_brain_automation_recipes()
|
|
267
|
+
|
|
268
|
+
@router.post("/workflows/api/automation/recipes/{recipe_id}")
|
|
269
|
+
async def install_automation_recipe(recipe_id: str, req: WorkflowRecipeInstallRequest, request: Request):
|
|
270
|
+
current_user = require_user(request)
|
|
271
|
+
scope = gate_write(request)
|
|
272
|
+
from latticeai.services.brain_automation import (
|
|
273
|
+
build_brain_automation_workflow,
|
|
274
|
+
find_installed_recipe_workflow,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
definition = build_brain_automation_workflow(recipe_id, enabled=req.enabled)
|
|
279
|
+
except KeyError as exc:
|
|
280
|
+
raise HTTPException(status_code=404, detail=f"Automation recipe not found: {recipe_id}") from exc
|
|
281
|
+
existing = find_installed_recipe_workflow(
|
|
282
|
+
store.list_workflows(workspace_id=scope).get("workflows"), recipe_id
|
|
283
|
+
)
|
|
284
|
+
if existing is not None:
|
|
285
|
+
return {
|
|
286
|
+
"workflow": existing,
|
|
287
|
+
"recipe": existing.get("metadata") or definition["metadata"],
|
|
288
|
+
"enabled": bool((existing.get("metadata") or {}).get("automation_state") == "enabled"),
|
|
289
|
+
"already_installed": True,
|
|
290
|
+
}
|
|
291
|
+
errors = validate_definition({"name": definition["name"], "nodes": definition["nodes"]})
|
|
292
|
+
if errors:
|
|
293
|
+
raise HTTPException(status_code=400, detail={"validation_errors": errors})
|
|
294
|
+
workflow = store.create_workflow(
|
|
295
|
+
name=definition["name"],
|
|
296
|
+
steps=[{"action": n.get("type"), "node": n.get("id")} for n in definition["nodes"]],
|
|
297
|
+
nodes=definition["nodes"],
|
|
298
|
+
metadata=definition["metadata"],
|
|
299
|
+
user_email=current_user or None,
|
|
300
|
+
graph=workspace_graph(),
|
|
301
|
+
workspace_id=scope,
|
|
302
|
+
)
|
|
303
|
+
append_audit_event(
|
|
304
|
+
"brain_automation_recipe_installed",
|
|
305
|
+
user_email=current_user,
|
|
306
|
+
workflow_id=workflow["id"],
|
|
307
|
+
recipe_id=recipe_id,
|
|
308
|
+
enabled=bool(req.enabled),
|
|
309
|
+
)
|
|
310
|
+
return {
|
|
311
|
+
"workflow": workflow,
|
|
312
|
+
"recipe": definition["metadata"],
|
|
313
|
+
"enabled": bool(req.enabled),
|
|
314
|
+
"already_installed": False,
|
|
315
|
+
}
|
|
316
|
+
|
|
257
317
|
@router.get("/workflows/api/runs/{run_id}/replay")
|
|
258
318
|
async def workflow_run_replay(run_id: str, request: Request):
|
|
259
319
|
require_user(request)
|
package/latticeai/app_factory.py
CHANGED
|
@@ -17,90 +17,16 @@ 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.brain_runtime import build_brain_runtime
|
|
21
|
+
from latticeai.runtime.config_runtime import build_config_runtime
|
|
22
|
+
from latticeai.runtime.security_runtime import build_security_runtime
|
|
23
|
+
|
|
20
24
|
if TYPE_CHECKING: # imports for annotations only — keep module import light
|
|
21
25
|
from fastapi import FastAPI
|
|
22
26
|
|
|
23
27
|
from latticeai.core.config import Config
|
|
24
28
|
|
|
25
29
|
|
|
26
|
-
def build_config_runtime(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
27
|
-
"""Build the app configuration runtime without importing heavy model code."""
|
|
28
|
-
|
|
29
|
-
from latticeai.core.config import Config
|
|
30
|
-
|
|
31
|
-
cfg = config if config is not None else Config.from_env()
|
|
32
|
-
return {
|
|
33
|
-
"CONFIG": cfg,
|
|
34
|
-
"APP_MODE": cfg.app_mode,
|
|
35
|
-
"IS_PUBLIC_MODE": cfg.is_public,
|
|
36
|
-
"DEFAULT_HOST": cfg.host,
|
|
37
|
-
"DEFAULT_PORT": cfg.port,
|
|
38
|
-
"NETWORK_EXPOSED": cfg.network_exposed,
|
|
39
|
-
"ENABLE_TELEGRAM": cfg.enable_telegram,
|
|
40
|
-
"ENABLE_GRAPH": cfg.enable_graph,
|
|
41
|
-
"AUTOLOAD_MODELS": cfg.autoload_models,
|
|
42
|
-
"MODEL_IDLE_UNLOAD_SECONDS": cfg.model_idle_unload_seconds,
|
|
43
|
-
"ALLOW_LOCAL_MODELS": cfg.allow_local_models,
|
|
44
|
-
"REQUIRE_AUTH": cfg.require_auth,
|
|
45
|
-
"ALLOW_PLAINTEXT_API_KEYS": cfg.allow_plaintext_api_keys,
|
|
46
|
-
"CORS_ALLOW_NETWORK": cfg.cors_allow_network,
|
|
47
|
-
"CORS_EXTRA_ORIGINS": cfg.cors_extra_origins,
|
|
48
|
-
"PUBLIC_MODEL": cfg.public_model,
|
|
49
|
-
"LOCAL_MODEL": cfg.local_model,
|
|
50
|
-
"LOCAL_DRAFT_MODEL": cfg.local_draft_model,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def build_security_runtime(config: "Config") -> Dict[str, Any]:
|
|
55
|
-
"""Build auth/security-derived runtime settings from the central config."""
|
|
56
|
-
|
|
57
|
-
from latticeai.core.security import configure_trusted_proxies
|
|
58
|
-
|
|
59
|
-
configure_trusted_proxies(config.trusted_proxies)
|
|
60
|
-
return {
|
|
61
|
-
"SSO_DISCOVERY_URL": config.sso_discovery_url,
|
|
62
|
-
"SSO_CLIENT_ID": config.sso_client_id,
|
|
63
|
-
"SSO_CLIENT_SECRET": config.sso_client_secret,
|
|
64
|
-
"SSO_REDIRECT_URI": config.sso_redirect_uri,
|
|
65
|
-
"SSO_PROVIDER_NAME": config.sso_provider_name,
|
|
66
|
-
"RATE_LIMIT_ENABLED": config.rate_limit_enabled,
|
|
67
|
-
"OPEN_REGISTRATION": config.open_registration,
|
|
68
|
-
"INVITE_CODE": config.invite_code,
|
|
69
|
-
"INVITE_GATE_ENABLED": config.invite_gate_enabled,
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def build_brain_runtime(
|
|
74
|
-
*,
|
|
75
|
-
data_dir: Any,
|
|
76
|
-
history_file: Any,
|
|
77
|
-
enable_graph: bool,
|
|
78
|
-
embedder: Any,
|
|
79
|
-
storage_engine: Any,
|
|
80
|
-
) -> Dict[str, Any]:
|
|
81
|
-
"""Construct Brain Core storage/conversation primitives behind one seam."""
|
|
82
|
-
|
|
83
|
-
from lattice_brain import BrainCore, ConversationStore
|
|
84
|
-
|
|
85
|
-
brain_core = BrainCore.from_paths(
|
|
86
|
-
data_dir,
|
|
87
|
-
embedder=embedder.provider,
|
|
88
|
-
storage_engine=storage_engine,
|
|
89
|
-
) if enable_graph else None
|
|
90
|
-
knowledge_graph = brain_core.knowledge if brain_core is not None else None
|
|
91
|
-
conversations = (
|
|
92
|
-
brain_core.conversations
|
|
93
|
-
if brain_core is not None
|
|
94
|
-
else ConversationStore(data_dir / "knowledge_graph.sqlite")
|
|
95
|
-
)
|
|
96
|
-
conversations.import_legacy_json(history_file)
|
|
97
|
-
return {
|
|
98
|
-
"BRAIN_CORE": brain_core,
|
|
99
|
-
"KNOWLEDGE_GRAPH": knowledge_graph,
|
|
100
|
-
"CONVERSATIONS": conversations,
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
30
|
def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
105
31
|
"""The legacy ``server_app`` assembly, moved verbatim into function scope.
|
|
106
32
|
|
|
@@ -1579,6 +1505,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1579
1505
|
HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
|
|
1580
1506
|
|
|
1581
1507
|
# Single AgentRuntime boundary over the orchestrator + run store.
|
|
1508
|
+
# (lattice_brain/runtime.agent_runtime.AgentRuntime — see runtime/__init__.py for full dep graph + entry mapping)
|
|
1582
1509
|
AGENT_RUNTIME = AgentRuntime(
|
|
1583
1510
|
store=WORKSPACE_OS,
|
|
1584
1511
|
orchestrator_factory=PLATFORM.build_orchestrator,
|
|
@@ -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 = "5.
|
|
22
|
+
WORKSPACE_OS_VERSION = "5.4.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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Brain Core runtime assembly for app startup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_brain_runtime(
|
|
9
|
+
*,
|
|
10
|
+
data_dir: Any,
|
|
11
|
+
history_file: Any,
|
|
12
|
+
enable_graph: bool,
|
|
13
|
+
embedder: Any,
|
|
14
|
+
storage_engine: Any,
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
"""Construct Brain Core storage/conversation primitives behind one seam."""
|
|
17
|
+
|
|
18
|
+
from lattice_brain import BrainCore, ConversationStore
|
|
19
|
+
|
|
20
|
+
brain_core = (
|
|
21
|
+
BrainCore.from_paths(
|
|
22
|
+
data_dir,
|
|
23
|
+
embedder=embedder.provider,
|
|
24
|
+
storage_engine=storage_engine,
|
|
25
|
+
)
|
|
26
|
+
if enable_graph
|
|
27
|
+
else None
|
|
28
|
+
)
|
|
29
|
+
knowledge_graph = brain_core.knowledge if brain_core is not None else None
|
|
30
|
+
conversations = (
|
|
31
|
+
brain_core.conversations
|
|
32
|
+
if brain_core is not None
|
|
33
|
+
else ConversationStore(data_dir / "knowledge_graph.sqlite")
|
|
34
|
+
)
|
|
35
|
+
conversations.import_legacy_json(history_file)
|
|
36
|
+
return {
|
|
37
|
+
"BRAIN_CORE": brain_core,
|
|
38
|
+
"KNOWLEDGE_GRAPH": knowledge_graph,
|
|
39
|
+
"CONVERSATIONS": conversations,
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Configuration runtime assembly for the FastAPI composition root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from latticeai.core.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_config_runtime(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
12
|
+
"""Build app configuration values without importing model/runtime code."""
|
|
13
|
+
|
|
14
|
+
from latticeai.core.config import Config
|
|
15
|
+
|
|
16
|
+
cfg = config if config is not None else Config.from_env()
|
|
17
|
+
return {
|
|
18
|
+
"CONFIG": cfg,
|
|
19
|
+
"APP_MODE": cfg.app_mode,
|
|
20
|
+
"IS_PUBLIC_MODE": cfg.is_public,
|
|
21
|
+
"DEFAULT_HOST": cfg.host,
|
|
22
|
+
"DEFAULT_PORT": cfg.port,
|
|
23
|
+
"NETWORK_EXPOSED": cfg.network_exposed,
|
|
24
|
+
"ENABLE_TELEGRAM": cfg.enable_telegram,
|
|
25
|
+
"ENABLE_GRAPH": cfg.enable_graph,
|
|
26
|
+
"AUTOLOAD_MODELS": cfg.autoload_models,
|
|
27
|
+
"MODEL_IDLE_UNLOAD_SECONDS": cfg.model_idle_unload_seconds,
|
|
28
|
+
"ALLOW_LOCAL_MODELS": cfg.allow_local_models,
|
|
29
|
+
"REQUIRE_AUTH": cfg.require_auth,
|
|
30
|
+
"ALLOW_PLAINTEXT_API_KEYS": cfg.allow_plaintext_api_keys,
|
|
31
|
+
"CORS_ALLOW_NETWORK": cfg.cors_allow_network,
|
|
32
|
+
"CORS_EXTRA_ORIGINS": cfg.cors_extra_origins,
|
|
33
|
+
"PUBLIC_MODEL": cfg.public_model,
|
|
34
|
+
"LOCAL_MODEL": cfg.local_model,
|
|
35
|
+
"LOCAL_DRAFT_MODEL": cfg.local_draft_model,
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Security runtime assembly for app startup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from latticeai.core.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_security_runtime(config: "Config") -> Dict[str, Any]:
|
|
12
|
+
"""Build auth/security-derived runtime settings from the central config."""
|
|
13
|
+
|
|
14
|
+
from latticeai.core.security import configure_trusted_proxies
|
|
15
|
+
|
|
16
|
+
configure_trusted_proxies(config.trusted_proxies)
|
|
17
|
+
return {
|
|
18
|
+
"SSO_DISCOVERY_URL": config.sso_discovery_url,
|
|
19
|
+
"SSO_CLIENT_ID": config.sso_client_id,
|
|
20
|
+
"SSO_CLIENT_SECRET": config.sso_client_secret,
|
|
21
|
+
"SSO_REDIRECT_URI": config.sso_redirect_uri,
|
|
22
|
+
"SSO_PROVIDER_NAME": config.sso_provider_name,
|
|
23
|
+
"RATE_LIMIT_ENABLED": config.rate_limit_enabled,
|
|
24
|
+
"OPEN_REGISTRATION": config.open_registration,
|
|
25
|
+
"INVITE_CODE": config.invite_code,
|
|
26
|
+
"INVITE_GATE_ENABLED": config.invite_gate_enabled,
|
|
27
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Consent-first Brain automation recipes.
|
|
2
|
+
|
|
3
|
+
The recipes here are product-level starter workflows, not hidden background
|
|
4
|
+
jobs. Installing one creates a disabled draft workflow so the user can inspect
|
|
5
|
+
and enable it deliberately.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class BrainAutomationRecipe:
|
|
17
|
+
id: str
|
|
18
|
+
name: str
|
|
19
|
+
summary: str
|
|
20
|
+
user_value: str
|
|
21
|
+
cadence: str
|
|
22
|
+
trigger: Dict[str, Any]
|
|
23
|
+
prompt: str
|
|
24
|
+
creates: List[str]
|
|
25
|
+
|
|
26
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"id": self.id,
|
|
29
|
+
"name": self.name,
|
|
30
|
+
"summary": self.summary,
|
|
31
|
+
"user_value": self.user_value,
|
|
32
|
+
"cadence": self.cadence,
|
|
33
|
+
"trigger": deepcopy(self.trigger),
|
|
34
|
+
"creates": list(self.creates),
|
|
35
|
+
"consent": {
|
|
36
|
+
"default_state": "draft_disabled",
|
|
37
|
+
"local_only": True,
|
|
38
|
+
"external_actions": False,
|
|
39
|
+
"requires_user_enable": True,
|
|
40
|
+
"review_before_run": True,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_RECIPES: List[BrainAutomationRecipe] = [
|
|
46
|
+
BrainAutomationRecipe(
|
|
47
|
+
id="daily-memory-digest",
|
|
48
|
+
name="Daily Memory Digest",
|
|
49
|
+
summary="Collects the day's new memories into a short review draft.",
|
|
50
|
+
user_value="Users see what the Brain kept today without searching through chats.",
|
|
51
|
+
cadence="daily",
|
|
52
|
+
trigger={"trigger": "interval", "interval_seconds": 86_400},
|
|
53
|
+
prompt=(
|
|
54
|
+
"Review today's new Brain memories and draft a concise digest with "
|
|
55
|
+
"important decisions, unresolved questions, and suggested next actions. "
|
|
56
|
+
"Do not contact external services."
|
|
57
|
+
),
|
|
58
|
+
creates=["memory digest", "decision summary", "next-action suggestions"],
|
|
59
|
+
),
|
|
60
|
+
BrainAutomationRecipe(
|
|
61
|
+
id="weekly-project-review",
|
|
62
|
+
name="Weekly Project Review",
|
|
63
|
+
summary="Turns project context into a weekly checkpoint draft.",
|
|
64
|
+
user_value="Users can restart a project without explaining the week again.",
|
|
65
|
+
cadence="weekly",
|
|
66
|
+
trigger={"trigger": "interval", "interval_seconds": 604_800},
|
|
67
|
+
prompt=(
|
|
68
|
+
"Review this workspace's recent memories, workflow runs, and decisions. "
|
|
69
|
+
"Draft a project checkpoint with progress, risks, blockers, and next steps. "
|
|
70
|
+
"Keep it local and ask before any external action."
|
|
71
|
+
),
|
|
72
|
+
creates=["project checkpoint", "risk list", "next-week plan"],
|
|
73
|
+
),
|
|
74
|
+
BrainAutomationRecipe(
|
|
75
|
+
id="follow-up-radar",
|
|
76
|
+
name="Follow-up Radar",
|
|
77
|
+
summary="Looks for follow-up candidates when new knowledge enters the Brain.",
|
|
78
|
+
user_value="Users get gentle reminders for loose ends without a noisy task system.",
|
|
79
|
+
cadence="when new memory is saved",
|
|
80
|
+
trigger={"trigger": "brain_event"},
|
|
81
|
+
prompt=(
|
|
82
|
+
"Inspect the new Brain memory for follow-up signals such as decisions, "
|
|
83
|
+
"promises, deadlines, unresolved questions, or 'later' language. "
|
|
84
|
+
"Return suggestions only; do not create tasks without approval."
|
|
85
|
+
),
|
|
86
|
+
creates=["follow-up suggestions", "open-question list", "approval-ready task drafts"],
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
_RECIPE_BY_ID = {recipe.id: recipe for recipe in _RECIPES}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def list_brain_automation_recipes() -> Dict[str, Any]:
|
|
94
|
+
"""Return user-facing, consent-first automation recipe metadata."""
|
|
95
|
+
return {
|
|
96
|
+
"recipes": [recipe.as_dict() for recipe in _RECIPES],
|
|
97
|
+
"principles": {
|
|
98
|
+
"local_first": True,
|
|
99
|
+
"drafts_before_automation": True,
|
|
100
|
+
"no_external_actions_without_consent": True,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def find_installed_recipe_workflow(
|
|
106
|
+
workflows: Any, recipe_id: str
|
|
107
|
+
) -> Dict[str, Any] | None:
|
|
108
|
+
"""Return an existing draft installed from ``recipe_id``, if any.
|
|
109
|
+
|
|
110
|
+
Installing a recipe is idempotent: clicking "Create reviewable draft" twice
|
|
111
|
+
should surface the existing draft instead of accumulating duplicates. We
|
|
112
|
+
match on the ``brain_automation_recipe`` provenance metadata stamped by
|
|
113
|
+
:func:`build_brain_automation_workflow`.
|
|
114
|
+
"""
|
|
115
|
+
for workflow in workflows or []:
|
|
116
|
+
metadata = (workflow or {}).get("metadata") or {}
|
|
117
|
+
if (
|
|
118
|
+
metadata.get("created_from") == "brain_automation_recipe"
|
|
119
|
+
and metadata.get("recipe_id") == recipe_id
|
|
120
|
+
):
|
|
121
|
+
return workflow
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_brain_automation_workflow(recipe_id: str, *, enabled: bool = False) -> Dict[str, Any]:
|
|
126
|
+
"""Build a workflow definition for a recipe.
|
|
127
|
+
|
|
128
|
+
``enabled`` defaults to ``False`` so installing a recipe creates an
|
|
129
|
+
inspectable draft. The trigger service treats explicit ``enabled: false`` as
|
|
130
|
+
disarmed, while legacy workflows without the field keep their behavior.
|
|
131
|
+
"""
|
|
132
|
+
recipe = _RECIPE_BY_ID.get(recipe_id)
|
|
133
|
+
if recipe is None:
|
|
134
|
+
raise KeyError(recipe_id)
|
|
135
|
+
|
|
136
|
+
trigger_config = {
|
|
137
|
+
**deepcopy(recipe.trigger),
|
|
138
|
+
"enabled": bool(enabled),
|
|
139
|
+
"consent_required": True,
|
|
140
|
+
"local_only": True,
|
|
141
|
+
"external_actions": False,
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"name": recipe.name,
|
|
145
|
+
"nodes": [
|
|
146
|
+
{
|
|
147
|
+
"id": "trigger",
|
|
148
|
+
"type": "trigger",
|
|
149
|
+
"name": "User-enabled schedule" if recipe.trigger["trigger"] == "interval" else "New Brain memory",
|
|
150
|
+
"config": trigger_config,
|
|
151
|
+
"next": "draft",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "draft",
|
|
155
|
+
"type": "agent",
|
|
156
|
+
"name": "Draft Brain review",
|
|
157
|
+
"config": {
|
|
158
|
+
"agent": "agent:planner",
|
|
159
|
+
"prompt": recipe.prompt,
|
|
160
|
+
"mode": "draft",
|
|
161
|
+
"local_only": True,
|
|
162
|
+
"external_actions": False,
|
|
163
|
+
"requires_review": True,
|
|
164
|
+
},
|
|
165
|
+
"next": "output",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"id": "output",
|
|
169
|
+
"type": "output",
|
|
170
|
+
"name": "Review before saving",
|
|
171
|
+
"config": {
|
|
172
|
+
"value": "Draft ready for review. Save, edit, or discard it before it becomes durable memory.",
|
|
173
|
+
},
|
|
174
|
+
"next": None,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
"metadata": {
|
|
178
|
+
"created_from": "brain_automation_recipe",
|
|
179
|
+
"recipe_id": recipe.id,
|
|
180
|
+
"recipe_summary": recipe.summary,
|
|
181
|
+
"recipe_user_value": recipe.user_value,
|
|
182
|
+
"automation_state": "enabled" if enabled else "draft_disabled",
|
|
183
|
+
"local_only": True,
|
|
184
|
+
"external_actions": False,
|
|
185
|
+
"requires_user_enable": not enabled,
|
|
186
|
+
"creates": list(recipe.creates),
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# === A방향 (Act/automation + BrainAutomationPanel) E2E 시나리오 초안 ===
|
|
192
|
+
# (backend 인터페이스 list_brain_automation_recipes / find_installed... / build_... 완료 후 즉시 작성)
|
|
193
|
+
# 1. Recipe 목록 노출: frontend BrainAutomationPanel이 list_brain_automation_recipes() 호출 → recipes + consent metadata 표시.
|
|
194
|
+
# 2. "Create reviewable draft" 클릭:
|
|
195
|
+
# - build_brain_automation_workflow(recipe_id, enabled=False) 로 draft 생성 (metadata.recipe_id + created_from=brain_automation_recipe)
|
|
196
|
+
# - find_installed_recipe_workflow 로 사전 dedup 체크 → 이미 있으면 기존 반환, UI disabled + "✓ Reviewable draft ready" 피드백.
|
|
197
|
+
# - 생성된 workflow는 automation_state="draft_disabled", trigger.enabled=False → TriggerService 무시.
|
|
198
|
+
# 3. Dedup guard (UI + backend): metadata.recipe_id + created_from 기준. 중복 클릭 가드 (no-dup) 로 double submit 방지.
|
|
199
|
+
# 4. User가 draft를 review/수정 후 enabled=True 로 전환 → TriggerService._triggered_workflows 에서 enabled True인 것만 arm.
|
|
200
|
+
# 5. Interval trigger E2E:
|
|
201
|
+
# - reconcile_missed() : 다운타임 중 missed → "skipped" 이벤트 기록 (catch-up storm 없음).
|
|
202
|
+
# - tick_intervals() : last_fired_at + interval + last_attempt_at dedup 가드 (10s cooldown) 로 중복 실행 방지.
|
|
203
|
+
# - LATTICE_TZ 환경변수: describe()에 "tz" 노출, 이벤트 at 값은 epoch이지만 클라이언트가 LATTICE_TZ로 현지화.
|
|
204
|
+
# 6. Brain event trigger E2E: kg_ingest.* post_tool hook → on_brain_event → matching source_type 필터 → _fire (dedup 5s).
|
|
205
|
+
# 7. Failure degraded:
|
|
206
|
+
# - _fire 에서 run_workflow 예외 → _record_fire_outcome → consecutive_failures++ , describe() "status":"degraded" ( >=3 ).
|
|
207
|
+
# - 성공 시 reset. (실행 내부 실패는 workflow run record에 남음, scheduler는 launch health만).
|
|
208
|
+
# 8. Run provenance: fired run의 inputs["__trigger__"] = {"type": "interval"|"brain_event", ...} 로 감사/디버그 가능.
|
|
209
|
+
# 9. Consent-first: draft_disabled 기본, user enable 전까지 절대 실행 안 됨. "review_before_run": True.
|
|
210
|
+
# 10. End-to-end Act: draft → enable → trigger fire → agent:planner "Draft Brain review" 노드 → output (requires_review) → user review → save or discard.
|
|
211
|
+
#
|
|
212
|
+
# 다음: 실제 API (e.g. POST /brain/automation/install-draft) 가 UI에서 호출되면 위 시나리오에 대한 통합 테스트 + RunExecutor 경로 검증 즉시 추가.
|
|
213
|
+
# 현재 상태: backend recipe 인터페이스 + TriggerService edge 하드닝 + AgentRuntime wiring 완료. UI (App.tsx + styles) + test_brain_automation.py 는 별도 완료 보고됨.
|
|
214
|
+
|
|
@@ -20,9 +20,8 @@ are never auto-fetched by the verifier. Large models (>12GB) explicitly note
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
-
import
|
|
24
|
-
from
|
|
25
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@dataclass(frozen=True)
|