ltcai 1.6.0 → 2.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 +40 -19
- package/docs/CHANGELOG.md +107 -0
- package/docs/EDITION_STRATEGY.md +14 -4
- package/docs/ENTERPRISE.md +11 -3
- package/docs/MULTI_AGENT_RUNTIME.md +410 -0
- package/docs/PLUGIN_SDK.md +651 -0
- package/docs/REALTIME_COLLABORATION.md +410 -0
- package/docs/V2_ARCHITECTURE.md +528 -0
- package/docs/WORKFLOW_DESIGNER.md +475 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +98 -0
- package/latticeai/api/plugins.py +115 -0
- package/latticeai/api/realtime.py +91 -0
- package/latticeai/api/workflow_designer.py +207 -0
- package/latticeai/core/multi_agent.py +270 -0
- package/latticeai/core/plugins.py +400 -0
- package/latticeai/core/realtime.py +190 -0
- package/latticeai/core/workflow_engine.py +329 -0
- package/latticeai/core/workspace_os.py +165 -2
- package/latticeai/server_app.py +76 -2
- package/latticeai/services/platform_runtime.py +200 -0
- package/package.json +17 -2
- package/plugins/README.md +35 -0
- package/plugins/git-insights/plugin.json +15 -0
- package/plugins/hello-world/plugin.json +16 -0
- package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
- package/static/activity.html +70 -0
- package/static/admin.html +62 -0
- package/static/agents.html +92 -0
- package/static/graph.html +7 -1
- package/static/lattice-reference.css +184 -0
- package/static/platform.css +75 -0
- package/static/plugins.html +82 -0
- package/static/scripts/admin.js +121 -1
- package/static/scripts/graph.js +296 -14
- package/static/scripts/platform.js +64 -0
- package/static/scripts/workspace.js +107 -10
- package/static/workflows.html +121 -0
- package/static/workspace.css +73 -0
- package/static/workspace.html +18 -2
package/latticeai/server_app.py
CHANGED
|
@@ -91,8 +91,16 @@ from latticeai.services.model_runtime import (
|
|
|
91
91
|
verify_cloud_models,
|
|
92
92
|
ensure_ollama_server,
|
|
93
93
|
)
|
|
94
|
-
from latticeai.api.workspace import create_workspace_router
|
|
94
|
+
from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
|
|
95
95
|
from latticeai.api.health import create_health_router
|
|
96
|
+
# ── v2.0 Agentic Workspace Platform layers ───────────────────────────────────
|
|
97
|
+
from latticeai.core.plugins import PluginRegistry
|
|
98
|
+
from latticeai.core.realtime import RealtimeBus
|
|
99
|
+
from latticeai.services.platform_runtime import PlatformRuntime
|
|
100
|
+
from latticeai.api.plugins import create_plugins_router
|
|
101
|
+
from latticeai.api.workflow_designer import create_workflow_designer_router
|
|
102
|
+
from latticeai.api.agents import create_agents_router
|
|
103
|
+
from latticeai.api.realtime import create_realtime_router
|
|
96
104
|
from latticeai.api.models import create_models_router
|
|
97
105
|
from latticeai.api.chat import create_chat_router
|
|
98
106
|
from latticeai.api.tools import create_tools_router
|
|
@@ -236,10 +244,16 @@ AUDIT_FILE = DATA_DIR / "audit_log.json"
|
|
|
236
244
|
SSO_FILE = DATA_DIR / "sso_config.json"
|
|
237
245
|
KNOWLEDGE_GRAPH = KnowledgeGraphStore(DATA_DIR / "knowledge_graph.sqlite", DATA_DIR / "knowledge_graph_blobs") if ENABLE_GRAPH else None
|
|
238
246
|
LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
|
|
239
|
-
|
|
247
|
+
# ── v2.0 Realtime bus: constructed first so the store can fan every timeline
|
|
248
|
+
# event into the realtime feed via a single additive sink (no per-call wiring).
|
|
249
|
+
REALTIME_BUS = RealtimeBus()
|
|
250
|
+
WORKSPACE_OS = WorkspaceOSStore(DATA_DIR, event_sink=REALTIME_BUS)
|
|
240
251
|
# Service layer (latticeai.services) wraps the store with scope/permission
|
|
241
252
|
# guardrails; routers and the app assembly share this single instance.
|
|
242
253
|
WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS)
|
|
254
|
+
# ── v2.0 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
|
|
255
|
+
PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
|
|
256
|
+
PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
|
|
243
257
|
|
|
244
258
|
def _require_graph():
|
|
245
259
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -1187,6 +1201,66 @@ app.include_router(create_workspace_router(
|
|
|
1187
1201
|
))
|
|
1188
1202
|
|
|
1189
1203
|
|
|
1204
|
+
# ── v2.0 Agentic Workspace Platform: cross-system wiring ─────────────────────
|
|
1205
|
+
# All cross-subsystem closures live in latticeai.services.platform_runtime to
|
|
1206
|
+
# keep this assembly file lean; server_app only constructs it and mounts routers.
|
|
1207
|
+
PLATFORM = PlatformRuntime(
|
|
1208
|
+
store=WORKSPACE_OS,
|
|
1209
|
+
workspace_service=WORKSPACE_SERVICE,
|
|
1210
|
+
plugin_registry=PLUGIN_REGISTRY,
|
|
1211
|
+
get_current_user=get_current_user,
|
|
1212
|
+
workspace_graph=_workspace_graph,
|
|
1213
|
+
workspace_scope_from_request=_workspace_scope_from_request,
|
|
1214
|
+
get_tool_permission=get_tool_permission,
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
app.include_router(create_plugins_router(
|
|
1218
|
+
registry=PLUGIN_REGISTRY,
|
|
1219
|
+
require_user=require_user,
|
|
1220
|
+
require_admin=require_admin,
|
|
1221
|
+
append_audit_event=append_audit_event,
|
|
1222
|
+
register_skill=PLATFORM.register_plugin_skill,
|
|
1223
|
+
plugin_runners_factory=lambda: PLATFORM.plugin_capability_runners(None, None),
|
|
1224
|
+
ui_file_response=ui_file_response,
|
|
1225
|
+
static_dir=STATIC_DIR,
|
|
1226
|
+
))
|
|
1227
|
+
|
|
1228
|
+
app.include_router(create_workflow_designer_router(
|
|
1229
|
+
store=WORKSPACE_OS,
|
|
1230
|
+
require_user=require_user,
|
|
1231
|
+
get_current_user=get_current_user,
|
|
1232
|
+
gate_read=PLATFORM.gate_read,
|
|
1233
|
+
gate_write=PLATFORM.gate_write,
|
|
1234
|
+
workspace_graph=_workspace_graph,
|
|
1235
|
+
build_runners=PLATFORM.build_workflow_runners,
|
|
1236
|
+
append_audit_event=append_audit_event,
|
|
1237
|
+
ui_file_response=ui_file_response,
|
|
1238
|
+
static_dir=STATIC_DIR,
|
|
1239
|
+
))
|
|
1240
|
+
|
|
1241
|
+
app.include_router(create_agents_router(
|
|
1242
|
+
store=WORKSPACE_OS,
|
|
1243
|
+
orchestrator_factory=PLATFORM.build_orchestrator,
|
|
1244
|
+
require_user=require_user,
|
|
1245
|
+
get_current_user=get_current_user,
|
|
1246
|
+
gate_read=PLATFORM.gate_read,
|
|
1247
|
+
gate_write=PLATFORM.gate_write,
|
|
1248
|
+
workspace_graph=_workspace_graph,
|
|
1249
|
+
append_audit_event=append_audit_event,
|
|
1250
|
+
ui_file_response=ui_file_response,
|
|
1251
|
+
static_dir=STATIC_DIR,
|
|
1252
|
+
))
|
|
1253
|
+
|
|
1254
|
+
app.include_router(create_realtime_router(
|
|
1255
|
+
bus=REALTIME_BUS,
|
|
1256
|
+
require_user=require_user,
|
|
1257
|
+
get_current_user=get_current_user,
|
|
1258
|
+
allowed_scopes=PLATFORM.allowed_scopes,
|
|
1259
|
+
ui_file_response=ui_file_response,
|
|
1260
|
+
static_dir=STATIC_DIR,
|
|
1261
|
+
))
|
|
1262
|
+
|
|
1263
|
+
|
|
1190
1264
|
# ── Health & Info ──────────────────────────────────────────────────────────────
|
|
1191
1265
|
|
|
1192
1266
|
# ── Model runtime/provider helpers moved to latticeai.services.model_runtime ──
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""v2.0 Agentic Workspace Platform runtime — cross-system wiring.
|
|
2
|
+
|
|
3
|
+
This is the single place the four v2.0 subsystems (Plugin SDK, Workflow
|
|
4
|
+
Designer, Multi-Agent Runtime, Realtime) connect to one another and to the
|
|
5
|
+
workspace. Keeping it out of ``server_app`` honours the AGENTS.md preference for
|
|
6
|
+
small, composable modules and keeps the wiring independently testable.
|
|
7
|
+
|
|
8
|
+
Recursion is bounded by construction: a workflow's ``agent`` node runs an
|
|
9
|
+
orchestrator *without* a workflow runner, and an orchestrator's workflow runner
|
|
10
|
+
runs an engine *without* an ``agent`` runner — so the deepest chains are
|
|
11
|
+
``agent → workflow → (tool|skill|plugin)`` and ``workflow → agent → plugin``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Callable, Dict, Optional, Set
|
|
17
|
+
|
|
18
|
+
from fastapi import HTTPException, Request
|
|
19
|
+
|
|
20
|
+
from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner
|
|
21
|
+
from latticeai.core.workflow_engine import WorkflowEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatformRuntime:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
store,
|
|
29
|
+
workspace_service,
|
|
30
|
+
plugin_registry,
|
|
31
|
+
get_current_user: Callable[[Request], Optional[str]],
|
|
32
|
+
workspace_graph: Callable[[], Any],
|
|
33
|
+
workspace_scope_from_request: Callable[[Request], Optional[str]],
|
|
34
|
+
get_tool_permission: Callable[..., Dict[str, Any]],
|
|
35
|
+
):
|
|
36
|
+
self.store = store
|
|
37
|
+
self.svc = workspace_service
|
|
38
|
+
self.registry = plugin_registry
|
|
39
|
+
self.get_current_user = get_current_user
|
|
40
|
+
self.workspace_graph = workspace_graph
|
|
41
|
+
self.scope_from_request = workspace_scope_from_request
|
|
42
|
+
self.get_tool_permission = get_tool_permission
|
|
43
|
+
|
|
44
|
+
# ── request gating ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def gate_read(self, request: Request) -> Optional[str]:
|
|
47
|
+
try:
|
|
48
|
+
return self.svc.resolve_read_scope(self.scope_from_request(request), self.get_current_user(request))
|
|
49
|
+
except PermissionError as exc:
|
|
50
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
51
|
+
|
|
52
|
+
def gate_write(self, request: Request) -> Optional[str]:
|
|
53
|
+
try:
|
|
54
|
+
return self.svc.resolve_write_scope(self.scope_from_request(request), self.get_current_user(request))
|
|
55
|
+
except PermissionError as exc:
|
|
56
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
57
|
+
|
|
58
|
+
def allowed_scopes(self, user: Optional[str]) -> Optional[Set[str]]:
|
|
59
|
+
try:
|
|
60
|
+
workspaces = self.svc.list_workspaces(user or None).get("workspaces", [])
|
|
61
|
+
return {ws.get("workspace_id") for ws in workspaces if ws.get("workspace_id")}
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# ── plugin lifecycle hooks ────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def register_plugin_skill(self, skill_name: str, plugin_id: str):
|
|
68
|
+
return self.store.mark_skill_installed(
|
|
69
|
+
skill_name, version=f"plugin:{plugin_id}", metadata={"source": f"plugin:{plugin_id}"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ── shared node runners ───────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def _tool_node_runner(self):
|
|
75
|
+
"""Workflow tool node: records the invocation + governance decision but
|
|
76
|
+
never silently executes exec/destructive tools (those need approval)."""
|
|
77
|
+
def runner(*, node, context):
|
|
78
|
+
cfg = node.get("config") or {}
|
|
79
|
+
name = cfg.get("tool") or ""
|
|
80
|
+
try:
|
|
81
|
+
permission = dict(self.get_tool_permission(name))
|
|
82
|
+
except Exception:
|
|
83
|
+
permission = {"tool": name, "risk": "unknown"}
|
|
84
|
+
return {"tool": name, "args": cfg.get("args") or {}, "recorded": True, "permission": permission}
|
|
85
|
+
return runner
|
|
86
|
+
|
|
87
|
+
def _skill_node_runner(self):
|
|
88
|
+
def runner(*, node, context):
|
|
89
|
+
cfg = node.get("config") or {}
|
|
90
|
+
name = cfg.get("skill") or ""
|
|
91
|
+
entry = self.store.load_state().get("skill_registry", {}).get(name) or {}
|
|
92
|
+
return {"skill": name, "found": bool(entry), "enabled": bool(entry.get("enabled"))}
|
|
93
|
+
return runner
|
|
94
|
+
|
|
95
|
+
def _context_provider(self, user, scope):
|
|
96
|
+
def provider(goal: str):
|
|
97
|
+
try:
|
|
98
|
+
mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
|
|
99
|
+
return [str(m.get("content") or "")[:160] for m in mems[:5]]
|
|
100
|
+
except Exception:
|
|
101
|
+
return []
|
|
102
|
+
return provider
|
|
103
|
+
|
|
104
|
+
def plugin_capability_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
|
|
105
|
+
"""Runners the Plugin SDK boundary dispatches to (one per capability)."""
|
|
106
|
+
def run_skill(*, plugin_id, action, args, manifest):
|
|
107
|
+
return {"plugin": plugin_id, "ran_skills": manifest.provides.get("skills", [])}
|
|
108
|
+
|
|
109
|
+
def run_tool(*, plugin_id, action, args, manifest):
|
|
110
|
+
tool = args.get("tool") or (manifest.provides.get("tools") or [None])[0]
|
|
111
|
+
try:
|
|
112
|
+
permission = dict(self.get_tool_permission(tool)) if tool else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
permission = {}
|
|
115
|
+
return {"plugin": plugin_id, "tool": tool, "permission": permission, "recorded": True}
|
|
116
|
+
|
|
117
|
+
def run_workflow(*, plugin_id, action, args, manifest):
|
|
118
|
+
wf_id = args.get("workflow_id")
|
|
119
|
+
if not wf_id:
|
|
120
|
+
return {"plugin": plugin_id, "skipped": "no workflow_id"}
|
|
121
|
+
return self.run_workflow_by_id(wf_id, user, scope, with_agent=False, inputs=args.get("inputs"))
|
|
122
|
+
|
|
123
|
+
def run_agent(*, plugin_id, action, args, manifest):
|
|
124
|
+
goal = args.get("goal") or f"Plugin {plugin_id} agent task"
|
|
125
|
+
return self.run_agent(goal, user, scope, with_workflow=False, inputs=args.get("inputs"))
|
|
126
|
+
|
|
127
|
+
return {"skills": run_skill, "tools": run_tool, "workflows": run_workflow, "agents": run_agent}
|
|
128
|
+
|
|
129
|
+
def _plugin_node_runner(self, user, scope):
|
|
130
|
+
def runner(*, node, context):
|
|
131
|
+
cfg = node.get("config") or {}
|
|
132
|
+
plugin_id = cfg.get("plugin_id") or cfg.get("plugin") or ""
|
|
133
|
+
action = cfg.get("action") or "run_skill"
|
|
134
|
+
result = self.registry.execute_action(
|
|
135
|
+
plugin_id, action, cfg.get("args") or {}, runners=self.plugin_capability_runners(user, scope)
|
|
136
|
+
)
|
|
137
|
+
return result.as_dict()
|
|
138
|
+
return runner
|
|
139
|
+
|
|
140
|
+
def _agent_node_runner(self, user, scope):
|
|
141
|
+
def runner(*, node, context):
|
|
142
|
+
cfg = node.get("config") or {}
|
|
143
|
+
goal = cfg.get("goal") or context.get("goal") or "Run agent"
|
|
144
|
+
return self.run_agent(goal, user, scope, with_workflow=False, roles=cfg.get("roles"), inputs=context.get("inputs"))
|
|
145
|
+
return runner
|
|
146
|
+
|
|
147
|
+
# ── cross-system runs ─────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def run_workflow_by_id(self, workflow_id, user, scope, *, with_agent: bool, inputs=None) -> Dict[str, Any]:
|
|
150
|
+
try:
|
|
151
|
+
workflow = self.store.get_workflow(workflow_id, workspace_id=scope)
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
return {"error": f"workflow not found: {workflow_id}"}
|
|
154
|
+
runners = {
|
|
155
|
+
"tool": self._tool_node_runner(),
|
|
156
|
+
"skill": self._skill_node_runner(),
|
|
157
|
+
"plugin": self._plugin_node_runner(user, scope),
|
|
158
|
+
}
|
|
159
|
+
if with_agent:
|
|
160
|
+
runners["agent"] = self._agent_node_runner(user, scope)
|
|
161
|
+
result = WorkflowEngine(runners).run(workflow, inputs=inputs or {})
|
|
162
|
+
run = self.store.record_workflow_run(
|
|
163
|
+
workflow_id=workflow_id, name=workflow.get("name") or "workflow",
|
|
164
|
+
status=result.status, timeline=result.timeline, outputs=result.outputs,
|
|
165
|
+
user_email=user, graph=self.workspace_graph(), workspace_id=scope,
|
|
166
|
+
)
|
|
167
|
+
return {"workflow_run_id": run["id"], "status": result.status}
|
|
168
|
+
|
|
169
|
+
def run_agent(self, goal, user, scope, *, with_workflow: bool, roles=None, inputs=None) -> Dict[str, Any]:
|
|
170
|
+
role_runner = default_role_runner(
|
|
171
|
+
workflow_runner=(lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs)) if with_workflow else None,
|
|
172
|
+
plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
|
|
173
|
+
context_provider=self._context_provider(user, scope),
|
|
174
|
+
)
|
|
175
|
+
result = MultiAgentOrchestrator(role_runner=role_runner).run(
|
|
176
|
+
goal, user_email=user, workspace_id=scope, roles=roles, inputs=inputs or {}
|
|
177
|
+
)
|
|
178
|
+
run = self.store.record_agent_run(
|
|
179
|
+
agent_id=result.agent_id, status=result.status, input_text=goal,
|
|
180
|
+
output_text=result.output, timeline=result.timeline, relationships=[],
|
|
181
|
+
user_email=user, graph=self.workspace_graph(), workspace_id=scope,
|
|
182
|
+
)
|
|
183
|
+
return {"agent_run_id": run["id"], "status": result.status, "output": result.output}
|
|
184
|
+
|
|
185
|
+
# ── factories passed to routers ───────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def build_workflow_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
|
|
188
|
+
return {
|
|
189
|
+
"tool": self._tool_node_runner(),
|
|
190
|
+
"skill": self._skill_node_runner(),
|
|
191
|
+
"plugin": self._plugin_node_runner(user, scope),
|
|
192
|
+
"agent": self._agent_node_runner(user, scope),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
|
|
196
|
+
return MultiAgentOrchestrator(role_runner=default_role_runner(
|
|
197
|
+
workflow_runner=lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs),
|
|
198
|
+
plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
|
|
199
|
+
context_provider=self._context_provider(user, scope),
|
|
200
|
+
))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -19,10 +19,16 @@
|
|
|
19
19
|
"dev": "python3 ltcai_cli.py --reload",
|
|
20
20
|
"build": "npm run build:python",
|
|
21
21
|
"build:python": "python3 -m build",
|
|
22
|
-
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
22
|
+
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
23
23
|
"test": "python3 -m pytest tests/ -v",
|
|
24
24
|
"test:unit": "python3 -m pytest tests/unit/ -v",
|
|
25
25
|
"test:integration": "python3 -m pytest tests/integration/ -v",
|
|
26
|
+
"test:visual": "playwright test",
|
|
27
|
+
"capture:workspace": "node scripts/capture/capture_workspace.js",
|
|
28
|
+
"capture:graph": "node scripts/capture/capture_graph.js",
|
|
29
|
+
"capture:skills": "node scripts/capture/capture_skills.js",
|
|
30
|
+
"capture:enterprise": "node scripts/capture/capture_enterprise.js",
|
|
31
|
+
"capture:onboarding": "node scripts/capture/capture_onboarding.js",
|
|
26
32
|
"publish:npm": "npm publish --access public",
|
|
27
33
|
"publish:pypi": "python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl"
|
|
28
34
|
},
|
|
@@ -62,18 +68,27 @@
|
|
|
62
68
|
"static/admin.html",
|
|
63
69
|
"static/graph.html",
|
|
64
70
|
"static/workspace.html",
|
|
71
|
+
"static/plugins.html",
|
|
72
|
+
"static/workflows.html",
|
|
73
|
+
"static/agents.html",
|
|
74
|
+
"static/activity.html",
|
|
65
75
|
"static/manifest.json",
|
|
66
76
|
"static/sw.js",
|
|
67
77
|
"static/lattice-reference.css",
|
|
68
78
|
"static/workspace.css",
|
|
79
|
+
"static/platform.css",
|
|
69
80
|
"static/scripts/",
|
|
70
81
|
"static/css/",
|
|
71
82
|
"static/icons/",
|
|
83
|
+
"plugins/",
|
|
72
84
|
"docs/",
|
|
73
85
|
"requirements.txt",
|
|
74
86
|
"README.md"
|
|
75
87
|
],
|
|
76
88
|
"publishConfig": {
|
|
77
89
|
"access": "public"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"@playwright/test": "^1.60.0"
|
|
78
93
|
}
|
|
79
94
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Lattice AI Plugins
|
|
2
|
+
|
|
3
|
+
This directory holds **Plugin SDK** packages discovered by the Lattice AI server
|
|
4
|
+
at runtime. Each plugin is a folder containing a `plugin.json` manifest.
|
|
5
|
+
|
|
6
|
+
A plugin is an additive, versioned, permissioned unit that **extends** the
|
|
7
|
+
existing Skill / Tool / Workflow surfaces — it never replaces them. Installed
|
|
8
|
+
standalone skills keep working unchanged.
|
|
9
|
+
|
|
10
|
+
See [`docs/PLUGIN_SDK.md`](../docs/PLUGIN_SDK.md) for the full manifest schema,
|
|
11
|
+
permission model, lifecycle, and execution boundary.
|
|
12
|
+
|
|
13
|
+
## Bundled examples
|
|
14
|
+
|
|
15
|
+
| Plugin | What it shows |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `hello-world` | Bundling a skill + a workflow template + a declarative action |
|
|
18
|
+
| `git-insights` | Declaring `run_tools` and surfacing read-only git insights through the permission boundary |
|
|
19
|
+
|
|
20
|
+
## Minimal manifest
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"id": "my-plugin",
|
|
25
|
+
"name": "My Plugin",
|
|
26
|
+
"version": "1.0.0",
|
|
27
|
+
"description": "What it does.",
|
|
28
|
+
"lattice_version": ">=2.0.0",
|
|
29
|
+
"permissions": ["read_workspace"],
|
|
30
|
+
"provides": { "skills": [], "tools": [], "workflows": [], "actions": [] }
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Permissions must be drawn from the SDK allow-list; the execution boundary
|
|
35
|
+
refuses any capability a plugin did not declare and was not granted at install.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "git-insights",
|
|
3
|
+
"name": "Git Insights",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Example plugin that surfaces read-only git status and log insights through the permissioned tool execution boundary.",
|
|
6
|
+
"author": "Lattice AI",
|
|
7
|
+
"lattice_version": ">=2.0.0",
|
|
8
|
+
"permissions": ["read_workspace", "run_tools"],
|
|
9
|
+
"provides": {
|
|
10
|
+
"tools": ["git_status", "git_log"],
|
|
11
|
+
"actions": ["summarize_repo"]
|
|
12
|
+
},
|
|
13
|
+
"entrypoint": "",
|
|
14
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hello-world",
|
|
3
|
+
"name": "Hello World",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Example plugin demonstrating the Lattice AI Plugin SDK: bundles a skill, a workflow template, and a greet action.",
|
|
6
|
+
"author": "Lattice AI",
|
|
7
|
+
"lattice_version": "2.0.0",
|
|
8
|
+
"permissions": ["read_workspace", "run_skills"],
|
|
9
|
+
"provides": {
|
|
10
|
+
"skills": ["hello_skill"],
|
|
11
|
+
"workflows": ["hello-workflow"],
|
|
12
|
+
"actions": ["greet"]
|
|
13
|
+
},
|
|
14
|
+
"entrypoint": "",
|
|
15
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hello_skill
|
|
3
|
+
description: A minimal example skill bundled by the hello-world plugin to show that plugins extend (not replace) the existing skill system.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hello Skill
|
|
7
|
+
|
|
8
|
+
This skill is contributed by the `hello-world` plugin. When the plugin is
|
|
9
|
+
installed, the Plugin SDK registers this skill into the existing Workspace skill
|
|
10
|
+
registry via the same `mark_skill_installed` path used by standalone skills —
|
|
11
|
+
demonstrating that plugins are an additive layer on top of skills.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Ask Lattice AI to greet a workspace member, or invoke the plugin action `greet`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Realtime Activity — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Realtime Activity</h1>
|
|
12
|
+
<p class="sub" id="sub">Live presence + activity feed across workspace, graph, agent, and workflow events (SSE).</p>
|
|
13
|
+
|
|
14
|
+
<div class="row">
|
|
15
|
+
<span class="badge" id="connBadge">connecting…</span>
|
|
16
|
+
<span class="badge" id="presenceBadge">presence: 0</span>
|
|
17
|
+
<div class="spacer"></div>
|
|
18
|
+
<button class="ghost" id="clearBtn">Clear view</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="section">
|
|
22
|
+
<h3>Live feed</h3>
|
|
23
|
+
<div id="feed"><div class="empty">Waiting for events…</div></div>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
|
|
27
|
+
<script type="module">
|
|
28
|
+
import { mountHeader, api, escapeHtml, badge } from "/static/scripts/platform.js";
|
|
29
|
+
mountHeader("/activity");
|
|
30
|
+
|
|
31
|
+
const feed = document.getElementById("feed");
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
|
|
34
|
+
function renderEvent(ev) {
|
|
35
|
+
if (ev.seq && seen.has(ev.seq)) return;
|
|
36
|
+
if (ev.seq) seen.add(ev.seq);
|
|
37
|
+
if (feed.querySelector(".empty")) feed.innerHTML = "";
|
|
38
|
+
const node = document.createElement("div");
|
|
39
|
+
node.className = "timeline-item";
|
|
40
|
+
node.innerHTML = `<div class="row">${badge(ev.area || "event")} <strong>${escapeHtml(ev.event_type || "")}</strong>
|
|
41
|
+
<span class="t-meta">${escapeHtml(ev.workspace_id || "personal")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</span></div>
|
|
42
|
+
<pre style="max-height:160px">${escapeHtml(JSON.stringify(ev.payload || {}, null, 2))}</pre>`;
|
|
43
|
+
feed.prepend(node);
|
|
44
|
+
while (feed.children.length > 100) feed.lastChild.remove();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function refreshPresence() {
|
|
48
|
+
try {
|
|
49
|
+
const data = await api("/realtime/presence");
|
|
50
|
+
document.getElementById("presenceBadge").textContent = `presence: ${data.presence.length}`;
|
|
51
|
+
document.getElementById("sub").textContent =
|
|
52
|
+
`Transport: ${data.stats.transport.toUpperCase()} · subscribers: ${data.stats.subscribers} · feed: ${data.stats.feed_size}`;
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.getElementById("clearBtn").addEventListener("click", () => { feed.innerHTML = `<div class="empty">Cleared.</div>`; });
|
|
57
|
+
|
|
58
|
+
// Seed with recent feed, then connect to the SSE stream.
|
|
59
|
+
api("/realtime/feed?limit=30").then((d) => (d.events || []).reverse().forEach(renderEvent)).catch(() => {});
|
|
60
|
+
|
|
61
|
+
const es = new EventSource("/realtime/stream");
|
|
62
|
+
es.onopen = () => { document.getElementById("connBadge").textContent = "● live"; document.getElementById("connBadge").className = "badge ok"; };
|
|
63
|
+
es.onerror = () => { document.getElementById("connBadge").textContent = "○ reconnecting"; document.getElementById("connBadge").className = "badge warn"; };
|
|
64
|
+
es.onmessage = (e) => { try { renderEvent(JSON.parse(e.data)); } catch {} };
|
|
65
|
+
|
|
66
|
+
refreshPresence();
|
|
67
|
+
setInterval(refreshPresence, 8000);
|
|
68
|
+
</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
package/static/admin.html
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<a href="#users" data-admin-nav="users"><i class="ti ti-users"></i> <span data-i18n="nav_users">사용자 관리</span></a>
|
|
27
27
|
<a href="#permissions" data-admin-nav="permissions"><i class="ti ti-key"></i> <span data-i18n="nav_permissions">권한 관리</span></a>
|
|
28
28
|
<a href="#sso" data-admin-nav="sso"><i class="ti ti-lock-access"></i> <span data-i18n="nav_sso">SSO 관리</span></a>
|
|
29
|
+
<a href="#enterprise" data-admin-nav="enterprise"><i class="ti ti-building-skyscraper"></i> <span data-i18n="nav_enterprise">Enterprise</span></a>
|
|
29
30
|
<a href="#security" data-admin-nav="security"><i class="ti ti-shield-check"></i> <span data-i18n="nav_security">보안 모니터링</span></a>
|
|
30
31
|
<a href="#audit" data-admin-nav="audit"><i class="ti ti-report-search"></i> <span data-i18n="nav_audit">감사 로그</span></a>
|
|
31
32
|
<a href="/workspace"><i class="ti ti-layout-dashboard"></i> <span>Workspace OS</span></a>
|
|
@@ -256,6 +257,67 @@
|
|
|
256
257
|
</section>
|
|
257
258
|
</section>
|
|
258
259
|
|
|
260
|
+
<section class="admin-view" id="admin-view-enterprise" data-admin-view="enterprise">
|
|
261
|
+
<section class="panel">
|
|
262
|
+
<div class="panel-header">
|
|
263
|
+
<div>
|
|
264
|
+
<h3 data-i18n="enterprise_title">Enterprise Admin</h3>
|
|
265
|
+
<p data-i18n="enterprise_desc">Admin policies, audit export, SIEM export, organization settings, and capability status.</p>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="tag-row" id="enterprise-status-tags"></div>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="panel-body">
|
|
270
|
+
<div class="enterprise-grid" id="enterprise-capability-status"></div>
|
|
271
|
+
</div>
|
|
272
|
+
</section>
|
|
273
|
+
|
|
274
|
+
<section class="panel-grid">
|
|
275
|
+
<article class="panel">
|
|
276
|
+
<div class="panel-header">
|
|
277
|
+
<div>
|
|
278
|
+
<h3 data-i18n="enterprise_policies">Admin Policies</h3>
|
|
279
|
+
<p data-i18n="enterprise_policies_desc">Effective Community policy and Enterprise policy-pack status.</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="panel-body" id="enterprise-admin-policies"></div>
|
|
283
|
+
</article>
|
|
284
|
+
<article class="panel">
|
|
285
|
+
<div class="panel-header">
|
|
286
|
+
<div>
|
|
287
|
+
<h3 data-i18n="enterprise_org">Organization Settings</h3>
|
|
288
|
+
<p data-i18n="enterprise_org_desc">Workspace governance and organization capability status.</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="panel-body" id="enterprise-org-settings"></div>
|
|
292
|
+
</article>
|
|
293
|
+
</section>
|
|
294
|
+
|
|
295
|
+
<section class="panel-grid">
|
|
296
|
+
<article class="panel">
|
|
297
|
+
<div class="panel-header">
|
|
298
|
+
<div>
|
|
299
|
+
<h3 data-i18n="enterprise_audit_export">Audit Export</h3>
|
|
300
|
+
<p data-i18n="enterprise_audit_export_desc">Local export remains available in Community; retention is an Enterprise extension point.</p>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="panel-body" id="enterprise-audit-export"></div>
|
|
304
|
+
</article>
|
|
305
|
+
<article class="panel">
|
|
306
|
+
<div class="panel-header">
|
|
307
|
+
<div>
|
|
308
|
+
<h3 data-i18n="enterprise_siem">SIEM Export</h3>
|
|
309
|
+
<p data-i18n="enterprise_siem_desc">Preview the SIEM envelope without streaming external events in Community.</p>
|
|
310
|
+
</div>
|
|
311
|
+
<button class="btn" id="refresh-siem-btn" type="button"><i class="ti ti-refresh"></i> <span>SIEM</span></button>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="panel-body">
|
|
314
|
+
<div id="enterprise-siem-export"></div>
|
|
315
|
+
<pre class="enterprise-json" id="enterprise-siem-preview"></pre>
|
|
316
|
+
</div>
|
|
317
|
+
</article>
|
|
318
|
+
</section>
|
|
319
|
+
</section>
|
|
320
|
+
|
|
259
321
|
<section class="admin-view" id="admin-view-security" data-admin-view="security">
|
|
260
322
|
<!-- Security & Audit Command Center (피드백 #5) -->
|
|
261
323
|
<section class="panel" id="security-overview-panel">
|