ltcai 3.5.0 → 4.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 +73 -35
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/HANDOVER_v3.6.0.md +46 -0
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/architecture.md +13 -12
- package/docs/kg-schema.md +102 -53
- package/docs/privacy.md +18 -2
- package/docs/security-model.md +17 -0
- package/kg_schema.py +139 -10
- package/knowledge_graph.py +874 -26
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +7 -1
- package/latticeai/api/auth.py +27 -4
- package/latticeai/api/browser.py +217 -0
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/knowledge_graph.py +146 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/portability.py +93 -0
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +2 -4
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/workflow_designer.py +46 -0
- package/latticeai/api/workspace.py +71 -49
- package/latticeai/app_factory.py +1710 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +37 -7
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +58 -10
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1504
- package/latticeai/services/agent_runtime.py +1 -0
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +318 -0
- package/latticeai/services/kg_portability.py +207 -0
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +31 -0
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +11 -8
- package/scripts/build_vsix.mjs +72 -0
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +82 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/account.html +2 -4
- package/static/admin.html +3 -5
- package/static/chat.html +3 -6
- package/static/graph.html +2 -4
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +20 -19
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
- package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
- package/static/v3/js/core/api.js +38 -0
- package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
- package/static/v3/js/core/routes.js +22 -22
- package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
- package/static/v3/js/core/shell.js +1 -1
- package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
- package/static/v3/js/core/store.js +1 -1
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
- package/static/v3/js/views/knowledge-graph.js +326 -54
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/static/workspace.html +2 -2
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
import json
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any,
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
28
|
|
|
29
29
|
# Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
|
|
30
30
|
WORKSPACE_KINDS = (
|
|
@@ -60,12 +60,16 @@ class MemoryService:
|
|
|
60
60
|
knowledge_graph: Any = None,
|
|
61
61
|
enable_graph: bool = True,
|
|
62
62
|
history_file: Optional[Path] = None,
|
|
63
|
+
conversation_store: Any = None,
|
|
63
64
|
):
|
|
64
65
|
self._store = store
|
|
65
66
|
self._kg = knowledge_graph
|
|
66
67
|
self._enable_graph = bool(enable_graph and knowledge_graph is not None)
|
|
67
68
|
self._data_dir = Path(data_dir)
|
|
68
69
|
self._history_file = Path(history_file) if history_file else (self._data_dir / "chat_history.json")
|
|
70
|
+
# v4: the durable SQLite conversation store supersedes the JSON file
|
|
71
|
+
# as the conversation tier's backing store when provided.
|
|
72
|
+
self._conversation_store = conversation_store
|
|
69
73
|
|
|
70
74
|
# ── helpers over the underlying stores ────────────────────────────────
|
|
71
75
|
def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
@@ -87,6 +91,14 @@ class MemoryService:
|
|
|
87
91
|
return []
|
|
88
92
|
|
|
89
93
|
def _conversations(self) -> List[Dict[str, Any]]:
|
|
94
|
+
if self._conversation_store is not None:
|
|
95
|
+
try:
|
|
96
|
+
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
|
97
|
+
for item in self._conversation_store.history():
|
|
98
|
+
grouped.setdefault(item.get("conversation_id") or "legacy-previous-history", []).append(item)
|
|
99
|
+
return [{"id": conv_id, "messages": msgs} for conv_id, msgs in grouped.items()]
|
|
100
|
+
except Exception:
|
|
101
|
+
return []
|
|
90
102
|
if not self._history_file.exists():
|
|
91
103
|
return []
|
|
92
104
|
try:
|
|
@@ -130,7 +142,10 @@ class MemoryService:
|
|
|
130
142
|
|
|
131
143
|
ws_bytes = _file_size(self._data_dir / "workspace_os.json")
|
|
132
144
|
kg_bytes = _file_size(self._data_dir / "knowledge_graph.sqlite")
|
|
133
|
-
|
|
145
|
+
if self._conversation_store is not None:
|
|
146
|
+
conv_bytes = int(getattr(self._conversation_store, "size_bytes", lambda: 0)())
|
|
147
|
+
else:
|
|
148
|
+
conv_bytes = _file_size(self._history_file)
|
|
134
149
|
|
|
135
150
|
node_total = sum((kg_stats or {}).get("nodes", {}).values()) if kg_stats else None
|
|
136
151
|
edge_total = sum((kg_stats or {}).get("edges", {}).values()) if kg_stats else None
|
|
@@ -159,7 +174,7 @@ class MemoryService:
|
|
|
159
174
|
{
|
|
160
175
|
"id": "conversation", "type": "conversation", "label": "Conversation Memory",
|
|
161
176
|
"count": len(convs), "size_bytes": conv_bytes,
|
|
162
|
-
"health": "ok" if self._history_file.exists() else "empty",
|
|
177
|
+
"health": "ok" if (self._conversation_store is not None or self._history_file.exists()) else "empty",
|
|
163
178
|
"detail": "Historical interaction memory from chat.",
|
|
164
179
|
},
|
|
165
180
|
{
|
|
@@ -202,6 +217,18 @@ class MemoryService:
|
|
|
202
217
|
limit: int = 20,
|
|
203
218
|
) -> Dict[str, Any]:
|
|
204
219
|
q = str(query or "").strip()
|
|
220
|
+
query_tokens = [tok for tok in q.lower().split() if tok]
|
|
221
|
+
|
|
222
|
+
def _lexical_score(*texts: Any) -> float:
|
|
223
|
+
# Honest, comparable relevance: fraction of query tokens present.
|
|
224
|
+
# Both tiers share this scorer so the cross-tier ranking is real,
|
|
225
|
+
# not an artifact of per-tier constants.
|
|
226
|
+
if not query_tokens:
|
|
227
|
+
return 0.0
|
|
228
|
+
haystack = " ".join(str(t or "") for t in texts).lower()
|
|
229
|
+
hits = sum(1 for tok in query_tokens if tok in haystack)
|
|
230
|
+
return round(hits / len(query_tokens), 4)
|
|
231
|
+
|
|
205
232
|
results: List[Dict[str, Any]] = []
|
|
206
233
|
|
|
207
234
|
try:
|
|
@@ -215,23 +242,24 @@ class MemoryService:
|
|
|
215
242
|
"title": (m.get("kind") or "memory"),
|
|
216
243
|
"snippet": str(m.get("content") or "")[:240],
|
|
217
244
|
"kind": m.get("kind"),
|
|
218
|
-
"score":
|
|
245
|
+
"score": _lexical_score(m.get("content"), " ".join(m.get("tags") or []), m.get("kind")),
|
|
219
246
|
"tags": m.get("tags") or [],
|
|
220
247
|
})
|
|
221
248
|
|
|
222
249
|
if self._enable_graph and q:
|
|
223
250
|
try:
|
|
224
|
-
|
|
251
|
+
# KnowledgeGraph.search returns {"query": ..., "matches": [...]}.
|
|
252
|
+
hits = self._kg.search(q, limit).get("matches", [])
|
|
225
253
|
except Exception:
|
|
226
254
|
hits = []
|
|
227
|
-
for
|
|
255
|
+
for hit in hits[:limit]:
|
|
228
256
|
results.append({
|
|
229
257
|
"source": "graph",
|
|
230
|
-
"id":
|
|
231
|
-
"title":
|
|
232
|
-
"snippet": str(
|
|
233
|
-
"kind":
|
|
234
|
-
"score":
|
|
258
|
+
"id": hit.get("id") or hit.get("node_id"),
|
|
259
|
+
"title": hit.get("title") or hit.get("name") or "node",
|
|
260
|
+
"snippet": str(hit.get("summary") or hit.get("content") or "")[:240],
|
|
261
|
+
"kind": hit.get("type") or "node",
|
|
262
|
+
"score": _lexical_score(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content")),
|
|
235
263
|
})
|
|
236
264
|
|
|
237
265
|
results.sort(key=lambda r: r.get("score", 0), reverse=True)
|
|
@@ -18,7 +18,6 @@ import re
|
|
|
18
18
|
import shutil
|
|
19
19
|
import subprocess
|
|
20
20
|
import sys
|
|
21
|
-
import tempfile
|
|
22
21
|
import threading
|
|
23
22
|
import time
|
|
24
23
|
import urllib.error
|
|
@@ -26,16 +25,14 @@ import urllib.request
|
|
|
26
25
|
from pathlib import Path
|
|
27
26
|
from typing import AsyncIterator, Dict, List, Optional
|
|
28
27
|
|
|
29
|
-
import httpx
|
|
30
28
|
from fastapi import HTTPException, Request
|
|
31
29
|
|
|
32
|
-
from
|
|
30
|
+
from latticeai.models.router import (
|
|
33
31
|
AsyncOpenAI,
|
|
34
32
|
HF_MODELS_ROOT,
|
|
35
33
|
OPENAI_COMPATIBLE_PROVIDERS,
|
|
36
34
|
ensure_mlx_runtime,
|
|
37
35
|
hf_model_dir,
|
|
38
|
-
normalize_branding,
|
|
39
36
|
parse_model_ref,
|
|
40
37
|
)
|
|
41
38
|
from latticeai.core.model_compat import (
|
|
@@ -89,7 +86,7 @@ def configure_model_runtime(**deps) -> None:
|
|
|
89
86
|
# Catalog data + version-dedup helpers live in ``model_catalog``; re-exported
|
|
90
87
|
# here so existing ``from ...model_runtime import ENGINE_MODEL_CATALOG`` imports
|
|
91
88
|
# keep working.
|
|
92
|
-
from latticeai.services.model_catalog import ( # noqa: F401
|
|
89
|
+
from latticeai.services.model_catalog import ( # noqa: E402, F401 (re-export after the module globals it documents)
|
|
93
90
|
ENGINE_INSTALLERS,
|
|
94
91
|
ENGINE_MODEL_CATALOG,
|
|
95
92
|
MODEL_ENGINE_ALIASES,
|
|
@@ -18,8 +18,9 @@ from typing import Any, Callable, Dict, Optional, Set
|
|
|
18
18
|
from fastapi import HTTPException, Request
|
|
19
19
|
|
|
20
20
|
from latticeai.core.hooks import dispatch_tool
|
|
21
|
-
from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner
|
|
22
|
-
from latticeai.core.workflow_engine import WorkflowEngine
|
|
21
|
+
from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner, llm_role_runner
|
|
22
|
+
from latticeai.core.workflow_engine import ApprovalRequired, WorkflowEngine
|
|
23
|
+
from tools import execute_tool
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class PlatformRuntime:
|
|
@@ -34,6 +35,9 @@ class PlatformRuntime:
|
|
|
34
35
|
workspace_scope_from_request: Callable[[Request], Optional[str]],
|
|
35
36
|
get_tool_permission: Callable[..., Dict[str, Any]],
|
|
36
37
|
hooks: Any = None,
|
|
38
|
+
llm_generate: Optional[Callable[..., str]] = None,
|
|
39
|
+
llm_available: Optional[Callable[[], bool]] = None,
|
|
40
|
+
agent_registry: Any = None,
|
|
37
41
|
):
|
|
38
42
|
self.store = store
|
|
39
43
|
self.svc = workspace_service
|
|
@@ -45,6 +49,12 @@ class PlatformRuntime:
|
|
|
45
49
|
# Lifecycle hooks registry — wires the workflow runtime + workflow tool
|
|
46
50
|
# nodes into the same pre_*/post_* lifecycle as the HTTP + agent paths.
|
|
47
51
|
self.hooks = hooks
|
|
52
|
+
# v4 (T7b): a synchronous model bridge. When a model is loaded,
|
|
53
|
+
# build_orchestrator returns the REAL (mode='llm') runner; otherwise
|
|
54
|
+
# the deterministic runner, honestly labeled mode='simulation'.
|
|
55
|
+
self.llm_generate = llm_generate
|
|
56
|
+
self.llm_available = llm_available or (lambda: False)
|
|
57
|
+
self.agent_registry = agent_registry
|
|
48
58
|
|
|
49
59
|
# ── request gating ────────────────────────────────────────────────────
|
|
50
60
|
|
|
@@ -77,31 +87,57 @@ class PlatformRuntime:
|
|
|
77
87
|
# ── shared node runners ───────────────────────────────────────────────
|
|
78
88
|
|
|
79
89
|
def _tool_node_runner(self):
|
|
80
|
-
"""Workflow tool node:
|
|
81
|
-
|
|
90
|
+
"""Workflow tool node: EXECUTES the tool under governance (v4).
|
|
91
|
+
|
|
92
|
+
Auto-approve tools run immediately through the shared dispatch_tool
|
|
93
|
+
lifecycle. Tools whose policy requires approval raise
|
|
94
|
+
:class:`ApprovalRequired` so the engine pauses the run into
|
|
95
|
+
``awaiting_approval`` — never a silent ``{recorded: true}`` success,
|
|
96
|
+
never an unapproved execution. A resumed run carries the approved
|
|
97
|
+
node id in ``context['__approved_nodes__']``.
|
|
98
|
+
"""
|
|
82
99
|
def runner(*, node, context):
|
|
83
100
|
cfg = node.get("config") or {}
|
|
84
101
|
name = cfg.get("tool") or ""
|
|
85
102
|
args = cfg.get("args") or {}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
if not name:
|
|
104
|
+
raise ValueError("tool node has no tool configured")
|
|
105
|
+
try:
|
|
106
|
+
permission = dict(self.get_tool_permission(name, args))
|
|
107
|
+
except TypeError:
|
|
108
|
+
permission = dict(self.get_tool_permission(name))
|
|
109
|
+
approved_nodes = set(context.get("__approved_nodes__") or [])
|
|
110
|
+
if permission.get("requires_approval") and node.get("id") not in approved_nodes:
|
|
111
|
+
raise ApprovalRequired(
|
|
112
|
+
f"tool '{name}' requires explicit approval before a workflow may run it",
|
|
113
|
+
tool=name, args=args, permission=permission,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _execute():
|
|
117
|
+
return execute_tool(name, args)
|
|
93
118
|
|
|
94
119
|
# Same tool lifecycle as the HTTP + agent paths (a pre_tool block
|
|
95
120
|
# raises PermissionError, surfaced as the node error by the engine).
|
|
96
|
-
|
|
121
|
+
result = dispatch_tool(self.hooks, name, args, _execute, source="workflow")
|
|
122
|
+
return {"tool": name, "args": args, "executed": True,
|
|
123
|
+
"permission": permission, "result": result}
|
|
97
124
|
return runner
|
|
98
125
|
|
|
99
126
|
def _skill_node_runner(self):
|
|
127
|
+
"""Skill nodes refuse honestly: a skill is an instruction package for
|
|
128
|
+
an LLM; without a model-driven executor there is nothing to run, and
|
|
129
|
+
pretending otherwise (the pre-v4 existence check that reported 'ok')
|
|
130
|
+
is exactly the fake functionality v4 bans."""
|
|
100
131
|
def runner(*, node, context):
|
|
101
132
|
cfg = node.get("config") or {}
|
|
102
133
|
name = cfg.get("skill") or ""
|
|
103
134
|
entry = self.store.load_state().get("skill_registry", {}).get(name) or {}
|
|
104
|
-
|
|
135
|
+
if not entry:
|
|
136
|
+
raise ValueError(f"skill '{name}' is not installed")
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"skill '{name}' requires LLM-driven execution, which workflow "
|
|
139
|
+
"skill nodes do not provide in this build — refusing to fake a result"
|
|
140
|
+
)
|
|
105
141
|
return runner
|
|
106
142
|
|
|
107
143
|
def _context_provider(self, user, scope):
|
|
@@ -116,15 +152,25 @@ class PlatformRuntime:
|
|
|
116
152
|
def plugin_capability_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
|
|
117
153
|
"""Runners the Plugin SDK boundary dispatches to (one per capability)."""
|
|
118
154
|
def run_skill(*, plugin_id, action, args, manifest):
|
|
119
|
-
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
f"plugin '{plugin_id}' skill execution requires an LLM-driven "
|
|
157
|
+
"runner, which this build does not provide — refusing to fake a result"
|
|
158
|
+
)
|
|
120
159
|
|
|
121
160
|
def run_tool(*, plugin_id, action, args, manifest):
|
|
122
161
|
tool = args.get("tool") or (manifest.provides.get("tools") or [None])[0]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
162
|
+
if not tool:
|
|
163
|
+
raise ValueError(f"plugin '{plugin_id}' run_tool needs a tool name")
|
|
164
|
+
permission = dict(self.get_tool_permission(tool))
|
|
165
|
+
if permission.get("requires_approval"):
|
|
166
|
+
raise ApprovalRequired(
|
|
167
|
+
f"plugin tool '{tool}' requires explicit approval",
|
|
168
|
+
tool=tool, args=args, permission=permission,
|
|
169
|
+
)
|
|
170
|
+
result = dispatch_tool(self.hooks, tool, args, lambda: execute_tool(tool, args),
|
|
171
|
+
source=f"plugin:{plugin_id}")
|
|
172
|
+
return {"plugin": plugin_id, "tool": tool, "permission": permission,
|
|
173
|
+
"executed": True, "result": result}
|
|
128
174
|
|
|
129
175
|
def run_workflow(*, plugin_id, action, args, manifest):
|
|
130
176
|
wf_id = args.get("workflow_id")
|
|
@@ -175,6 +221,9 @@ class PlatformRuntime:
|
|
|
175
221
|
workflow_id=workflow_id, name=workflow.get("name") or "workflow",
|
|
176
222
|
status=result.status, timeline=result.timeline, outputs=result.outputs,
|
|
177
223
|
user_email=user, graph=self.workspace_graph(), workspace_id=scope,
|
|
224
|
+
mode="live",
|
|
225
|
+
pause={"node": result.paused_node, "pending": result.pending_approval,
|
|
226
|
+
"context": result.paused_context} if result.status == "awaiting_approval" else None,
|
|
178
227
|
)
|
|
179
228
|
return {"workflow_run_id": run["id"], "status": result.status}
|
|
180
229
|
|
|
@@ -209,8 +258,36 @@ class PlatformRuntime:
|
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
|
|
261
|
+
workflow_runner = lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs) # noqa: E731
|
|
262
|
+
plugin_runner = lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope).as_dict() # noqa: E731
|
|
263
|
+
context_provider = self._context_provider(user, scope)
|
|
264
|
+
custom_agents = {}
|
|
265
|
+
if self.agent_registry is not None:
|
|
266
|
+
try:
|
|
267
|
+
custom_agents = {
|
|
268
|
+
a["id"]: a for a in self.agent_registry.all()
|
|
269
|
+
if str(a.get("id", "")).startswith("agent:custom:") and a.get("enabled", True)
|
|
270
|
+
}
|
|
271
|
+
except Exception:
|
|
272
|
+
custom_agents = {}
|
|
273
|
+
if self.llm_generate is not None and self.llm_available():
|
|
274
|
+
from latticeai.core.agent_prompts import CRITIC_PROMPT, PLANNER_PROMPT
|
|
275
|
+
|
|
276
|
+
return MultiAgentOrchestrator(
|
|
277
|
+
role_runner=llm_role_runner(
|
|
278
|
+
generate=self.llm_generate,
|
|
279
|
+
planner_prompt=PLANNER_PROMPT,
|
|
280
|
+
critic_prompt=CRITIC_PROMPT,
|
|
281
|
+
context_provider=context_provider,
|
|
282
|
+
workflow_runner=workflow_runner,
|
|
283
|
+
plugin_runner=plugin_runner,
|
|
284
|
+
custom_agents=custom_agents,
|
|
285
|
+
),
|
|
286
|
+
mode="llm",
|
|
287
|
+
custom_agents=custom_agents,
|
|
288
|
+
)
|
|
212
289
|
return MultiAgentOrchestrator(role_runner=default_role_runner(
|
|
213
|
-
workflow_runner=
|
|
214
|
-
plugin_runner=
|
|
215
|
-
context_provider=
|
|
216
|
-
))
|
|
290
|
+
workflow_runner=workflow_runner,
|
|
291
|
+
plugin_runner=plugin_runner,
|
|
292
|
+
context_provider=context_provider,
|
|
293
|
+
), mode="simulation", custom_agents=custom_agents)
|
|
@@ -7,7 +7,7 @@ keyword search into UI-ready contracts without tying routers to store internals.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Dict,
|
|
10
|
+
from typing import Any, Dict, Mapping, Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
DEFAULT_HYBRID_WEIGHTS = {
|
|
@@ -34,7 +34,15 @@ class SearchService:
|
|
|
34
34
|
raise ValueError("knowledge graph is disabled")
|
|
35
35
|
return self.graph_store
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def _scope(self, matches, allowed_workspaces):
|
|
38
|
+
"""Drop matches scoped to workspaces the caller is not a member of
|
|
39
|
+
(None = no scoping; legacy-global rows stay visible — documented)."""
|
|
40
|
+
if allowed_workspaces is None:
|
|
41
|
+
return matches
|
|
42
|
+
graph = self._require_graph()
|
|
43
|
+
return graph.filter_scoped_nodes(matches, allowed_workspaces)
|
|
44
|
+
|
|
45
|
+
def keyword_search(self, query: str, *, limit: int = 30, allowed_workspaces=None) -> Dict[str, Any]:
|
|
38
46
|
graph = self._require_graph()
|
|
39
47
|
payload = graph.search(query, limit)
|
|
40
48
|
matches = []
|
|
@@ -53,9 +61,9 @@ class SearchService:
|
|
|
53
61
|
"metadata": match.get("metadata") or {},
|
|
54
62
|
"updated_at": match.get("updated_at"),
|
|
55
63
|
})
|
|
56
|
-
return {"query": query, "mode": "keyword", "matches": matches}
|
|
64
|
+
return {"query": query, "mode": "keyword", "matches": self._scope(matches, allowed_workspaces)}
|
|
57
65
|
|
|
58
|
-
def vector_search(self, query: str, *, limit: int = 30, min_score: float = 0.0) -> Dict[str, Any]:
|
|
66
|
+
def vector_search(self, query: str, *, limit: int = 30, min_score: float = 0.0, allowed_workspaces=None) -> Dict[str, Any]:
|
|
59
67
|
graph = self._require_graph()
|
|
60
68
|
payload = graph.vector_search(query, limit=limit, min_score=min_score)
|
|
61
69
|
matches = []
|
|
@@ -80,10 +88,10 @@ class SearchService:
|
|
|
80
88
|
"mode": "vector",
|
|
81
89
|
"embedding_model": payload.get("embedding_model"),
|
|
82
90
|
"embedding_dim": payload.get("embedding_dim"),
|
|
83
|
-
"matches": matches,
|
|
91
|
+
"matches": self._scope(matches, allowed_workspaces),
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
def graph_search(self, query: str, *, limit: int = 30, expand_depth: int = 1) -> Dict[str, Any]:
|
|
94
|
+
def graph_search(self, query: str, *, limit: int = 30, expand_depth: int = 1, allowed_workspaces=None) -> Dict[str, Any]:
|
|
87
95
|
graph = self._require_graph()
|
|
88
96
|
limit = max(1, min(int(limit or 30), 100))
|
|
89
97
|
expand_depth = max(0, min(int(expand_depth or 1), 3))
|
|
@@ -157,7 +165,7 @@ class SearchService:
|
|
|
157
165
|
match["rank"] = rank
|
|
158
166
|
match["score"] = round(float(match["score"]), 6)
|
|
159
167
|
match["source_scores"]["graph"] = round(float(match["source_scores"]["graph"]), 6)
|
|
160
|
-
return {"query": query, "mode": "graph", "expand_depth": expand_depth, "matches": matches}
|
|
168
|
+
return {"query": query, "mode": "graph", "expand_depth": expand_depth, "matches": self._scope(matches, allowed_workspaces)}
|
|
161
169
|
|
|
162
170
|
def hybrid_search(
|
|
163
171
|
self,
|
|
@@ -168,6 +176,7 @@ class SearchService:
|
|
|
168
176
|
vector_limit: int = 30,
|
|
169
177
|
graph_limit: int = 30,
|
|
170
178
|
weights: Optional[Mapping[str, float]] = None,
|
|
179
|
+
allowed_workspaces=None,
|
|
171
180
|
) -> Dict[str, Any]:
|
|
172
181
|
weights = {**DEFAULT_HYBRID_WEIGHTS, **dict(weights or {})}
|
|
173
182
|
channels = {
|
|
@@ -202,7 +211,7 @@ class SearchService:
|
|
|
202
211
|
current.setdefault("graph_context", [])
|
|
203
212
|
current["graph_context"].extend(result.get("graph_context") or [])
|
|
204
213
|
|
|
205
|
-
matches = sorted(fused.values(), key=lambda item: item["score"], reverse=True)[: max(1, min(limit, 100))]
|
|
214
|
+
matches = self._scope(sorted(fused.values(), key=lambda item: item["score"], reverse=True), allowed_workspaces)[: max(1, min(limit, 100))]
|
|
206
215
|
for rank, match in enumerate(matches, start=1):
|
|
207
216
|
match["rank"] = rank
|
|
208
217
|
match["score"] = round(float(match["score"]), 6)
|
|
@@ -22,8 +22,16 @@ from latticeai.core.tool_registry import ToolPermission, ToolPolicy
|
|
|
22
22
|
from tools import AGENT_ROOT, DEFAULT_TOOL_REGISTRY, ToolError, ensure_agent_root
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
def _default_load_users() -> Dict[str, Any]:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _default_get_user_role(_email, _users=None) -> str:
|
|
30
|
+
return "user"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_load_users: Callable[[], Dict[str, Any]] = _default_load_users
|
|
34
|
+
_get_user_role: Callable[..., str] = _default_get_user_role
|
|
27
35
|
|
|
28
36
|
FILE_CREATE_ACTIONS = set(DEFAULT_TOOL_REGISTRY.file_create_actions)
|
|
29
37
|
TOOL_GOVERNANCE: Dict[str, ToolPolicy] = dict(DEFAULT_TOOL_REGISTRY.governance)
|
|
@@ -104,6 +112,7 @@ def build_agent_runtime(
|
|
|
104
112
|
knowledge_save: Callable[..., Dict[str, Any]],
|
|
105
113
|
audit: Callable[..., None],
|
|
106
114
|
hooks: Any = None,
|
|
115
|
+
brain_memory: Any = None,
|
|
107
116
|
) -> AgentRuntime:
|
|
108
117
|
ensure_agent_root()
|
|
109
118
|
deps = AgentDeps(
|
|
@@ -125,6 +134,7 @@ def build_agent_runtime(
|
|
|
125
134
|
memory_updater_prompt=MEMORY_UPDATER_PROMPT,
|
|
126
135
|
agent_root=AGENT_ROOT,
|
|
127
136
|
hooks=hooks,
|
|
137
|
+
brain_memory=brain_memory,
|
|
128
138
|
)
|
|
129
139
|
return AgentRuntime(deps)
|
|
130
140
|
|