ltcai 3.0.1 → 3.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 +54 -21
- package/docs/CHANGELOG.md +90 -0
- package/docs/V3_2_AUDIT.md +82 -0
- package/docs/V3_FRONTEND.md +20 -17
- package/docs/architecture.md +6 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_registry.py +103 -0
- package/latticeai/api/auth.py +4 -1
- package/latticeai/api/hooks.py +113 -0
- package/latticeai/api/marketplace.py +13 -0
- package/latticeai/api/memory.py +109 -0
- package/latticeai/api/search.py +4 -0
- package/latticeai/core/agent_registry.py +234 -0
- package/latticeai/core/config.py +2 -0
- package/latticeai/core/embedding_providers.py +123 -0
- package/latticeai/core/hooks.py +284 -0
- package/latticeai/core/marketplace.py +87 -2
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +63 -6
- package/latticeai/services/memory_service.py +324 -0
- package/package.json +9 -4
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +9 -9
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +1 -1
- package/static/graph.html +9 -9
- package/static/plugins.html +4 -4
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +55 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +2 -2
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/index.html +38 -9
- package/static/v3/js/app.a5adc0f3.js +26 -0
- package/static/v3/js/core/api.603b978f.js +408 -0
- package/static/v3/js/core/api.js +132 -51
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +9 -2
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/routes.07ad6696.js +89 -0
- package/static/v3/js/core/routes.js +17 -1
- package/static/v3/js/core/shell.ea0b9ae5.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +1 -1
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +4 -5
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +4 -5
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +2 -5
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +4 -5
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +6 -6
- package/static/v3/js/views/agents.c373d48c.js +293 -0
- package/static/v3/js/views/agents.js +101 -2
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +2 -3
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +27 -21
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/hooks.f3edebca.js +99 -0
- package/static/v3/js/views/hooks.js +99 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +1 -1
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +2 -3
- package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
- package/static/v3/js/views/marketplace.js +141 -0
- package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
- package/static/v3/js/views/mcp.js +114 -0
- package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
- package/static/v3/js/views/memory.js +146 -0
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +17 -8
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +5 -5
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +3 -7
- package/static/v3/js/views/planning.9ac3e313.js +153 -0
- package/static/v3/js/views/planning.js +153 -0
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +6 -14
- package/static/v3/js/views/skills.c6c2f965.js +109 -0
- package/static/v3/js/views/skills.js +109 -0
- package/static/v3/js/views/tools.e4f11276.js +108 -0
- package/static/v3/js/views/tools.js +108 -0
- package/static/v3/js/views/workflows.26c57290.js +128 -0
- package/static/v3/js/views/workflows.js +128 -0
- package/static/workflows.html +4 -4
- package/static/workspace.html +5 -5
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
- package/static/v3/js/core/fixtures.js +0 -171
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Hooks platform — a persisted registry of lifecycle extension points.
|
|
2
|
+
|
|
3
|
+
Lattice AI runs several behaviours at well-defined points in the agent / tool /
|
|
4
|
+
workflow lifecycle (audit logging, secret redaction, sensitive-data
|
|
5
|
+
classification, tool-permission gating, memory snapshots, workflow replay
|
|
6
|
+
logging). v3.2.0 makes those points *first-class and inspectable*: every hook is
|
|
7
|
+
listed, ordered, and individually enable/disable-able, and users can register
|
|
8
|
+
their own custom hooks alongside the built-ins.
|
|
9
|
+
|
|
10
|
+
The registry owns metadata, ordering and the enabled flag (persisted to
|
|
11
|
+
``data_dir/hooks.json``). Built-in hooks carry ``source="builtin"`` and map onto
|
|
12
|
+
behaviour the platform already performs; ``managed`` records whether the
|
|
13
|
+
behaviour is enforced by the platform (``platform``) or is an advisory hook the
|
|
14
|
+
user has registered (``user``). The registry never silently drops a hook: the
|
|
15
|
+
full set is always returned so the UI can show exactly what runs and in which
|
|
16
|
+
order.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import tempfile
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
HOOK_KINDS = (
|
|
30
|
+
"pre_run",
|
|
31
|
+
"post_run",
|
|
32
|
+
"pre_tool",
|
|
33
|
+
"post_tool",
|
|
34
|
+
"agent",
|
|
35
|
+
"pipeline",
|
|
36
|
+
"workflow",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _now() -> str:
|
|
41
|
+
return datetime.now().isoformat(timespec="seconds")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Built-in hooks describe lifecycle points the platform already exercises. They
|
|
45
|
+
# are honest reflections of existing behaviour (see the `binding` field), made
|
|
46
|
+
# visible and orderable here. Disabling a `managed="platform"` hook is recorded
|
|
47
|
+
# and surfaced, but core safety behaviours remain enforced by their owning
|
|
48
|
+
# subsystem — the UI states this explicitly so nothing is misrepresented.
|
|
49
|
+
BUILTIN_HOOKS: List[Dict[str, Any]] = [
|
|
50
|
+
{
|
|
51
|
+
"id": "builtin:redact-secrets",
|
|
52
|
+
"name": "Redact secrets",
|
|
53
|
+
"kind": "pre_run",
|
|
54
|
+
"order": 10,
|
|
55
|
+
"description": "Strip secret-like fields (token, password, api_key…) from agent context packets before a run.",
|
|
56
|
+
"binding": "latticeai.core.multi_agent._redact",
|
|
57
|
+
"managed": "platform",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "builtin:research-memory-snapshot",
|
|
61
|
+
"name": "Research memory snapshot",
|
|
62
|
+
"kind": "agent",
|
|
63
|
+
"order": 20,
|
|
64
|
+
"description": "Capture a short-term memory snapshot after the researcher stage gathers context.",
|
|
65
|
+
"binding": "latticeai.core.multi_agent.default_role_runner",
|
|
66
|
+
"managed": "platform",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "builtin:tool-permission-gate",
|
|
70
|
+
"name": "Tool permission gate",
|
|
71
|
+
"kind": "pre_tool",
|
|
72
|
+
"order": 10,
|
|
73
|
+
"description": "Require explicit approval for tools whose governance policy is not auto-approve.",
|
|
74
|
+
"binding": "latticeai.core.tool_registry.ToolRegistry.permission",
|
|
75
|
+
"managed": "platform",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "builtin:sensitive-data-guard",
|
|
79
|
+
"name": "Sensitive-data guard",
|
|
80
|
+
"kind": "pre_tool",
|
|
81
|
+
"order": 20,
|
|
82
|
+
"description": "Classify outgoing content for sensitive data before tool execution.",
|
|
83
|
+
"binding": "server_app.classify_sensitive_message",
|
|
84
|
+
"managed": "platform",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "builtin:audit-agent-run",
|
|
88
|
+
"name": "Audit agent run",
|
|
89
|
+
"kind": "post_run",
|
|
90
|
+
"order": 10,
|
|
91
|
+
"description": "Append every completed agent run to the workspace audit log.",
|
|
92
|
+
"binding": "latticeai.services.agent_runtime.AgentRuntime.start",
|
|
93
|
+
"managed": "platform",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "builtin:workflow-replay-log",
|
|
97
|
+
"name": "Workflow replay log",
|
|
98
|
+
"kind": "workflow",
|
|
99
|
+
"order": 10,
|
|
100
|
+
"description": "Record each workflow run's timeline so it can be replayed step by step.",
|
|
101
|
+
"binding": "latticeai.api.workflow_designer",
|
|
102
|
+
"managed": "platform",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "builtin:pipeline-index-status",
|
|
106
|
+
"name": "Pipeline index status",
|
|
107
|
+
"kind": "pipeline",
|
|
108
|
+
"order": 10,
|
|
109
|
+
"description": "Publish ingest / embed / graph-build pipeline state to the retrieval index status.",
|
|
110
|
+
"binding": "latticeai.api.search",
|
|
111
|
+
"managed": "platform",
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class HooksRegistry:
|
|
117
|
+
"""Persisted registry of lifecycle hooks (built-in + user-registered)."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, path: Path):
|
|
120
|
+
self.path = Path(path)
|
|
121
|
+
self._state: Dict[str, Any] = self._load()
|
|
122
|
+
|
|
123
|
+
# ── persistence ───────────────────────────────────────────────────────
|
|
124
|
+
def _load(self) -> Dict[str, Any]:
|
|
125
|
+
if self.path.exists():
|
|
126
|
+
try:
|
|
127
|
+
with open(self.path, "r", encoding="utf-8") as fh:
|
|
128
|
+
data = json.load(fh)
|
|
129
|
+
if isinstance(data, dict):
|
|
130
|
+
data.setdefault("custom", [])
|
|
131
|
+
data.setdefault("overrides", {})
|
|
132
|
+
return data
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
return {"custom": [], "overrides": {}}
|
|
136
|
+
|
|
137
|
+
def _save(self) -> None:
|
|
138
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), suffix=".tmp")
|
|
140
|
+
try:
|
|
141
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
142
|
+
json.dump(self._state, fh, ensure_ascii=False, indent=2)
|
|
143
|
+
os.replace(tmp, self.path)
|
|
144
|
+
finally:
|
|
145
|
+
if os.path.exists(tmp):
|
|
146
|
+
os.unlink(tmp)
|
|
147
|
+
|
|
148
|
+
# ── views ─────────────────────────────────────────────────────────────
|
|
149
|
+
def _materialize(self) -> List[Dict[str, Any]]:
|
|
150
|
+
overrides: Dict[str, Any] = self._state.get("overrides", {})
|
|
151
|
+
hooks: List[Dict[str, Any]] = []
|
|
152
|
+
for base in BUILTIN_HOOKS:
|
|
153
|
+
ov = overrides.get(base["id"], {})
|
|
154
|
+
hook = dict(base)
|
|
155
|
+
hook["source"] = "builtin"
|
|
156
|
+
hook["enabled"] = bool(ov.get("enabled", True))
|
|
157
|
+
if "order" in ov:
|
|
158
|
+
hook["order"] = ov["order"]
|
|
159
|
+
hook["removable"] = False
|
|
160
|
+
hooks.append(hook)
|
|
161
|
+
for custom in self._state.get("custom", []):
|
|
162
|
+
hook = dict(custom)
|
|
163
|
+
hook["source"] = "user"
|
|
164
|
+
hook.setdefault("managed", "user")
|
|
165
|
+
hook.setdefault("binding", "advisory")
|
|
166
|
+
hook["enabled"] = bool(custom.get("enabled", True))
|
|
167
|
+
hook["removable"] = True
|
|
168
|
+
hooks.append(hook)
|
|
169
|
+
hooks.sort(key=lambda h: (HOOK_KINDS.index(h["kind"]) if h["kind"] in HOOK_KINDS else 99, h.get("order", 100), h["id"]))
|
|
170
|
+
return hooks
|
|
171
|
+
|
|
172
|
+
def list(self, kind: Optional[str] = None) -> Dict[str, Any]:
|
|
173
|
+
hooks = self._materialize()
|
|
174
|
+
if kind:
|
|
175
|
+
hooks = [h for h in hooks if h["kind"] == kind]
|
|
176
|
+
counts: Dict[str, Dict[str, int]] = {}
|
|
177
|
+
for h in self._materialize():
|
|
178
|
+
bucket = counts.setdefault(h["kind"], {"total": 0, "enabled": 0})
|
|
179
|
+
bucket["total"] += 1
|
|
180
|
+
if h["enabled"]:
|
|
181
|
+
bucket["enabled"] += 1
|
|
182
|
+
return {
|
|
183
|
+
"hooks": hooks,
|
|
184
|
+
"kinds": list(HOOK_KINDS),
|
|
185
|
+
"counts": counts,
|
|
186
|
+
"total": len(hooks),
|
|
187
|
+
"enabled": sum(1 for h in hooks if h["enabled"]),
|
|
188
|
+
"generated_at": _now(),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def get(self, hook_id: str) -> Optional[Dict[str, Any]]:
|
|
192
|
+
return next((h for h in self._materialize() if h["id"] == hook_id), None)
|
|
193
|
+
|
|
194
|
+
def inspect(self, hook_id: str) -> Dict[str, Any]:
|
|
195
|
+
hook = self.get(hook_id)
|
|
196
|
+
if hook is None:
|
|
197
|
+
raise KeyError(hook_id)
|
|
198
|
+
detail = dict(hook)
|
|
199
|
+
detail["advisory"] = hook.get("managed") != "platform"
|
|
200
|
+
detail["note"] = (
|
|
201
|
+
"Enforced by its owning subsystem; the registry controls visibility and ordering."
|
|
202
|
+
if hook.get("managed") == "platform"
|
|
203
|
+
else "User-registered hook: listed, ordered and inspectable; runs advisory in this build."
|
|
204
|
+
)
|
|
205
|
+
return detail
|
|
206
|
+
|
|
207
|
+
# ── mutations ─────────────────────────────────────────────────────────
|
|
208
|
+
def set_enabled(self, hook_id: str, enabled: bool) -> Dict[str, Any]:
|
|
209
|
+
if self.get(hook_id) is None:
|
|
210
|
+
raise KeyError(hook_id)
|
|
211
|
+
if hook_id.startswith("builtin:"):
|
|
212
|
+
self._state.setdefault("overrides", {}).setdefault(hook_id, {})["enabled"] = bool(enabled)
|
|
213
|
+
else:
|
|
214
|
+
for custom in self._state.get("custom", []):
|
|
215
|
+
if custom["id"] == hook_id:
|
|
216
|
+
custom["enabled"] = bool(enabled)
|
|
217
|
+
self._save()
|
|
218
|
+
return self.get(hook_id) # type: ignore[return-value]
|
|
219
|
+
|
|
220
|
+
def set_order(self, hook_id: str, order: int) -> Dict[str, Any]:
|
|
221
|
+
if self.get(hook_id) is None:
|
|
222
|
+
raise KeyError(hook_id)
|
|
223
|
+
order = int(order)
|
|
224
|
+
if hook_id.startswith("builtin:"):
|
|
225
|
+
self._state.setdefault("overrides", {}).setdefault(hook_id, {})["order"] = order
|
|
226
|
+
else:
|
|
227
|
+
for custom in self._state.get("custom", []):
|
|
228
|
+
if custom["id"] == hook_id:
|
|
229
|
+
custom["order"] = order
|
|
230
|
+
self._save()
|
|
231
|
+
return self.get(hook_id) # type: ignore[return-value]
|
|
232
|
+
|
|
233
|
+
def reorder(self, kind: str, ordered_ids: List[str]) -> Dict[str, Any]:
|
|
234
|
+
for idx, hook_id in enumerate(ordered_ids):
|
|
235
|
+
try:
|
|
236
|
+
self.set_order(hook_id, (idx + 1) * 10)
|
|
237
|
+
except KeyError:
|
|
238
|
+
continue
|
|
239
|
+
return self.list(kind=kind)
|
|
240
|
+
|
|
241
|
+
def register(
|
|
242
|
+
self,
|
|
243
|
+
*,
|
|
244
|
+
name: str,
|
|
245
|
+
kind: str,
|
|
246
|
+
description: str = "",
|
|
247
|
+
command: str = "",
|
|
248
|
+
order: Optional[int] = None,
|
|
249
|
+
enabled: bool = True,
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
if not str(name).strip():
|
|
252
|
+
raise ValueError("name is required")
|
|
253
|
+
if kind not in HOOK_KINDS:
|
|
254
|
+
raise ValueError(f"kind must be one of {', '.join(HOOK_KINDS)}")
|
|
255
|
+
slug = str(name).strip().lower().replace(" ", "-")
|
|
256
|
+
hook_id = f"user:{slug}"
|
|
257
|
+
existing = {c["id"] for c in self._state.get("custom", [])}
|
|
258
|
+
if hook_id in existing:
|
|
259
|
+
hook_id = f"user:{slug}-{len(existing) + 1}"
|
|
260
|
+
entry = {
|
|
261
|
+
"id": hook_id,
|
|
262
|
+
"name": str(name).strip(),
|
|
263
|
+
"kind": kind,
|
|
264
|
+
"description": str(description or "").strip(),
|
|
265
|
+
"command": str(command or "").strip(),
|
|
266
|
+
"order": int(order) if order is not None else 100,
|
|
267
|
+
"enabled": bool(enabled),
|
|
268
|
+
"managed": "user",
|
|
269
|
+
"binding": "advisory",
|
|
270
|
+
"created_at": _now(),
|
|
271
|
+
}
|
|
272
|
+
self._state.setdefault("custom", []).append(entry)
|
|
273
|
+
self._save()
|
|
274
|
+
return entry
|
|
275
|
+
|
|
276
|
+
def remove(self, hook_id: str) -> Dict[str, Any]:
|
|
277
|
+
if hook_id.startswith("builtin:"):
|
|
278
|
+
raise ValueError("Built-in hooks cannot be removed; disable them instead.")
|
|
279
|
+
before = len(self._state.get("custom", []))
|
|
280
|
+
self._state["custom"] = [c for c in self._state.get("custom", []) if c["id"] != hook_id]
|
|
281
|
+
if len(self._state["custom"]) == before:
|
|
282
|
+
raise KeyError(hook_id)
|
|
283
|
+
self._save()
|
|
284
|
+
return {"removed": hook_id}
|
|
@@ -11,10 +11,39 @@ from copy import deepcopy
|
|
|
11
11
|
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
MARKETPLACE_VERSION = "
|
|
14
|
+
MARKETPLACE_VERSION = "3.2.0"
|
|
15
15
|
TEMPLATE_KINDS = ("plugin", "workflow", "agent")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _agent_template(
|
|
19
|
+
template_id: str,
|
|
20
|
+
name: str,
|
|
21
|
+
description: str,
|
|
22
|
+
*,
|
|
23
|
+
roles: List[str],
|
|
24
|
+
capabilities: List[str],
|
|
25
|
+
suggested_tools: List[str],
|
|
26
|
+
category: str,
|
|
27
|
+
max_retries: int = 2,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
"""Build a portable agent template entry (Part 4 reusable templates)."""
|
|
30
|
+
return {
|
|
31
|
+
"id": template_id,
|
|
32
|
+
"kind": "agent",
|
|
33
|
+
"name": name,
|
|
34
|
+
"version": "1.0.0",
|
|
35
|
+
"description": description,
|
|
36
|
+
"metadata": {"category": category, "installable": True, "agent_template": True},
|
|
37
|
+
"definition": {
|
|
38
|
+
"roles": roles,
|
|
39
|
+
"max_retries": max_retries,
|
|
40
|
+
"capabilities": capabilities,
|
|
41
|
+
"suggested_tools": suggested_tools,
|
|
42
|
+
"constraints": ["workspace scoped", "no secret leakage", "replayable timeline"],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
18
47
|
class MarketplaceError(Exception):
|
|
19
48
|
"""Raised for invalid template operations."""
|
|
20
49
|
|
|
@@ -73,7 +102,47 @@ BUILTIN_TEMPLATES: Dict[str, List[Dict[str, Any]]] = {
|
|
|
73
102
|
"max_retries": 2,
|
|
74
103
|
"constraints": ["workspace scoped", "no secret leakage", "replayable timeline"],
|
|
75
104
|
},
|
|
76
|
-
}
|
|
105
|
+
},
|
|
106
|
+
_agent_template(
|
|
107
|
+
"agent-research-assistant", "Research Assistant",
|
|
108
|
+
"Retrieves workspace context, plans an inquiry, and synthesizes a reviewed answer.",
|
|
109
|
+
roles=["researcher", "planner", "reviewer"],
|
|
110
|
+
capabilities=["context-retrieval", "hybrid-search", "memory-recall", "synthesis"],
|
|
111
|
+
suggested_tools=["knowledge_search", "knowledge_graph_search", "read_file"],
|
|
112
|
+
category="research",
|
|
113
|
+
),
|
|
114
|
+
_agent_template(
|
|
115
|
+
"agent-coding-assistant", "Coding Assistant",
|
|
116
|
+
"Plans a change, edits files, runs the build, and reviews the result before finishing.",
|
|
117
|
+
roles=["planner", "executor", "reviewer"],
|
|
118
|
+
capabilities=["task-decomposition", "tool-use", "file-write", "verification"],
|
|
119
|
+
suggested_tools=["edit_file", "write_file", "run_command", "build_project", "git_diff"],
|
|
120
|
+
category="coding",
|
|
121
|
+
),
|
|
122
|
+
_agent_template(
|
|
123
|
+
"agent-knowledge-curator", "Knowledge Curator",
|
|
124
|
+
"Captures, structures, and saves knowledge into the graph and memory.",
|
|
125
|
+
roles=["researcher", "executor"],
|
|
126
|
+
capabilities=["context-retrieval", "graph-read", "knowledge-save"],
|
|
127
|
+
suggested_tools=["knowledge_save", "knowledge_graph_ingest", "knowledge_tree"],
|
|
128
|
+
category="knowledge",
|
|
129
|
+
),
|
|
130
|
+
_agent_template(
|
|
131
|
+
"agent-documentation-writer", "Documentation Writer",
|
|
132
|
+
"Plans a document, drafts and writes it, and reviews for completeness.",
|
|
133
|
+
roles=["planner", "executor", "reviewer"],
|
|
134
|
+
capabilities=["task-decomposition", "file-write", "summarize", "verification"],
|
|
135
|
+
suggested_tools=["create_docx", "create_pdf", "write_file", "read_document"],
|
|
136
|
+
category="documentation",
|
|
137
|
+
),
|
|
138
|
+
_agent_template(
|
|
139
|
+
"agent-workflow-builder", "Workflow Builder",
|
|
140
|
+
"Plans and assembles a multi-step workflow definition for repeatable automation.",
|
|
141
|
+
roles=["planner", "executor"],
|
|
142
|
+
capabilities=["task-decomposition", "workflow-run", "delegation"],
|
|
143
|
+
suggested_tools=["todo_write", "workspace_tree"],
|
|
144
|
+
category="automation",
|
|
145
|
+
),
|
|
77
146
|
],
|
|
78
147
|
}
|
|
79
148
|
|
|
@@ -110,6 +179,22 @@ class TemplateCatalog:
|
|
|
110
179
|
return deepcopy(template)
|
|
111
180
|
raise MarketplaceError(f"template not found: {item_kind}/{template_id}")
|
|
112
181
|
|
|
182
|
+
def clone_template(self, kind: str, template_id: str, new_name: Optional[str] = None) -> Dict[str, Any]:
|
|
183
|
+
"""Return an editable copy of a template with a fresh id (Part 4 clone)."""
|
|
184
|
+
template = self.get_template(kind, template_id)
|
|
185
|
+
name = (new_name or f"{template['name']} (Copy)").strip()
|
|
186
|
+
slug = name.lower().replace(" ", "-").replace("(", "").replace(")", "")
|
|
187
|
+
clone = deepcopy(template)
|
|
188
|
+
clone["id"] = f"{template['id']}-copy-{slug}"[:80]
|
|
189
|
+
clone["name"] = name
|
|
190
|
+
clone["version"] = "1.0.0"
|
|
191
|
+
clone["metadata"] = {
|
|
192
|
+
**(template.get("metadata") or {}),
|
|
193
|
+
"cloned_from": template_id,
|
|
194
|
+
"editable": True,
|
|
195
|
+
}
|
|
196
|
+
return clone
|
|
197
|
+
|
|
113
198
|
def export_template(self, kind: str, template_id: str) -> Dict[str, Any]:
|
|
114
199
|
template = self.get_template(kind, template_id)
|
|
115
200
|
return {
|
|
@@ -14,7 +14,7 @@ from datetime import datetime
|
|
|
14
14
|
from typing import Any, Callable, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
MULTI_AGENT_VERSION = "
|
|
17
|
+
MULTI_AGENT_VERSION = "3.2.0"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
WORKSPACE_OS_VERSION = "3.0
|
|
21
|
+
WORKSPACE_OS_VERSION = "3.2.0"
|
|
22
22
|
|
|
23
23
|
# Workspace types separate single-user Personal workspaces from shared
|
|
24
24
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
package/latticeai/server_app.py
CHANGED
|
@@ -73,7 +73,7 @@ from latticeai.services.workspace_service import WorkspaceService
|
|
|
73
73
|
from latticeai.services.model_service import ModelService
|
|
74
74
|
from latticeai.services.chat_service import ChatService
|
|
75
75
|
from latticeai.services.search_service import SearchService
|
|
76
|
-
from latticeai.core.embedding_providers import resolve_embedder
|
|
76
|
+
from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
|
|
77
77
|
from latticeai.services.agent_runtime import AgentRuntime
|
|
78
78
|
from latticeai.services.model_runtime import (
|
|
79
79
|
CLOUD_VERIFY_TTL_SECONDS,
|
|
@@ -113,6 +113,12 @@ from latticeai.api.tools import create_tools_router
|
|
|
113
113
|
from latticeai.api.static_routes import create_static_routes_router
|
|
114
114
|
from latticeai.api.garden import create_garden_router
|
|
115
115
|
from latticeai.api.setup import create_setup_router
|
|
116
|
+
from latticeai.api.hooks import create_hooks_router
|
|
117
|
+
from latticeai.core.hooks import HooksRegistry
|
|
118
|
+
from latticeai.api.agent_registry import create_agent_registry_router
|
|
119
|
+
from latticeai.core.agent_registry import AgentRegistry
|
|
120
|
+
from latticeai.api.memory import create_memory_router
|
|
121
|
+
from latticeai.services.memory_service import MemoryService
|
|
116
122
|
from latticeai.services.tool_dispatch import (
|
|
117
123
|
LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
|
|
118
124
|
TOOL_GOVERNANCE,
|
|
@@ -122,6 +128,7 @@ from latticeai.services.tool_dispatch import (
|
|
|
122
128
|
configure_tool_dispatch,
|
|
123
129
|
get_tool_permission,
|
|
124
130
|
list_tool_permissions,
|
|
131
|
+
tool_response as _tool_response,
|
|
125
132
|
)
|
|
126
133
|
from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF
|
|
127
134
|
from mcp_registry import (
|
|
@@ -251,15 +258,26 @@ SSO_FILE = DATA_DIR / "sso_config.json"
|
|
|
251
258
|
# Resolve the configured embedding provider once at startup. Degrades to the
|
|
252
259
|
# offline hash fallback when the requested provider is unavailable, while
|
|
253
260
|
# recording the requested-vs-active provider for the Embeddings status surface.
|
|
261
|
+
try:
|
|
262
|
+
EMBEDDING_PROFILE = resolve_embedding_profile(CONFIG.embedding_profile)
|
|
263
|
+
except ValueError as exc:
|
|
264
|
+
logging.warning("Embedding profile ignored: %s", exc)
|
|
265
|
+
EMBEDDING_PROFILE = {}
|
|
266
|
+
_embedding_provider = CONFIG.embedding_provider
|
|
267
|
+
_embedding_model = CONFIG.embedding_model or str(EMBEDDING_PROFILE.get("model") or "")
|
|
268
|
+
_embedding_dim = CONFIG.embedding_dim or int(EMBEDDING_PROFILE.get("dimensions") or 0)
|
|
269
|
+
if CONFIG.embedding_profile and CONFIG.embedding_provider in {"", "hash", "local", "fallback"}:
|
|
270
|
+
_embedding_provider = str(EMBEDDING_PROFILE.get("provider") or CONFIG.embedding_provider)
|
|
271
|
+
|
|
254
272
|
EMBEDDER = resolve_embedder(
|
|
255
|
-
|
|
256
|
-
model=
|
|
273
|
+
_embedding_provider,
|
|
274
|
+
model=_embedding_model,
|
|
257
275
|
base_url=CONFIG.embedding_base_url,
|
|
258
276
|
api_key=CONFIG.embedding_api_key,
|
|
259
|
-
dim=
|
|
277
|
+
dim=_embedding_dim,
|
|
260
278
|
timeout=CONFIG.embedding_timeout,
|
|
261
279
|
extra={"target": CONFIG.embedding_custom_target},
|
|
262
|
-
probe=
|
|
280
|
+
probe=_embedding_provider not in {"", "hash", "local", "fallback"},
|
|
263
281
|
)
|
|
264
282
|
if EMBEDDER.fell_back:
|
|
265
283
|
logging.warning("Embedding provider %s unavailable: %s", EMBEDDER.requested, EMBEDDER.detail)
|
|
@@ -280,6 +298,19 @@ WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
|
|
|
280
298
|
PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
|
|
281
299
|
PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
|
|
282
300
|
TEMPLATE_CATALOG = TemplateCatalog()
|
|
301
|
+
# ── v3.2 platform registries: lifecycle hooks + agent registry, persisted under
|
|
302
|
+
# DATA_DIR so the /app Hooks and Agent Registry views read/write real state.
|
|
303
|
+
HOOKS_REGISTRY = HooksRegistry(DATA_DIR / "hooks.json")
|
|
304
|
+
AGENT_REGISTRY = AgentRegistry(DATA_DIR / "agent_registry.json")
|
|
305
|
+
# Unified long-term memory platform fronting workspace memories, agent
|
|
306
|
+
# snapshots, conversation history, and the KG graph/vector index.
|
|
307
|
+
MEMORY_SERVICE = MemoryService(
|
|
308
|
+
store=WORKSPACE_OS,
|
|
309
|
+
data_dir=DATA_DIR,
|
|
310
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
311
|
+
enable_graph=ENABLE_GRAPH,
|
|
312
|
+
history_file=HISTORY_FILE,
|
|
313
|
+
)
|
|
283
314
|
|
|
284
315
|
def _require_graph():
|
|
285
316
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -805,6 +836,8 @@ def _require_local_approval(
|
|
|
805
836
|
|
|
806
837
|
def require_admin(request: Request) -> tuple[str, Dict]:
|
|
807
838
|
users = load_users()
|
|
839
|
+
if not REQUIRE_AUTH:
|
|
840
|
+
return "", users
|
|
808
841
|
token = _extract_bearer_token(request)
|
|
809
842
|
if token:
|
|
810
843
|
email = get_session_email(token)
|
|
@@ -1100,6 +1133,7 @@ app.include_router(create_auth_router(
|
|
|
1100
1133
|
get_sso_settings=get_sso_settings, get_sso_discovery=_get_sso_discovery,
|
|
1101
1134
|
public_sso_config=public_sso_config,
|
|
1102
1135
|
open_registration=OPEN_REGISTRATION, session_ttl=_SESSION_TTL,
|
|
1136
|
+
require_auth=REQUIRE_AUTH,
|
|
1103
1137
|
))
|
|
1104
1138
|
|
|
1105
1139
|
def _graph_stats_safe():
|
|
@@ -1387,9 +1421,11 @@ app.include_router(create_chat_router(
|
|
|
1387
1421
|
))
|
|
1388
1422
|
|
|
1389
1423
|
def _embedding_info() -> dict:
|
|
1390
|
-
from latticeai.core.embedding_providers import PROVIDER_TYPES
|
|
1424
|
+
from latticeai.core.embedding_providers import PROVIDER_TYPES, embedding_provider_profiles
|
|
1391
1425
|
info = EMBEDDER.as_dict()
|
|
1392
1426
|
info["available_providers"] = list(PROVIDER_TYPES)
|
|
1427
|
+
info["profile"] = CONFIG.embedding_profile or ""
|
|
1428
|
+
info["profiles"] = embedding_provider_profiles()
|
|
1393
1429
|
return info
|
|
1394
1430
|
|
|
1395
1431
|
|
|
@@ -1423,6 +1459,27 @@ app.include_router(create_tools_router(
|
|
|
1423
1459
|
mcp_public_item=mcp_public_item,
|
|
1424
1460
|
))
|
|
1425
1461
|
|
|
1462
|
+
app.include_router(create_hooks_router(
|
|
1463
|
+
registry=HOOKS_REGISTRY,
|
|
1464
|
+
require_user=require_user,
|
|
1465
|
+
append_audit_event=append_audit_event,
|
|
1466
|
+
))
|
|
1467
|
+
|
|
1468
|
+
app.include_router(create_agent_registry_router(
|
|
1469
|
+
registry=AGENT_REGISTRY,
|
|
1470
|
+
require_user=require_user,
|
|
1471
|
+
append_audit_event=append_audit_event,
|
|
1472
|
+
))
|
|
1473
|
+
|
|
1474
|
+
app.include_router(create_memory_router(
|
|
1475
|
+
service=MEMORY_SERVICE,
|
|
1476
|
+
require_user=require_user,
|
|
1477
|
+
get_current_user=get_current_user,
|
|
1478
|
+
gate_read=PLATFORM.gate_read,
|
|
1479
|
+
gate_write=PLATFORM.gate_write,
|
|
1480
|
+
append_audit_event=append_audit_event,
|
|
1481
|
+
))
|
|
1482
|
+
|
|
1426
1483
|
app.include_router(create_garden_router(gardener=gardener, require_user=require_user))
|
|
1427
1484
|
app.include_router(create_setup_router(model_router=router, require_user=require_user))
|
|
1428
1485
|
|